From 78510b6fd3d61c23da9ca0ffa1072c9073128bec Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Fri, 25 Oct 2024 20:09:59 +0400 Subject: [PATCH 001/167] core, ios: get messages for multiple last notifications; separately get notification connections before requesting messages (to avoid acknowledgement races in case of parralel nse threads); coordinate nse threads (#5084) * core, ios: get messages for multiple last notifications (#5047) * ios: refactor notification service (#5086) * core, ios: separately get notification connections before requesting messages; coordinate nse threads (#5085) --- apps/ios/Shared/Model/NtfManager.swift | 14 +- .../ios/SimpleX NSE/NotificationService.swift | 496 +++++++++++++----- apps/ios/SimpleX.xcodeproj/project.pbxproj | 40 +- apps/ios/SimpleXChat/APITypes.swift | 24 +- apps/ios/SimpleXChat/ChatTypes.swift | 15 +- apps/ios/SimpleXChat/Notifications.swift | 48 +- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- src/Simplex/Chat.hs | 56 +- src/Simplex/Chat/Controller.hs | 19 +- src/Simplex/Chat/View.hs | 4 +- 11 files changed, 487 insertions(+), 233 deletions(-) diff --git a/apps/ios/Shared/Model/NtfManager.swift b/apps/ios/Shared/Model/NtfManager.swift index 18fabc5a32..b2fa6a0200 100644 --- a/apps/ios/Shared/Model/NtfManager.swift +++ b/apps/ios/Shared/Model/NtfManager.swift @@ -199,6 +199,12 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject { actions: [], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: NSLocalizedString("SimpleX encrypted message or connection event", comment: "notification") + ), + UNNotificationCategory( + identifier: ntfCategoryManyEvents, + actions: [], + intentIdentifiers: [], + hiddenPreviewsBodyPlaceholder: NSLocalizedString("New events", comment: "notification") ) ]) } @@ -228,24 +234,24 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject { func notifyContactRequest(_ user: any UserLike, _ contactRequest: UserContactRequest) { logger.debug("NtfManager.notifyContactRequest") - addNotification(createContactRequestNtf(user, contactRequest)) + addNotification(createContactRequestNtf(user, contactRequest, 0)) } func notifyContactConnected(_ user: any UserLike, _ contact: Contact) { logger.debug("NtfManager.notifyContactConnected") - addNotification(createContactConnectedNtf(user, contact)) + addNotification(createContactConnectedNtf(user, contact, 0)) } func notifyMessageReceived(_ user: any UserLike, _ cInfo: ChatInfo, _ cItem: ChatItem) { logger.debug("NtfManager.notifyMessageReceived") if cInfo.ntfsEnabled { - addNotification(createMessageReceivedNtf(user, cInfo, cItem)) + addNotification(createMessageReceivedNtf(user, cInfo, cItem, 0)) } } func notifyCallInvitation(_ invitation: RcvCallInvitation) { logger.debug("NtfManager.notifyCallInvitation") - addNotification(createCallInvitationNtf(invitation)) + addNotification(createCallInvitationNtf(invitation, 0)) } func setNtfBadgeCount(_ count: Int) { diff --git a/apps/ios/SimpleX NSE/NotificationService.swift b/apps/ios/SimpleX NSE/NotificationService.swift index 99bedb891f..ce80adf38f 100644 --- a/apps/ios/SimpleX NSE/NotificationService.swift +++ b/apps/ios/SimpleX NSE/NotificationService.swift @@ -26,14 +26,52 @@ enum NSENotification { case nse(UNMutableNotificationContent) case callkit(RcvCallInvitation) case empty - case msgInfo(NtfMsgAckInfo) +} - var isCallInvitation: Bool { +public enum NSENotificationData { + case connectionEvent(_ user: User, _ connEntity: ConnectionEntity) + case contactConnected(_ user: any UserLike, _ contact: Contact) + case contactRequest(_ user: any UserLike, _ contactRequest: UserContactRequest) + case messageReceived(_ user: any UserLike, _ cInfo: ChatInfo, _ cItem: ChatItem) + case callInvitation(_ invitation: RcvCallInvitation) + case msgInfo(NtfMsgAckInfo) + case noNtf + + var callInvitation: RcvCallInvitation? { switch self { - case let .nse(ntf): ntf.categoryIdentifier == ntfCategoryCallInvitation - case .callkit: true - case .empty: false - case .msgInfo: false + case let .callInvitation(invitation): invitation + default: nil + } + } + + func notificationContent(_ badgeCount: Int) -> UNMutableNotificationContent { + return switch self { + case let .connectionEvent(user, connEntity): createConnectionEventNtf(user, connEntity, badgeCount) + case let .contactConnected(user, contact): createContactConnectedNtf(user, contact, badgeCount) + case let .contactRequest(user, contactRequest): createContactRequestNtf(user, contactRequest, badgeCount) + case let .messageReceived(user, cInfo, cItem): createMessageReceivedNtf(user, cInfo, cItem, badgeCount) + case let .callInvitation(invitation): createCallInvitationNtf(invitation, badgeCount) + case .msgInfo: UNMutableNotificationContent() + case .noNtf: UNMutableNotificationContent() + } + } + + var notificationEvent: NSENotificationData? { + return switch self { + case .connectionEvent: self + case .contactConnected: self + case .contactRequest: self + case .messageReceived: self + case .callInvitation: self + case .msgInfo: nil + case .noNtf: nil + } + } + + var newMsgData: (any UserLike, ChatInfo)? { + return switch self { + case let .messageReceived(user, cInfo, _): (user, cInfo) + default: nil } } } @@ -43,9 +81,10 @@ enum NSENotification { // or when background notification is received. class NSEThreads { static let shared = NSEThreads() - private static let queue = DispatchQueue(label: "chat.simplex.app.SimpleX-NSE.notification-threads.lock") + static let queue = DispatchQueue(label: "chat.simplex.app.SimpleX-NSE.notification-threads.lock") private var allThreads: Set = [] - private var activeThreads: [(UUID, NotificationService)] = [] + var activeThreads: [(UUID, NotificationService)] = [] + var droppedNotifications: [(ChatId, NSENotificationData)] = [] func newThread() -> UUID { NSEThreads.queue.sync { @@ -64,22 +103,19 @@ class NSEThreads { } } - func processNotification(_ id: ChatId, _ ntf: NSENotification) async -> Void { - var waitTime: Int64 = 5_000_000000 - while waitTime > 0 { - if let (_, nse) = rcvEntityThread(id), - nse.shouldProcessNtf && nse.processReceivedNtf(ntf) { - break - } else { - try? await Task.sleep(nanoseconds: 10_000000) - waitTime -= 10_000000 - } + func processNotification(_ id: ChatId, _ ntf: NSENotificationData) async -> Void { + if let (_, nse) = rcvEntityThread(id), + nse.expectedMessages[id]?.shouldProcessNtf ?? false { + nse.processReceivedNtf(id, ntf, signalReady: true) } } private func rcvEntityThread(_ id: ChatId) -> (UUID, NotificationService)? { NSEThreads.queue.sync { - activeThreads.first(where: { (_, nse) in nse.receiveEntityId == id }) + // this selects the earliest thread that: + // 1) has this connection in nse.expectedMessages + // 2) has not completed processing messages for this connection (not ready) + activeThreads.first(where: { (_, nse) in nse.expectedMessages[id]?.ready == false }) } } @@ -106,31 +142,38 @@ class NSEThreads { } } +struct ExpectedMessage { + var ntfConn: UserNtfConn + var receiveConnId: String? + var expectedMsgId: String? + var allowedGetNextAttempts: Int + var msgBestAttemptNtf: NSENotificationData? + var ready: Bool + var shouldProcessNtf: Bool + var startedProcessingNewMsgs: Bool + var semaphore: DispatchSemaphore +} + // Notification service extension creates a new instance of the class and calls didReceive for each notification. // Each didReceive is called in its own thread, but multiple calls can be made in one process, and, empirically, there is never // more than one process of notification service extension exists at a time. // Soon after notification service delivers the last notification it is either suspended or terminated. class NotificationService: UNNotificationServiceExtension { var contentHandler: ((UNNotificationContent) -> Void)? - var bestAttemptNtf: NSENotification? + // served as notification if no message attempts (msgBestAttemptNtf) could be produced + var serviceBestAttemptNtf: NSENotification? var badgeCount: Int = 0 // thread is added to allThreads here - if thread did not start chat, // chat does not need to be suspended but NSE state still needs to be set to "suspended". var threadId: UUID? = NSEThreads.shared.newThread() - var notificationInfo: NtfMessages? - var receiveEntityId: String? - var receiveConnId: String? - var expectedMessage: String? - var allowedGetNextAttempts: Int = 3 - // return true if the message is taken - it prevents sending it to another NotificationService instance for processing - var shouldProcessNtf = false + var expectedMessages: Dictionary = [:] // key is receiveEntityId var appSubscriber: AppSubscriber? var returnedSuspension = false override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { logger.debug("DEBUGGING: NotificationService.didReceive") let ntf = if let ntf_ = request.content.mutableCopy() as? UNMutableNotificationContent { ntf_ } else { UNMutableNotificationContent() } - setBestAttemptNtf(ntf) + setServiceBestAttemptNtf(ntf) self.contentHandler = contentHandler registerGroupDefaults() let appState = appStateGroupDefault.get() @@ -138,13 +181,11 @@ class NotificationService: UNNotificationServiceExtension { switch appState { case .stopped: setBadgeCount() - setBestAttemptNtf(createAppStoppedNtf()) + setServiceBestAttemptNtf(createAppStoppedNtf(badgeCount)) deliverBestAttemptNtf() case .suspended: - setBadgeCount() receiveNtfMessages(request, contentHandler) case .suspending: - setBadgeCount() Task { let state: AppState = await withCheckedContinuation { cont in appSubscriber = appStateSubscriber { s in @@ -171,8 +212,9 @@ class NotificationService: UNNotificationServiceExtension { deliverBestAttemptNtf() } } - default: - deliverBestAttemptNtf() + case .active: contentHandler(UNMutableNotificationContent()) + case .activating: contentHandler(UNMutableNotificationContent()) + case .bgRefresh: contentHandler(UNMutableNotificationContent()) } } @@ -192,78 +234,165 @@ class NotificationService: UNNotificationServiceExtension { if let t = threadId { NSEThreads.shared.startThread(t, self) } let dbStatus = startChat() if case .ok = dbStatus, - let ntfInfo = apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo) { - logger.debug("NotificationService: receiveNtfMessages: apiGetNtfMessage \(String(describing: ntfInfo.receivedMsg_ == nil ? 0 : 1))") - if let connEntity = ntfInfo.connEntity_ { - setBestAttemptNtf( - ntfInfo.ntfsEnabled - ? .nse(createConnectionEventNtf(ntfInfo.user, connEntity)) - : .empty - ) - if let id = connEntity.id, ntfInfo.expectedMsg_ != nil { - notificationInfo = ntfInfo - receiveEntityId = id - receiveConnId = connEntity.conn.agentConnId - let expectedMsgId = ntfInfo.expectedMsg_?.msgId - let receivedMsgId = ntfInfo.receivedMsg_?.msgId - logger.debug("NotificationService: receiveNtfMessages: expectedMsgId = \(expectedMsgId ?? "nil", privacy: .private), receivedMsgId = \(receivedMsgId ?? "nil", privacy: .private)") - expectedMessage = expectedMsgId - shouldProcessNtf = true - return + let ntfConns = apiGetNtfConns(nonce: nonce, encNtfInfo: encNtfInfo) { + logger.debug("NotificationService: receiveNtfMessages: apiGetNtfConns ntfConns count = \(ntfConns.count)") + + for ntfConn in ntfConns { + addExpectedMessage(ntfConn: ntfConn) + } + + let connIdsToGet = expectedMessages.compactMap { (id, _) in + let started = NSEThreads.queue.sync { + let canStart = checkCanStart(id) + if let t = threadId { logger.debug("NotificationService thread \(t, privacy: .private): receiveNtfMessages: can start: \(canStart)") } + if canStart { + processDroppedNotifications(id) + expectedMessages[id]?.startedProcessingNewMsgs = true + expectedMessages[id]?.shouldProcessNtf = true + } + return canStart + } + if started { + return expectedMessages[id]?.receiveConnId + } else { + if let t = threadId { logger.debug("NotificationService thread \(t, privacy: .private): receiveNtfMessages: entity \(id, privacy: .private) waiting on semaphore") } + expectedMessages[id]?.semaphore.wait() + if let t = threadId { logger.debug("NotificationService thread \(t, privacy: .private): receiveNtfMessages: entity \(id, privacy: .private) proceeding after semaphore") } + Task { + NSEThreads.queue.sync { + processDroppedNotifications(id) + expectedMessages[id]?.startedProcessingNewMsgs = true + expectedMessages[id]?.shouldProcessNtf = true + } + if let connId = expectedMessages[id]?.receiveConnId { + let _ = getConnNtfMessage(connId: connId) + } + } + return nil } } + + if !connIdsToGet.isEmpty { + if let r = apiGetConnNtfMessages(connIds: connIdsToGet) { + logger.debug("NotificationService: receiveNtfMessages: apiGetConnNtfMessages count = \(r.count)") + } + return + } } else if let dbStatus = dbStatus { - setBestAttemptNtf(createErrorNtf(dbStatus)) + setServiceBestAttemptNtf(createErrorNtf(dbStatus, badgeCount)) } } deliverBestAttemptNtf() } + func addExpectedMessage(ntfConn: UserNtfConn) { + if let connEntity = ntfConn.connEntity_, + let receiveEntityId = connEntity.id, ntfConn.expectedMsg_ != nil { + let expectedMsgId = ntfConn.expectedMsg_?.msgId + logger.debug("NotificationService: addExpectedMessage: expectedMsgId = \(expectedMsgId ?? "nil", privacy: .private)") + expectedMessages[receiveEntityId] = ExpectedMessage( + ntfConn: ntfConn, + receiveConnId: connEntity.conn.agentConnId, + expectedMsgId: expectedMsgId, + allowedGetNextAttempts: 3, + msgBestAttemptNtf: ntfConn.defaultBestAttemptNtf, + ready: false, + shouldProcessNtf: false, + startedProcessingNewMsgs: false, + semaphore: DispatchSemaphore(value: 0) + ) + } + } + + func checkCanStart(_ entityId: String) -> Bool { + return !NSEThreads.shared.activeThreads.contains(where: { + (tId, nse) in tId != threadId && nse.expectedMessages.contains(where: { $0.key == entityId }) + }) + } + + func processDroppedNotifications(_ entityId: String) { + if !NSEThreads.shared.droppedNotifications.isEmpty { + let messagesToProcess = NSEThreads.shared.droppedNotifications.filter { (eId, _) in eId == entityId } + NSEThreads.shared.droppedNotifications.removeAll(where: { (eId, _) in eId == entityId }) + for (index, (_, ntf)) in messagesToProcess.enumerated() { + if let t = threadId { logger.debug("NotificationService thread \(t, privacy: .private): entity \(entityId, privacy: .private): processing dropped notification \(index, privacy: .private)") } + processReceivedNtf(entityId, ntf, signalReady: false) + } + } + } + override func serviceExtensionTimeWillExpire() { logger.debug("DEBUGGING: NotificationService.serviceExtensionTimeWillExpire") deliverBestAttemptNtf(urgent: true) } - func processReceivedNtf(_ ntf: NSENotification) -> Bool { - guard let ntfInfo = notificationInfo, let expectedMsgTs = ntfInfo.expectedMsg_?.msgTs else { return false } - if !ntfInfo.user.showNotifications { - self.setBestAttemptNtf(.empty) + var expectingMoreMessages: Bool { + !expectedMessages.allSatisfy { $0.value.ready } + } + + func processReceivedNtf(_ id: ChatId, _ ntf: NSENotificationData, signalReady: Bool) { + guard let expectedMessage = expectedMessages[id] else { + return + } + guard let expectedMsgTs = expectedMessage.ntfConn.expectedMsg_?.msgTs else { + NSEThreads.shared.droppedNotifications.append((id, ntf)) + if signalReady { entityReady(id) } + return } if case let .msgInfo(info) = ntf { - if info.msgId == expectedMessage { - expectedMessage = nil + if info.msgId == expectedMessage.expectedMsgId { logger.debug("NotificationService processNtf: msgInfo msgId = \(info.msgId, privacy: .private): expected") + expectedMessages[id]?.expectedMsgId = nil + if signalReady { entityReady(id) } self.deliverBestAttemptNtf() - return true } else if let msgTs = info.msgTs_, msgTs > expectedMsgTs { logger.debug("NotificationService processNtf: msgInfo msgId = \(info.msgId, privacy: .private): unexpected msgInfo, let other instance to process it, stopping this one") + NSEThreads.shared.droppedNotifications.append((id, ntf)) + if signalReady { entityReady(id) } self.deliverBestAttemptNtf() - return false - } else if allowedGetNextAttempts > 0, let receiveConnId = receiveConnId { + } else if (expectedMessages[id]?.allowedGetNextAttempts ?? 0) > 0, let receiveConnId = expectedMessages[id]?.receiveConnId { logger.debug("NotificationService processNtf: msgInfo msgId = \(info.msgId, privacy: .private): unexpected msgInfo, get next message") - allowedGetNextAttempts -= 1 - if let receivedMsg = apiGetConnNtfMessage(connId: receiveConnId) { - logger.debug("NotificationService processNtf, on apiGetConnNtfMessage: msgInfo msgId = \(info.msgId, privacy: .private), receivedMsg msgId = \(receivedMsg.msgId, privacy: .private)") - return true + expectedMessages[id]?.allowedGetNextAttempts -= 1 + if let receivedMsg = getConnNtfMessage(connId: receiveConnId) { + logger.debug("NotificationService processNtf, on getConnNtfMessage: msgInfo msgId = \(info.msgId, privacy: .private), receivedMsg msgId = \(receivedMsg.msgId, privacy: .private)") } else { - logger.debug("NotificationService processNtf, on apiGetConnNtfMessage: msgInfo msgId = \(info.msgId, privacy: .private): no next message, deliver best attempt") + logger.debug("NotificationService processNtf, on getConnNtfMessage: msgInfo msgId = \(info.msgId, privacy: .private): no next message, deliver best attempt") + NSEThreads.shared.droppedNotifications.append((id, ntf)) + if signalReady { entityReady(id) } self.deliverBestAttemptNtf() - return false } } else { logger.debug("NotificationService processNtf: msgInfo msgId = \(info.msgId, privacy: .private): unknown message, let other instance to process it") + NSEThreads.shared.droppedNotifications.append((id, ntf)) + if signalReady { entityReady(id) } self.deliverBestAttemptNtf() - return false } - } else if ntfInfo.user.showNotifications { + } else if expectedMessage.ntfConn.user.showNotifications { logger.debug("NotificationService processNtf: setting best attempt") - self.setBestAttemptNtf(ntf) - if ntf.isCallInvitation { - self.deliverBestAttemptNtf() + if ntf.notificationEvent != nil { + setBadgeCount() } - return true + let prevBestAttempt = expectedMessages[id]?.msgBestAttemptNtf + if prevBestAttempt?.callInvitation != nil { + if ntf.callInvitation != nil { // replace with newer call + expectedMessages[id]?.msgBestAttemptNtf = ntf + } // otherwise keep call as best attempt + } else { + expectedMessages[id]?.msgBestAttemptNtf = ntf + } + } else { + NSEThreads.shared.droppedNotifications.append((id, ntf)) + if signalReady { entityReady(id) } + } + } + + func entityReady(_ entityId: ChatId) { + if let t = threadId { logger.debug("NotificationService thread \(t, privacy: .private): entityReady: entity \(entityId, privacy: .private)") } + expectedMessages[entityId]?.ready = true + if let (tNext, nse) = NSEThreads.shared.activeThreads.first(where: { (_, nse) in nse.expectedMessages[entityId]?.startedProcessingNewMsgs == false }) { + if let t = threadId { logger.debug("NotificationService thread \(t, privacy: .private): entityReady: signal next thread \(tNext, privacy: .private) for entity \(entityId, privacy: .private)") } + nse.expectedMessages[entityId]?.semaphore.signal() } - return false } func setBadgeCount() { @@ -271,37 +400,32 @@ class NotificationService: UNNotificationServiceExtension { ntfBadgeCountGroupDefault.set(badgeCount) } - func setBestAttemptNtf(_ ntf: UNMutableNotificationContent) { - setBestAttemptNtf(.nse(ntf)) - } - - func setBestAttemptNtf(_ ntf: NSENotification) { - logger.debug("NotificationService.setBestAttemptNtf") - if case let .nse(notification) = ntf { - notification.badge = badgeCount as NSNumber - bestAttemptNtf = .nse(notification) - } else { - bestAttemptNtf = ntf - } + func setServiceBestAttemptNtf(_ ntf: UNMutableNotificationContent) { + logger.debug("NotificationService.setServiceBestAttemptNtf") + serviceBestAttemptNtf = .nse(ntf) } private func deliverBestAttemptNtf(urgent: Bool = false) { - logger.debug("NotificationService.deliverBestAttemptNtf") - // stop processing other messages - shouldProcessNtf = false + if (urgent || !expectingMoreMessages) { + logger.debug("NotificationService.deliverBestAttemptNtf") + // stop processing other messages + for (key, _) in expectedMessages { + expectedMessages[key]?.shouldProcessNtf = false + } - let suspend: Bool - if let t = threadId { - threadId = nil - suspend = NSEThreads.shared.endThread(t) && NSEThreads.shared.noThreads - } else { - suspend = false + let suspend: Bool + if let t = threadId { + threadId = nil + suspend = NSEThreads.shared.endThread(t) && NSEThreads.shared.noThreads + } else { + suspend = false + } + deliverCallkitOrNotification(urgent: urgent, suspend: suspend) } - deliverCallkitOrNotification(urgent: urgent, suspend: suspend) } private func deliverCallkitOrNotification(urgent: Bool, suspend: Bool = false) { - if case .callkit = bestAttemptNtf { + if useCallKit() && expectedMessages.contains(where: { $0.value.msgBestAttemptNtf?.callInvitation != nil }) { logger.debug("NotificationService.deliverCallkitOrNotification: will suspend, callkit") if urgent { // suspending NSE even though there may be other notifications @@ -339,19 +463,13 @@ class NotificationService: UNNotificationServiceExtension { } private func deliverNotification() { - if let handler = contentHandler, let ntf = bestAttemptNtf { + if let handler = contentHandler, let ntf = prepareNotification() { contentHandler = nil - bestAttemptNtf = nil - let deliver: (UNMutableNotificationContent?) -> Void = { ntf in - let useNtf = if let ntf = ntf { - appStateGroupDefault.get().running ? UNMutableNotificationContent() : ntf - } else { - UNMutableNotificationContent() - } - handler(useNtf) - } + serviceBestAttemptNtf = nil switch ntf { - case let .nse(content): deliver(content) + case let .nse(content): + content.badge = badgeCount as NSNumber + handler(content) case let .callkit(invitation): logger.debug("NotificationService reportNewIncomingVoIPPushPayload for \(invitation.contact.id)") CXProvider.reportNewIncomingVoIPPushPayload([ @@ -362,13 +480,85 @@ class NotificationService: UNNotificationServiceExtension { "callTs": invitation.callTs.timeIntervalSince1970 ]) { error in logger.debug("reportNewIncomingVoIPPushPayload result: \(error)") - deliver(error == nil ? nil : createCallInvitationNtf(invitation)) + handler(error == nil ? UNMutableNotificationContent() : createCallInvitationNtf(invitation, self.badgeCount)) } - case .empty: deliver(nil) // used to mute notifications that did not unsubscribe yet - case .msgInfo: deliver(nil) // unreachable, the best attempt is never set to msgInfo + case .empty: + handler(UNMutableNotificationContent()) // used to mute notifications that did not unsubscribe yet } } } + + private func prepareNotification() -> NSENotification? { + if expectedMessages.isEmpty { + return serviceBestAttemptNtf + } else if let callNtfKV = expectedMessages.first(where: { $0.value.msgBestAttemptNtf?.callInvitation != nil }), + let callInv = callNtfKV.value.msgBestAttemptNtf?.callInvitation, + let callNtf = callNtfKV.value.msgBestAttemptNtf { + return useCallKit() ? .callkit(callInv) : .nse(callNtf.notificationContent(badgeCount)) + } else { + let ntfEvents = expectedMessages.compactMap { $0.value.msgBestAttemptNtf?.notificationEvent } + if ntfEvents.isEmpty { + return .empty + } else if let ntfEvent = ntfEvents.count == 1 ? ntfEvents.first : nil { + return .nse(ntfEvent.notificationContent(badgeCount)) + } else { + return .nse(createJointNtf(ntfEvents)) + } + } + } + + private func createJointNtf(_ ntfEvents: [NSENotificationData]) -> UNMutableNotificationContent { + let previewMode = ntfPreviewModeGroupDefault.get() + let newMsgsData: [(any UserLike, ChatInfo)] = ntfEvents.compactMap { $0.newMsgData } + if !newMsgsData.isEmpty, let userId = newMsgsData.first?.0.userId { + let newMsgsChats: [ChatInfo] = newMsgsData.map { $0.1 } + let uniqueChatsNames = uniqueNewMsgsChatsNames(newMsgsChats) + var body: String + if previewMode == .hidden { + body = String.localizedStringWithFormat(NSLocalizedString("New messages in %d chats", comment: "notification body"), uniqueChatsNames.count) + } else { + body = String.localizedStringWithFormat(NSLocalizedString("From: %@", comment: "notification body"), newMsgsChatsNamesStr(uniqueChatsNames)) + } + return createNotification( + categoryIdentifier: ntfCategoryManyEvents, + title: NSLocalizedString("New messages", comment: "notification"), + body: body, + userInfo: ["userId": userId], + badgeCount: badgeCount + ) + } else { + return createNotification( + categoryIdentifier: ntfCategoryManyEvents, + title: NSLocalizedString("New events", comment: "notification"), + body: String.localizedStringWithFormat(NSLocalizedString("%d new events", comment: "notification body"), ntfEvents.count), + badgeCount: badgeCount + ) + } + } + + private func uniqueNewMsgsChatsNames(_ newMsgsChats: [ChatInfo]) -> [String] { + var seenChatIds = Set() + var uniqueChatsNames: [String] = [] + for chat in newMsgsChats { + if !seenChatIds.contains(chat.id) { + seenChatIds.insert(chat.id) + uniqueChatsNames.append(chat.chatViewName) + } + } + return uniqueChatsNames + } + + private func newMsgsChatsNamesStr(_ names: [String]) -> String { + return switch names.count { + case 1: names[0] + case 2: "\(names[0]) and \(names[1])" + case 3: "\(names[0] + ", " + names[1]) and \(names[2])" + default: + names.count > 3 + ? "\(names[0]), \(names[1]) and \(names.count - 2) other chats" + : "" + } + } } // nseStateGroupDefault must not be used in NSE directly, only via this singleton @@ -582,28 +772,25 @@ func chatRecvMsg() async -> ChatResponse? { private let isInChina = SKStorefront().countryCode == "CHN" private func useCallKit() -> Bool { !isInChina && callKitEnabledGroupDefault.get() } -func receivedMsgNtf(_ res: ChatResponse) async -> (String, NSENotification)? { +func receivedMsgNtf(_ res: ChatResponse) async -> (String, NSENotificationData)? { logger.debug("NotificationService receivedMsgNtf: \(res.responseType)") switch res { case let .contactConnected(user, contact, _): - return (contact.id, .nse(createContactConnectedNtf(user, contact))) + return (contact.id, .contactConnected(user, contact)) // case let .contactConnecting(contact): // TODO profile update case let .receivedContactRequest(user, contactRequest): - return (UserContact(contactRequest: contactRequest).id, .nse(createContactRequestNtf(user, contactRequest))) + return (UserContact(contactRequest: contactRequest).id, .contactRequest(user, contactRequest)) case let .newChatItems(user, chatItems): // Received items are created one at a time if let chatItem = chatItems.first { let cInfo = chatItem.chatInfo var cItem = chatItem.chatItem - if !cInfo.ntfsEnabled { - ntfBadgeCountGroupDefault.set(max(0, ntfBadgeCountGroupDefault.get() - 1)) - } if let file = cItem.autoReceiveFile() { cItem = autoReceiveFile(file) ?? cItem } - let ntf: NSENotification = cInfo.ntfsEnabled ? .nse(createMessageReceivedNtf(user, cInfo, cItem)) : .empty - return cItem.showNotification ? (chatItem.chatId, ntf) : nil + let ntf: NSENotificationData = (cInfo.ntfsEnabled && cItem.showNotification) ? .messageReceived(user, cInfo, cItem) : .noNtf + return (chatItem.chatId, ntf) } else { return nil } @@ -620,10 +807,7 @@ func receivedMsgNtf(_ res: ChatResponse) async -> (String, NSENotification)? { return nil case let .callInvitation(invitation): // Do not post it without CallKit support, iOS will stop launching the app without showing CallKit - return ( - invitation.contact.id, - useCallKit() ? .callkit(invitation) : .nse(createCallInvitationNtf(invitation)) - ) + return (invitation.contact.id, .callInvitation(invitation)) case let .ntfMessage(_, connEntity, ntfMessage): return if let id = connEntity.id { (id, .msgInfo(ntfMessage)) } else { nil } case .chatSuspended: @@ -704,15 +888,15 @@ func apiSetEncryptLocalFiles(_ enable: Bool) throws { throw r } -func apiGetNtfMessage(nonce: String, encNtfInfo: String) -> NtfMessages? { +func apiGetNtfConns(nonce: String, encNtfInfo: String) -> [UserNtfConn]? { guard apiGetActiveUser() != nil else { logger.debug("no active user") return nil } - let r = sendSimpleXCmd(.apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo)) - if case let .ntfMessages(user, connEntity_, expectedMsg_, receivedMsg_) = r, let user = user { - logger.debug("apiGetNtfMessage response ntfMessages: \(receivedMsg_ == nil ? 0 : 1)") - return NtfMessages(user: user, connEntity_: connEntity_, expectedMsg_: expectedMsg_, receivedMsg_: receivedMsg_) + let r = sendSimpleXCmd(.apiGetNtfConns(nonce: nonce, encNtfInfo: encNtfInfo)) + if case let .ntfConns(ntfConns) = r { + logger.debug("apiGetNtfConns response ntfConns: \(ntfConns.count)") + return ntfConns.compactMap { toUserNtfConn($0) } } else if case let .chatCmdError(_, error) = r { logger.debug("apiGetNtfMessage error response: \(String.init(describing: error))") } else { @@ -721,17 +905,33 @@ func apiGetNtfMessage(nonce: String, encNtfInfo: String) -> NtfMessages? { return nil } -func apiGetConnNtfMessage(connId: String) -> NtfMsgInfo? { +func toUserNtfConn(_ ntfConn: NtfConn) -> UserNtfConn? { + if let user = ntfConn.user_ { + return UserNtfConn(user: user, connEntity_: ntfConn.connEntity_, expectedMsg_: ntfConn.expectedMsg_) + } else { + return nil + } +} + +func apiGetConnNtfMessages(connIds: [String]) -> [NtfMsgInfo?]? { guard apiGetActiveUser() != nil else { logger.debug("no active user") return nil } - let r = sendSimpleXCmd(.apiGetConnNtfMessage(connId: connId)) - if case let .connNtfMessage(receivedMsg_) = r { - logger.debug("apiGetConnNtfMessage response receivedMsg_: \(receivedMsg_ == nil ? 0 : 1)") - return receivedMsg_ + let r = sendSimpleXCmd(.apiGetConnNtfMessages(connIds: connIds)) + if case let .connNtfMessages(receivedMsgs) = r { + logger.debug("apiGetConnNtfMessages response receivedMsgs: \(receivedMsgs.count)") + return receivedMsgs + } + logger.debug("apiGetConnNtfMessages error: \(responseError(r))") + return nil +} + +func getConnNtfMessage(connId: String) -> NtfMsgInfo? { + let r_ = apiGetConnNtfMessages(connIds: [connId]) + if let r = r_, let receivedMsg = r.count == 1 ? r.first : nil { + return receivedMsg } - logger.debug("apiGetConnNtfMessage error: \(responseError(r))") return nil } @@ -769,13 +969,33 @@ func setNetworkConfig(_ cfg: NetCfg) throws { throw r } -struct NtfMessages { +struct UserNtfConn { var user: User var connEntity_: ConnectionEntity? var expectedMsg_: NtfMsgInfo? - var receivedMsg_: NtfMsgInfo? - var ntfsEnabled: Bool { - user.showNotifications && (connEntity_?.ntfsEnabled ?? false) + var defaultBestAttemptNtf: NSENotificationData { + return if !user.showNotifications { + .noNtf + } else if let connEntity = connEntity_ { + switch connEntity { + case let .rcvDirectMsgConnection(_, contact): + contact?.chatSettings.enableNtfs == .all + ? .connectionEvent(user, connEntity) + : .noNtf + case let .rcvGroupMsgConnection(_, groupInfo, _): + groupInfo.chatSettings.enableNtfs == .all + ? .connectionEvent(user, connEntity) + : .noNtf + case .sndFileConnection: .noNtf + case .rcvFileConnection: .noNtf + case let .userContactConnection(_, userContact): + userContact.groupId == nil + ? .connectionEvent(user, connEntity) + : .noNtf + } + } else { + .noNtf + } } } diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index a21d47c16c..2b1160061c 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -148,6 +148,11 @@ 6419EC562AB8BC8B004A607A /* ContextInvitingContactMemberView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6419EC552AB8BC8B004A607A /* ContextInvitingContactMemberView.swift */; }; 6419EC582AB97507004A607A /* CIMemberCreatedContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6419EC572AB97507004A607A /* CIMemberCreatedContactView.swift */; }; 6432857C2925443C00FBE5C8 /* GroupPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */; }; + 643B3B452CCBEB080083A2CF /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643B3B402CCBEB080083A2CF /* libgmpxx.a */; }; + 643B3B462CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643B3B412CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9-ghc9.6.3.a */; }; + 643B3B472CCBEB080083A2CF /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643B3B422CCBEB080083A2CF /* libffi.a */; }; + 643B3B482CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643B3B432CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9.a */; }; + 643B3B492CCBEB080083A2CF /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643B3B442CCBEB080083A2CF /* libgmp.a */; }; 6440CA00288857A10062C672 /* CIEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440C9FF288857A10062C672 /* CIEventView.swift */; }; 6440CA03288AECA70062C672 /* AddGroupMembersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440CA02288AECA70062C672 /* AddGroupMembersView.swift */; }; 6442E0BA287F169300CEC0F9 /* AddGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6442E0B9287F169300CEC0F9 /* AddGroupView.swift */; }; @@ -220,15 +225,10 @@ 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 */; }; - E56F46202CC2B2E300F1559D /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E56F461E2CC2B2E300F1559D /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9.a */; }; - E56F46212CC2B2E300F1559D /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E56F461F2CC2B2E300F1559D /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9-ghc9.6.3.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 */; }; - E5E997C92CBA891A00D7A2FA /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5E997C42CBA891A00D7A2FA /* libgmpxx.a */; }; - E5E997CA2CBA891A00D7A2FA /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5E997C52CBA891A00D7A2FA /* libgmp.a */; }; - E5E997CC2CBA891A00D7A2FA /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5E997C72CBA891A00D7A2FA /* libffi.a */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -491,6 +491,11 @@ 6419EC552AB8BC8B004A607A /* ContextInvitingContactMemberView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextInvitingContactMemberView.swift; sourceTree = ""; }; 6419EC572AB97507004A607A /* CIMemberCreatedContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIMemberCreatedContactView.swift; sourceTree = ""; }; 6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupPreferencesView.swift; sourceTree = ""; }; + 643B3B402CCBEB080083A2CF /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libgmpxx.a; path = Libraries/libgmpxx.a; sourceTree = ""; }; + 643B3B412CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9-ghc9.6.3.a"; path = "Libraries/libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9-ghc9.6.3.a"; sourceTree = ""; }; + 643B3B422CCBEB080083A2CF /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libffi.a; path = Libraries/libffi.a; sourceTree = ""; }; + 643B3B432CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9.a"; path = "Libraries/libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9.a"; sourceTree = ""; }; + 643B3B442CCBEB080083A2CF /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libgmp.a; path = Libraries/libgmp.a; sourceTree = ""; }; 6440C9FF288857A10062C672 /* CIEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIEventView.swift; sourceTree = ""; }; 6440CA02288AECA70062C672 /* AddGroupMembersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGroupMembersView.swift; sourceTree = ""; }; 6442E0B9287F169300CEC0F9 /* AddGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGroupView.swift; sourceTree = ""; }; @@ -562,8 +567,6 @@ 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 = ""; }; - E56F461E2CC2B2E300F1559D /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9.a"; sourceTree = ""; }; - E56F461F2CC2B2E300F1559D /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9-ghc9.6.3.a"; sourceTree = ""; }; E5DCF9702C590272007928CC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; E5DCF9722C590274007928CC /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Localizable.strings; sourceTree = ""; }; E5DCF9732C590275007928CC /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; @@ -616,9 +619,6 @@ E5DCF9A62C590731007928CC /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = th; path = th.lproj/InfoPlist.strings; sourceTree = ""; }; E5DCF9A72C590732007928CC /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/InfoPlist.strings; sourceTree = ""; }; E5DCF9A82C590732007928CC /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/InfoPlist.strings; sourceTree = ""; }; - E5E997C42CBA891A00D7A2FA /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; - E5E997C52CBA891A00D7A2FA /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; - E5E997C72CBA891A00D7A2FA /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -657,14 +657,14 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - E5E997C92CBA891A00D7A2FA /* libgmpxx.a in Frameworks */, - E5E997CA2CBA891A00D7A2FA /* libgmp.a in Frameworks */, + 643B3B452CCBEB080083A2CF /* libgmpxx.a in Frameworks */, + 643B3B472CCBEB080083A2CF /* libffi.a in Frameworks */, + 643B3B492CCBEB080083A2CF /* libgmp.a in Frameworks */, 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, - E5E997CC2CBA891A00D7A2FA /* libffi.a in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, - E56F46202CC2B2E300F1559D /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9.a in Frameworks */, CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */, - E56F46212CC2B2E300F1559D /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9-ghc9.6.3.a in Frameworks */, + 643B3B482CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9.a in Frameworks */, + 643B3B462CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9-ghc9.6.3.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -741,11 +741,6 @@ 5C764E5C279C70B7000C6508 /* Libraries */ = { isa = PBXGroup; children = ( - E5E997C72CBA891A00D7A2FA /* libffi.a */, - E5E997C52CBA891A00D7A2FA /* libgmp.a */, - E5E997C42CBA891A00D7A2FA /* libgmpxx.a */, - E56F461F2CC2B2E300F1559D /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9-ghc9.6.3.a */, - E56F461E2CC2B2E300F1559D /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9.a */, ); path = Libraries; sourceTree = ""; @@ -817,6 +812,11 @@ 5CC2C0FA2809BF11000C35E3 /* Localizable.strings */, 5C422A7C27A9A6FA0097A1E1 /* SimpleX (iOS).entitlements */, 5C764E5C279C70B7000C6508 /* Libraries */, + 643B3B422CCBEB080083A2CF /* libffi.a */, + 643B3B442CCBEB080083A2CF /* libgmp.a */, + 643B3B402CCBEB080083A2CF /* libgmpxx.a */, + 643B3B412CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9-ghc9.6.3.a */, + 643B3B432CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9.a */, 5CA059C2279559F40002BEB4 /* Shared */, 5CDCAD462818589900503DA2 /* SimpleX NSE */, CEE723A82C3BD3D70009AE93 /* SimpleX SE */, diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index ea567427fe..3c9b91fa0b 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -55,8 +55,8 @@ public enum ChatCommand { case apiRegisterToken(token: DeviceToken, notificationMode: NotificationsMode) case apiVerifyToken(token: DeviceToken, nonce: String, code: String) case apiDeleteToken(token: DeviceToken) - case apiGetNtfMessage(nonce: String, encNtfInfo: String) - case apiGetConnNtfMessage(connId: String) + case apiGetNtfConns(nonce: String, encNtfInfo: String) + case apiGetConnNtfMessages(connIds: [String]) case apiNewGroup(userId: Int64, incognito: Bool, groupProfile: GroupProfile) case apiAddMember(groupId: Int64, contactId: Int64, memberRole: GroupMemberRole) case apiJoinGroup(groupId: Int64) @@ -214,8 +214,8 @@ public enum ChatCommand { case let .apiRegisterToken(token, notificationMode): return "/_ntf register \(token.cmdString) \(notificationMode.rawValue)" case let .apiVerifyToken(token, nonce, code): return "/_ntf verify \(token.cmdString) \(nonce) \(code)" case let .apiDeleteToken(token): return "/_ntf delete \(token.cmdString)" - case let .apiGetNtfMessage(nonce, encNtfInfo): return "/_ntf message \(nonce) \(encNtfInfo)" - case let .apiGetConnNtfMessage(connId): return "/_ntf conn message \(connId)" + case let .apiGetNtfConns(nonce, encNtfInfo): return "/_ntf conns \(nonce) \(encNtfInfo)" + case let .apiGetConnNtfMessages(connIds): return "/_ntf conn messages \(connIds.joined(separator: ","))" case let .apiNewGroup(userId, incognito, groupProfile): return "/_group \(userId) incognito=\(onOff(incognito)) \(encodeJSON(groupProfile))" case let .apiAddMember(groupId, contactId, memberRole): return "/_add #\(groupId) \(contactId) \(memberRole)" case let .apiJoinGroup(groupId): return "/_join #\(groupId)" @@ -369,8 +369,8 @@ public enum ChatCommand { case .apiRegisterToken: return "apiRegisterToken" case .apiVerifyToken: return "apiVerifyToken" case .apiDeleteToken: return "apiDeleteToken" - case .apiGetNtfMessage: return "apiGetNtfMessage" - case .apiGetConnNtfMessage: return "apiGetConnNtfMessage" + case .apiGetNtfConns: return "apiGetNtfConns" + case .apiGetConnNtfMessages: return "apiGetConnNtfMessages" case .apiNewGroup: return "apiNewGroup" case .apiAddMember: return "apiAddMember" case .apiJoinGroup: return "apiJoinGroup" @@ -682,8 +682,8 @@ public enum ChatResponse: Decodable, Error { case callInvitations(callInvitations: [RcvCallInvitation]) case ntfTokenStatus(status: NtfTknStatus) case ntfToken(token: DeviceToken, status: NtfTknStatus, ntfMode: NotificationsMode, ntfServer: String) - case ntfMessages(user_: User?, connEntity_: ConnectionEntity?, expectedMsg_: NtfMsgInfo?, receivedMsg_: NtfMsgInfo?) - case connNtfMessage(receivedMsg_: NtfMsgInfo?) + case ntfConns(ntfConns: [NtfConn]) + case connNtfMessages(receivedMsgs: [NtfMsgInfo?]) case ntfMessage(user: UserRef, connEntity: ConnectionEntity, ntfMessage: NtfMsgAckInfo) case contactConnectionDeleted(user: UserRef, connection: PendingContactConnection) case contactDisabled(user: UserRef, contact: Contact) @@ -851,8 +851,8 @@ public enum ChatResponse: Decodable, Error { case .callInvitations: return "callInvitations" case .ntfTokenStatus: return "ntfTokenStatus" case .ntfToken: return "ntfToken" - case .ntfMessages: return "ntfMessages" - case .connNtfMessage: return "connNtfMessage" + case .ntfConns: return "ntfConns" + case .connNtfMessages: return "connNtfMessages" case .ntfMessage: return "ntfMessage" case .contactConnectionDeleted: return "contactConnectionDeleted" case .contactDisabled: return "contactDisabled" @@ -1029,8 +1029,8 @@ public enum ChatResponse: Decodable, Error { case let .callInvitations(invs): return String(describing: invs) case let .ntfTokenStatus(status): return String(describing: status) case let .ntfToken(token, status, ntfMode, ntfServer): return "token: \(token)\nstatus: \(status.rawValue)\nntfMode: \(ntfMode.rawValue)\nntfServer: \(ntfServer)" - case let .ntfMessages(u, connEntity, expectedMsg_, receivedMsg_): return withUser(u, "connEntity: \(String(describing: connEntity))\nexpectedMsg_: \(String(describing: expectedMsg_))\nreceivedMsg_: \(String(describing: receivedMsg_))") - case let .connNtfMessage(receivedMsg_): return "receivedMsg_: \(String(describing: receivedMsg_))" + case let .ntfConns(ntfConns): return String(describing: ntfConns) + case let .connNtfMessages(receivedMsgs): return "receivedMsgs: \(String(describing: receivedMsgs))" case let .ntfMessage(u, connEntity, ntfMessage): return withUser(u, "connEntity: \(String(describing: connEntity))\nntfMessage: \(String(describing: ntfMessage))") case let .contactConnectionDeleted(u, connection): return withUser(u, String(describing: connection)) case let .contactDisabled(u, contact): return withUser(u, String(describing: contact)) diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 45dab17cf2..1bd5673f01 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -2270,16 +2270,13 @@ public enum ConnectionEntity: Decodable, Hashable { case let .userContactConnection(entityConnection, _): entityConnection } } +} + +public struct NtfConn: Decodable, Hashable { + public var user_: User? + public var connEntity_: ConnectionEntity? + public var expectedMsg_: NtfMsgInfo? - public var ntfsEnabled: Bool { - switch self { - case let .rcvDirectMsgConnection(_, contact): return contact?.chatSettings.enableNtfs == .all - case let .rcvGroupMsgConnection(_, groupInfo, _): return groupInfo.chatSettings.enableNtfs == .all - case .sndFileConnection: return false - case .rcvFileConnection: return false - case let .userContactConnection(_, userContact): return userContact.groupId == nil - } - } } public struct NtfMsgInfo: Decodable, Hashable { diff --git a/apps/ios/SimpleXChat/Notifications.swift b/apps/ios/SimpleXChat/Notifications.swift index 65bb06a7e8..a922e3a816 100644 --- a/apps/ios/SimpleXChat/Notifications.swift +++ b/apps/ios/SimpleXChat/Notifications.swift @@ -15,13 +15,14 @@ public let ntfCategoryContactConnected = "NTF_CAT_CONTACT_CONNECTED" public let ntfCategoryMessageReceived = "NTF_CAT_MESSAGE_RECEIVED" public let ntfCategoryCallInvitation = "NTF_CAT_CALL_INVITATION" public let ntfCategoryConnectionEvent = "NTF_CAT_CONNECTION_EVENT" +public let ntfCategoryManyEvents = "NTF_CAT_MANY_EVENTS" public let ntfCategoryCheckMessage = "NTF_CAT_CHECK_MESSAGE" public let appNotificationId = "chat.simplex.app.notification" let contactHidden = NSLocalizedString("Contact hidden:", comment: "notification") -public func createContactRequestNtf(_ user: any UserLike, _ contactRequest: UserContactRequest) -> UNMutableNotificationContent { +public func createContactRequestNtf(_ user: any UserLike, _ contactRequest: UserContactRequest, _ badgeCount: Int) -> UNMutableNotificationContent { let hideContent = ntfPreviewModeGroupDefault.get() == .hidden return createNotification( categoryIdentifier: ntfCategoryContactRequest, @@ -34,11 +35,12 @@ public func createContactRequestNtf(_ user: any UserLike, _ contactRequest: User hideContent ? NSLocalizedString("this contact", comment: "notification title") : contactRequest.chatViewName ), targetContentIdentifier: nil, - userInfo: ["chatId": contactRequest.id, "contactRequestId": contactRequest.apiId, "userId": user.userId] + userInfo: ["chatId": contactRequest.id, "contactRequestId": contactRequest.apiId, "userId": user.userId], + badgeCount: badgeCount ) } -public func createContactConnectedNtf(_ user: any UserLike, _ contact: Contact) -> UNMutableNotificationContent { +public func createContactConnectedNtf(_ user: any UserLike, _ contact: Contact, _ badgeCount: Int) -> UNMutableNotificationContent { let hideContent = ntfPreviewModeGroupDefault.get() == .hidden return createNotification( categoryIdentifier: ntfCategoryContactConnected, @@ -51,12 +53,13 @@ public func createContactConnectedNtf(_ user: any UserLike, _ contact: Contact) hideContent ? NSLocalizedString("this contact", comment: "notification title") : contact.chatViewName ), targetContentIdentifier: contact.id, - userInfo: ["userId": user.userId] + userInfo: ["userId": user.userId], // userInfo: ["chatId": contact.id, "contactId": contact.apiId] + badgeCount: badgeCount ) } -public func createMessageReceivedNtf(_ user: any UserLike, _ cInfo: ChatInfo, _ cItem: ChatItem) -> UNMutableNotificationContent { +public func createMessageReceivedNtf(_ user: any UserLike, _ cInfo: ChatInfo, _ cItem: ChatItem, _ badgeCount: Int) -> UNMutableNotificationContent { let previewMode = ntfPreviewModeGroupDefault.get() var title: String if case let .group(groupInfo) = cInfo, case let .groupRcv(groupMember) = cItem.chatDir { @@ -69,12 +72,13 @@ public func createMessageReceivedNtf(_ user: any UserLike, _ cInfo: ChatInfo, _ title: title, body: previewMode == .message ? hideSecrets(cItem) : NSLocalizedString("new message", comment: "notification"), targetContentIdentifier: cInfo.id, - userInfo: ["userId": user.userId] + userInfo: ["userId": user.userId], // userInfo: ["chatId": cInfo.id, "chatItemId": cItem.id] + badgeCount: badgeCount ) } -public func createCallInvitationNtf(_ invitation: RcvCallInvitation) -> UNMutableNotificationContent { +public func createCallInvitationNtf(_ invitation: RcvCallInvitation, _ badgeCount: Int) -> UNMutableNotificationContent { let text = invitation.callType.media == .video ? NSLocalizedString("Incoming video call", comment: "notification") : NSLocalizedString("Incoming audio call", comment: "notification") @@ -84,11 +88,12 @@ public func createCallInvitationNtf(_ invitation: RcvCallInvitation) -> UNMutabl title: hideContent ? contactHidden : "\(invitation.contact.chatViewName):", body: text, targetContentIdentifier: nil, - userInfo: ["chatId": invitation.contact.id, "userId": invitation.user.userId] + userInfo: ["chatId": invitation.contact.id, "userId": invitation.user.userId], + badgeCount: badgeCount ) } -public func createConnectionEventNtf(_ user: User, _ connEntity: ConnectionEntity) -> UNMutableNotificationContent { +public func createConnectionEventNtf(_ user: User, _ connEntity: ConnectionEntity, _ badgeCount: Int) -> UNMutableNotificationContent { let hideContent = ntfPreviewModeGroupDefault.get() == .hidden var title: String var body: String? = nil @@ -118,11 +123,12 @@ public func createConnectionEventNtf(_ user: User, _ connEntity: ConnectionEntit title: title, body: body, targetContentIdentifier: targetContentIdentifier, - userInfo: ["userId": user.userId] + userInfo: ["userId": user.userId], + badgeCount: badgeCount ) } -public func createErrorNtf(_ dbStatus: DBMigrationResult) -> UNMutableNotificationContent { +public func createErrorNtf(_ dbStatus: DBMigrationResult, _ badgeCount: Int) -> UNMutableNotificationContent { var title: String switch dbStatus { case .errorNotADatabase: @@ -142,14 +148,16 @@ public func createErrorNtf(_ dbStatus: DBMigrationResult) -> UNMutableNotificati } return createNotification( categoryIdentifier: ntfCategoryConnectionEvent, - title: title + title: title, + badgeCount: badgeCount ) } -public func createAppStoppedNtf() -> UNMutableNotificationContent { +public func createAppStoppedNtf(_ badgeCount: Int) -> UNMutableNotificationContent { return createNotification( categoryIdentifier: ntfCategoryConnectionEvent, - title: NSLocalizedString("Encrypted message: app is stopped", comment: "notification") + title: NSLocalizedString("Encrypted message: app is stopped", comment: "notification"), + badgeCount: badgeCount ) } @@ -159,8 +167,15 @@ private func groupMsgNtfTitle(_ groupInfo: GroupInfo, _ groupMember: GroupMember : "#\(groupInfo.displayName) \(groupMember.chatViewName):" } -public func createNotification(categoryIdentifier: String, title: String, subtitle: String? = nil, body: String? = nil, - targetContentIdentifier: String? = nil, userInfo: [AnyHashable : Any] = [:]) -> UNMutableNotificationContent { +public func createNotification( + categoryIdentifier: String, + title: String, + subtitle: String? = nil, + body: String? = nil, + targetContentIdentifier: String? = nil, + userInfo: [AnyHashable : Any] = [:], + badgeCount: Int +) -> UNMutableNotificationContent { let content = UNMutableNotificationContent() content.categoryIdentifier = categoryIdentifier content.title = title @@ -170,6 +185,7 @@ public func createNotification(categoryIdentifier: String, title: String, subtit content.userInfo = userInfo // TODO move logic of adding sound here, so it applies to background notifications too content.sound = .default + content.badge = badgeCount as NSNumber // content.interruptionLevel = .active // content.relevanceScore = 0.5 // 0-1 return content diff --git a/cabal.project b/cabal.project index 4fc8a72d45..e98f8122d0 100644 --- a/cabal.project +++ b/cabal.project @@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 967afaf802d7ea98480eaf280bfc6f35d4d43f05 + tag: a8471eed5be93e7c3741aa4742b24193c9a2d6f5 source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index d68731083f..8f53d078dc 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."967afaf802d7ea98480eaf280bfc6f35d4d43f05" = "0k8m07hxfgn8h8pqrfchqd8490fvv1jf8slw8qjp0vxdpxa84n3i"; + "https://github.com/simplex-chat/simplexmq.git"."a8471eed5be93e7c3741aa4742b24193c9a2d6f5" = "093i40api0dp7rvw6f1f3pww3q5iv6mvbj577nlxp3qqcbvyh6fs"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index c875f9afa3..885d4303c8 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -204,8 +204,8 @@ _defaultSMPServers = _defaultNtfServers :: [NtfServer] _defaultNtfServers = [ "ntf://FB-Uop7RTaZZEG0ZLD2CIaTjsPh-Fw0zFAnb7QyA8Ks=@ntf2.simplex.im,5ex3mupcazy3zlky64ab27phjhijpemsiby33qzq3pliejipbtx5xgad.onion" - -- "ntf://KmpZNNXiVZJx_G2T7jRUmDFxWXM3OAnunz3uLT0tqAA=@ntf3.simplex.im,pxculznuryunjdvtvh6s6szmanyadumpbmvevgdpe4wk5c65unyt4yid.onion", - -- "ntf://CJ5o7X6fCxj2FFYRU2KuCo70y4jSqz7td2HYhLnXWbU=@ntf4.simplex.im,wtvuhdj26jwprmomnyfu5wfuq2hjkzfcc72u44vi6gdhrwxldt6xauad.onion" + -- "ntf://KmpZNNXiVZJx_G2T7jRUmDFxWXM3OAnunz3uLT0tqAA=@ntf3.simplex.im,pxculznuryunjdvtvh6s6szmanyadumpbmvevgdpe4wk5c65unyt4yid.onion", + -- "ntf://CJ5o7X6fCxj2FFYRU2KuCo70y4jSqz7td2HYhLnXWbU=@ntf4.simplex.im,wtvuhdj26jwprmomnyfu5wfuq2hjkzfcc72u44vi6gdhrwxldt6xauad.onion" ] maxImageSize :: Integer @@ -1457,26 +1457,31 @@ processChatCommand' vr = \case CRNtfTokenStatus <$> withAgent (\a -> registerNtfToken a token mode) APIVerifyToken token nonce code -> withUser $ \_ -> withAgent (\a -> verifyNtfToken a token nonce code) >> ok_ APIDeleteToken token -> withUser $ \_ -> withAgent (`deleteNtfToken` token) >> ok_ - APIGetNtfMessage nonce encNtfInfo -> withUser $ \_ -> do - (NotificationInfo {ntfConnId, ntfMsgMeta = nMsgMeta}, msg) <- withAgent $ \a -> getNotificationMessage a nonce encNtfInfo - let agentConnId = AgentConnId ntfConnId - user_ <- withStore' (`getUserByAConnId` agentConnId) - connEntity_ <- - pure user_ $>>= \user -> - withStore (\db -> Just <$> getConnectionEntity db vr user agentConnId) `catchChatError` (\e -> toView (CRChatError (Just user) e) $> Nothing) - pure - CRNtfMessages - { user_, - connEntity_, - -- Decrypted ntf meta of the expected message (the one notification was sent for) - expectedMsg_ = expectedMsgInfo <$> nMsgMeta, - -- Info of the first message retrieved by agent using GET - -- (may differ from the expected message due to, for example, coalescing or loss of notifications) - receivedMsg_ = receivedMsgInfo <$> msg - } - ApiGetConnNtfMessage (AgentConnId connId) -> withUser $ \_ -> do - msg <- withAgent $ \a -> getConnectionMessage a connId - pure $ CRConnNtfMessage (receivedMsgInfo <$> msg) + APIGetNtfConns nonce encNtfInfo -> withUser $ \user -> do + ntfInfos <- withAgent $ \a -> getNotificationConns a nonce encNtfInfo + (errs, ntfMsgs) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (getMsgConn db) (L.toList ntfInfos)) + unless (null errs) $ toView $ CRChatErrors (Just user) errs + pure $ CRNtfConns ntfMsgs + where + getMsgConn :: DB.Connection -> NotificationInfo -> IO NtfConn + getMsgConn db NotificationInfo {ntfConnId, ntfMsgMeta = nMsgMeta} = do + let agentConnId = AgentConnId ntfConnId + user_ <- getUserByAConnId db agentConnId + connEntity_ <- + pure user_ $>>= \user -> + eitherToMaybe <$> runExceptT (getConnectionEntity db vr user agentConnId) + pure $ + NtfConn + { user_, + connEntity_, + -- Decrypted ntf meta of the expected message (the one notification was sent for) + expectedMsg_ = expectedMsgInfo <$> nMsgMeta + } + ApiGetConnNtfMessages connIds -> withUser $ \_ -> do + let acIds = L.map (\(AgentConnId acId) -> acId) connIds + msgs <- lift $ withAgent' $ \a -> getConnectionMessages a acIds + let ntfMsgs = L.map (\msg -> receivedMsgInfo <$> msg) msgs + pure $ CRConnNtfMessages ntfMsgs APIGetUserProtoServers userId (AProtocolType p) -> withUserId userId $ \user -> withServerProtocol p $ do cfg@ChatConfig {defaultServers} <- asks config servers <- withFastStore' (`getProtocolServers` user) @@ -1785,7 +1790,8 @@ processChatCommand' vr = \case Nothing -> joinNewConn chatV dm Just (RcvDirectMsgConnection conn@Connection {connId, connStatus, contactConnInitiated} Nothing) | connStatus == ConnNew && contactConnInitiated -> joinNewConn chatV dm -- own connection link - | connStatus == ConnPrepared -> do -- retrying join after error + | connStatus == ConnPrepared -> do + -- retrying join after error pcc <- withFastStore $ \db -> getPendingContactConnection db userId connId joinPreparedConn (aConnId conn) pcc dm Just ent -> throwChatError $ CECommandError $ "connection exists: " <> show (connEntityInfo ent) @@ -8061,8 +8067,8 @@ chatCommandP = "/_ntf register " *> (APIRegisterToken <$> strP_ <*> strP), "/_ntf verify " *> (APIVerifyToken <$> strP <* A.space <*> strP <* A.space <*> strP), "/_ntf delete " *> (APIDeleteToken <$> strP), - "/_ntf message " *> (APIGetNtfMessage <$> strP <* A.space <*> strP), - "/_ntf conn message " *> (ApiGetConnNtfMessage <$> strP), + "/_ntf conns " *> (APIGetNtfConns <$> strP <* A.space <*> strP), + "/_ntf conn messages " *> (ApiGetConnNtfMessages <$> strP), "/_add #" *> (APIAddMember <$> A.decimal <* A.space <*> A.decimal <*> memberRole), "/_join #" *> (APIJoinGroup <$> A.decimal), "/_member role #" *> (APIMemberRole <$> A.decimal <* A.space <*> A.decimal <*> memberRole), diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 700dec9d2e..b39b4d7456 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -330,8 +330,8 @@ data ChatCommand | APIRegisterToken DeviceToken NotificationsMode | APIVerifyToken DeviceToken C.CbNonce ByteString | APIDeleteToken DeviceToken - | APIGetNtfMessage {nonce :: C.CbNonce, encNtfInfo :: ByteString} - | ApiGetConnNtfMessage {connId :: AgentConnId} + | APIGetNtfConns {nonce :: C.CbNonce, encNtfInfo :: ByteString} + | ApiGetConnNtfMessages {connIds :: NonEmpty AgentConnId} | APIAddMember GroupId ContactId GroupMemberRole | APIJoinGroup GroupId | APIMemberRole GroupId GroupMemberId GroupMemberRole @@ -745,8 +745,8 @@ data ChatResponse | CRUserContactLinkSubError {chatError :: ChatError} -- TODO delete | CRNtfTokenStatus {status :: NtfTknStatus} | CRNtfToken {token :: DeviceToken, status :: NtfTknStatus, ntfMode :: NotificationsMode, ntfServer :: NtfServer} - | CRNtfMessages {user_ :: Maybe User, connEntity_ :: Maybe ConnectionEntity, expectedMsg_ :: Maybe NtfMsgInfo, receivedMsg_ :: Maybe NtfMsgInfo} - | CRConnNtfMessage {receivedMsg_ :: Maybe NtfMsgInfo} + | CRNtfConns {ntfConns :: [NtfConn]} + | CRConnNtfMessages {receivedMsgs :: NonEmpty (Maybe NtfMsgInfo)} | CRNtfMessage {user :: User, connEntity :: ConnectionEntity, ntfMessage :: NtfMsgAckInfo} | CRContactConnectionDeleted {user :: User, connection :: PendingContactConnection} | CRRemoteHostList {remoteHosts :: [RemoteHostInfo]} @@ -1010,7 +1010,7 @@ defaultSimpleNetCfg = smpWebPort = False, tcpTimeout_ = Nothing, logTLSErrors = False - } + } data ContactSubStatus = ContactSubStatus { contact :: Contact, @@ -1063,6 +1063,13 @@ instance FromJSON ComposedMessage where parseJSON invalid = JT.prependFailure "bad ComposedMessage, " (JT.typeMismatch "Object" invalid) +data NtfConn = NtfConn + { user_ :: Maybe User, + connEntity_ :: Maybe ConnectionEntity, + expectedMsg_ :: Maybe NtfMsgInfo + } + deriving (Show) + data NtfMsgInfo = NtfMsgInfo {msgId :: Text, msgTs :: UTCTime} deriving (Show) @@ -1535,6 +1542,8 @@ $(JQ.deriveJSON defaultJSON ''UserProfileUpdateSummary) $(JQ.deriveJSON defaultJSON ''NtfMsgInfo) +$(JQ.deriveJSON defaultJSON ''NtfConn) + $(JQ.deriveJSON defaultJSON ''NtfMsgAckInfo) $(JQ.deriveJSON defaultJSON ''SwitchProgress) diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 5ecde0a99c..ade36476c7 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -325,8 +325,8 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe CRContactConnectionDeleted u PendingContactConnection {pccConnId} -> ttyUser u ["connection :" <> sShow pccConnId <> " deleted"] CRNtfTokenStatus status -> ["device token status: " <> plain (smpEncode status)] CRNtfToken _ status mode srv -> ["device token status: " <> plain (smpEncode status) <> ", notifications mode: " <> plain (strEncode mode) <> ", server: " <> sShow srv] - CRNtfMessages {} -> [] - CRConnNtfMessage {} -> [] + CRNtfConns {} -> [] + CRConnNtfMessages {} -> [] CRNtfMessage {} -> [] CRCurrentRemoteHost rhi_ -> [ maybe From 37b78edb91f60a9f78d727c5e78a49c9a4885f41 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Mon, 28 Oct 2024 18:18:26 +0400 Subject: [PATCH 002/167] ios: move Network and servers settings modules to folder (#5110) --- .../AdvancedNetworkSettings.swift | 0 .../NetworkAndServers.swift | 0 .../ProtocolServerView.swift | 0 .../ProtocolServersView.swift | 0 .../ScanProtocolServer.swift | 0 apps/ios/SimpleX.xcodeproj/project.pbxproj | 18 +++++++++++++----- 6 files changed, 13 insertions(+), 5 deletions(-) rename apps/ios/Shared/Views/UserSettings/{ => NetworkAndServers}/AdvancedNetworkSettings.swift (100%) rename apps/ios/Shared/Views/UserSettings/{ => NetworkAndServers}/NetworkAndServers.swift (100%) rename apps/ios/Shared/Views/UserSettings/{ => NetworkAndServers}/ProtocolServerView.swift (100%) rename apps/ios/Shared/Views/UserSettings/{ => NetworkAndServers}/ProtocolServersView.swift (100%) rename apps/ios/Shared/Views/UserSettings/{ => NetworkAndServers}/ScanProtocolServer.swift (100%) diff --git a/apps/ios/Shared/Views/UserSettings/AdvancedNetworkSettings.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/AdvancedNetworkSettings.swift similarity index 100% rename from apps/ios/Shared/Views/UserSettings/AdvancedNetworkSettings.swift rename to apps/ios/Shared/Views/UserSettings/NetworkAndServers/AdvancedNetworkSettings.swift diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift similarity index 100% rename from apps/ios/Shared/Views/UserSettings/NetworkAndServers.swift rename to apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift diff --git a/apps/ios/Shared/Views/UserSettings/ProtocolServerView.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServerView.swift similarity index 100% rename from apps/ios/Shared/Views/UserSettings/ProtocolServerView.swift rename to apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServerView.swift diff --git a/apps/ios/Shared/Views/UserSettings/ProtocolServersView.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServersView.swift similarity index 100% rename from apps/ios/Shared/Views/UserSettings/ProtocolServersView.swift rename to apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServersView.swift diff --git a/apps/ios/Shared/Views/UserSettings/ScanProtocolServer.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ScanProtocolServer.swift similarity index 100% rename from apps/ios/Shared/Views/UserSettings/ScanProtocolServer.swift rename to apps/ios/Shared/Views/UserSettings/NetworkAndServers/ScanProtocolServer.swift diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 2b1160061c..7aaa439adb 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -912,10 +912,9 @@ 5CB924DF27A8678B00ACCCDD /* UserSettings */ = { isa = PBXGroup; children = ( + 643B3B4C2CCFD34B0083A2CF /* NetworkAndServers */, 5CB924D627A8563F00ACCCDD /* SettingsView.swift */, 5CB346E62868D76D001FD2EF /* NotificationsView.swift */, - 5C9C2DA6289957AE00CC63B1 /* AdvancedNetworkSettings.swift */, - 5C9C2DA82899DA6F00CC63B1 /* NetworkAndServers.swift */, 5CADE79929211BB900072E13 /* PreferencesView.swift */, 5C5DB70D289ABDD200730FFF /* AppearanceSettings.swift */, 5C05DF522840AA1D00C683F9 /* CallSettings.swift */, @@ -923,9 +922,6 @@ 5CC036DF29C488D500C0EF20 /* HiddenProfileView.swift */, 5C577F7C27C83AA10006112D /* MarkdownHelp.swift */, 5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */, - 5C93292E29239A170090FFF9 /* ProtocolServersView.swift */, - 5C93293029239BED0090FFF9 /* ProtocolServerView.swift */, - 5C9329402929248A0090FFF9 /* ScanProtocolServer.swift */, 5CB2084E28DA4B4800D024EC /* RTCServers.swift */, 64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */, 18415845648CA4F5A8BCA272 /* UserProfilesView.swift */, @@ -1056,6 +1052,18 @@ path = Database; sourceTree = ""; }; + 643B3B4C2CCFD34B0083A2CF /* NetworkAndServers */ = { + isa = PBXGroup; + children = ( + 5C9329402929248A0090FFF9 /* ScanProtocolServer.swift */, + 5C93293029239BED0090FFF9 /* ProtocolServerView.swift */, + 5C93292E29239A170090FFF9 /* ProtocolServersView.swift */, + 5C9C2DA6289957AE00CC63B1 /* AdvancedNetworkSettings.swift */, + 5C9C2DA82899DA6F00CC63B1 /* NetworkAndServers.swift */, + ); + path = NetworkAndServers; + sourceTree = ""; + }; 6440CA01288AEC770062C672 /* Group */ = { isa = PBXGroup; children = ( From 24090fe3500ed596c0d8610b91e59c8037a4a7d1 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Fri, 1 Nov 2024 00:11:26 +0700 Subject: [PATCH 003/167] android, desktop: update to Compose 1.7.0 (#5038) * docs: correction * android, desktop: update to Compose 1.7.0 - support image drag-and-drop from other applications right to a chat (with and without transparent pixels - will be png or jpg) * stable * workaround --------- Co-authored-by: Evgeny Poberezkin --- .../common/platform/Modifier.android.kt | 2 +- .../platform/PlatformTextField.android.kt | 11 ++- .../common/views/call/CallView.android.kt | 4 +- .../views/chatlist/ChatListView.android.kt | 7 +- .../helpers/WorkaroundFocusSearchLayout.kt | 41 +++++++++++ .../chat/simplex/common/platform/Modifier.kt | 2 +- .../chat/simplex/common/views/WelcomeView.kt | 1 + .../simplex/common/views/chat/ChatView.kt | 9 +-- .../simplex/common/views/chat/SendMsgView.kt | 7 +- .../views/helpers/DefaultBasicTextField.kt | 3 - .../helpers/ExposedDropDownSettingRow.kt | 4 +- .../common/views/helpers/TextEditor.kt | 1 + .../common/views/newchat/NewChatView.kt | 1 + .../simplex/common/platform/Images.desktop.kt | 32 +++++++++ .../common/platform/Modifier.desktop.kt | 72 ++++++++++++++++--- .../platform/PlatformTextField.desktop.kt | 4 +- .../chatlist/ChatListNavLinkView.desktop.kt | 18 ++--- .../common/views/helpers/Utils.desktop.kt | 4 +- apps/multiplatform/gradle.properties | 4 +- 19 files changed, 176 insertions(+), 51 deletions(-) create mode 100644 apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/WorkaroundFocusSearchLayout.kt diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Modifier.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Modifier.android.kt index 2ff2a3e021..2aa66bc69b 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Modifier.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Modifier.android.kt @@ -21,7 +21,7 @@ actual fun ProvideWindowInsets( actual fun Modifier.desktopOnExternalDrag( enabled: Boolean, onFiles: (List) -> Unit, - onImage: (Painter) -> Unit, + onImage: (File) -> Unit, onText: (String) -> Unit ): Modifier = this diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/PlatformTextField.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/PlatformTextField.android.kt index 0b17a3aadf..5365db6a4c 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/PlatformTextField.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/PlatformTextField.android.kt @@ -26,6 +26,7 @@ import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.core.graphics.drawable.DrawableCompat +import androidx.core.view.children import androidx.core.view.inputmethod.EditorInfoCompat import androidx.core.view.inputmethod.InputConnectionCompat import androidx.core.widget.doAfterTextChanged @@ -94,8 +95,8 @@ actual fun PlatformTextField( } val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl - AndroidView(modifier = Modifier, factory = { - val editText = @SuppressLint("AppCompatCustomView") object: EditText(it) { + AndroidView(modifier = Modifier, factory = { context -> + val editText = @SuppressLint("AppCompatCustomView") object: EditText(context) { override fun setOnReceiveContentListener( mimeTypes: Array?, listener: OnReceiveContentListener? @@ -148,8 +149,12 @@ actual fun PlatformTextField( } } editText.doAfterTextChanged { text -> if (composeState.value.preview is ComposePreview.VoicePreview && text.toString() != "") editText.setText("") } - editText + val workaround = WorkaroundFocusSearchLayout(context) + workaround.addView(editText) + workaround.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + workaround }) { + val it = it.children.first() as EditText it.setTextColor(textColor.toArgb()) it.setHintTextColor(hintColor.toArgb()) it.hint = placeholder diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt index 3fc5620222..f3a9be6132 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt @@ -22,7 +22,6 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* -import androidx.compose.material.ripple.rememberRipple import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.snapshots.SnapshotStateList @@ -411,6 +410,7 @@ private fun ControlButton(icon: Painter, iconText: StringResource, enabled: Bool @Composable private fun ControlButtonWrap(enabled: Boolean = true, action: () -> Unit, background: Color = controlButtonsBackground(), size: Dp, content: @Composable () -> Unit) { + val ripple = remember { ripple(bounded = false, radius = size / 2, color = background.lighter(0.1f)) } Box( Modifier .background(background, CircleShape) @@ -419,7 +419,7 @@ private fun ControlButtonWrap(enabled: Boolean = true, action: () -> Unit, backg onClick = action, role = Role.Button, interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(bounded = false, radius = size / 2, color = background.lighter(0.1f)), + indication = ripple, enabled = enabled ), contentAlignment = Alignment.Center diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.android.kt index e0fd81f7b6..4681a5a64d 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.android.kt @@ -6,7 +6,6 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.* -import androidx.compose.material.ripple.rememberRipple import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -40,8 +39,8 @@ actual fun ActiveCallInteractiveArea(call: Call) { val onClick = { platform.androidStartCallActivity(false) } Box(Modifier.offset(y = CALL_TOP_OFFSET).height(CALL_INTERACTIVE_AREA_HEIGHT)) { val source = remember { MutableInteractionSource() } - val indication = rememberRipple(bounded = true, 3000.dp) - Box(Modifier.height(CALL_TOP_GREEN_LINE_HEIGHT).clickable(onClick = onClick, indication = indication, interactionSource = source)) { + val ripple = remember { ripple(bounded = true, 3000.dp) } + Box(Modifier.height(CALL_TOP_GREEN_LINE_HEIGHT).clickable(onClick = onClick, indication = ripple, interactionSource = source)) { GreenLine(call) } Box( @@ -50,7 +49,7 @@ actual fun ActiveCallInteractiveArea(call: Call) { .size(CALL_BOTTOM_ICON_HEIGHT) .background(SimplexGreen, CircleShape) .clip(CircleShape) - .clickable(onClick = onClick, indication = indication, interactionSource = source) + .clickable(onClick = onClick, indication = ripple, interactionSource = source) .align(Alignment.BottomCenter), contentAlignment = Alignment.Center ) { diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/WorkaroundFocusSearchLayout.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/WorkaroundFocusSearchLayout.kt new file mode 100644 index 0000000000..d111b99385 --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/WorkaroundFocusSearchLayout.kt @@ -0,0 +1,41 @@ +package chat.simplex.common.views.helpers + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.widget.FrameLayout + +/** + * A workaround for the ANR issue on Compose 1.7.x. + * https://issuetracker.google.com/issues/369354336 + * Code from: + * https://issuetracker.google.com/issues/369354336#comment8 +*/ +class WorkaroundFocusSearchLayout : FrameLayout { + + constructor( + context: Context, + ) : super(context) + + constructor( + context: Context, + attrs: AttributeSet?, + ) : super(context, attrs) + + constructor( + context: Context, + attrs: AttributeSet?, + defStyleAttr: Int, + ) : super(context, attrs, defStyleAttr) + + constructor( + context: Context, + attrs: AttributeSet?, + defStyleAttr: Int, + defStyleRes: Int, + ) : super(context, attrs, defStyleAttr, defStyleRes) + + override fun focusSearch(focused: View?, direction: Int): View? { + return null + } +} \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Modifier.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Modifier.kt index 6683ea7d33..5281fcb1ad 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Modifier.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Modifier.kt @@ -27,7 +27,7 @@ expect fun ProvideWindowInsets( expect fun Modifier.desktopOnExternalDrag( enabled: Boolean = true, onFiles: (List) -> Unit = {}, - onImage: (Painter) -> Unit = {}, + onImage: (File) -> Unit = {}, onText: (String) -> Unit = {} ): Modifier diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt index f5dcc6b54a..0d5350dbe0 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt @@ -289,6 +289,7 @@ fun ProfileNameField(name: MutableState, placeholder: String = "", isVal enabled = true, isError = false, interactionSource = remember { MutableInteractionSource() }, + colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.Unspecified) ) } ) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index d89782148a..570f763e99 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -642,14 +642,7 @@ fun ChatLayout( .desktopOnExternalDrag( enabled = remember(attachmentDisabled.value, chatInfo.value?.userCanSend) { mutableStateOf(!attachmentDisabled.value && chatInfo.value?.userCanSend == true) }.value, onFiles = { paths -> composeState.onFilesAttached(paths.map { it.toURI() }) }, - onImage = { - // TODO: file is not saved anywhere?! - val tmpFile = File.createTempFile("image", ".bmp", tmpDir) - tmpFile.deleteOnExit() - chatModel.filesToDelete.add(tmpFile) - val uri = tmpFile.toURI() - CoroutineScope(Dispatchers.IO).launch { composeState.processPickedMedia(listOf(uri), null) } - }, + onImage = { file -> CoroutineScope(Dispatchers.IO).launch { composeState.processPickedMedia(listOf(file.toURI()), null) } }, onText = { // Need to parse HTML in order to correctly display the content //composeState.value = composeState.value.copy(message = composeState.value.message + it) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt index 162e753b18..76c4fc4a62 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt @@ -7,7 +7,6 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.* import androidx.compose.material.* -import androidx.compose.material.ripple.rememberRipple import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.* @@ -423,6 +422,7 @@ private fun SendMsgButton( onLongClick: (() -> Unit)? = null ) { val interactionSource = remember { MutableInteractionSource() } + val ripple = remember { ripple(bounded = false, radius = 24.dp) } Box( modifier = Modifier.requiredSize(36.dp) .combinedClickable( @@ -431,7 +431,7 @@ private fun SendMsgButton( enabled = enabled, role = Role.Button, interactionSource = interactionSource, - indication = rememberRipple(bounded = false, radius = 24.dp) + indication = ripple ) .onRightClick { onLongClick?.invoke() }, contentAlignment = Alignment.Center @@ -454,6 +454,7 @@ private fun SendMsgButton( @Composable private fun StartLiveMessageButton(enabled: Boolean, onClick: () -> Unit) { val interactionSource = remember { MutableInteractionSource() } + val ripple = remember { ripple(bounded = false, radius = 24.dp) } Box( modifier = Modifier.requiredSize(36.dp) .clickable( @@ -461,7 +462,7 @@ private fun StartLiveMessageButton(enabled: Boolean, onClick: () -> Unit) { enabled = enabled, role = Role.Button, interactionSource = interactionSource, - indication = rememberRipple(bounded = false, radius = 24.dp) + indication = ripple ), contentAlignment = Alignment.Center ) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultBasicTextField.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultBasicTextField.kt index a6f0d2c9b6..b0366cceb3 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultBasicTextField.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultBasicTextField.kt @@ -3,7 +3,6 @@ package chat.simplex.common.views.helpers import androidx.compose.foundation.background import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.defaultMinSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.shape.ZeroCornerSize import androidx.compose.foundation.text.* import androidx.compose.material.* @@ -22,13 +21,11 @@ import androidx.compose.ui.text.input.* import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.* import chat.simplex.common.views.database.PassphraseStrength -import chat.simplex.common.views.database.validKey import chat.simplex.res.MR import kotlinx.coroutines.delay import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch -@OptIn(ExperimentalComposeUiApi::class) @Composable fun DefaultBasicTextField( modifier: Modifier, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ExposedDropDownSettingRow.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ExposedDropDownSettingRow.kt index 8349841973..7ed91adbd9 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ExposedDropDownSettingRow.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ExposedDropDownSettingRow.kt @@ -6,7 +6,6 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.* -import androidx.compose.material.ripple.rememberRipple import dev.icerock.moko.resources.compose.painterResource import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -107,6 +106,7 @@ fun ExposedDropDownSettingWithIcon( expanded.value = !expanded.value && enabled.value } ) { + val ripple = remember { ripple(bounded = false, radius = boxSize / 2, color = background.lighter(0.1f)) } Box( Modifier .background(background, CircleShape) @@ -115,7 +115,7 @@ fun ExposedDropDownSettingWithIcon( onClick = {}, role = Role.Button, interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(bounded = false, radius = boxSize / 2, color = background.lighter(0.1f)), + indication = ripple, enabled = enabled.value ), contentAlignment = Alignment.Center diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/TextEditor.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/TextEditor.kt index 45accccc59..ab7e562697 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/TextEditor.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/TextEditor.kt @@ -87,6 +87,7 @@ fun TextEditor( enabled = true, isError = false, interactionSource = remember { MutableInteractionSource() }, + colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.Unspecified) ) } ) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt index 8acddc2aa6..5298e11e75 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt @@ -616,6 +616,7 @@ fun LinkTextView(link: String, share: Boolean) { enabled = false, isError = false, interactionSource = remember { MutableInteractionSource() }, + colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.Unspecified) ) }) } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Images.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Images.desktop.kt index e65adea70e..0f53adaf0b 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Images.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Images.desktop.kt @@ -206,3 +206,35 @@ fun BufferedImage.flip(vertically: Boolean, horizontally: Boolean): BufferedImag } return AffineTransformOp(tx, AffineTransformOp.TYPE_NEAREST_NEIGHBOR).filter(this, null) } + +fun BufferedImage.saveInTmpFile(): File? { + val formats = arrayOf("jpg", "png") + for (format in formats) { + val tmpFile = File.createTempFile("image", ".$format", tmpDir) + try { + // May fail on JPG, using PNG as an alternative + val success = ImageIO.write(this, format, tmpFile) + if (success) { + tmpFile.deleteOnExit() + chatModel.filesToDelete.add(tmpFile) + return tmpFile + } else { + tmpFile.delete() + } + } catch (e: Exception) { + Log.e(TAG, e.stackTraceToString()) + tmpFile.delete() + return null + } + } + return null +} + +fun BufferedImage.hasAlpha(): Boolean { + for (x in 0 until width) { + for (y in 0 until height) { + if (getRGB(x, y) == 0) return true + } + } + return false +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Modifier.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Modifier.desktop.kt index 97f8bc129a..150885cbc8 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Modifier.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Modifier.desktop.kt @@ -1,10 +1,17 @@ package chat.simplex.common.platform import androidx.compose.foundation.contextMenuOpenDetector +import androidx.compose.foundation.draganddrop.dragAndDropTarget import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.* -import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.draganddrop.* +import androidx.compose.ui.draganddrop.DragData import androidx.compose.ui.input.pointer.* +import java.awt.Image +import java.awt.datatransfer.DataFlavor +import java.awt.datatransfer.Transferable +import java.awt.image.BufferedImage import java.io.File import java.net.URI @@ -23,16 +30,61 @@ actual fun ProvideWindowInsets( actual fun Modifier.desktopOnExternalDrag( enabled: Boolean, onFiles: (List) -> Unit, - onImage: (Painter) -> Unit, + onImage: (File) -> Unit, onText: (String) -> Unit -): Modifier = -onExternalDrag(enabled) { - when(val data = it.dragData) { - // data.readFiles() returns filePath in URI format (where spaces replaces with %20). But it's an error-prone idea to work later - // with such format when everywhere we use absolutePath in File() format - is DragData.FilesList -> onFiles(data.readFiles().map { URI.create(it).toFile() }) - is DragData.Image -> onImage(data.readImage()) - is DragData.Text -> onText(data.readText()) +): Modifier { + val callback = remember { + object : DragAndDropTarget { + override fun onDrop(event: DragAndDropEvent): Boolean { + when (val data = event.dragData()) { + // data.readFiles() returns filePath in URI format (where spaces replaces with %20). But it's an error-prone idea to work later + // with such format when everywhere we use absolutePath in File() format + is DragData.FilesList -> { + val files = data.readFiles() + // When dragging and dropping an image from browser, it comes to FilesList section but no files inside + if (files.isNotEmpty()) { + onFiles(files.map { URI.create(it).toFile() }) + } else { + try { + val transferable = event.awtTransferable + if (transferable.isDataFlavorSupported(DataFlavor.imageFlavor)) { + onImage(DragDataImageImpl(transferable).bufferedImage().saveInTmpFile() ?: return false) + } else { + return false + } + } catch (e: Exception) { + Log.e(TAG, e.stackTraceToString()) + return false + } + } + } + is DragData.Image -> onImage(DragDataImageImpl(event.awtTransferable).bufferedImage().saveInTmpFile() ?: return false) + is DragData.Text -> onText(data.readText()) + } + return true + } + } + } + return dragAndDropTarget(shouldStartDragAndDrop = { true }, target = callback) +} + +// Copied from AwtDragData and modified +private class DragDataImageImpl(private val transferable: Transferable) { + fun bufferedImage(): BufferedImage = (transferable.getTransferData(DataFlavor.imageFlavor) as Image).bufferedImage() + private fun Image.bufferedImage(): BufferedImage { + if (this is BufferedImage && hasAlpha()) { + // Such image cannot be drawn as JPG, only PNG + return this + } + // Creating non-transparent image which can be drawn as JPG + val bufferedImage = BufferedImage(getWidth(null), getHeight(null), BufferedImage.TYPE_INT_RGB) + val g2 = bufferedImage.createGraphics() + try { + g2.drawImage(this, 0, 0, null) + } finally { + g2.dispose() + } + return bufferedImage } } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/PlatformTextField.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/PlatformTextField.desktop.kt index 5b0db7c94a..e37e99f3e9 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/PlatformTextField.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/PlatformTextField.desktop.kt @@ -10,6 +10,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.input.key.* import androidx.compose.ui.platform.* @@ -109,7 +110,7 @@ actual fun PlatformTextField( maxLines = 16, keyboardOptions = KeyboardOptions.Default.copy( capitalization = KeyboardCapitalization.Sentences, - autoCorrect = true + autoCorrectEnabled = true ), modifier = Modifier .padding(vertical = 4.dp) @@ -193,6 +194,7 @@ actual fun PlatformTextField( interactionSource = remember { MutableInteractionSource() }, contentPadding = PaddingValues(), visualTransformation = VisualTransformation.None, + colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.Unspecified) ) Spacer(Modifier.height(10.dp)) } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.desktop.kt index 189f1842dd..9789fa3d1a 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.desktop.kt @@ -10,20 +10,20 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.drawscope.ContentDrawScope +import androidx.compose.ui.node.DelegatableNode +import androidx.compose.ui.node.DrawModifierNode import androidx.compose.ui.unit.dp import chat.simplex.common.platform.onRightClick import chat.simplex.common.views.helpers.* -object NoIndication : Indication { - private object NoIndicationInstance : IndicationInstance { - override fun ContentDrawScope.drawIndication() { - drawContent() - } - } - @Composable - override fun rememberUpdatedInstance(interactionSource: InteractionSource): IndicationInstance { - return NoIndicationInstance +object NoIndication : IndicationNodeFactory { + // Should be as a class, not an object. Otherwise, crash + private class NoIndicationInstance : Modifier.Node(), DrawModifierNode { + override fun ContentDrawScope.draw() { drawContent() } } + override fun create(interactionSource: InteractionSource): DelegatableNode = NoIndicationInstance() + override fun hashCode(): Int = -1 + override fun equals(other: Any?) = other === this } @Composable diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/Utils.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/Utils.desktop.kt index 641ddb8744..d541a5780e 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/Utils.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/Utils.desktop.kt @@ -156,7 +156,9 @@ actual fun getFileSize(uri: URI): Long? = uri.toFile().length() actual fun getBitmapFromUri(uri: URI, withAlertOnException: Boolean): ImageBitmap? = try { - ImageIO.read(uri.inputStream()).toComposeImageBitmap() + uri.inputStream().use { + ImageIO.read(it).toComposeImageBitmap() + } } catch (e: Exception) { Log.e(TAG, "Error while decoding drawable: ${e.stackTraceToString()}") if (withAlertOnException) showImageDecodingException() diff --git a/apps/multiplatform/gradle.properties b/apps/multiplatform/gradle.properties index 32bd7579c8..d795257a76 100644 --- a/apps/multiplatform/gradle.properties +++ b/apps/multiplatform/gradle.properties @@ -21,8 +21,6 @@ kotlin.code.style=official # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library android.nonTransitiveRClass=true -# Automatically convert third-party libraries to use AndroidX -android.enableJetifier=true kotlin.mpp.androidSourceSetLayoutVersion=2 kotlin.jvm.target=11 @@ -34,4 +32,4 @@ desktop.version_code=74 kotlin.version=1.9.23 gradle.plugin.version=8.2.0 -compose.version=1.6.1 +compose.version=1.7.0 From 4162bccc468a011ec06be99aab8fee1753f75132 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Fri, 1 Nov 2024 00:26:17 +0700 Subject: [PATCH 004/167] multiplatform: edge to edge design (#5051) * multiplatform: insets * more features and better performance * calls and removed unused code * changes * removed logs * status and nav bar colors * chatList and newChatSheet search fields * overhaul * search fields, devtools, chatlist, newchatsheet, onehand on desktop, scrollbars * android, desktop: update to Compose 1.7.0 - support image drag-and-drop from other applications right to a chat (with and without transparent pixels - will be png or jpg) * stable * workaround * changes * ideal adapting height layout * dropdownmenu, userpicker, onehandui, call layout, columns * rename bars properties and strings * faster update and better layout * gallery in landscape with cutout * better cutout * 1% step on slider * app bar moves to bottom in one hand ui * default alpha * changes * userpicker colors * changes * blur * fix wrong drawing area in chatview * fix * fixed differently * changes * changes * android fix * Revert "android fix" This reverts commit 7d417afd9b011045b68921546c6218a0f97912aa. * changes * changes * blur * swap * no logs * fix build * old Android support * fix position of menu * disable blur on Android 12 * call button padding * useless code * fix padding in group info view * rename * rename * newline * one more fix * changes --------- Co-authored-by: Evgeny Poberezkin --- .../java/chat/simplex/app/MainActivity.kt | 14 +- .../main/java/chat/simplex/app/SimplexApp.kt | 92 +-- apps/multiplatform/common/build.gradle.kts | 1 - .../common/platform/Modifier.android.kt | 12 - .../platform/PlatformTextField.android.kt | 10 +- .../common/platform/Resources.android.kt | 10 +- .../platform/ScrollableColumn.android.kt | 159 ++++- .../simplex/common/platform/UI.android.kt | 30 +- .../common/views/call/CallView.android.kt | 19 +- .../views/chat/item/CIImageView.android.kt | 9 - .../views/chatlist/ChatListView.android.kt | 27 +- .../views/chatlist/UserPicker.android.kt | 133 ++-- .../views/helpers/GetImageView.android.kt | 2 + .../views/usersettings/Appearance.android.kt | 14 +- .../usersettings/SettingsView.android.kt | 3 +- .../kotlin/chat/simplex/common/App.kt | 119 ++-- .../chat/simplex/common/model/ChatModel.kt | 3 + .../chat/simplex/common/model/SimpleXAPI.kt | 9 +- .../chat/simplex/common/platform/Modifier.kt | 9 - .../chat/simplex/common/platform/Platform.kt | 5 +- .../common/platform/ScrollableColumn.kt | 34 + .../chat/simplex/common/ui/theme/Theme.kt | 36 +- .../simplex/common/ui/theme/ThemeManager.kt | 7 +- .../chat/simplex/common/views/TerminalView.kt | 147 ++--- .../chat/simplex/common/views/WelcomeView.kt | 95 ++- .../views/call/IncomingCallAlertView.kt | 2 +- .../simplex/common/views/chat/ChatInfoView.kt | 5 +- .../common/views/chat/ChatItemInfoView.kt | 8 +- .../simplex/common/views/chat/ChatView.kt | 419 ++++++------- .../simplex/common/views/chat/ComposeView.kt | 6 +- .../common/views/chat/ContactPreferences.kt | 5 +- .../simplex/common/views/chat/ScanCodeView.kt | 7 +- .../views/chat/SelectableChatItemToolbars.kt | 12 +- .../simplex/common/views/chat/SendMsgView.kt | 6 +- .../common/views/chat/VerifyCodeView.kt | 6 +- .../views/chat/group/AddGroupMembersView.kt | 5 +- .../views/chat/group/GroupChatInfoView.kt | 15 +- .../common/views/chat/group/GroupLinkView.kt | 4 +- .../views/chat/group/GroupMemberInfoView.kt | 5 +- .../views/chat/group/GroupPreferences.kt | 4 +- .../views/chat/group/GroupProfileView.kt | 8 +- .../views/chat/group/WelcomeMessageView.kt | 4 +- .../common/views/chat/item/FramedItemView.kt | 67 ++ .../views/chat/item/ImageFullScreenView.kt | 13 +- .../common/views/chatlist/ChatListView.kt | 393 +++++++----- .../views/chatlist/ServersSummaryView.kt | 20 +- .../common/views/chatlist/ShareListView.kt | 122 ++-- .../common/views/chatlist/UserPicker.kt | 22 +- .../common/views/database/ChatArchiveView.kt | 4 +- .../views/database/DatabaseEncryptionView.kt | 2 +- .../views/database/DatabaseErrorView.kt | 5 +- .../common/views/database/DatabaseView.kt | 4 +- .../common/views/helpers/AppBarTitle.kt | 71 +++ .../common/views/helpers/BlurModifier.kt | 139 +++++ .../common/views/helpers/ChatWallpaper.kt | 93 +-- .../views/helpers/ChooseAttachmentView.kt | 2 + .../common/views/helpers/CloseSheetBar.kt | 181 ------ .../common/views/helpers/CollapsingAppBar.kt | 56 +- .../views/helpers/DefaultDropdownMenu.kt | 3 +- .../common/views/helpers/DefaultTopAppBar.kt | 243 ++++++-- .../simplex/common/views/helpers/ModalView.kt | 60 +- .../common/views/helpers/SearchTextField.kt | 25 +- .../common/views/helpers/TextEditor.kt | 1 - .../common/views/helpers/ThemeModeEditor.kt | 10 +- .../views/migration/MigrateFromDevice.kt | 4 +- .../common/views/migration/MigrateToDevice.kt | 4 +- .../views/newchat/AddContactLearnMore.kt | 4 +- .../common/views/newchat/AddGroupView.kt | 16 +- .../newchat/ContactConnectionInfoView.kt | 6 +- .../common/views/newchat/NewChatSheet.kt | 583 ++++++++++-------- .../common/views/newchat/NewChatView.kt | 19 +- .../views/onboarding/CreateSimpleXAddress.kt | 8 +- .../common/views/onboarding/HowItWorks.kt | 6 +- .../views/onboarding/LinkAMobileView.kt | 55 +- .../views/onboarding/SetNotificationsMode.kt | 11 +- .../onboarding/SetupDatabasePassphrase.kt | 7 +- .../common/views/onboarding/SimpleXInfo.kt | 19 +- .../common/views/onboarding/WhatsNewView.kt | 3 +- .../common/views/remote/ConnectDesktopView.kt | 8 +- .../common/views/remote/ConnectMobileView.kt | 12 +- .../usersettings/AdvancedNetworkSettings.kt | 13 +- .../common/views/usersettings/Appearance.kt | 143 ++++- .../common/views/usersettings/CallSettings.kt | 2 +- .../views/usersettings/DeveloperView.kt | 10 +- .../common/views/usersettings/HelpView.kt | 6 +- .../views/usersettings/HiddenProfileView.kt | 5 +- .../views/usersettings/NetworkAndServers.kt | 11 +- .../usersettings/NotificationsSettingsView.kt | 12 +- .../common/views/usersettings/Preferences.kt | 4 +- .../views/usersettings/PrivacySettings.kt | 8 +- .../views/usersettings/ProtocolServerView.kt | 5 +- .../views/usersettings/ProtocolServersView.kt | 5 +- .../views/usersettings/ScanProtocolServer.kt | 6 +- .../usersettings/SetDeliveryReceiptsView.kt | 5 +- .../common/views/usersettings/SettingsView.kt | 20 +- .../usersettings/UserAddressLearnMore.kt | 6 +- .../views/usersettings/UserProfileView.kt | 6 +- .../views/usersettings/UserProfilesView.kt | 10 +- .../views/usersettings/VersionInfoView.kt | 4 +- .../commonMain/resources/MR/base/strings.xml | 3 + .../kotlin/chat/simplex/common/DesktopApp.kt | 15 +- .../common/platform/Modifier.desktop.kt | 11 - .../platform/PlatformTextField.desktop.kt | 40 +- .../platform/ScrollableColumn.desktop.kt | 210 ++++++- .../views/chatlist/ChatListView.desktop.kt | 179 ++++-- .../views/chatlist/UserPicker.desktop.kt | 10 +- .../views/usersettings/Appearance.desktop.kt | 13 +- .../usersettings/SettingsView.desktop.kt | 3 +- 108 files changed, 2651 insertions(+), 1935 deletions(-) create mode 100644 apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AppBarTitle.kt create mode 100644 apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/BlurModifier.kt delete mode 100644 apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CloseSheetBar.kt diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt index f29c0c3387..2d2829f1f2 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt @@ -4,8 +4,10 @@ import android.content.Context import android.content.Intent import android.net.Uri import android.os.* +import android.view.View import android.view.WindowManager import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge import androidx.compose.ui.platform.ClipboardManager import androidx.fragment.app.FragmentActivity import chat.simplex.app.model.NtfManager @@ -13,7 +15,6 @@ import chat.simplex.app.model.NtfManager.getUserIdFromIntent import chat.simplex.common.* import chat.simplex.common.helpers.* import chat.simplex.common.model.* -import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chatlist.* import chat.simplex.common.views.helpers.* @@ -24,13 +25,21 @@ import kotlinx.coroutines.* import java.lang.ref.WeakReference class MainActivity: FragmentActivity() { + companion object { + const val OLD_ANDROID_UI_FLAGS = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + } override fun onCreate(savedInstanceState: Bundle?) { mainActivity = WeakReference(this) platform.androidSetNightModeIfSupported() val c = CurrentColors.value.colors - platform.androidSetStatusAndNavBarColors(c.isLight, c.background, !appPrefs.oneHandUI.get(), appPrefs.oneHandUI.get()) + platform.androidSetStatusAndNavigationBarAppearance(c.isLight, c.isLight) applyAppLocale(ChatModel.controller.appPrefs.appLanguage) + // This flag makes status bar and navigation bar fully transparent. But on API level < 30 it breaks insets entirely + // https://issuetracker.google.com/issues/236862874 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + window.setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS) + } super.onCreate(savedInstanceState) // testJson() // When call ended and orientation changes, it re-process old intent, it's unneeded. @@ -47,6 +56,7 @@ class MainActivity: FragmentActivity() { WindowManager.LayoutParams.FLAG_SECURE ) } + enableEdgeToEdge() setContent { AppScreen() } diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt index 40e8ffa9bc..13f9b888b9 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt @@ -7,6 +7,7 @@ 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.animation.core.* import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect @@ -16,6 +17,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.core.view.ViewCompat import androidx.lifecycle.* import androidx.work.* +import chat.simplex.app.MainActivity.Companion.OLD_ANDROID_UI_FLAGS import chat.simplex.app.model.NtfManager import chat.simplex.app.model.NtfManager.AcceptCallAction import chat.simplex.app.views.call.CallActivity @@ -26,7 +28,6 @@ import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.call.* -import chat.simplex.common.views.chatlist.statusBarColorAfterCall import chat.simplex.common.views.helpers.* import chat.simplex.common.views.onboarding.OnboardingStage import com.jakewharton.processphoenix.ProcessPhoenix @@ -274,79 +275,32 @@ class SimplexApp: Application(), LifecycleEventObserver { uiModeManager.setApplicationNightMode(mode) } - override fun androidSetDrawerStatusAndNavBarColor( - isLight: Boolean, - drawerShadingColor: Color, - toolbarOnTop: Boolean, - navBarColor: Color, - ) { - val window = mainActivity.get()?.window ?: return - - @Suppress("DEPRECATION") - val windowInsetController = ViewCompat.getWindowInsetsController(window.decorView) - // Blend status bar color to the animated color - val colors = CurrentColors.value.colors - val baseBackgroundColor = if (toolbarOnTop) colors.background.mixWith(colors.onBackground, 0.97f) else colors.background - var statusBar = baseBackgroundColor.mixWith(drawerShadingColor.copy(1f), 1 - drawerShadingColor.alpha).toArgb() - var statusBarLight = isLight - - // SimplexGreen while in call - if (window.statusBarColor == SimplexGreen.toArgb()) { - statusBarColorAfterCall.intValue = statusBar - statusBar = SimplexGreen.toArgb() - statusBarLight = false - } - window.statusBarColor = statusBar - val navBar = navBarColor.toArgb() - if (windowInsetController?.isAppearanceLightStatusBars != statusBarLight) { - windowInsetController?.isAppearanceLightStatusBars = statusBarLight - } - if (window.navigationBarColor != navBar) { - window.navigationBarColor = navBar - } - if (windowInsetController?.isAppearanceLightNavigationBars != isLight) { - windowInsetController?.isAppearanceLightNavigationBars = isLight - } - } - - override fun androidSetStatusAndNavBarColors(isLight: Boolean, backgroundColor: Color, hasTop: Boolean, hasBottom: Boolean) { + override fun androidSetStatusAndNavigationBarAppearance(isLightStatusBar: Boolean, isLightNavBar: Boolean, blackNavBar: Boolean, themeBackgroundColor: Color) { val window = mainActivity.get()?.window ?: return @Suppress("DEPRECATION") + val statusLight = isLightStatusBar && chatModel.activeCall.value == null + val navBarLight = isLightNavBar || windowOrientation() == WindowOrientation.LANDSCAPE val windowInsetController = ViewCompat.getWindowInsetsController(window.decorView) - - var statusBar = (if (hasTop && appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete) { - backgroundColor.mixWith(CurrentColors.value.colors.onBackground, 0.97f) - } else { - if (CurrentColors.value.base == DefaultTheme.SIMPLEX) { - backgroundColor.lighter(0.4f) + if (windowInsetController?.isAppearanceLightStatusBars != statusLight) { + windowInsetController?.isAppearanceLightStatusBars = statusLight + } + window.navigationBarColor = Color.Transparent.toArgb() + if (windowInsetController?.isAppearanceLightNavigationBars != navBarLight) { + windowInsetController?.isAppearanceLightNavigationBars = navBarLight + } + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + window.decorView.systemUiVisibility = if (statusLight && navBarLight) { + View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR or OLD_ANDROID_UI_FLAGS + } else if (statusLight) { + View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR or OLD_ANDROID_UI_FLAGS + } else if (navBarLight) { + View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR or OLD_ANDROID_UI_FLAGS } else { - backgroundColor + OLD_ANDROID_UI_FLAGS } - }).toArgb() - var statusBarLight = isLight - - // SimplexGreen while in call - if (window.statusBarColor == SimplexGreen.toArgb()) { - statusBarColorAfterCall.intValue = statusBar - statusBar = SimplexGreen.toArgb() - statusBarLight = false - } - val navBar = (if (hasBottom && appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete) { - backgroundColor.mixWith(CurrentColors.value.colors.onBackground, 0.97f) + window.navigationBarColor = if (blackNavBar) Color.Black.toArgb() else themeBackgroundColor.toArgb() } else { - backgroundColor - }).toArgb() - if (window.statusBarColor != statusBar) { - window.statusBarColor = statusBar - } - if (windowInsetController?.isAppearanceLightStatusBars != statusBarLight) { - windowInsetController?.isAppearanceLightStatusBars = statusBarLight - } - if (window.navigationBarColor != navBar) { - window.navigationBarColor = navBar - } - if (windowInsetController?.isAppearanceLightNavigationBars != isLight) { - windowInsetController?.isAppearanceLightNavigationBars = isLight + window.navigationBarColor = Color.Transparent.toArgb() } } @@ -401,6 +355,8 @@ class SimplexApp: Application(), LifecycleEventObserver { } return true } + + override val androidApiLevel: Int get() = Build.VERSION.SDK_INT } } } diff --git a/apps/multiplatform/common/build.gradle.kts b/apps/multiplatform/common/build.gradle.kts index 1aaa061daa..0e45c66efd 100644 --- a/apps/multiplatform/common/build.gradle.kts +++ b/apps/multiplatform/common/build.gradle.kts @@ -64,7 +64,6 @@ kotlin { 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") // Video support implementation("com.google.android.exoplayer:exoplayer:2.19.1") diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Modifier.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Modifier.android.kt index 2aa66bc69b..5d07aae088 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Modifier.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Modifier.android.kt @@ -3,20 +3,8 @@ package chat.simplex.common.platform import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.painter.Painter -import com.google.accompanist.insets.navigationBarsWithImePadding import java.io.File -actual fun Modifier.navigationBarsWithImePadding(): Modifier = navigationBarsWithImePadding() - -@Composable -actual fun ProvideWindowInsets( - consumeWindowInsets: Boolean, - windowInsetsAnimationsEnabled: Boolean, - content: @Composable () -> Unit -) { - com.google.accompanist.insets.ProvideWindowInsets(content = content) -} - @Composable actual fun Modifier.desktopOnExternalDrag( enabled: Boolean, diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/PlatformTextField.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/PlatformTextField.android.kt index 5365db6a4c..7b820aa67e 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/PlatformTextField.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/PlatformTextField.android.kt @@ -6,8 +6,7 @@ import android.graphics.drawable.ColorDrawable import android.os.Build import android.text.InputType import android.util.Log -import android.view.OnReceiveContentListener -import android.view.ViewGroup +import android.view.* import android.view.inputmethod.* import android.widget.EditText import android.widget.TextView @@ -141,6 +140,13 @@ actual fun PlatformTextField( Log.e(TAG, e.stackTraceToString()) } } + editText.onFocusChangeListener = View.OnFocusChangeListener { _, hasFocus -> + // shows keyboard when user had search field on ChatView focused before clicking on this text field + // it still produce weird animation of closing/opening keyboard but the solution is to replace this Android EditText with Compose BasicTextField + if (hasFocus) { + showKeyboard = true + } + } editText.doOnTextChanged { text, _, _, _ -> if (!composeState.value.inProgress) { onMessageChange(text.toString()) diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Resources.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Resources.android.kt index 73c920b940..d4b77274ba 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Resources.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Resources.android.kt @@ -6,11 +6,11 @@ import android.content.Context import android.content.SharedPreferences import android.content.res.Configuration import android.text.BidiFormatter +import androidx.compose.foundation.layout.* import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.asImageBitmap -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.* import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight @@ -50,7 +50,11 @@ actual fun windowOrientation(): WindowOrientation = when (mainActivity.get()?.re } @Composable -actual fun windowWidth(): Dp = LocalConfiguration.current.screenWidthDp.dp +actual fun windowWidth(): Dp { + val direction = LocalLayoutDirection.current + val cutout = WindowInsets.displayCutout.only(WindowInsetsSides.Horizontal).asPaddingValues() + return LocalConfiguration.current.screenWidthDp.dp - cutout.calculateStartPadding(direction) - cutout.calculateEndPadding(direction) +} @Composable actual fun windowHeight(): Dp = LocalConfiguration.current.screenHeightDp.dp diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/ScrollableColumn.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/ScrollableColumn.android.kt index 6851970b81..d70177ffb9 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/ScrollableColumn.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/ScrollableColumn.android.kt @@ -7,10 +7,12 @@ import androidx.compose.foundation.lazy.* 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 androidx.compose.ui.unit.dp +import chat.simplex.common.model.ChatController.appPrefs +import chat.simplex.common.ui.theme.DEFAULT_PADDING +import chat.simplex.common.views.chatlist.NavigationBarBackground import chat.simplex.common.views.helpers.* import kotlinx.coroutines.flow.filter import kotlin.math.absoluteValue @@ -25,25 +27,74 @@ actual fun LazyColumnWithScrollBar( horizontalAlignment: Alignment.Horizontal, flingBehavior: FlingBehavior, userScrollEnabled: Boolean, + additionalBarOffset: State?, + fillMaxSize: Boolean, content: LazyListScope.() -> Unit ) { - val state = state ?: LocalAppBarHandler.current?.listState ?: rememberLazyListState() - val connection = LocalAppBarHandler.current?.connection + val handler = LocalAppBarHandler.current + require(handler != null) { "Using LazyColumnWithScrollBar and without AppBarHandler is an error. Use LazyColumnWithScrollBarNoAppBar instead" } + + val state = state ?: handler.listState + val connection = handler.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 (reverseLayout) { + snapshotFlow { state.layoutInfo.visibleItemsInfo.lastOrNull()?.offset ?: 0 } + .collect { scrollPosition -> + connection.appBarOffset = if (state.layoutInfo.visibleItemsInfo.lastOrNull()?.index == state.layoutInfo.totalItemsCount - 1) { + state.layoutInfo.viewportEndOffset - scrollPosition.toFloat() - state.layoutInfo.afterContentPadding + } else { + // show always when last item is not visible + -1000f + } + //Log.d(TAG, "Scrolling position changed from $offset to ${connection.appBarOffset}") } - } + } else { + snapshotFlow { state.firstVisibleItemScrollOffset } + .filter { state.firstVisibleItemIndex == 0 } + .collect { scrollPosition -> + val offset = connection.appBarOffset + if ((offset + scrollPosition + state.layoutInfo.afterContentPadding).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) + LazyColumn( + if (fillMaxSize) { + Modifier.fillMaxSize().copyViewToAppBar(remember { appPrefs.appearanceBarsBlurRadius.state }.value, LocalAppBarHandler.current?.graphicsLayer).then(modifier).nestedScroll(connection) + } else { + Modifier.copyViewToAppBar(remember { appPrefs.appearanceBarsBlurRadius.state }.value, LocalAppBarHandler.current?.graphicsLayer).then(modifier).nestedScroll(connection) + }, + state, + contentPadding, + reverseLayout, + verticalArrangement, + horizontalAlignment, + flingBehavior, + userScrollEnabled + ) { + content() + } +} + + +@Composable +actual fun LazyColumnWithScrollBarNoAppBar( + modifier: Modifier, + state: LazyListState?, + contentPadding: PaddingValues, + reverseLayout: Boolean, + verticalArrangement: Arrangement.Vertical, + horizontalAlignment: Alignment.Horizontal, + flingBehavior: FlingBehavior, + userScrollEnabled: Boolean, + additionalBarOffset: State?, + content: LazyListScope.() -> Unit +) { + val state = state ?: rememberLazyListState() + LazyColumn(modifier, state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled) { + content() } } @@ -54,32 +105,80 @@ actual fun ColumnWithScrollBar( horizontalAlignment: Alignment.Horizontal, state: ScrollState?, maxIntrinsicSize: Boolean, + fillMaxSize: Boolean, content: @Composable() (ColumnScope.() -> Unit) ) { - val state = state ?: LocalAppBarHandler.current?.scrollState ?: rememberScrollState() - val connection = LocalAppBarHandler.current?.connection + val handler = LocalAppBarHandler.current + require(handler != null) { "Using ColumnWithScrollBar and without AppBarHandler is an error. Use ColumnWithScrollBarNoAppBar instead" } + + val modifier = if (fillMaxSize) Modifier.fillMaxSize().then(modifier).imePadding() else modifier.imePadding() + val state = state ?: handler.scrollState + val connection = handler.connection LaunchedEffect(Unit) { snapshotFlow { state.value } .collect { scrollPosition -> - val offset = connection?.appBarOffset - if (offset != null && (offset + scrollPosition).absoluteValue > 1) { + val offset = connection.appBarOffset + if ((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) { + val oneHandUI = remember { appPrefs.oneHandUI.state } + Box(Modifier.fillMaxHeight()) { + Column( + if (maxIntrinsicSize) { + Modifier.copyViewToAppBar(remember { appPrefs.appearanceBarsBlurRadius.state }.value, LocalAppBarHandler.current?.graphicsLayer).then(modifier).nestedScroll(connection).verticalScroll(state).height(IntrinsicSize.Max) + } else { + Modifier.copyViewToAppBar(remember { appPrefs.appearanceBarsBlurRadius.state }.value, LocalAppBarHandler.current?.graphicsLayer).then(modifier).nestedScroll(connection).verticalScroll(state) + }, verticalArrangement, horizontalAlignment + ) { + if (oneHandUI.value) { + Spacer(Modifier.padding(top = DEFAULT_PADDING + 5.dp).windowInsetsTopHeight(WindowInsets.statusBars)) + content() + Spacer(Modifier.navigationBarsPadding().padding(bottom = AppBarHeight * fontSizeSqrtMultiplier)) + } else { + Spacer(Modifier.statusBarsPadding().padding(top = AppBarHeight * fontSizeSqrtMultiplier)) + content() + Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.systemBars)) + } + } + if (!oneHandUI.value) { + NavigationBarBackground(false, false) + } + } +} + +@Composable +actual fun ColumnWithScrollBarNoAppBar( + modifier: Modifier, + verticalArrangement: Arrangement.Vertical, + horizontalAlignment: Alignment.Horizontal, + state: ScrollState?, + maxIntrinsicSize: Boolean, + content: @Composable() (ColumnScope.() -> Unit) +) { + val modifier = modifier.imePadding() + val state = state ?: rememberScrollState() + val oneHandUI = remember { appPrefs.oneHandUI.state } + Box(Modifier.fillMaxHeight()) { + Column( + if (maxIntrinsicSize) { modifier.verticalScroll(state).height(IntrinsicSize.Max) } else { modifier.verticalScroll(state) - }, verticalArrangement, horizontalAlignment, content) + }, verticalArrangement, horizontalAlignment + ) { + if (oneHandUI.value) { + Spacer(Modifier.windowInsetsTopHeight(WindowInsets.systemBars)) + content() + } else { + content() + Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.systemBars)) + } + } + if (!oneHandUI.value) { + NavigationBarBackground(false, false) + } } } diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/UI.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/UI.android.kt index c90946c95b..7ab6bf525f 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/UI.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/UI.android.kt @@ -3,17 +3,18 @@ package chat.simplex.common.platform import android.app.Activity import android.content.Context import android.content.pm.ActivityInfo -import android.graphics.Rect import android.os.* import android.view.* import android.view.inputmethod.InputMethodManager import android.widget.Toast import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.ime import androidx.compose.runtime.* +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalView import chat.simplex.common.AppScreen import chat.simplex.common.model.clear -import chat.simplex.common.ui.theme.SimpleXTheme import chat.simplex.common.views.helpers.* import androidx.compose.ui.platform.LocalContext as LocalContext1 import chat.simplex.res.MR @@ -43,28 +44,13 @@ actual fun LocalMultiplatformView(): Any? = LocalView.current @Composable actual fun getKeyboardState(): State { - val keyboardState = remember { mutableStateOf(KeyboardState.Closed) } - val view = LocalView.current - DisposableEffect(view) { - val onGlobalListener = ViewTreeObserver.OnGlobalLayoutListener { - val rect = Rect() - view.getWindowVisibleDisplayFrame(rect) - val screenHeight = view.rootView.height - val keypadHeight = screenHeight - rect.bottom - keyboardState.value = if (keypadHeight > screenHeight * 0.15) { - KeyboardState.Opened - } else { - KeyboardState.Closed - } - } - view.viewTreeObserver.addOnGlobalLayoutListener(onGlobalListener) - - onDispose { - view.viewTreeObserver.removeOnGlobalLayoutListener(onGlobalListener) + val density = LocalDensity.current + val ime = WindowInsets.ime + return remember { + derivedStateOf { + if (ime.getBottom(density) == 0) KeyboardState.Closed else KeyboardState.Opened } } - - return keyboardState } actual fun hideKeyboard(view: Any?, clearFocus: Boolean) { diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt index f3a9be6132..601b907902 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt @@ -47,6 +47,7 @@ import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.onboarding.OnboardingStage import chat.simplex.res.MR import com.google.accompanist.permissions.* import dev.icerock.moko.resources.StringResource @@ -328,11 +329,14 @@ private fun ActiveCallOverlayLayout( flipCamera: () -> Unit ) { Column { - CloseSheetBar({ chatModel.activeCallViewIsCollapsed.value = true }, true, tintColor = Color(0xFFFFFFD8)) { - if (call.hasVideo) { - Text(call.contact.chatViewName, Modifier.fillMaxWidth().padding(end = DEFAULT_PADDING), color = Color(0xFFFFFFD8), style = MaterialTheme.typography.h2, overflow = TextOverflow.Ellipsis, maxLines = 1) - } - } + CallAppBar( + title = { + if (call.hasVideo) { + Text(call.contact.chatViewName, Modifier.offset(x = (-4).dp).padding(end = DEFAULT_PADDING), color = Color(0xFFFFFFD8), style = MaterialTheme.typography.h2, overflow = TextOverflow.Ellipsis, maxLines = 1) + } + }, + onBack = { chatModel.activeCallViewIsCollapsed.value = true } + ) Column(Modifier.padding(horizontal = DEFAULT_PADDING)) { @Composable fun SelectSoundDevice(size: Dp) { @@ -590,8 +594,9 @@ fun CallPermissionsView(pipActive: Boolean, hasVideo: Boolean, cancel: () -> Uni } } } else { - ModalView(background = Color.Black, showClose = false, close = {}) { - ColumnWithScrollBar(Modifier.fillMaxSize()) { + ModalView(background = Color.Black, showAppBar = false, close = {}) { + Column { + Spacer(Modifier.height(AppBarHeight * fontSizeSqrtMultiplier)) AppBarTitle(stringResource(MR.strings.permissions_required)) Spacer(Modifier.weight(1f)) val onClick = { diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.android.kt index 05a9430ff1..ae5b8043ed 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.android.kt @@ -8,7 +8,6 @@ import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.platform.LocalContext import chat.simplex.common.model.CIFile -import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.CurrentColors import chat.simplex.common.views.helpers.ModalManager @@ -39,14 +38,6 @@ actual fun SimpleAndAnimatedImageView( if (getLoadedFilePath(file) != null) { ModalManager.fullscreen.showCustomModal(animated = false) { close -> ImageFullScreenView(imageProvider, close) - if (smallView) { - DisposableEffect(Unit) { - onDispose { - val c = CurrentColors.value.colors - platform.androidSetStatusAndNavBarColors(c.isLight, c.background, !appPrefs.oneHandUI.get(), appPrefs.oneHandUI.get()) - } - } - } } } } diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.android.kt index 4681a5a64d..7db39b7d3e 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.android.kt @@ -1,6 +1,5 @@ package chat.simplex.common.views.chatlist -import android.app.Activity import androidx.compose.foundation.* import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* @@ -11,21 +10,17 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.* import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp +import androidx.compose.ui.unit.* import chat.simplex.common.ANDROID_CALL_TOP_PADDING import chat.simplex.common.model.durationText import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.call.* -import chat.simplex.common.views.helpers.* import chat.simplex.res.MR import dev.icerock.moko.resources.compose.painterResource import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.datetime.Clock private val CALL_INTERACTIVE_AREA_HEIGHT = 74.dp @@ -37,11 +32,12 @@ private val CALL_BOTTOM_ICON_HEIGHT = CALL_INTERACTIVE_AREA_HEIGHT + CALL_BOTTOM @Composable actual fun ActiveCallInteractiveArea(call: Call) { val onClick = { platform.androidStartCallActivity(false) } - Box(Modifier.offset(y = CALL_TOP_OFFSET).height(CALL_INTERACTIVE_AREA_HEIGHT)) { + val statusBar = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + Box(Modifier.offset(y = CALL_TOP_OFFSET).height(CALL_INTERACTIVE_AREA_HEIGHT + statusBar)) { val source = remember { MutableInteractionSource() } val ripple = remember { ripple(bounded = true, 3000.dp) } - Box(Modifier.height(CALL_TOP_GREEN_LINE_HEIGHT).clickable(onClick = onClick, indication = ripple, interactionSource = source)) { - GreenLine(call) + Box(Modifier.height(CALL_TOP_GREEN_LINE_HEIGHT + statusBar).clickable(onClick = onClick, indication = ripple, interactionSource = source)) { + GreenLine(statusBar, call) } Box( Modifier @@ -62,16 +58,13 @@ actual fun ActiveCallInteractiveArea(call: Call) { } } -// Temporary solution for storing a color that needs to be applied after call ends -var statusBarColorAfterCall = mutableIntStateOf(CurrentColors.value.colors.background.toArgb()) - @Composable -private fun GreenLine(call: Call) { +private fun GreenLine(statusBarHeight: Dp, call: Call) { Row( Modifier .fillMaxSize() .background(SimplexGreen) - .padding(top = -CALL_TOP_OFFSET) + .padding(top = -CALL_TOP_OFFSET + statusBarHeight) .padding(horizontal = DEFAULT_PADDING), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center @@ -80,12 +73,10 @@ private fun GreenLine(call: Call) { Spacer(Modifier.weight(1f)) CallDuration(call) } - val window = (LocalContext.current as Activity).window DisposableEffect(Unit) { - statusBarColorAfterCall.intValue = window.statusBarColor - window.statusBarColor = SimplexGreen.toArgb() + platform.androidSetStatusAndNavigationBarAppearance(false, CurrentColors.value.colors.isLight) onDispose { - window.statusBarColor = statusBarColorAfterCall.intValue + platform.androidSetStatusAndNavigationBarAppearance(CurrentColors.value.colors.isLight, CurrentColors.value.colors.isLight) } } } diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.android.kt index b68756c669..54e3061d25 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.android.kt @@ -19,13 +19,11 @@ import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.* -import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.User import chat.simplex.common.model.UserInfo import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* -import chat.simplex.common.views.onboarding.OnboardingStage import kotlinx.coroutines.CancellationException import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow @@ -36,6 +34,7 @@ private val USER_PICKER_ROW_PADDING = 16.dp @Composable actual fun UserPickerUsersSection( users: List, + iconColor: Color, stopped: Boolean, onUserClicked: (user: User) -> Unit, ) { @@ -140,87 +139,73 @@ actual fun PlatformUserPicker(modifier: Modifier, pickerState: MutableStateFlow< } else { Modifier } - Box( - Modifier - .fillMaxSize() - .then(clickableModifier) - .drawBehind { - val pos = when { - dismissState.progress.from == DismissValue.Default && dismissState.progress.to == DismissValue.Default -> 1f - dismissState.progress.from == DismissValue.DismissedToEnd && dismissState.progress.to == DismissValue.DismissedToEnd -> 0f - dismissState.progress.to == DismissValue.Default -> dismissState.progress.fraction - else -> 1 - dismissState.progress.fraction - } - val colors = CurrentColors.value.colors - val resultingColor = if (colors.isLight) colors.onSurface.copy(alpha = ScrimOpacity) else Color.Black.copy(0.64f) - val adjustedAlpha = resultingColor.alpha * calculateFraction(pos = pos) - val shadingColor = resultingColor.copy(alpha = adjustedAlpha) - - if (pickerState.value.isVisible()) { - platform.androidSetDrawerStatusAndNavBarColor( - isLight = colors.isLight, - drawerShadingColor = shadingColor, - toolbarOnTop = !appPrefs.oneHandUI.get(), - navBarColor = colors.background.mixWith(colors.onBackground, 1 - userPickerAlpha()) - ) - } else if (ModalManager.start.modalCount.value == 0) { - platform.androidSetDrawerStatusAndNavBarColor( - isLight = colors.isLight, - drawerShadingColor = shadingColor, - toolbarOnTop = !appPrefs.oneHandUI.get(), - navBarColor = (if (appPrefs.oneHandUI.get() && appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete) { - colors.background.mixWith(CurrentColors.value.colors.onBackground, 0.97f) - } else { - colors.background - }) - ) - } - drawRect( - if (pos != 0f) resultingColor else Color.Transparent, - alpha = calculateFraction(pos = pos) - ) - } - .graphicsLayer { - if (heightValue == 0) { - alpha = 0f - } - translationY = dismissState.offset.value - }, - contentAlignment = Alignment.BottomCenter - ) { + Box { Box( - Modifier.onSizeChanged { height.intValue = it.height } - ) { - KeyChangeEffect(pickerIsVisible) { - if (pickerState.value.isVisible()) { - try { - dismissState.animateTo(DismissValue.Default, userPickerAnimSpec()) - } catch (e: CancellationException) { - Log.e(TAG, "Cancelled animateTo: ${e.stackTraceToString()}") - pickerState.value = AnimatedViewState.GONE + Modifier + .fillMaxSize() + .then(clickableModifier) + .drawBehind { + val pos = calculatePosition(dismissState) + val colors = CurrentColors.value.colors + val resultingColor = if (colors.isLight) colors.onSurface.copy(alpha = ScrimOpacity) else Color.Black.copy(0.64f) + drawRect( + if (pos != 0f) resultingColor else Color.Transparent, + alpha = calculateFraction(pos = pos) + ) + } + .graphicsLayer { + if (heightValue == 0) { + alpha = 0f } - } else { - try { - dismissState.animateTo(DismissValue.DismissedToEnd, userPickerAnimSpec()) - } catch (e: CancellationException) { - Log.e(TAG, "Cancelled animateTo2: ${e.stackTraceToString()}") - pickerState.value = AnimatedViewState.VISIBLE + translationY = dismissState.offset.value + }, + contentAlignment = Alignment.BottomCenter + ) { + Box( + Modifier.onSizeChanged { height.intValue = it.height } + ) { + KeyChangeEffect(pickerIsVisible) { + if (pickerState.value.isVisible()) { + try { + dismissState.animateTo(DismissValue.Default, userPickerAnimSpec()) + } catch (e: CancellationException) { + Log.e(TAG, "Cancelled animateTo: ${e.stackTraceToString()}") + pickerState.value = AnimatedViewState.GONE + } + } else { + try { + dismissState.animateTo(DismissValue.DismissedToEnd, userPickerAnimSpec()) + } catch (e: CancellationException) { + Log.e(TAG, "Cancelled animateTo2: ${e.stackTraceToString()}") + pickerState.value = AnimatedViewState.VISIBLE + } } } - } - val draggableModifier = if (height.intValue != 0) - Modifier.draggableBottomDrawerModifier( - state = dismissState, - swipeDistance = height.intValue.toFloat(), - ) - else Modifier - Box(draggableModifier.then(modifier)) { - content() + val draggableModifier = if (height.intValue != 0) + Modifier.draggableBottomDrawerModifier( + state = dismissState, + swipeDistance = height.intValue.toFloat(), + ) + else Modifier + Box(draggableModifier.then(modifier).navigationBarsPadding()) { + content() + } } } + NavigationBarBackground( + modifier = Modifier.graphicsLayer { alpha = if (calculatePosition(dismissState) > 0.1f) 1f else 0f }, + color = MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, alpha = 1 - userPickerAlpha()) + ) } } +private fun calculatePosition(dismissState: DismissState): Float = when { + dismissState.progress.from == DismissValue.Default && dismissState.progress.to == DismissValue.Default -> 1f + dismissState.progress.from == DismissValue.DismissedToEnd && dismissState.progress.to == DismissValue.DismissedToEnd -> 0f + dismissState.progress.to == DismissValue.Default -> dismissState.progress.fraction + else -> 1 - dismissState.progress.fraction +} + private fun Modifier.draggableBottomDrawerModifier( state: DismissState, swipeDistance: Float, diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/GetImageView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/GetImageView.android.kt index 98d1f8fb19..1c7ba1dcf0 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/GetImageView.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/GetImageView.android.kt @@ -171,6 +171,8 @@ actual fun GetImageBottomSheet( modifier = Modifier .fillMaxWidth() .wrapContentHeight() + .imePadding() + .navigationBarsPadding() .onFocusChanged { focusState -> if (!focusState.hasFocus) hideBottomSheet() } diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/Appearance.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/Appearance.android.kt index 418174a8e9..e5450e8e49 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/Appearance.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/Appearance.android.kt @@ -2,6 +2,7 @@ package chat.simplex.common.views.usersettings import SectionBottomSpacer import SectionDividerSpaced +import SectionSpacer import SectionView import android.app.Activity import android.content.ComponentName @@ -31,6 +32,7 @@ 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 @@ -75,9 +77,7 @@ fun AppearanceScope.AppearanceLayout( systemDarkTheme: SharedPreference, changeIcon: (AppIcon) -> Unit, ) { - ColumnWithScrollBar( - Modifier.fillMaxWidth(), - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.appearance_settings)) SectionView(stringResource(MR.strings.settings_section_title_interface), contentPadding = PaddingValues()) { val context = LocalContext.current @@ -106,15 +106,15 @@ fun AppearanceScope.AppearanceLayout( } // } - SettingsPreferenceItem(icon = null, stringResource(MR.strings.one_hand_ui), ChatModel.controller.appPrefs.oneHandUI) { - val c = CurrentColors.value.colors - platform.androidSetStatusAndNavBarColors(c.isLight, c.background, false, false) - } + SettingsPreferenceItem(icon = null, stringResource(MR.strings.one_hand_ui), ChatModel.controller.appPrefs.oneHandUI) } SectionDividerSpaced() ThemesSection(systemDarkTheme) + SectionDividerSpaced() + AppToolbarsSection() + SectionDividerSpaced() MessageShapeSection() diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.android.kt index 96b4a43e1a..04b59732dd 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.android.kt @@ -13,14 +13,13 @@ import dev.icerock.moko.resources.compose.stringResource @Composable actual fun SettingsSectionApp( showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), - showCustomModal: (@Composable ModalData.(ChatModel, () -> Unit) -> Unit) -> (() -> Unit), showVersion: () -> Unit, 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) 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) }) + SettingsActionItem(painterResource(MR.images.ic_code), stringResource(MR.strings.settings_developer_tools), showSettingsModal { DeveloperView(withAuth) }) AppVersionItem(showVersion) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt index b95aed45d2..ee38cb80fe 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt @@ -11,10 +11,13 @@ import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clipToBounds -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.draw.* +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.* +import androidx.compose.ui.graphics.drawscope.clipRect import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.dp import chat.simplex.common.views.usersettings.SetDeliveryReceiptsView import chat.simplex.common.model.* @@ -39,14 +42,39 @@ import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import kotlinx.coroutines.* import kotlinx.coroutines.flow.* +import kotlin.math.absoluteValue @Composable fun AppScreen() { AppBarHandler.appBarMaxHeightPx = with(LocalDensity.current) { AppBarHeight.roundToPx() } SimpleXTheme { - ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { - Surface(color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) { - MainScreen() + Surface(color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) { + // This padding applies to landscape view only taking care of navigation bar and holes in screen in status bar area + // (because nav bar and holes located on vertical sides of screen in landscape view) + val direction = LocalLayoutDirection.current + val safePadding = WindowInsets.safeDrawing.asPaddingValues() + val cutout = WindowInsets.displayCutout.asPaddingValues() + val cutoutStart = cutout.calculateStartPadding(direction) + val cutoutEnd = cutout.calculateEndPadding(direction) + val cutoutMax = maxOf(cutoutStart, cutoutEnd) + val paddingStartUntouched = safePadding.calculateStartPadding(direction) + val paddingStart = paddingStartUntouched - cutoutStart + val paddingEndUntouched = safePadding.calculateEndPadding(direction) + val paddingEnd = paddingEndUntouched - cutoutEnd + // Such a strange layout is needed because the main content should be covered by solid color in order to hide overflow + // of some elements that may have negative offset (so, can't use Row {}). + // To check: go to developer settings of Android, choose Display cutout -> Punch hole, and rotate the phone to landscape, open any chat + Box { + val fullscreenGallery = remember { chatModel.fullscreenGalleryVisible } + Box(Modifier.padding(start = paddingStart + cutoutMax, end = paddingEnd + cutoutMax).consumeWindowInsets(PaddingValues(start = paddingStartUntouched, end = paddingEndUntouched))) { + Box(Modifier.drawBehind { + if (fullscreenGallery.value) { + drawRect(Color.Black, topLeft = Offset(-(paddingStart + cutoutMax).toPx(), 0f), Size(size.width + (paddingStart + cutoutMax).toPx() + (paddingEnd + cutoutMax).toPx(), size.height)) + } + }) { + MainScreen() + } + } } } } @@ -138,7 +166,9 @@ fun MainScreen() { } SetupClipboardListener() if (appPlatform.isAndroid) { - AndroidScreen(userPickerState) + AndroidWrapInCallLayout { + AndroidScreen(userPickerState) + } } else { DesktopScreen(userPickerState) } @@ -170,7 +200,9 @@ fun MainScreen() { } } if (appPlatform.isAndroid) { - ModalManager.fullscreen.showInView() + AndroidWrapInCallLayout { + ModalManager.fullscreen.showInView() + } SwitchingUsersView() } @@ -237,19 +269,39 @@ fun MainScreen() { val ANDROID_CALL_TOP_PADDING = 40.dp +@Composable +fun AndroidWrapInCallLayout(content: @Composable () -> Unit) { + val call = remember { chatModel.activeCall}.value + val showCallArea = call != null && call.callState != CallState.WaitCapabilities && call.callState != CallState.InvitationAccepted + Box { + Box(Modifier.padding(top = if (showCallArea) ANDROID_CALL_TOP_PADDING else 0.dp)) { + content() + } + if (call != null && showCallArea) { + ActiveCallInteractiveArea(call) + } + } +} + @Composable fun AndroidScreen(userPickerState: MutableStateFlow) { BoxWithConstraints { - val call = remember { chatModel.activeCall} .value - val showCallArea = call != null && call.callState != CallState.WaitCapabilities && call.callState != CallState.InvitationAccepted val currentChatId = remember { mutableStateOf(chatModel.chatId.value) } val offset = remember { Animatable(if (chatModel.chatId.value == null) 0f else maxWidth.value) } + val cutout = WindowInsets.displayCutout.only(WindowInsetsSides.Horizontal).asPaddingValues() + val direction = LocalLayoutDirection.current + val hasCutout = cutout.calculateStartPadding(direction) + cutout.calculateEndPadding(direction) > 0.dp Box( Modifier + // clipping only for devices with cutout currently visible on sides. It prevents showing chat list with open chat view + // In order cases it's not needed to use clip + .then(if (hasCutout) Modifier.clip(RectangleShape) else Modifier) .graphicsLayer { - translationX = -offset.value.dp.toPx() + // minOf thing is needed for devices with holes in screen while the user on ChatView rotates his phone from portrait to landscape + // because in this case (at least in emulator) maxWidth changes in two steps: big first, smaller on next frame. + // But offset is remembered already, so this is a better way than dropping a value of offset + translationX = -minOf(offset.value.dp, maxWidth).toPx() } - .padding(top = if (showCallArea) ANDROID_CALL_TOP_PADDING else 0.dp) ) { StartPartOfScreen(userPickerState) } @@ -271,51 +323,40 @@ fun AndroidScreen(userPickerState: MutableStateFlow) { snapshotFlow { chatModel.chatId.value } .distinctUntilChanged() .collect { - if (it == null) { - platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, CurrentColors.value.colors.background, !appPrefs.oneHandUI.get(), appPrefs.oneHandUI.get()) - onComposed(null) - } + if (it == null) onComposed(null) currentChatId.value = it } } } - LaunchedEffect(Unit) { - snapshotFlow { ModalManager.center.modalCount.value > 0 } - .filter { chatModel.chatId.value == null } - .collect { modalBackground -> - if (chatModel.newChatSheetVisible.value) { - platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, CurrentColors.value.colors.background, false, appPrefs.oneHandUI.get()) - } else if (modalBackground) { - platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, CurrentColors.value.colors.background, false, false) - } else { - platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, CurrentColors.value.colors.background, !appPrefs.oneHandUI.get(), appPrefs.oneHandUI.get()) - } - } - } Box(Modifier - .graphicsLayer { translationX = maxWidth.toPx() - offset.value.dp.toPx() } - .padding(top = if (showCallArea) ANDROID_CALL_TOP_PADDING else 0.dp) + .then(if (hasCutout) Modifier.clip(RectangleShape) else Modifier) + .graphicsLayer { translationX = maxWidth.toPx() - minOf(offset.value.dp, maxWidth).toPx() } ) Box2@{ currentChatId.value?.let { ChatView(currentChatId, onComposed) } } - if (call != null && showCallArea) { - ActiveCallInteractiveArea(call) - } } } @Composable fun StartPartOfScreen(userPickerState: MutableStateFlow) { if (chatModel.setDeliveryReceipts.value) { - SetDeliveryReceiptsView(chatModel) + CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) { + SetDeliveryReceiptsView(chatModel) + } } else { val stopped = chatModel.chatRunning.value == false - if (chatModel.sharedContent.value == null) - ChatListView(chatModel, userPickerState, AppLock::setPerformLA, stopped) - else - ShareListView(chatModel, stopped) + if (chatModel.sharedContent.value == null) { + CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) { + ChatListView(chatModel, userPickerState, AppLock::setPerformLA, stopped) + } + } else { + // LALAL initial load of view doesn't show blur. Focusing text field shows it + CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler(keyboardCoversBar = false)) { + ShareListView(chatModel, stopped) + } + } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index 6bc565097f..422eb1e77f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -90,6 +90,9 @@ object ChatModel { // Needed to check for bottom nav bar and to apply or not navigation bar color on Android val newChatSheetVisible = mutableStateOf(false) + // Needed to apply black color to left/right cutout area on Android + val fullscreenGalleryVisible = mutableStateOf(false) + // preferences val notificationPreviewMode by lazy { mutableStateOf( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index 29d45a3b9b..fab85fa679 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -118,6 +118,9 @@ class AppPreferences { val privacyEncryptLocalFiles = mkBoolPreference(SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES, true) val privacyAskToApproveRelays = mkBoolPreference(SHARED_PREFS_PRIVACY_ASK_TO_APPROVE_RELAYS, true) val privacyMediaBlurRadius = mkIntPreference(SHARED_PREFS_PRIVACY_MEDIA_BLUR_RADIUS, 0) + // Blur broken on Android 12, see https://github.com/chrisbanes/haze/issues/77. And not available before 12 + val deviceSupportsBlur = appPlatform.isDesktop || (platform.androidApiLevel ?: 0) >= 32 + val appearanceBarsBlurRadius = mkIntPreference(SHARED_PREFS_APPEARANCE_BARS_BLUR_RADIUS, if (deviceSupportsBlur) 50 else 0) val experimentalCalls = mkBoolPreference(SHARED_PREFS_EXPERIMENTAL_CALLS, false) val showUnreadAndFavorites = mkBoolPreference(SHARED_PREFS_SHOW_UNREAD_AND_FAVORITES, false) val chatArchiveName = mkStrPreference(SHARED_PREFS_CHAT_ARCHIVE_NAME, null) @@ -223,6 +226,8 @@ class AppPreferences { val chatItemTail = mkBoolPreference(SHARED_PREFS_CHAT_ITEM_TAIL, true) val fontScale = mkFloatPreference(SHARED_PREFS_FONT_SCALE, 1f) val densityScale = mkFloatPreference(SHARED_PREFS_DENSITY_SCALE, 1f) + val inAppBarsDefaultAlpha = if (deviceSupportsBlur) 0.875f else 0.975f + val inAppBarsAlpha = mkFloatPreference(SHARED_PREFS_IN_APP_BARS_ALPHA, inAppBarsDefaultAlpha) val whatsNewVersion = mkStrPreference(SHARED_PREFS_WHATS_NEW_VERSION, null) val lastMigratedVersionCode = mkIntPreference(SHARED_PREFS_LAST_MIGRATED_VERSION_CODE, 0) @@ -244,7 +249,7 @@ class AppPreferences { val iosCallKitEnabled = mkBoolPreference(SHARED_PREFS_IOS_CALL_KIT_ENABLED, true) val iosCallKitCallsInRecents = mkBoolPreference(SHARED_PREFS_IOS_CALL_KIT_CALLS_IN_RECENTS, false) - val oneHandUI = mkBoolPreference(SHARED_PREFS_ONE_HAND_UI, appPlatform.isAndroid) + val oneHandUI = mkBoolPreference(SHARED_PREFS_ONE_HAND_UI, true) val hintPreferences: List, Boolean>> = listOf( laNoticeShown to false, @@ -362,6 +367,7 @@ class AppPreferences { private const val SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES = "PrivacyEncryptLocalFiles" private const val SHARED_PREFS_PRIVACY_ASK_TO_APPROVE_RELAYS = "PrivacyAskToApproveRelays" private const val SHARED_PREFS_PRIVACY_MEDIA_BLUR_RADIUS = "PrivacyMediaBlurRadius" + private const val SHARED_PREFS_APPEARANCE_BARS_BLUR_RADIUS = "AppearanceBarsBlurRadius" const val SHARED_PREFS_PRIVACY_FULL_BACKUP = "FullBackup" private const val SHARED_PREFS_EXPERIMENTAL_CALLS = "ExperimentalCalls" private const val SHARED_PREFS_SHOW_UNREAD_AND_FAVORITES = "ShowUnreadAndFavorites" @@ -428,6 +434,7 @@ class AppPreferences { private const val SHARED_PREFS_CHAT_ITEM_TAIL = "ChatItemTail" private const val SHARED_PREFS_FONT_SCALE = "FontScale" private const val SHARED_PREFS_DENSITY_SCALE = "DensityScale" + private const val SHARED_PREFS_IN_APP_BARS_ALPHA = "InAppBarsAlpha" private const val SHARED_PREFS_WHATS_NEW_VERSION = "WhatsNewVersion" private const val SHARED_PREFS_LAST_MIGRATED_VERSION_CODE = "LastMigratedVersionCode" private const val SHARED_PREFS_CUSTOM_DISAPPEARING_MESSAGE_TIME = "CustomDisappearingMessageTime" diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Modifier.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Modifier.kt index 5281fcb1ad..be7022ca80 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Modifier.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Modifier.kt @@ -14,15 +14,6 @@ import kotlinx.coroutines.* import kotlinx.coroutines.flow.filter import java.io.File -expect fun Modifier.navigationBarsWithImePadding(): Modifier - -@Composable -expect fun ProvideWindowInsets( - consumeWindowInsets: Boolean = true, - windowInsetsAnimationsEnabled: Boolean = true, - content: @Composable () -> Unit -) - @Composable expect fun Modifier.desktopOnExternalDrag( enabled: Boolean = true, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt index 5dfa5aa200..23ab450cb6 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt @@ -8,6 +8,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import chat.simplex.common.model.ChatId import chat.simplex.common.model.NotificationsMode +import chat.simplex.common.ui.theme.CurrentColors import kotlinx.coroutines.Job interface PlatformInterface { @@ -20,12 +21,12 @@ interface PlatformInterface { fun androidChatInitializedAndStarted() {} fun androidIsBackgroundCallAllowed(): Boolean = true fun androidSetNightModeIfSupported() {} - fun androidSetStatusAndNavBarColors(isLight: Boolean, backgroundColor: Color, hasTop: Boolean, hasBottom: Boolean) {} - fun androidSetDrawerStatusAndNavBarColor(isLight: Boolean, drawerShadingColor: Color, toolbarOnTop: Boolean, navBarColor: Color) {} + fun androidSetStatusAndNavigationBarAppearance(isLightStatusBar: Boolean, isLightNavBar: Boolean, blackNavBar: Boolean = false, themeBackgroundColor: Color = CurrentColors.value.colors.background) {} fun androidStartCallActivity(acceptCall: Boolean, remoteHostId: Long? = null, chatId: ChatId? = null) {} fun androidPictureInPictureAllowed(): Boolean = true fun androidCallEnded() {} fun androidRestartNetworkObserver() {} + val androidApiLevel: Int? get() = null @Composable fun androidLockPortraitOrientation() {} suspend fun androidAskToAllowBackgroundCalls(): Boolean = true @Composable fun desktopShowAppUpdateNotice() {} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/ScrollableColumn.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/ScrollableColumn.kt index 532bfddfcf..b0be547a31 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/ScrollableColumn.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/ScrollableColumn.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.lazy.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @Composable @@ -21,11 +22,44 @@ expect fun LazyColumnWithScrollBar( horizontalAlignment: Alignment.Horizontal = Alignment.Start, flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(), userScrollEnabled: Boolean = true, + additionalBarOffset: State? = null, + // by default, this function will include .fillMaxSize() without you doing anything. If you don't need it, pass `false` here + // maxSize (at least maxHeight) is needed for blur on appBars to work correctly + fillMaxSize: Boolean = true, + content: LazyListScope.() -> Unit +) + +@Composable +expect fun LazyColumnWithScrollBarNoAppBar( + modifier: Modifier = Modifier, + state: LazyListState? = null, + contentPadding: PaddingValues = PaddingValues(0.dp), + reverseLayout: Boolean = false, + verticalArrangement: Arrangement.Vertical = + if (!reverseLayout) Arrangement.Top else Arrangement.Bottom, + horizontalAlignment: Alignment.Horizontal = Alignment.Start, + flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(), + userScrollEnabled: Boolean = true, + additionalBarOffset: State? = null, content: LazyListScope.() -> Unit ) @Composable expect fun ColumnWithScrollBar( + modifier: Modifier = Modifier, + verticalArrangement: Arrangement.Vertical = Arrangement.Top, + horizontalAlignment: Alignment.Horizontal = Alignment.Start, + state: ScrollState? = null, + // set true when you want to show something in the center with respected .fillMaxSize() + maxIntrinsicSize: Boolean = false, + // by default, this function will include .fillMaxSize() without you doing anything. If you don't need it, pass `false` here + // maxSize (at least maxHeight) is needed for blur on appBars to work correctly + fillMaxSize: Boolean = true, + content: @Composable ColumnScope.() -> Unit +) + +@Composable +expect fun ColumnWithScrollBarNoAppBar( modifier: Modifier = Modifier, verticalArrangement: Arrangement.Vertical = Arrangement.Top, horizontalAlignment: Alignment.Horizontal = Alignment.Start, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt index 595c22e3e2..80542ced02 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt @@ -1,14 +1,14 @@ package chat.simplex.common.ui.theme -import androidx.compose.foundation.background import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.* +import androidx.compose.ui.graphics.layer.GraphicsLayer import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.unit.Density -import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.* import chat.simplex.common.model.ChatController import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.platform.* @@ -587,21 +587,27 @@ data class ThemeModeOverride ( } } -fun Modifier.themedBackground(baseTheme: DefaultTheme = CurrentColors.value.base, shape: Shape = RectangleShape): Modifier { - return if (baseTheme == DefaultTheme.SIMPLEX) { - this.background(brush = Brush.linearGradient( - listOf( - CurrentColors.value.colors.background.darker(0.4f), - CurrentColors.value.colors.background.lighter(0.4f) - ), - Offset(0f, Float.POSITIVE_INFINITY), - Offset(Float.POSITIVE_INFINITY, 0f) - ), shape = shape) - } else { - this.background(color = CurrentColors.value.colors.background, shape = shape) +fun Modifier.themedBackground(baseTheme: DefaultTheme = CurrentColors.value.base, bgLayerSize: MutableState?, bgLayer: GraphicsLayer?/*, shape: Shape = RectangleShape*/): Modifier { + return drawBehind { + copyBackgroundToAppBar(bgLayerSize, bgLayer) { + if (baseTheme == DefaultTheme.SIMPLEX) { + drawRect(brush = themedBackgroundBrush()) + } else { + drawRect(CurrentColors.value.colors.background) + } + } } } +fun themedBackgroundBrush(): Brush = Brush.linearGradient( + listOf( + CurrentColors.value.colors.background.darker(0.4f), + CurrentColors.value.colors.background.lighter(0.4f) + ), + Offset(0f, Float.POSITIVE_INFINITY), + Offset(Float.POSITIVE_INFINITY, 0f) +) + val DEFAULT_PADDING = 20.dp val DEFAULT_SPACE_AFTER_ICON = 4.dp val DEFAULT_PADDING_HALF = DEFAULT_PADDING / 2 diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt index 7f19f58949..a5293b6a24 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt @@ -1,7 +1,6 @@ package chat.simplex.common.ui.theme import androidx.compose.material.Colors -import androidx.compose.material.MaterialTheme import androidx.compose.runtime.MutableState import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb @@ -107,7 +106,7 @@ object ThemeManager { CurrentColors.value = currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) platform.androidSetNightModeIfSupported() val c = CurrentColors.value.colors - platform.androidSetStatusAndNavBarColors(c.isLight, c.background, !ChatController.appPrefs.oneHandUI.get(), ChatController.appPrefs.oneHandUI.get()) + platform.androidSetStatusAndNavigationBarAppearance(c.isLight, c.isLight) } fun changeDarkTheme(theme: String) { @@ -125,10 +124,6 @@ object ThemeManager { themeIds[nonSystemThemeName] = prevValue.themeId appPrefs.currentThemeIds.set(themeIds) CurrentColors.value = currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) - if (name == ThemeColor.BACKGROUND) { - val c = CurrentColors.value.colors - platform.androidSetStatusAndNavBarColors(c.isLight, c.background, false, false) - } } fun applyThemeColor(name: ThemeColor, color: Color? = null, pref: MutableState) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt index d89803f8e4..d4eb416081 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt @@ -7,40 +7,34 @@ import androidx.compose.foundation.lazy.* import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.* import androidx.compose.runtime.* +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.layoutId import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp +import androidx.compose.ui.unit.* import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chat.* import chat.simplex.common.views.helpers.* import chat.simplex.common.model.ChatModel import chat.simplex.common.platform.* -import kotlinx.coroutines.flow.collect +import chat.simplex.common.views.chat.item.CONSOLE_COMPOSE_LAYOUT_ID +import chat.simplex.common.views.chat.item.AdaptingBottomPaddingLayout +import chat.simplex.common.views.chatlist.NavigationBarBackground import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch @Composable -fun TerminalView(floating: Boolean = false, close: () -> Unit) { +fun TerminalView(floating: Boolean = false) { val composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = false)) } - val close = { - close() - if (appPlatform.isDesktop) { - ModalManager.center.closeModals() - } - } - BackHandler(onBack = { - close() - }) TerminalLayout( composeState, floating, sendCommand = { sendCommand(chatModel, composeState) }, - close ) } @@ -69,7 +63,6 @@ fun TerminalLayout( composeState: MutableState, floating: Boolean, sendCommand: () -> Unit, - close: () -> Unit ) { val smallFont = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground) val textStyle = remember { mutableStateOf(smallFont) } @@ -77,65 +70,63 @@ fun TerminalLayout( fun onMessageChange(s: String) { composeState.value = composeState.value.copy(message = s) } - - ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { - Scaffold( - topBar = { CloseSheetBar(close) }, - bottomBar = { - Column { - Divider() - Box(Modifier.padding(horizontal = 8.dp)) { - SendMsgView( - composeState = composeState, - showVoiceRecordIcon = false, - recState = remember { mutableStateOf(RecordingState.NotStarted) }, - isDirectChat = false, - liveMessageAlertShown = SharedPreference(get = { false }, set = {}), - sendMsgEnabled = true, - sendButtonEnabled = true, - nextSendGrpInv = false, - needToAllowVoiceToContact = false, - allowedVoiceByPrefs = false, - userIsObserver = false, - userCanSend = true, - allowVoiceToContact = {}, - placeholder = "", - sendMessage = { sendCommand() }, - sendLiveMessage = null, - updateLiveMessage = null, - editPrevMessage = {}, - onMessageChange = ::onMessageChange, - onFilesPasted = {}, - textStyle = textStyle - ) - } - } - }, - contentColor = LocalContentColor.current, - modifier = Modifier.navigationBarsWithImePadding() - ) { contentPadding -> - Surface( - modifier = Modifier - .padding(contentPadding) - .fillMaxWidth(), - color = MaterialTheme.colors.background, - contentColor = LocalContentColor.current + val oneHandUI = remember { appPrefs.oneHandUI.state } + Box(Modifier.fillMaxSize()) { + val composeViewHeight = remember { mutableStateOf(0.dp) } + AdaptingBottomPaddingLayout(Modifier, CONSOLE_COMPOSE_LAYOUT_ID, composeViewHeight) { + TerminalLog(floating, composeViewHeight) + Column( + Modifier + .layoutId(CONSOLE_COMPOSE_LAYOUT_ID) + .align(Alignment.BottomCenter) + .navigationBarsPadding() + .consumeWindowInsets(PaddingValues(bottom = if (oneHandUI.value) AppBarHeight * fontSizeSqrtMultiplier else 0.dp)) + .imePadding() + .padding(bottom = if (oneHandUI.value) AppBarHeight * fontSizeSqrtMultiplier else 0.dp) + .background(MaterialTheme.colors.background) ) { - TerminalLog(floating) + Divider() + Box(Modifier.padding(horizontal = 8.dp)) { + SendMsgView( + composeState = composeState, + showVoiceRecordIcon = false, + recState = remember { mutableStateOf(RecordingState.NotStarted) }, + isDirectChat = false, + liveMessageAlertShown = SharedPreference(get = { false }, set = {}), + sendMsgEnabled = true, + sendButtonEnabled = true, + nextSendGrpInv = false, + needToAllowVoiceToContact = false, + allowedVoiceByPrefs = false, + userIsObserver = false, + userCanSend = true, + allowVoiceToContact = {}, + placeholder = "", + sendMessage = { sendCommand() }, + sendLiveMessage = null, + updateLiveMessage = null, + editPrevMessage = {}, + onMessageChange = ::onMessageChange, + onFilesPasted = {}, + textStyle = textStyle + ) + } } } + if (!oneHandUI.value) { + NavigationBarBackground(true, oneHandUI.value) + } } } @Composable -fun TerminalLog(floating: Boolean) { +fun TerminalLog(floating: Boolean, composeViewHeight: State) { val reversedTerminalItems by remember { derivedStateOf { chatModel.terminalItems.value.asReversed() } } - val clipboard = LocalClipboardManager.current val listState = LocalAppBarHandler.current?.listState ?: rememberLazyListState() LaunchedEffect(Unit) { - var autoScrollToBottom = true + var autoScrollToBottom = listState.firstVisibleItemIndex <= 1 launch { snapshotFlow { listState.layoutInfo.totalItemsCount } .filter { autoScrollToBottom } @@ -150,12 +141,21 @@ fun TerminalLog(floating: Boolean) { launch { snapshotFlow { listState.firstVisibleItemIndex } .collect { - autoScrollToBottom = listState.firstVisibleItemIndex == 0 + autoScrollToBottom = it == 0 } } } - LazyColumnWithScrollBar(reverseLayout = true, state = listState) { + LazyColumnWithScrollBar ( + reverseLayout = true, + contentPadding = PaddingValues( + top = topPaddingToContent(), + bottom = composeViewHeight.value + ), + state = listState, + additionalBarOffset = composeViewHeight + ) { items(reversedTerminalItems, key = { item -> item.id to item.createdAtNanos }) { item -> + val clipboard = LocalClipboardManager.current val rhId = item.remoteHostId val rhIdStr = if (rhId == null) "" else "$rhId " Text( @@ -172,13 +172,15 @@ fun TerminalLog(floating: Boolean) { ModalManager.start } modalPlace.showModal(endButtons = { ShareButton { clipboard.shareText(item.details) } }) { - SelectionContainer(modifier = Modifier.verticalScroll(rememberScrollState())) { - val details = item.details - .let { - if (it.length < 100_000) it - else it.substring(0, 100_000) - } - Text(details, modifier = Modifier.heightIn(max = 50_000.dp).padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING)) + ColumnWithScrollBar { + SelectionContainer { + val details = item.details + .let { + if (it.length < 100_000) it + else it.substring(0, 100_000) + } + Text(details, modifier = Modifier.heightIn(max = 50_000.dp).padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING)) + } } } }.padding(horizontal = 8.dp, vertical = 4.dp) @@ -208,8 +210,7 @@ fun PreviewTerminalLayout() { TerminalLayout( composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = false)) }, sendCommand = {}, - floating = false, - close = {} + floating = false ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt index 0d5350dbe0..e1e3dcb56b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt @@ -40,8 +40,6 @@ fun CreateProfile(chatModel: ChatModel, close: () -> Unit) { val scrollState = rememberScrollState() val keyboardState by getKeyboardState() var savedKeyboardState by remember { mutableStateOf(keyboardState) } - - ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { Box( modifier = Modifier .fillMaxSize() @@ -50,11 +48,9 @@ fun CreateProfile(chatModel: ChatModel, close: () -> Unit) { val displayName = rememberSaveable { mutableStateOf("") } val focusRequester = remember { FocusRequester() } - ColumnWithScrollBar( - modifier = Modifier.fillMaxSize() - ) { + ColumnWithScrollBar { Column(Modifier.padding(horizontal = DEFAULT_PADDING)) { - AppBarTitle(stringResource(MR.strings.create_profile), bottomPadding = DEFAULT_PADDING) + AppBarTitle(stringResource(MR.strings.create_profile), withPadding = false, bottomPadding = DEFAULT_PADDING) Row(Modifier.padding(bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Text( stringResource(MR.strings.display_name), @@ -102,7 +98,6 @@ fun CreateProfile(chatModel: ChatModel, close: () -> Unit) { } } } - } } @Composable @@ -111,59 +106,42 @@ fun CreateFirstProfile(chatModel: ChatModel, close: () -> Unit) { val scrollState = rememberScrollState() val keyboardState by getKeyboardState() var savedKeyboardState by remember { mutableStateOf(keyboardState) } - val handler = remember { AppBarHandler() } - CompositionLocalProvider( - LocalAppBarHandler provides handler - ) { - 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() + CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) { + ModalView({ + if (chatModel.users.none { !it.user.hidden }) { + appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) + } else { + close() + } + }) { + ColumnWithScrollBar { + 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) } - }) - BackHandler(onBack = { - appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) - }) + 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) + } - 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) { + delay(300) + focusRequester.requestFocus() } } LaunchedEffect(Unit) { @@ -255,7 +233,6 @@ fun ProfileNameField(name: MutableState, placeholder: String = "", isVal val modifier = Modifier .fillMaxWidth() .heightIn(min = 50.dp) - .navigationBarsWithImePadding() .onFocusChanged { focused = it.isFocused } Column( Modifier diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/IncomingCallAlertView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/IncomingCallAlertView.kt index 32681234fa..4d8c1fae46 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/IncomingCallAlertView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/IncomingCallAlertView.kt @@ -53,7 +53,7 @@ fun IncomingCallAlertLayout( acceptCall: () -> Unit ) { val color = if (isInDarkTheme()) MaterialTheme.colors.surface else IncomingCallLight - Column(Modifier.fillMaxWidth().background(color).padding(top = DEFAULT_PADDING, bottom = DEFAULT_PADDING, start = DEFAULT_PADDING, end = 8.dp)) { + Column(Modifier.fillMaxWidth().background(color).statusBarsPadding().padding(top = DEFAULT_PADDING, bottom = DEFAULT_PADDING, start = DEFAULT_PADDING, end = 8.dp)) { IncomingCallInfo(invitation, chatModel) Spacer(Modifier.height(8.dp)) Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt index 9149b039ef..df13368900 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt @@ -529,10 +529,7 @@ fun ChatInfoLayout( KeyChangeEffect(chat.id) { scope.launch { scrollState.scrollTo(0) } } - ColumnWithScrollBar( - Modifier - .fillMaxWidth() - ) { + ColumnWithScrollBar { Row( Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt index bdbfdb89c3..30bbe72a72 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt @@ -276,7 +276,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools @Composable fun HistoryTab() { - ColumnWithScrollBar(Modifier.fillMaxWidth()) { + ColumnWithScrollBar { Details() SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true) val versions = ciInfo.itemVersions @@ -300,7 +300,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools @Composable fun QuoteTab(qi: CIQuote) { - ColumnWithScrollBar(Modifier.fillMaxWidth()) { + ColumnWithScrollBar { Details() SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true) SectionView(contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) { @@ -313,7 +313,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools @Composable fun ForwardedFromTab(forwardedFromItem: AChatItem) { - ColumnWithScrollBar(Modifier.fillMaxWidth()) { + ColumnWithScrollBar { Details() SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true) SectionView { @@ -375,7 +375,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools @Composable fun DeliveryTab(memberDeliveryStatuses: List) { - ColumnWithScrollBar(Modifier.fillMaxWidth()) { + ColumnWithScrollBar { Details() SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true) val mss = membersStatuses(chatModel, memberDeliveryStatuses) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index 570f763e99..2c43a81f7d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -12,10 +12,11 @@ import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.* -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.drawWithCache +import androidx.compose.ui.draw.* import androidx.compose.ui.graphics.* +import androidx.compose.ui.graphics.layer.GraphicsLayer import androidx.compose.ui.layout.layoutId +import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.* import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource @@ -108,6 +109,7 @@ fun ChatView(staleChatId: State, onComposed: suspend (chatId: String) - } } val clipboard = LocalClipboardManager.current + CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler(chatInfo.id, keyboardCoversBar = false)) { when (chatInfo) { 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 } @@ -523,28 +525,10 @@ fun ChatView(staleChatId: State, onComposed: suspend (chatId: String) - showViaProxy = chatModel.controller.appPrefs.showSentViaProxy.get(), showSearch = showSearch ) - if (appPlatform.isAndroid) { - val backgroundColor = MaterialTheme.colors.background - val backgroundColorState = rememberUpdatedState(backgroundColor) - LaunchedEffect(Unit) { - snapshotFlow { ModalManager.center.modalCount.value > 0 } - .collect { modalBackground -> - if (modalBackground) { - platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, CurrentColors.value.colors.background, false, false) - } else { - platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, backgroundColorState.value, true, false) - } - } - } - } } } is ChatInfo.ContactConnection -> { val close = { chatModel.chatId.value = null } - val handler = remember { AppBarHandler() } - CompositionLocalProvider( - LocalAppBarHandler provides handler - ) { ModalView(close, showClose = appPlatform.isAndroid, content = { ContactConnectionInfoView(chatModel, chatRh, chatInfo.contactConnection.connReqInv, chatInfo.contactConnection, false, close) }) @@ -553,14 +537,9 @@ fun ChatView(staleChatId: State, onComposed: suspend (chatId: String) - ModalManager.end.closeModals() chatModel.chatItems.clear() } - } } is ChatInfo.InvalidJSON -> { val close = { chatModel.chatId.value = null } - val handler = remember { AppBarHandler() } - CompositionLocalProvider( - LocalAppBarHandler provides handler - ) { ModalView(close, showClose = appPlatform.isAndroid, endButtons = { ShareButton { clipboard.shareText(chatInfo.json) } }, content = { InvalidJSONView(chatInfo.json) }) @@ -569,10 +548,10 @@ fun ChatView(staleChatId: State, onComposed: suspend (chatId: String) - ModalManager.end.closeModals() chatModel.chatItems.clear() } - } } else -> {} } + } } } @@ -649,67 +628,60 @@ fun ChatLayout( }, ) ) { - ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { - ModalBottomSheetLayout( - scrimColor = Color.Black.copy(alpha = 0.12F), - modifier = Modifier.navigationBarsWithImePadding(), - sheetElevation = 0.dp, - sheetContent = { - ChooseAttachmentView( - attachmentOption, - hide = { scope.launch { attachmentBottomSheetState.hide() } } - ) - }, - sheetState = attachmentBottomSheetState, - sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp) - ) { - val floatingButton: MutableState<@Composable () -> Unit> = remember { mutableStateOf({}) } - val setFloatingButton = { button: @Composable () -> Unit -> - floatingButton.value = button - } - - Scaffold( - topBar = { - if (selectedChatItems.value == null) { - val chatInfo = chatInfo.value - if (chatInfo != null) { - ChatInfoToolbar(chatInfo, back, info, startCall, endCall, addMembers, openGroupLink, changeNtfsState, onSearchValueChanged, showSearch) - } - } else { - SelectedItemsTopToolbar(selectedChatItems) - } - }, - bottomBar = composeView, - modifier = Modifier.navigationBarsWithImePadding(), - floatingActionButton = { floatingButton.value() }, - contentColor = LocalContentColor.current, - backgroundColor = Color.Unspecified - ) { contentPadding -> - val wallpaperImage = MaterialTheme.wallpaper.type.image - val wallpaperType = MaterialTheme.wallpaper.type - val backgroundColor = MaterialTheme.wallpaper.background ?: wallpaperType.defaultBackgroundColor(CurrentColors.value.base, MaterialTheme.colors.background) - val tintColor = MaterialTheme.wallpaper.tint ?: wallpaperType.defaultTintColor(CurrentColors.value.base) - BoxWithConstraints(Modifier - .fillMaxSize() - .background(MaterialTheme.colors.background) - .then(if (wallpaperImage != null) - Modifier.drawWithCache { chatViewBackground(wallpaperImage, wallpaperType, backgroundColor, tintColor) } - else - Modifier) - .padding(contentPadding) - ) { - val remoteHostId = remember { remoteHostId }.value - val chatInfo = remember { chatInfo }.value - if (chatInfo != null) { + ModalBottomSheetLayout( + scrimColor = Color.Black.copy(alpha = 0.12F), + sheetElevation = 0.dp, + sheetContent = { + ChooseAttachmentView( + attachmentOption, + hide = { scope.launch { attachmentBottomSheetState.hide() } } + ) + }, + sheetState = attachmentBottomSheetState, + sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp) + ) { + val composeViewHeight = remember { mutableStateOf(0.dp) } + Box(Modifier.fillMaxSize().chatViewBackgroundModifier(MaterialTheme.colors, MaterialTheme.wallpaper, LocalAppBarHandler.current?.backgroundGraphicsLayerSize, LocalAppBarHandler.current?.backgroundGraphicsLayer)) { + val remoteHostId = remember { remoteHostId }.value + val chatInfo = remember { chatInfo }.value + AdaptingBottomPaddingLayout(Modifier, CHAT_COMPOSE_LAYOUT_ID, composeViewHeight) { + if (chatInfo != null) { + Box(Modifier.fillMaxSize()) { ChatItemsList( - remoteHostId, chatInfo, unreadCount, composeState, searchValue, + remoteHostId, chatInfo, unreadCount, composeState, composeViewHeight, searchValue, useLinkPreviews, linkMode, selectedChatItems, showMemberInfo, loadPrevMessages, deleteMessage, deleteMessages, receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, forwardItem, updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember, - setReaction, showItemDetails, markRead, setFloatingButton, onComposed, developerTools, showViaProxy, + setReaction, showItemDetails, markRead, remember { { onComposed(it) } }, developerTools, showViaProxy, ) } } + val oneHandUI = remember { appPrefs.oneHandUI.state } + Box( + Modifier + .layoutId(CHAT_COMPOSE_LAYOUT_ID) + .align(Alignment.BottomCenter) + .imePadding() + .navigationBarsPadding() + .then(if (oneHandUI.value) Modifier.padding(bottom = AppBarHeight * fontSizeSqrtMultiplier) else Modifier) + ) { + composeView() + } + } + val oneHandUI = remember { appPrefs.oneHandUI.state } + if (oneHandUI.value) { + StatusBarBackground() + } else { + NavigationBarBackground(true, oneHandUI.value, noAlpha = true) + } + Box(if (oneHandUI.value) Modifier.align(Alignment.BottomStart).imePadding() else Modifier) { + if (selectedChatItems.value == null) { + if (chatInfo != null) { + ChatInfoToolbar(chatInfo, back, info, startCall, endCall, addMembers, openGroupLink, changeNtfsState, onSearchValueChanged, showSearch) + } + } else { + SelectedItemsTopToolbar(selectedChatItems) + } } } } @@ -717,7 +689,7 @@ fun ChatLayout( } @Composable -fun ChatInfoToolbar( +fun BoxScope.ChatInfoToolbar( chatInfo: ChatInfo, back: () -> Unit, info: () -> Unit, @@ -869,21 +841,33 @@ fun ChatInfoToolbar( } } } - - DefaultTopAppBar( + val oneHandUI = remember { appPrefs.oneHandUI.state } + DefaultAppBar( navigationButton = { if (appPlatform.isAndroid || showSearch.value) { NavigationButtonBack(onBackClicked) } }, title = { ChatInfoToolbarTitle(chatInfo) }, onTitleClick = if (chatInfo is ChatInfo.Local) null else info, showSearch = showSearch.value, + onTop = !oneHandUI.value, onSearchValueChanged = onSearchValueChanged, - buttons = barButtons + buttons = { barButtons.forEach { it() } } ) - - Divider(Modifier.padding(top = AppBarHeight * fontSizeSqrtMultiplier)) - - Box(Modifier.fillMaxWidth().wrapContentSize(Alignment.TopEnd).offset(y = AppBarHeight * fontSizeSqrtMultiplier)) { - DefaultDropdownMenu(showMenu) { - menuItems.forEach { it() } + Box(Modifier.fillMaxWidth().wrapContentSize(Alignment.TopEnd)) { + val density = LocalDensity.current + val width = remember { mutableStateOf(250.dp) } + val height = remember { mutableStateOf(0.dp) } + DefaultDropdownMenu( + showMenu, + modifier = Modifier.onSizeChanged { with(density) { + width.value = it.width.toDp().coerceAtLeast(250.dp) + if (oneHandUI.value && (appPlatform.isDesktop || (platform.androidApiLevel ?: 0) >= 30)) height.value = it.height.toDp() + } }, + offset = DpOffset(-width.value, if (oneHandUI.value) -height.value else AppBarHeight) + ) { + if (oneHandUI.value) { + menuItems.asReversed().forEach { it() } + } else { + menuItems.forEach { it() } + } } } } @@ -927,11 +911,12 @@ private fun ContactVerifiedShield() { } @Composable -fun BoxWithConstraintsScope.ChatItemsList( +fun BoxScope.ChatItemsList( remoteHostId: Long?, chatInfo: ChatInfo, unreadCount: State, composeState: MutableState, + composeViewHeight: State, searchValue: State, useLinkPreviews: Boolean, linkMode: SimplexLinkMode, @@ -956,7 +941,6 @@ fun BoxWithConstraintsScope.ChatItemsList( setReaction: (ChatInfo, ChatItem, Boolean, MsgReaction) -> Unit, showItemDetails: (ChatInfo, ChatItem) -> Unit, markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit, - setFloatingButton: (@Composable () -> Unit) -> Unit, onComposed: suspend (chatId: String) -> Unit, developerTools: Boolean, showViaProxy: Boolean @@ -980,13 +964,18 @@ fun BoxWithConstraintsScope.ChatItemsList( PreloadItems(chatInfo.id, listState, ChatPagination.UNTIL_PRELOAD_COUNT, loadPrevMessages) Spacer(Modifier.size(8.dp)) - val reversedChatItems by remember { derivedStateOf { chatModel.chatItems.asReversed() } } - val maxHeightRounded = with(LocalDensity.current) { maxHeight.roundToPx() } - val scrollToItem: (Long) -> Unit = { itemId: Long -> - val index = reversedChatItems.indexOfFirst { it.id == itemId } - if (index != -1) { - scope.launch { listState.animateScrollToItem(kotlin.math.min(reversedChatItems.lastIndex, index + 1), -maxHeightRounded) } - } + val reversedChatItems = remember { derivedStateOf { chatModel.chatItems.asReversed() } } + val topPaddingToContentPx = rememberUpdatedState(with(LocalDensity.current) { topPaddingToContent().roundToPx() }) + val maxHeight = remember { derivedStateOf { listState.layoutInfo.viewportEndOffset - topPaddingToContentPx.value } } + val scrollToItem: State<(Long) -> Unit> = remember { + mutableStateOf( + { itemId: Long -> + val index = reversedChatItems.value.indexOfFirst { it.id == itemId } + if (index != -1) { + scope.launch { listState.animateScrollToItem(kotlin.math.min(reversedChatItems.value.lastIndex, index + 1), -maxHeight.value) } + } + } + ) } // TODO: Having this block on desktop makes ChatItemsList() to recompose twice on chatModel.chatId update instead of once LaunchedEffect(chatInfo.id) { @@ -1004,8 +993,18 @@ fun BoxWithConstraintsScope.ChatItemsList( VideoPlayerHolder.releaseAll() } ) - LazyColumnWithScrollBar(Modifier.align(Alignment.BottomCenter), state = listState, reverseLayout = true) { - itemsIndexed(reversedChatItems, key = { _, item -> item.id to item.meta.createdAt.toEpochMilliseconds() }) { i, cItem -> + LazyColumnWithScrollBar( + Modifier.align(Alignment.BottomCenter), + state = listState, + reverseLayout = true, + contentPadding = PaddingValues( + top = topPaddingToContent(), + bottom = composeViewHeight.value + ), + additionalBarOffset = composeViewHeight + ) { + itemsIndexed(reversedChatItems.value, key = { _, item -> item.id to item.meta.createdAt.toEpochMilliseconds() }) { i, cItem -> + val itemScope = rememberCoroutineScope() CompositionLocalProvider( // Makes horizontal and vertical scrolling to coexist nicely. // With default touchSlop when you scroll LazyColumn, you can unintentionally open reply view @@ -1013,10 +1012,10 @@ fun BoxWithConstraintsScope.ChatItemsList( ) { val provider = { providerForGallery(i, chatModel.chatItems.value, cItem.id) { indexInReversed -> - scope.launch { + itemScope.launch { listState.scrollToItem( - kotlin.math.min(reversedChatItems.lastIndex, indexInReversed + 1), - -maxHeightRounded + kotlin.math.min(reversedChatItems.value.lastIndex, indexInReversed + 1), + -maxHeight.value ) } } @@ -1029,7 +1028,7 @@ fun BoxWithConstraintsScope.ChatItemsList( tryOrShowError("${cItem.id}ChatItem", error = { CIBrokenComposableView(if (cItem.chatDir.sent) Alignment.CenterEnd else Alignment.CenterStart) }) { - ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, range = range, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp) + ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, range = range, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem.value, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp) } } @@ -1037,7 +1036,7 @@ fun BoxWithConstraintsScope.ChatItemsList( fun ChatItemView(cItem: ChatItem, range: IntRange?, prevItem: ChatItem?, itemSeparation: ItemSeparation, previousItemSeparation: ItemSeparation?) { val dismissState = rememberDismissState(initialValue = DismissValue.Default) { if (it == DismissValue.DismissedToStart) { - scope.launch { + itemScope.launch { if ((cItem.content is CIContent.SndMsgContent || cItem.content is CIContent.RcvMsgContent) && chatInfo !is ChatInfo.Local) { if (composeState.value.editing) { composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews) @@ -1234,16 +1233,17 @@ fun BoxWithConstraintsScope.ChatItemsList( } val range = chatViewItemsRange(currIndex, prevHidden) + val reversed = reversedChatItems.value if (revealed.value && range != null) { - reversedChatItems.subList(range.first, range.last + 1).forEachIndexed { index, ci -> - val prev = if (index + range.first == prevHidden) prevItem else reversedChatItems[index + range.first + 1] + reversed.subList(range.first, range.last + 1).forEachIndexed { index, ci -> + val prev = if (index + range.first == prevHidden) prevItem else reversed[index + range.first + 1] ChatItemView(ci, null, prev, itemSeparation, previousItemSeparation) } } else { ChatItemView(cItem, range, prevItem, itemSeparation, previousItemSeparation) } - if (i == reversedChatItems.lastIndex) { + if (i == reversed.lastIndex) { DateSeparator(cItem.meta.itemTs) } } @@ -1251,7 +1251,7 @@ fun BoxWithConstraintsScope.ChatItemsList( if (cItem.isRcvNew && chatInfo.id == ChatModel.chatId.value) { LaunchedEffect(cItem.id) { - scope.launch { + itemScope.launch { delay(600) markRead(CC.ItemRange(cItem.id, cItem.id), null) } @@ -1260,10 +1260,10 @@ fun BoxWithConstraintsScope.ChatItemsList( } } } - FloatingButtons(chatModel.chatItems, unreadCount, remoteHostId, chatInfo, searchValue, markRead, setFloatingButton, listState) + FloatingButtons(chatModel.chatItems, unreadCount, composeViewHeight, remoteHostId, chatInfo, searchValue, markRead, listState) FloatingDate( - Modifier.padding(top = 10.dp).align(Alignment.TopCenter), + Modifier.padding(top = 10.dp + topPaddingToContent()).align(Alignment.TopCenter), listState, ) @@ -1318,87 +1318,65 @@ private fun ScrollToBottom(chatId: ChatId, listState: LazyListState, chatItems: } @Composable -fun BoxWithConstraintsScope.FloatingButtons( +fun BoxScope.FloatingButtons( chatItems: State>, unreadCount: State, + composeViewHeight: State, remoteHostId: Long?, chatInfo: ChatInfo, searchValue: State, markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit, - setFloatingButton: (@Composable () -> Unit) -> Unit, listState: LazyListState ) { val scope = rememberCoroutineScope() - var firstVisibleIndex by remember { mutableStateOf(listState.firstVisibleItemIndex) } - var lastIndexOfVisibleItems by remember { mutableStateOf(listState.layoutInfo.visibleItemsInfo.lastIndex) } - var firstItemIsVisible by remember { mutableStateOf(firstVisibleIndex == 0) } - - LaunchedEffect(listState) { - snapshotFlow { listState.firstVisibleItemIndex } - .distinctUntilChanged() - .collect { - firstVisibleIndex = it - firstItemIsVisible = firstVisibleIndex == 0 - } - } - - LaunchedEffect(listState) { - // When both snapshotFlows located in one LaunchedEffect second block will never be called because coroutine is paused on first block - // so separate them into two LaunchedEffects - snapshotFlow { listState.layoutInfo.visibleItemsInfo.lastIndex } - .distinctUntilChanged() - .collect { - lastIndexOfVisibleItems = it - } - } - val bottomUnreadCount by remember { + val maxHeight = remember { derivedStateOf { listState.layoutInfo.viewportSize.height } } + val bottomUnreadCount = remember { derivedStateOf { if (unreadCount.value == 0) return@derivedStateOf 0 val items = chatItems.value - val from = items.lastIndex - firstVisibleIndex - lastIndexOfVisibleItems + val from = items.lastIndex - listState.firstVisibleItemIndex - listState.layoutInfo.visibleItemsInfo.lastIndex if (items.size <= from || from < 0) return@derivedStateOf 0 items.subList(from, items.size).count { it.isRcvNew } } } - val firstVisibleOffset = (-with(LocalDensity.current) { maxHeight.roundToPx() } * 0.8).toInt() - LaunchedEffect(bottomUnreadCount, firstItemIsVisible) { - val showButtonWithCounter = bottomUnreadCount > 0 && !firstItemIsVisible && searchValue.value.isEmpty() - val showButtonWithArrow = !showButtonWithCounter && !firstItemIsVisible - setFloatingButton( - bottomEndFloatingButton( - bottomUnreadCount, - showButtonWithCounter, - showButtonWithArrow, - onClickArrowDown = { - scope.launch { listState.animateScrollToItem(0) } - }, - onClickCounter = { - scope.launch { listState.animateScrollToItem(kotlin.math.max(0, bottomUnreadCount - 1), firstVisibleOffset) } - } - )) - } + val showBottomButtonWithCounter = remember { derivedStateOf { bottomUnreadCount.value > 0 && listState.firstVisibleItemIndex != 0 && searchValue.value.isEmpty() } } + val showBottomButtonWithArrow = remember { derivedStateOf { !showBottomButtonWithCounter.value && listState.firstVisibleItemIndex != 0 } } + BottomEndFloatingButton( + bottomUnreadCount, + showBottomButtonWithCounter, + showBottomButtonWithArrow, + composeViewHeight, + onClickArrowDown = { + scope.launch { listState.animateScrollToItem(0) } + }, + onClickCounter = { + val firstVisibleOffset = (-maxHeight.value * 0.8).toInt() + scope.launch { listState.animateScrollToItem(kotlin.math.max(0, bottomUnreadCount.value - 1), firstVisibleOffset) } + } + ) // Don't show top FAB if is in search if (searchValue.value.isNotEmpty()) return val fabSize = 56.dp - val topUnreadCount by remember { - derivedStateOf { unreadCount.value - bottomUnreadCount } - } - val showButtonWithCounter = topUnreadCount > 0 - val height = with(LocalDensity.current) { maxHeight.toPx() } + val topUnreadCount = remember { derivedStateOf { unreadCount.value - bottomUnreadCount.value } } val showDropDown = remember { mutableStateOf(false) } TopEndFloatingButton( - Modifier.padding(end = DEFAULT_PADDING, top = 24.dp).align(Alignment.TopEnd), + Modifier.padding(end = DEFAULT_PADDING, top = 24.dp + topPaddingToContent()).align(Alignment.TopEnd), topUnreadCount, - showButtonWithCounter, - onClick = { scope.launch { listState.animateScrollBy(height) } }, + onClick = { scope.launch { listState.animateScrollBy(maxHeight.value.toFloat()) } }, onLongClick = { showDropDown.value = true } ) - Box { - DefaultDropdownMenu(showDropDown, offset = DpOffset(this@FloatingButtons.maxWidth - DEFAULT_PADDING, 24.dp + fabSize)) { + Box(Modifier.fillMaxWidth().wrapContentSize(Alignment.TopEnd)) { + val density = LocalDensity.current + val width = remember { mutableStateOf(250.dp) } + DefaultDropdownMenu( + showDropDown, + modifier = Modifier.onSizeChanged { with(density) { width.value = it.width.toDp().coerceAtLeast(250.dp) } }, + offset = DpOffset(-DEFAULT_PADDING - width.value, 24.dp + fabSize + topPaddingToContent()) + ) { ItemAction( generalGetString(MR.strings.mark_read), painterResource(MR.images.ic_check), @@ -1406,7 +1384,7 @@ fun BoxWithConstraintsScope.FloatingButtons( val minUnreadItemId = chatModel.chats.value.firstOrNull { it.remoteHostId == remoteHostId && it.id == chatInfo.id }?.chatStats?.minUnreadItemId ?: return@ItemAction markRead( CC.ItemRange(minUnreadItemId, chatItems.value[chatItems.value.size - listState.layoutInfo.visibleItemsInfo.lastIndex - 1].id - 1), - bottomUnreadCount + bottomUnreadCount.value ) showDropDown.value = false }) @@ -1468,12 +1446,11 @@ fun MemberImage(member: GroupMember) { @Composable private fun TopEndFloatingButton( modifier: Modifier = Modifier, - unreadCount: Int, - showButtonWithCounter: Boolean, + unreadCount: State, onClick: () -> Unit, onLongClick: () -> Unit ) = when { - showButtonWithCounter -> { + unreadCount.value > 0 -> { val interactionSource = interactionSourceWithDetection(onClick, onLongClick) FloatingActionButton( {}, // no action here @@ -1483,7 +1460,7 @@ private fun TopEndFloatingButton( interactionSource = interactionSource, ) { Text( - unreadCountStr(unreadCount), + unreadCountStr(unreadCount.value), color = MaterialTheme.colors.primary, fontSize = 14.sp, ) @@ -1493,6 +1470,16 @@ private fun TopEndFloatingButton( } } +@Composable +fun topPaddingToContent(): Dp { + val oneHandUI = remember { appPrefs.oneHandUI.state } + return if (oneHandUI.value) { + WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + } else { + AppBarHeight * fontSizeSqrtMultiplier + WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + } +} + @Composable private fun FloatingDate( modifier: Modifier, @@ -1502,8 +1489,9 @@ private fun FloatingDate( var isNearBottom by remember { mutableStateOf(true) } val lastVisibleItemDate = remember { derivedStateOf { - if (listState.layoutInfo.visibleItemsInfo.lastIndex >= 0 && listState.firstVisibleItemIndex >= 0) { - val lastVisibleChatItemIndex = chatModel.chatItems.value.lastIndex - listState.firstVisibleItemIndex - listState.layoutInfo.visibleItemsInfo.lastIndex + if (listState.layoutInfo.visibleItemsInfo.lastIndex >= 0) { + val lastFullyVisibleOffset = listState.layoutInfo.viewportEndOffset + val lastVisibleChatItemIndex = chatModel.chatItems.value.lastIndex - (listState.layoutInfo.visibleItemsInfo.lastOrNull { item -> item.offset + item.size <= lastFullyVisibleOffset && item.size > 0 }?.index ?: 0) val item = chatModel.chatItems.value.getOrNull(lastVisibleChatItemIndex) val timeZone = TimeZone.currentSystemDefault() item?.meta?.itemTs?.toLocalDateTime(timeZone)?.date?.atStartOfDayIn(timeZone) @@ -1690,48 +1678,44 @@ fun openGroupLink(groupInfo: GroupInfo, rhId: Long?, view: Any? = null, close: ( } } -private fun bottomEndFloatingButton( - unreadCount: Int, - showButtonWithCounter: Boolean, - showButtonWithArrow: Boolean, +@Composable +private fun BoxScope.BottomEndFloatingButton( + unreadCount: State, + showButtonWithCounter: State, + showButtonWithArrow: State, + composeViewHeight: State, onClickArrowDown: () -> Unit, onClickCounter: () -> Unit -): @Composable () -> Unit = when { - showButtonWithCounter -> { - { - FloatingActionButton( - onClick = onClickCounter, - elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp), - modifier = Modifier.size(48.dp), - backgroundColor = MaterialTheme.colors.secondaryVariant, - ) { - Text( - unreadCountStr(unreadCount), - color = MaterialTheme.colors.primary, - fontSize = 14.sp, - ) - } +) = when { + showButtonWithCounter.value -> { + FloatingActionButton( + onClick = onClickCounter, + elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp), + modifier = Modifier.padding(end = DEFAULT_PADDING, bottom = DEFAULT_PADDING + composeViewHeight.value).align(Alignment.BottomEnd).size(48.dp), + backgroundColor = MaterialTheme.colors.secondaryVariant, + ) { + Text( + unreadCountStr(unreadCount.value), + color = MaterialTheme.colors.primary, + fontSize = 14.sp, + ) } } - showButtonWithArrow -> { - { - FloatingActionButton( - onClick = onClickArrowDown, - elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp), - modifier = Modifier.size(48.dp), - backgroundColor = MaterialTheme.colors.secondaryVariant, - ) { - Icon( - painter = painterResource(MR.images.ic_keyboard_arrow_down), - contentDescription = null, - tint = MaterialTheme.colors.primary - ) - } + showButtonWithArrow.value -> { + FloatingActionButton( + onClick = onClickArrowDown, + elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp), + modifier = Modifier.padding(end = DEFAULT_PADDING, bottom = DEFAULT_PADDING + composeViewHeight.value).align(Alignment.BottomEnd).size(48.dp), + backgroundColor = MaterialTheme.colors.secondaryVariant, + ) { + Icon( + painter = painterResource(MR.images.ic_keyboard_arrow_down), + contentDescription = null, + tint = MaterialTheme.colors.primary + ) } } - else -> { - {} - } + else -> {} } @Composable @@ -1858,6 +1842,25 @@ private fun memberNames(member: GroupMember, prevMember: GroupMember?, memCount: } } +fun Modifier.chatViewBackgroundModifier( + colors: Colors, + wallpaper: AppWallpaper, + backgroundGraphicsLayerSize: MutableState?, + backgroundGraphicsLayer: GraphicsLayer? +): Modifier { + val wallpaperImage = wallpaper.type.image + val wallpaperType = wallpaper.type + val backgroundColor = wallpaper.background ?: wallpaperType.defaultBackgroundColor(CurrentColors.value.base, colors.background) + val tintColor = wallpaper.tint ?: wallpaperType.defaultTintColor(CurrentColors.value.base) + + return this + .then(if (wallpaperImage != null) + Modifier.drawWithCache { chatViewBackground(wallpaperImage, wallpaperType, backgroundColor, tintColor, backgroundGraphicsLayerSize, backgroundGraphicsLayer) } + else + Modifier.drawWithCache { onDrawBehind { copyBackgroundToAppBar(backgroundGraphicsLayerSize, backgroundGraphicsLayer) { drawRect(backgroundColor) } } } + ) +} + fun chatViewItemsRange(currIndex: Int?, prevHidden: Int?): IntRange? = if (currIndex != null && prevHidden != null && prevHidden > currIndex) { currIndex..prevHidden diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt index cad18af9bb..fd1d3ab92d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt @@ -13,12 +13,14 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.text.font.FontStyle import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.model.ChatModel.filesToDelete import chat.simplex.common.model.ChatModel.withChats @@ -896,7 +898,7 @@ fun ComposeView( } } } - Column(Modifier.background(MaterialTheme.colors.background)) { + Box(Modifier.background(MaterialTheme.colors.background)) { Divider() Row(Modifier.padding(end = 8.dp), verticalAlignment = Alignment.Bottom) { val isGroupAndProhibitedFiles = chat.chatInfo is ChatInfo.Group && !chat.chatInfo.groupInfo.fullGroupPreferences.files.on(chat.chatInfo.groupInfo.membership) @@ -918,7 +920,7 @@ fun ComposeView( && !nextSendGrpInv.value IconButton( attachmentClicked, - Modifier.padding(bottom = if (appPlatform.isAndroid) 2.sp.toDp() else 5.sp.toDp() * fontSizeSqrtMultiplier), + Modifier.padding(start = 3.dp, end = 1.dp, bottom = if (appPlatform.isAndroid) 2.sp.toDp() else 5.sp.toDp() * fontSizeSqrtMultiplier), enabled = attachmentEnabled ) { Icon( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContactPreferences.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContactPreferences.kt index 725367e150..b1e9bf750e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContactPreferences.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContactPreferences.kt @@ -81,10 +81,7 @@ private fun ContactPreferencesLayout( reset: () -> Unit, savePrefs: () -> Unit, ) { - ColumnWithScrollBar( - Modifier - .fillMaxWidth(), - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.contact_preferences)) val timedMessages: MutableState = remember(featuresAllowed) { mutableStateOf(featuresAllowed.timedMessagesAllowed) } val onTTLUpdated = { ttl: Int? -> diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ScanCodeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ScanCodeView.kt index 73017c3d42..a12a75b747 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ScanCodeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ScanCodeView.kt @@ -1,9 +1,11 @@ package chat.simplex.common.views.chat +import SectionBottomSpacer import androidx.compose.foundation.layout.* import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import chat.simplex.common.platform.ColumnWithScrollBar import chat.simplex.common.ui.theme.DEFAULT_PADDING import chat.simplex.common.views.helpers.* import chat.simplex.common.views.newchat.QRCodeScanner @@ -12,9 +14,7 @@ import dev.icerock.moko.resources.compose.stringResource @Composable fun ScanCodeView(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, close: () -> Unit) { - Column( - Modifier.fillMaxSize() - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.scan_code)) QRCodeScanner { text -> verifyCode(text) { @@ -28,5 +28,6 @@ fun ScanCodeView(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, close: () } } Text(stringResource(MR.strings.scan_code_from_contacts_app), Modifier.padding(horizontal = DEFAULT_PADDING)) + SectionBottomSpacer() } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SelectableChatItemToolbars.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SelectableChatItemToolbars.kt index 5cf9ebb6c7..838398c503 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SelectableChatItemToolbars.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SelectableChatItemToolbars.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.platform.BackHandler import chat.simplex.common.platform.chatModel import chat.simplex.common.views.helpers.* @@ -20,11 +21,12 @@ import chat.simplex.res.MR import dev.icerock.moko.resources.compose.painterResource @Composable -fun SelectedItemsTopToolbar(selectedChatItems: MutableState?>) { +fun BoxScope.SelectedItemsTopToolbar(selectedChatItems: MutableState?>) { val onBackClicked = { selectedChatItems.value = null } BackHandler(onBack = onBackClicked) val count = selectedChatItems.value?.size ?: 0 - DefaultTopAppBar( + val oneHandUI = remember { appPrefs.oneHandUI.state } + DefaultAppBar( navigationButton = { NavigationButtonClose(onButtonClicked = onBackClicked) }, title = { Text( @@ -39,10 +41,9 @@ fun SelectedItemsTopToolbar(selectedChatItems: MutableState?>) { ) }, onTitleClick = null, - showSearch = false, + onTop = !oneHandUI.value, onSearchValueChanged = {}, ) - Divider(Modifier.padding(top = AppBarHeight * fontSizeSqrtMultiplier)) } @Composable @@ -68,6 +69,8 @@ fun SelectedItemsBottomToolbar( Modifier .matchParentSize() .background(MaterialTheme.colors.background) + .padding(horizontal = 2.dp) + .height(AppBarHeight * fontSizeSqrtMultiplier) .pointerInput(Unit) { detectGesture { true @@ -103,6 +106,7 @@ fun SelectedItemsBottomToolbar( ) } } + Divider(Modifier.align(Alignment.TopStart)) } LaunchedEffect(chatInfo, chatItems, selectedChatItems.value) { recheckItems(chatInfo, chatItems, selectedChatItems, deleteEnabled, deleteForEveryoneEnabled, canModerate, moderateEnabled, forwardEnabled, deleteCountProhibited, forwardCountProhibited) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt index 76c4fc4a62..d912f8e030 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.* @@ -60,7 +61,8 @@ fun SendMsgView( ) { val showCustomDisappearingMessageDialog = remember { mutableStateOf(false) } - Box(Modifier.padding(vertical = if (appPlatform.isAndroid) 8.dp else 6.dp)) { + val padding = if (appPlatform.isAndroid) PaddingValues(vertical = 8.dp) else PaddingValues(top = 3.dp, bottom = 4.dp) + Box(Modifier.padding(padding)) { val cs = composeState.value var progressByTimeout by rememberSaveable { mutableStateOf(false) } LaunchedEffect(composeState.value.inProgress) { @@ -146,7 +148,7 @@ fun SendMsgView( && (cs.preview !is ComposePreview.VoicePreview || !stopRecOnNextClick.value) && cs.contextItem is ComposeContextItem.NoContextItem ) { - Spacer(Modifier.width(10.dp)) + Spacer(Modifier.width(12.dp)) StartLiveMessageButton(userCanSend) { if (composeState.value.preview is ComposePreview.NoPreview) { startLiveMessage(scope, sendLiveMessage, updateLiveMessage, sendButtonSize, sendButtonAlpha, composeState, liveMessageAlertShown) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/VerifyCodeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/VerifyCodeView.kt index 5bd707ab66..69087ecd60 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/VerifyCodeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/VerifyCodeView.kt @@ -56,11 +56,7 @@ private fun VerifyCodeLayout( connectionVerified: Boolean, verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, ) { - ColumnWithScrollBar( - Modifier - .fillMaxSize() - .padding(horizontal = DEFAULT_PADDING) - ) { + ColumnWithScrollBar(Modifier.padding(horizontal = DEFAULT_PADDING)) { AppBarTitle(stringResource(MR.strings.security_code), withPadding = false) val splitCode = splitToParts(connectionCode, 24) Row(Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING_HALF), horizontalArrangement = Arrangement.Center) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt index 18a3a0d14d..b351f56c29 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt @@ -130,10 +130,7 @@ fun AddGroupMembersLayout( } } - ColumnWithScrollBar( - Modifier - .fillMaxWidth(), - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.button_add_members)) profileText() Spacer(Modifier.size(DEFAULT_PADDING)) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt index a14d227074..76f2866950 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt @@ -25,6 +25,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.* import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* @@ -283,9 +284,14 @@ fun ModalData.GroupChatInfoLayout( if (s.isEmpty()) members else members.filter { m -> m.anyNameContains(s) } } } + Box { + val oneHandUI = remember { appPrefs.oneHandUI.state } LazyColumnWithScrollBar( - Modifier - .fillMaxWidth(), + contentPadding = if (oneHandUI.value) { + PaddingValues(top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + DEFAULT_PADDING + 5.dp, bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()) + } else { + PaddingValues(top = topPaddingToContent()) + }, state = listState ) { item { @@ -397,6 +403,11 @@ fun ModalData.GroupChatInfoLayout( } } SectionBottomSpacer() + Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars)) + } + } + if (!oneHandUI.value) { + NavigationBarBackground(oneHandUI.value, oneHandUI.value) } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt index 5291520566..956ee575de 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt @@ -119,9 +119,7 @@ fun GroupLinkLayout( ) } - ColumnWithScrollBar( - Modifier, - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.group_link)) Text( stringResource(MR.strings.you_can_share_group_link_anybody_will_be_able_to_connect), diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt index 9981d70a52..a03cff2bb0 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt @@ -313,10 +313,7 @@ fun GroupMemberInfoLayout( } } - ColumnWithScrollBar( - Modifier - .fillMaxWidth(), - ) { + ColumnWithScrollBar { Row( Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt index b7d66dd4f6..128dfe2d97 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt @@ -82,9 +82,7 @@ private fun GroupPreferencesLayout( reset: () -> Unit, savePrefs: () -> Unit, ) { - ColumnWithScrollBar( - Modifier.fillMaxWidth(), - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.group_preferences)) val timedMessages = remember(preferences) { mutableStateOf(preferences.timedMessages.enable) } val onTTLUpdated = { ttl: Int? -> diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupProfileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupProfileView.kt index 6375ef1a20..e81722f3f0 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupProfileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupProfileView.kt @@ -82,10 +82,9 @@ fun GroupProfileLayout( }, close) } } - ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { ModalBottomSheetLayout( scrimColor = Color.Black.copy(alpha = 0.12F), - modifier = Modifier.navigationBarsWithImePadding(), + modifier = Modifier.imePadding(), sheetContent = { GetImageBottomSheet( chosenImage, @@ -98,9 +97,7 @@ fun GroupProfileLayout( sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp) ) { ModalView(close = closeWithAlert) { - ColumnWithScrollBar( - Modifier - ) { + ColumnWithScrollBar { Column( Modifier.fillMaxWidth() .padding(horizontal = DEFAULT_PADDING) @@ -177,7 +174,6 @@ fun GroupProfileLayout( } } } - } } private fun canUpdateProfile(displayName: String, groupProfile: GroupProfile): Boolean = diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/WelcomeMessageView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/WelcomeMessageView.kt index b6312e4d82..7f0af360e7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/WelcomeMessageView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/WelcomeMessageView.kt @@ -95,9 +95,7 @@ private fun GroupWelcomeLayout( linkMode: SimplexLinkMode, save: () -> Unit, ) { - ColumnWithScrollBar( - Modifier.fillMaxWidth(), - ) { + ColumnWithScrollBar { val editMode = remember { mutableStateOf(true) } AppBarTitle(stringResource(MR.strings.group_welcome_title)) val wt = rememberSaveable { welcomeText } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt index f346402957..f0480a5c50 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt @@ -10,6 +10,7 @@ import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.layout.* +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.UriHandler import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource @@ -320,6 +321,8 @@ fun CIMarkdownText( const val CHAT_IMAGE_LAYOUT_ID = "chatImage" const val CHAT_BUBBLE_LAYOUT_ID = "chatBubble" +const val CHAT_COMPOSE_LAYOUT_ID = "chatCompose" +const val CONSOLE_COMPOSE_LAYOUT_ID = "consoleCompose" /** * Equal to [androidx.compose.ui.unit.Constraints.MaxFocusMask], which is 0x3FFFF - 1 * Other values make a crash `java.lang.IllegalArgumentException: Can't represent a width of 123456 and height of 9909 in Constraints` @@ -398,6 +401,70 @@ fun DependentLayout( } } } + +// The purpose of this layout is to make measuring of bottom compose view and adapt top lazy column to its size in the same frame (not on the next frame as you would expect). +// So, steps are: +// - measuring the layout: measured height of compose view before this step is 0, it's added to content padding of lazy column (so it's == 0) +// - measured the layout: measured height of compose view now is correct, but it's not yet applied to lazy column content padding (so it's == 0) and lazy column is placed higher than compose view in view with respect to compose view's height +// - on next frame measured height is correct and content padding is the same, lazy column placed to occupy all parent view's size +// - every added/removed line in compose view goes through the same process. +@Composable +fun AdaptingBottomPaddingLayout( + modifier: Modifier = Modifier, + mainLayoutId: String, + expectedHeight: MutableState, + content: @Composable () -> Unit +) { + val expected = with(LocalDensity.current) { expectedHeight.value.roundToPx() } + Layout( + content = content, + modifier = modifier + ) { measureable, constraints -> + require(measureable.size <= 2) { "Should be exactly one or two elements in this layout, you have ${measureable.size}" } + val mainPlaceable = measureable.firstOrNull { it.layoutId == mainLayoutId }!!.measure(constraints) + val placeables: List = measureable.map { + if (it.layoutId == mainLayoutId) + mainPlaceable + else + it.measure(constraints.copy(maxHeight = if (expected != mainPlaceable.measuredHeight) constraints.maxHeight - mainPlaceable.measuredHeight + expected else constraints.maxHeight)) } + expectedHeight.value = mainPlaceable.measuredHeight.toDp() + layout(constraints.maxWidth, constraints.maxHeight) { + var y = 0 + placeables.forEach { + if (it !== mainPlaceable) { + it.place(0, y) + y += it.measuredHeight + } else { + it.place(0, constraints.maxHeight - mainPlaceable.measuredHeight) + y += it.measuredHeight + } + } + } + } +} + +@Composable +fun CenteredRowLayout( + modifier: Modifier = Modifier, + content: @Composable () -> Unit +) { + Layout( + content = content, + modifier = modifier + ) { measureable, constraints -> + require(measureable.size == 3) { "Should be exactly three elements in this layout, you have ${measureable.size}" } + val first = measureable[0].measure(constraints.copy(minWidth = 0, minHeight = 0)) + val third = measureable[2].measure(constraints.copy(minWidth = first.measuredWidth, minHeight = 0)) + val second = measureable[1].measure(constraints.copy(minWidth = 0, minHeight = 0, maxWidth = (constraints.maxWidth - first.measuredWidth - third.measuredWidth).coerceAtLeast(0))) + // Limit width for every other element to width of important element and height for a sum of all elements. + layout(constraints.maxWidth, constraints.maxHeight) { + first.place(0, ((constraints.maxHeight - first.measuredHeight) / 2).coerceAtLeast(0)) + second.place((constraints.maxWidth - second.measuredWidth) / 2, ((constraints.maxHeight - second.measuredHeight) / 2).coerceAtLeast(0)) + third.place(constraints.maxWidth - third.measuredWidth, ((constraints.maxHeight - third.measuredHeight) / 2).coerceAtLeast(0)) + } + } +} + /* class EditedProvider: PreviewParameterProvider { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.kt index ab3918549d..70d6fa4aa8 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.kt @@ -12,7 +12,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.* import androidx.compose.ui.input.pointer.* import androidx.compose.ui.layout.onGloballyPositioned -import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.CryptoFile import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.CurrentColors @@ -58,9 +57,17 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () -> val playersToRelease = rememberSaveable { mutableSetOf() } DisposableEffectOnGone( always = { - platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, Color.Black, false, false) + platform.androidSetStatusAndNavigationBarAppearance(false, false, blackNavBar = true) + chatModel.fullscreenGalleryVisible.value = true }, - whenGone = { playersToRelease.forEach { VideoPlayerHolder.release(it, true, true) } } + whenDispose = { + val c = CurrentColors.value.colors + platform.androidSetStatusAndNavigationBarAppearance(c.isLight, c.isLight) + chatModel.fullscreenGalleryVisible.value = false + }, + whenGone = { + playersToRelease.forEach { VideoPlayerHolder.release(it, true, true) } + } ) @Composable diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt index 50949b0b16..586bca87d0 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt @@ -10,8 +10,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier +import androidx.compose.ui.* import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.* import androidx.compose.ui.graphics.* @@ -34,6 +33,7 @@ import chat.simplex.common.views.onboarding.shouldShowWhatsNew import chat.simplex.common.platform.* import chat.simplex.common.views.call.Call import chat.simplex.common.views.chat.item.CIFileViewScope +import chat.simplex.common.views.chat.topPaddingToContent import chat.simplex.common.views.newchat.* import chat.simplex.common.views.usersettings.* import chat.simplex.res.MR @@ -41,7 +41,6 @@ import kotlinx.coroutines.* import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.serialization.json.Json -import java.net.URI import kotlin.time.Duration.Companion.seconds private fun showNewChatSheet(oneHandUI: State) { @@ -55,7 +54,7 @@ private fun showNewChatSheet(oneHandUI: State) { chatModel.newChatSheetVisible.value = false close() } - ModalView(close, closeOnTop = !oneHandUI.value) { + ModalView(close, showAppBar = !oneHandUI.value) { if (appPlatform.isAndroid) { BackHandler { close() @@ -122,11 +121,7 @@ fun ToggleChatListCard() { SharedPreferenceToggle( appPrefs.oneHandUI, - enabled = true, - onChange = { - val c = CurrentColors.value.colors - platform.androidSetStatusAndNavBarColors(c.isLight, c.background, !appPrefs.oneHandUI.get(), appPrefs.oneHandUI.get()) - } + enabled = true ) } } @@ -154,74 +149,36 @@ fun ChatListView(chatModel: ChatModel, userPickerState: MutableStateFlow, listState: LazyListState) { + if (!chatModel.desktopNoUserNoRemote) { + ChatList(searchText = searchText, listState) + } + if (chatModel.chats.value.isEmpty() && !chatModel.switchingUsersAndHosts.value && !chatModel.desktopNoUserNoRemote) { + Text( + stringResource( + if (chatModel.chatRunning.value == null) MR.strings.loading_chats else MR.strings.you_have_no_chats + ), Modifier.align(Alignment.Center), color = MaterialTheme.colors.secondary + ) + } +} + +@Composable +private fun BoxScope.NewChatSheetFloatingButton(oneHandUI: State, stopped: Boolean) { + FloatingActionButton( + onClick = { + if (!stopped) { + showNewChatSheet(oneHandUI) + } + }, + Modifier + .navigationBarsPadding() + .padding(end = DEFAULT_PADDING, bottom = DEFAULT_PADDING) + .align(Alignment.BottomEnd) + .size(AppBarHeight * fontSizeSqrtMultiplier), + elevation = FloatingActionButtonDefaults.elevation( + defaultElevation = 0.dp, + pressedElevation = 0.dp, + hoveredElevation = 0.dp, + focusedElevation = 0.dp, + ), + backgroundColor = if (!stopped) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, + contentColor = Color.White + ) { + Icon(painterResource(MR.images.ic_edit_filled), stringResource(MR.strings.add_contact_or_create_group), Modifier.size(22.dp * fontSizeSqrtMultiplier)) + } +} + @Composable private fun ConnectButton(text: String, onClick: () -> Unit) { Button( @@ -256,7 +253,7 @@ private fun ConnectButton(text: String, onClick: () -> Unit) { } @Composable -private fun ChatListToolbar(userPickerState: MutableStateFlow, stopped: Boolean, setPerformLA: (Boolean) -> Unit) { +private fun ChatListToolbar(userPickerState: MutableStateFlow, listState: LazyListState, stopped: Boolean, setPerformLA: (Boolean) -> Unit) { val serversSummary: MutableState = remember { mutableStateOf(null) } val barButtons = arrayListOf<@Composable RowScope.() -> Unit>() val updatingProgress = remember { chatModel.updatingProgress }.value @@ -265,6 +262,18 @@ private fun ChatListToolbar(userPickerState: MutableStateFlow if (oneHandUI.value) { val sp16 = with(LocalDensity.current) { 16.sp.toDp() } + if (appPlatform.isDesktop && oneHandUI.value) { + val call = remember { chatModel.activeCall } + if (call.value != null) { + barButtons.add { + val c = call.value + if (c != null) { + ActiveCallInteractiveArea(c) + Spacer(Modifier.width(5.dp)) + } + } + } + } if (!stopped) { barButtons.add { IconButton( @@ -323,7 +332,9 @@ private fun ChatListToolbar(userPickerState: MutableStateFlow } } val clipboard = LocalClipboardManager.current - DefaultTopAppBar( + val scope = rememberCoroutineScope() + val canScrollToZero = remember { derivedStateOf { listState.firstVisibleItemIndex != 0 || listState.firstVisibleItemScrollOffset != 0 } } + DefaultAppBar( navigationButton = { if (chatModel.users.isEmpty() && !chatModel.desktopNoUserNoRemote) { NavigationButtonMenu { @@ -351,15 +362,14 @@ private fun ChatListToolbar(userPickerState: MutableStateFlow SubscriptionStatusIndicator( click = { ModalManager.start.closeModals() + val summary = serversSummary.value ModalManager.start.showModalCloseable( endButtons = { - val summary = serversSummary.value if (summary != null) { ShareButton { val json = Json { prettyPrint = true } - val text = json.encodeToString(PresentedServersSummary.serializer(), summary) clipboard.shareText(text) } @@ -370,10 +380,10 @@ private fun ChatListToolbar(userPickerState: MutableStateFlow ) } }, - onTitleClick = null, - showSearch = false, + onTitleClick = if (canScrollToZero.value) { { scrollToBottom(scope, listState) } } else null, + onTop = !oneHandUI.value, onSearchValueChanged = {}, - buttons = barButtons + buttons = { barButtons.forEach { it() } } ) } @@ -491,74 +501,78 @@ fun connectIfOpenedViaUri(rhId: Long?, uri: String, chatModel: ChatModel) { @Composable private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState, searchShowingSimplexLink: MutableState, searchChatFilteredBySimplexLink: MutableState) { - Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { - val focusRequester = remember { FocusRequester() } - var focused by remember { mutableStateOf(false) } - Icon( - painterResource(MR.images.ic_search), - contentDescription = null, - Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING_HALF).size(22.dp * fontSizeSqrtMultiplier), - tint = MaterialTheme.colors.secondary - ) - SearchTextField( - Modifier.weight(1f).onFocusChanged { focused = it.hasFocus }.focusRequester(focusRequester), - placeholder = stringResource(MR.strings.search_or_paste_simplex_link), - alwaysVisible = true, - searchText = searchText, - enabled = !remember { searchShowingSimplexLink }.value, - trailingContent = null, - ) { - searchText.value = searchText.value.copy(it) - } - val hasText = remember { derivedStateOf { searchText.value.text.isNotEmpty() } } - if (hasText.value) { - val hideSearchOnBack: () -> Unit = { searchText.value = TextFieldValue() } - BackHandler(onBack = hideSearchOnBack) - KeyChangeEffect(chatModel.currentRemoteHost.value) { - hideSearchOnBack() + Box { + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { + val focusRequester = remember { FocusRequester() } + var focused by remember { mutableStateOf(false) } + Icon( + painterResource(MR.images.ic_search), + contentDescription = null, + Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING_HALF).size(22.dp * fontSizeSqrtMultiplier), + tint = MaterialTheme.colors.secondary + ) + SearchTextField( + Modifier.weight(1f).onFocusChanged { focused = it.hasFocus }.focusRequester(focusRequester), + placeholder = stringResource(MR.strings.search_or_paste_simplex_link), + alwaysVisible = true, + searchText = searchText, + enabled = !remember { searchShowingSimplexLink }.value, + trailingContent = null, + ) { + searchText.value = searchText.value.copy(it) } - } else { - val padding = if (appPlatform.isDesktop) 0.dp else 7.dp - if (chatModel.chats.value.isNotEmpty()) { - ToggleFilterEnabledButton() - } - Spacer(Modifier.width(padding)) - } - val focusManager = LocalFocusManager.current - val keyboardState = getKeyboardState() - LaunchedEffect(keyboardState.value) { - if (keyboardState.value == KeyboardState.Closed && focused) { - focusManager.clearFocus() - } - } - val view = LocalMultiplatformView() - LaunchedEffect(Unit) { - snapshotFlow { searchText.value.text } - .distinctUntilChanged() - .collect { - val link = strHasSingleSimplexLink(it.trim()) - if (link != null) { - // if SimpleX link is pasted, show connection dialogue - hideKeyboard(view) - if (link.format is Format.SimplexLink) { - val linkText = link.simplexLinkText(link.format.linkType, link.format.smpHosts) - searchText.value = searchText.value.copy(linkText, selection = TextRange.Zero) - } - searchShowingSimplexLink.value = true - searchChatFilteredBySimplexLink.value = null - connect(link.text, searchChatFilteredBySimplexLink) { searchText.value = TextFieldValue() } - } else if (!searchShowingSimplexLink.value || it.isEmpty()) { - if (it.isNotEmpty()) { - // if some other text is pasted, enter search mode - focusRequester.requestFocus() - } else if (listState.layoutInfo.totalItemsCount > 0) { - listState.scrollToItem(0) - } - searchShowingSimplexLink.value = false - searchChatFilteredBySimplexLink.value = null - } + val hasText = remember { derivedStateOf { searchText.value.text.isNotEmpty() } } + if (hasText.value) { + val hideSearchOnBack: () -> Unit = { searchText.value = TextFieldValue() } + BackHandler(onBack = hideSearchOnBack) + KeyChangeEffect(chatModel.currentRemoteHost.value) { + hideSearchOnBack() } + } else { + val padding = if (appPlatform.isDesktop) 0.dp else 7.dp + if (chatModel.chats.value.isNotEmpty()) { + ToggleFilterEnabledButton() + } + Spacer(Modifier.width(padding)) + } + val focusManager = LocalFocusManager.current + val keyboardState = getKeyboardState() + LaunchedEffect(keyboardState.value) { + if (keyboardState.value == KeyboardState.Closed && focused) { + focusManager.clearFocus() + } + } + val view = LocalMultiplatformView() + LaunchedEffect(Unit) { + snapshotFlow { searchText.value.text } + .distinctUntilChanged() + .collect { + val link = strHasSingleSimplexLink(it.trim()) + if (link != null) { + // if SimpleX link is pasted, show connection dialogue + hideKeyboard(view) + if (link.format is Format.SimplexLink) { + val linkText = link.simplexLinkText(link.format.linkType, link.format.smpHosts) + searchText.value = searchText.value.copy(linkText, selection = TextRange.Zero) + } + searchShowingSimplexLink.value = true + searchChatFilteredBySimplexLink.value = null + connect(link.text, searchChatFilteredBySimplexLink) { searchText.value = TextFieldValue() } + } else if (!searchShowingSimplexLink.value || it.isEmpty()) { + if (it.isNotEmpty()) { + // if some other text is pasted, enter search mode + focusRequester.requestFocus() + } else if (listState.layoutInfo.totalItemsCount > 0) { + listState.scrollToItem(0) + } + searchShowingSimplexLink.value = false + searchChatFilteredBySimplexLink.value = null + } + } + } } + val oneHandUI = remember { appPrefs.oneHandUI.state } + Divider(Modifier.align(if (oneHandUI.value) Alignment.TopStart else Alignment.BottomStart)) } } @@ -590,8 +604,37 @@ enum class ScrollDirection { } @Composable -private fun ChatList(chatModel: ChatModel, searchText: MutableState) { - val listState = rememberLazyListState(lazyListState.first, lazyListState.second) +fun BoxScope.StatusBarBackground() { + if (appPlatform.isAndroid) { + val finalColor = MaterialTheme.colors.background.copy(0.88f) + Box(Modifier.fillMaxWidth().windowInsetsTopHeight(WindowInsets.statusBars).background(finalColor)) + } +} + +@Composable +fun BoxScope.NavigationBarBackground(appBarOnBottom: Boolean = false, mixedColor: Boolean, noAlpha: Boolean = false) { + if (appPlatform.isAndroid) { + val barPadding = WindowInsets.navigationBars.asPaddingValues() + val paddingBottom = barPadding.calculateBottomPadding() + val color = if (mixedColor) MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.97f) else MaterialTheme.colors.background + val finalColor = color.copy(if (noAlpha) 1f else if (appBarOnBottom) remember { appPrefs.inAppBarsAlpha.state }.value else 0.6f) + Box(Modifier.align(Alignment.BottomStart).height(paddingBottom).fillMaxWidth().background(finalColor)) + } +} + +@Composable +fun BoxScope.NavigationBarBackground(modifier: Modifier, color: Color = MaterialTheme.colors.background) { + val keyboardState = getKeyboardState() + if (appPlatform.isAndroid && keyboardState.value == KeyboardState.Closed) { + val barPadding = WindowInsets.navigationBars.asPaddingValues() + val paddingBottom = barPadding.calculateBottomPadding() + val finalColor = color.copy(0.6f) + Box(modifier.align(Alignment.BottomStart).height(paddingBottom).fillMaxWidth().background(finalColor)) + } +} + +@Composable +private fun BoxScope.ChatList(searchText: MutableState, listState: LazyListState) { var scrollDirection by remember { mutableStateOf(ScrollDirection.Idle) } var previousIndex by remember { mutableStateOf(0) } var previousScrollOffset by remember { mutableStateOf(0) } @@ -628,40 +671,45 @@ private fun ChatList(chatModel: ChatModel, searchText: MutableState(null) } val chats = filteredChats(showUnreadAndFavorites, searchShowingSimplexLink, searchChatFilteredBySimplexLink, searchText.value.text, allChats.value.toList()) + val topPaddingToContent = topPaddingToContent() + val blankSpaceSize = if (oneHandUI.value) WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + AppBarHeight * fontSizeSqrtMultiplier else topPaddingToContent LazyColumnWithScrollBar( - Modifier.fillMaxSize(), + if (!oneHandUI.value) Modifier.imePadding() else Modifier, listState, reverseLayout = oneHandUI.value ) { + item { Spacer(Modifier.height(blankSpaceSize)) } stickyHeader { Column( Modifier + .zIndex(1f) .offset { - val y = if (searchText.value.text.isEmpty()) { - val offsetMultiplier = if (oneHandUI.value) 1 else -1 - if ( - (oneHandUI.value && scrollDirection == ScrollDirection.Up) || - (appPlatform.isAndroid && keyboardState == KeyboardState.Opened) - ) { - 0 - } else if (listState.firstVisibleItemIndex == 0) offsetMultiplier * listState.firstVisibleItemScrollOffset else offsetMultiplier * 1000 + val offsetMultiplier = if (oneHandUI.value) 1 else -1 + val y = if (searchText.value.text.isNotEmpty() || (appPlatform.isAndroid && keyboardState == KeyboardState.Opened) || scrollDirection == ScrollDirection.Up) { + if (listState.firstVisibleItemIndex == 0) -offsetMultiplier * listState.firstVisibleItemScrollOffset + else -offsetMultiplier * blankSpaceSize.roundToPx() } else { - 0 + when (listState.firstVisibleItemIndex) { + 0 -> 0 + 1 -> offsetMultiplier * listState.firstVisibleItemScrollOffset + else -> offsetMultiplier * 1000 + } } IntOffset(0, y) } - .background(MaterialTheme.colors.background), + .background(MaterialTheme.colors.background) ) { if (oneHandUI.value) { - Divider() - } - ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink) - if (!oneHandUI.value) { - Divider() + Column(Modifier.consumeWindowInsets(WindowInsets.navigationBars).consumeWindowInsets(PaddingValues(bottom = AppBarHeight))) { + ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink) + Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime)) + } + } else { + ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink) } } } - if (appPlatform.isAndroid && !oneHandUICardShown.value && chats.count() > 1) { + if (!oneHandUICardShown.value && chats.size > 1) { item { ToggleChatListCard() } @@ -672,17 +720,30 @@ private fun ChatList(chatModel: ChatModel, searchText: MutableState= 3) appPrefs.oneHandUICardShown.set(true) + } + } } fun filteredChats( @@ -727,3 +788,7 @@ private fun filtered(chat: Chat): Boolean = (chat.chatInfo.chatSettings?.favorite ?: false) || chat.chatStats.unreadChat || (chat.chatInfo.ntfsEnabled && chat.chatStats.unreadCount > 0) + +fun scrollToBottom(scope: CoroutineScope, listState: LazyListState) { + scope.launch { try { listState.animateScrollToItem(0) } catch (e: Exception) { Log.e(TAG, e.stackTraceToString()) } } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ServersSummaryView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ServersSummaryView.kt index 6219252b54..4e3ee2340c 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ServersSummaryView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ServersSummaryView.kt @@ -620,9 +620,7 @@ fun ModalData.SMPServerSummaryView( ModalView( close = close ) { - ColumnWithScrollBar( - Modifier.fillMaxSize(), - ) { + ColumnWithScrollBar { val bottomPadding = DEFAULT_PADDING AppBarTitle( stringResource(MR.strings.smp_server), @@ -645,9 +643,7 @@ fun ModalData.DetailedXFTPStatsView( ModalView( close = close ) { - ColumnWithScrollBar( - Modifier.fillMaxSize(), - ) { + ColumnWithScrollBar { Box(contentAlignment = Alignment.Center) { val bottomPadding = DEFAULT_PADDING AppBarTitle( @@ -671,9 +667,7 @@ fun ModalData.DetailedSMPStatsView( ModalView( close = close ) { - ColumnWithScrollBar( - Modifier.fillMaxSize(), - ) { + ColumnWithScrollBar { Box(contentAlignment = Alignment.Center) { val bottomPadding = DEFAULT_PADDING AppBarTitle( @@ -697,9 +691,7 @@ fun ModalData.XFTPServerSummaryView( ModalView( close = close ) { - ColumnWithScrollBar( - Modifier.fillMaxSize(), - ) { + ColumnWithScrollBar { Box(contentAlignment = Alignment.Center) { val bottomPadding = DEFAULT_PADDING AppBarTitle( @@ -715,9 +707,7 @@ fun ModalData.XFTPServerSummaryView( @Composable fun ModalData.ServersSummaryView(rh: RemoteHostInfo?, serversSummary: MutableState) { - ColumnWithScrollBar( - Modifier.fillMaxSize(), - ) { + ColumnWithScrollBar { var showUserSelection by remember { mutableStateOf(false) } val selectedUserCategory = remember { stateGetOrPut("selectedUserCategory") { PresentedUserCategory.ALL_USERS } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt index 769a0b83f6..9ca2c1e2cd 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt @@ -11,10 +11,13 @@ import androidx.compose.ui.graphics.Color import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp import chat.simplex.common.model.* import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.views.helpers.* import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.themedBackground +import chat.simplex.common.views.chat.topPaddingToContent import chat.simplex.common.views.newchat.ActiveProfilePicker import chat.simplex.res.MR @@ -22,26 +25,7 @@ import chat.simplex.res.MR fun ShareListView(chatModel: ChatModel, stopped: Boolean) { var searchInList by rememberSaveable { mutableStateOf("") } val oneHandUI = remember { appPrefs.oneHandUI.state } - - Scaffold( - contentColor = LocalContentColor.current, - topBar = { - if (!oneHandUI.value) { - Column { - ShareListToolbar(chatModel, stopped) { searchInList = it.trim() } - Divider() - } - } - }, - bottomBar = { - if (oneHandUI.value) { - Column { - Divider() - ShareListToolbar(chatModel, stopped) { searchInList = it.trim() } - } - } - } - ) { + Box(Modifier.fillMaxSize().themedBackground(bgLayerSize = LocalAppBarHandler.current?.backgroundGraphicsLayerSize, bgLayer = LocalAppBarHandler.current?.backgroundGraphicsLayer)) { val sharedContent = chatModel.sharedContent.value var isMediaOrFileAttachment = false var isVoice = false @@ -69,22 +53,24 @@ fun ShareListView(chatModel: ChatModel, stopped: Boolean) { } null -> {} } - Box(Modifier.padding(it)) { - Column( - modifier = Modifier.fillMaxSize() - ) { - if (chatModel.chats.value.isNotEmpty()) { - ShareList( - chatModel, - search = searchInList, - isMediaOrFileAttachment = isMediaOrFileAttachment, - isVoice = isVoice, - hasSimplexLink = hasSimplexLink, - ) - } else { - EmptyList() - } - } + if (chatModel.chats.value.isNotEmpty()) { + ShareList( + chatModel, + search = searchInList, + isMediaOrFileAttachment = isMediaOrFileAttachment, + isVoice = isVoice, + hasSimplexLink = hasSimplexLink, + ) + } else { + EmptyList() + } + if (oneHandUI.value) { + StatusBarBackground() + } else { + NavigationBarBackground(oneHandUI.value, true) + } + Box(Modifier.align(if (oneHandUI.value) Alignment.BottomStart else Alignment.TopStart)) { + ShareListToolbar(chatModel, stopped) { searchInList = it.trim() } } } } @@ -108,7 +94,6 @@ private fun ShareListToolbar(chatModel: ChatModel, stopped: Boolean, onSearchVal if (showSearch) { BackHandler(onBack = hideSearchOnBack) } - val barButtons = arrayListOf<@Composable RowScope.() -> Unit>() val users by remember { derivedStateOf { chatModel.users.filter { u -> u.user.activeUser || !u.user.hidden } } } val navButton: @Composable RowScope.() -> Unit = { when { @@ -118,13 +103,13 @@ private fun ShareListToolbar(chatModel: ChatModel, stopped: Boolean, onSearchVal .filter { u -> !u.user.activeUser && !u.user.hidden } .all { u -> u.unreadCount == 0 } UserProfileButton(chatModel.currentUser.value?.profile?.image, allRead) { - ModalManager.start.showCustomModal { close -> + ModalManager.start.showCustomModal(keyboardCoversBar = false) { close -> val search = rememberSaveable { mutableStateOf("") } ModalView( { close() }, - endButtons = { - SearchTextField(Modifier.fillMaxWidth(), placeholder = stringResource(MR.strings.search_verb), alwaysVisible = true) { search.value = it } - }, + showSearch = true, + searchAlwaysVisible = true, + onSearchValueChanged = { search.value = it }, content = { ActiveProfilePicker( search = search, @@ -148,31 +133,8 @@ private fun ShareListToolbar(chatModel: ChatModel, stopped: Boolean, onSearchVal }) } } - if (chatModel.chats.value.size >= 8) { - barButtons.add { - IconButton({ showSearch = true }) { - Icon(painterResource(MR.images.ic_search_500), stringResource(MR.strings.search_verb), tint = MaterialTheme.colors.primary) - } - } - } - if (stopped) { - barButtons.add { - IconButton(onClick = { - AlertManager.shared.showAlertMsg( - generalGetString(MR.strings.chat_is_stopped_indication), - generalGetString(MR.strings.you_can_start_chat_via_setting_or_by_restarting_the_app) - ) - }) { - Icon( - painterResource(MR.images.ic_report_filled), - generalGetString(MR.strings.chat_is_stopped_indication), - tint = Color.Red, - ) - } - } - } - DefaultTopAppBar( + DefaultAppBar( navigationButton = navButton, title = { Row(verticalAlignment = Alignment.CenterVertically) { @@ -191,8 +153,29 @@ private fun ShareListToolbar(chatModel: ChatModel, stopped: Boolean, onSearchVal }, onTitleClick = null, showSearch = showSearch, + onTop = !remember { appPrefs.oneHandUI.state }.value, onSearchValueChanged = onSearchValueChanged, - buttons = barButtons + buttons = { + if (chatModel.chats.value.size >= 8) { + IconButton({ showSearch = true }) { + Icon(painterResource(MR.images.ic_search_500), stringResource(MR.strings.search_verb), tint = MaterialTheme.colors.primary) + } + } + if (stopped) { + IconButton(onClick = { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.chat_is_stopped_indication), + generalGetString(MR.strings.you_can_start_chat_via_setting_or_by_restarting_the_app) + ) + }) { + Icon( + painterResource(MR.images.ic_report_filled), + generalGetString(MR.strings.chat_is_stopped_indication), + tint = Color.Red, + ) + } + } + } ) } @@ -211,8 +194,13 @@ private fun ShareList( filteredChats(false, mutableStateOf(false), mutableStateOf(null), search, sorted) } } + val topPaddingToContent = topPaddingToContent() LazyColumnWithScrollBar( - modifier = Modifier.fillMaxSize(), + modifier = Modifier.then(if (oneHandUI.value) Modifier.consumeWindowInsets(WindowInsets.navigationBars.only(WindowInsetsSides.Vertical)) else Modifier).imePadding(), + contentPadding = PaddingValues( + top = if (oneHandUI.value) WindowInsets.statusBars.asPaddingValues().calculateTopPadding() else topPaddingToContent, + bottom = if (oneHandUI.value) WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + AppBarHeight * fontSizeSqrtMultiplier else 0.dp + ), reverseLayout = oneHandUI.value ) { items(chats) { chat -> diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt index 4a3bce7752..2709c7760b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt @@ -23,6 +23,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.* import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatController.stopRemoteHostAndReloadHosts import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.ui.theme.* @@ -137,12 +138,16 @@ fun UserPicker( } } + val oneHandUI = remember { appPrefs.oneHandUI.state } + val iconColor = MaterialTheme.colors.secondaryVariant + val background = if (appPlatform.isAndroid) MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, alpha = 1 - userPickerAlpha()) else MaterialTheme.colors.surface PlatformUserPicker( modifier = Modifier .height(IntrinsicSize.Min) .fillMaxWidth() - .then(if (newChat.isVisible()) Modifier.shadow(8.dp, clip = true) else Modifier) - .background(if (appPlatform.isAndroid) MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, alpha = 1 - userPickerAlpha()) else MaterialTheme.colors.surface) + .then(if (newChat.isVisible()) Modifier.shadow(8.dp, clip = true, ambientColor = background) else Modifier) + .padding(top = if (appPlatform.isDesktop && oneHandUI.value) 7.dp else 0.dp) + .background(background) .padding(bottom = USER_PICKER_SECTION_SPACING - DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL), pickerState = userPickerState ) { @@ -198,12 +203,13 @@ fun UserPicker( UserPickerUsersSection( users = users, onUserClicked = onUserClicked, + iconColor = iconColor, stopped = stopped ) } } else if (currentUser != null) { SectionItemView({ onUserClicked(currentUser) }, 80.dp, padding = PaddingValues(start = 16.dp, end = DEFAULT_PADDING), disabled = stopped) { - ProfilePreview(currentUser.profile, stopped = stopped) + ProfilePreview(currentUser.profile, iconColor = iconColor, stopped = stopped) } } } @@ -234,6 +240,7 @@ fun UserPicker( Column(modifier = Modifier.padding(vertical = DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL)) { UserPickerUsersSection( users = inactiveUsers, + iconColor = iconColor, onUserClicked = onUserClicked, stopped = stopped ) @@ -265,13 +272,15 @@ fun UserPicker( generalGetString(MR.strings.auth_open_chat_profiles), generalGetString(MR.strings.auth_log_in_using_credential) ) { - ModalManager.start.showCustomModal { close -> + ModalManager.start.showCustomModal(keyboardCoversBar = false) { close -> val search = rememberSaveable { mutableStateOf("") } val profileHidden = rememberSaveable { mutableStateOf(false) } ModalView( { close() }, - endButtons = { - SearchTextField(Modifier.fillMaxWidth(), placeholder = stringResource(MR.strings.search_verb), alwaysVisible = true) { search.value = it } + showSearch = true, + searchAlwaysVisible = true, + onSearchValueChanged = { + search.value = it }, content = { UserProfilesView(chatModel, search, profileHidden) }) } @@ -519,6 +528,7 @@ private fun DevicePickerRow( @Composable expect fun UserPickerUsersSection( users: List, + iconColor: Color, stopped: Boolean, onUserClicked: (user: User) -> Unit, ) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/ChatArchiveView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/ChatArchiveView.kt index 6846d1c735..96acea5446 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/ChatArchiveView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/ChatArchiveView.kt @@ -46,9 +46,7 @@ fun ChatArchiveLayout( saveArchive: () -> Unit, deleteArchiveAlert: () -> Unit ) { - ColumnWithScrollBar( - Modifier.fillMaxWidth(), - ) { + ColumnWithScrollBar { AppBarTitle(title) SectionView(stringResource(MR.strings.chat_archive_section)) { SettingsActionItem( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.kt index b73a0ca0bc..654d250274 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.kt @@ -203,7 +203,7 @@ fun DatabaseEncryptionLayout( Layout() } } else { - ColumnWithScrollBar(Modifier.fillMaxWidth(), maxIntrinsicSize = true) { + ColumnWithScrollBar(maxIntrinsicSize = true) { Layout() } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseErrorView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseErrorView.kt index 333c73e195..9264ca69af 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseErrorView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseErrorView.kt @@ -77,10 +77,7 @@ fun DatabaseErrorView( Text(String.format(generalGetString(MR.strings.database_migrations), ms.joinToString(", "))) } - ColumnWithScrollBar( - Modifier.fillMaxSize(), - verticalArrangement = Arrangement.Center, - ) { + ColumnWithScrollBarNoAppBar(Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center) { val buttonEnabled = validKey(dbKey.value) && !progressIndicator.value when (val status = chatDbStatus.value) { is DBMigrationResult.ErrorNotADatabase -> diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt index b287847ace..d36bd255e3 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt @@ -156,9 +156,7 @@ fun DatabaseLayout( val stopped = !runChat val operationsDisabled = (!stopped || progressIndicator) && !chatModel.desktopNoUserNoRemote - ColumnWithScrollBar( - Modifier.fillMaxWidth(), - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.your_chat_database)) if (!chatModel.desktopNoUserNoRemote) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AppBarTitle.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AppBarTitle.kt new file mode 100644 index 0000000000..195ec020e5 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AppBarTitle.kt @@ -0,0 +1,71 @@ +package chat.simplex.common.views.helpers + +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.graphics.* +import androidx.compose.ui.unit.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.chatlist.* +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource +import kotlin.math.absoluteValue + +@Composable +fun AppBarTitle(title: String, hostDevice: Pair? = null, withPadding: Boolean = true, bottomPadding: Dp = DEFAULT_PADDING * 1.5f + 8.dp) { + val handler = LocalAppBarHandler.current + val connection = handler?.connection + LaunchedEffect(title) { + handler?.title?.value = title + } + val theme = CurrentColors.collectAsState() + val titleColor = MaterialTheme.appColors.title + val brush = if (theme.value.base == DefaultTheme.SIMPLEX) + Brush.linearGradient(listOf(titleColor.darker(0.2f), titleColor.lighter(0.35f)), Offset(0f, Float.POSITIVE_INFINITY), Offset(Float.POSITIVE_INFINITY, 0f)) + else // color is not updated when changing themes if I pass null here + Brush.linearGradient(listOf(titleColor, titleColor), Offset(0f, Float.POSITIVE_INFINITY), Offset(Float.POSITIVE_INFINITY, 0f)) + Column { + Text( + title, + Modifier + .padding(start = if (withPadding) DEFAULT_PADDING else 0.dp, top = DEFAULT_PADDING_HALF, end = if (withPadding) DEFAULT_PADDING else 0.dp,) + .graphicsLayer { + alpha = bottomTitleAlpha(connection) + }, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.h1.copy(brush = brush), + color = MaterialTheme.colors.primaryVariant, + textAlign = TextAlign.Start + ) + if (hostDevice != null) { + Box(Modifier.padding(start = if (withPadding) DEFAULT_PADDING else 0.dp, end = if (withPadding) DEFAULT_PADDING else 0.dp).graphicsLayer { + alpha = bottomTitleAlpha(connection) + }) { + HostDeviceTitle(hostDevice) + } + } + Spacer(Modifier.height(bottomPadding)) + } +} + +private fun bottomTitleAlpha(connection: CollapsingAppBarNestedScrollConnection?) = + if ((connection?.appBarOffset ?: 0f).absoluteValue < AppBarHandler.appBarMaxHeightPx / 3) 1f + else ((AppBarHandler.appBarMaxHeightPx) + (connection?.appBarOffset ?: 0f) / 1.5f).coerceAtLeast(0f) / AppBarHandler.appBarMaxHeightPx + +@Composable +private fun HostDeviceTitle(hostDevice: Pair, extraPadding: Boolean = false) { + Row(Modifier.fillMaxWidth().padding(top = 5.dp, bottom = if (extraPadding) DEFAULT_PADDING * 2 else DEFAULT_PADDING_HALF), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start) { + DevicePill( + active = true, + onClick = {}, + actionButtonVisible = false, + icon = painterResource(if (hostDevice.first == null) MR.images.ic_desktop else MR.images.ic_smartphone_300), + text = hostDevice.second + ) + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/BlurModifier.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/BlurModifier.kt new file mode 100644 index 0000000000..096b6c55ac --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/BlurModifier.kt @@ -0,0 +1,139 @@ +package chat.simplex.common.views.helpers + +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.runtime.State +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.* +import androidx.compose.ui.graphics.drawscope.clipRect +import androidx.compose.ui.graphics.drawscope.translate +import androidx.compose.ui.graphics.layer.GraphicsLayer +import androidx.compose.ui.graphics.layer.drawLayer +import androidx.compose.ui.unit.* +import chat.simplex.common.platform.appPlatform +import chat.simplex.common.ui.theme.CurrentColors + +fun Modifier.blurredBackgroundModifier( + keyboardInset: WindowInsets, + handler: AppBarHandler?, + blurRadius: State, + prefAlpha: State, + keyboardCoversBar: Boolean, + onTop: Boolean, + density: Density +): Modifier { + val graphicsLayer = handler?.graphicsLayer + val backgroundGraphicsLayer = handler?.backgroundGraphicsLayer + val backgroundGraphicsLayerSize = handler?.backgroundGraphicsLayerSize + if (handler == null || graphicsLayer == null || backgroundGraphicsLayer == null || blurRadius.value == 0 || prefAlpha.value == 1f || backgroundGraphicsLayerSize === null) + return this + + return if (appPlatform.isAndroid) { + this.androidBlurredModifier(keyboardInset, blurRadius.value, keyboardCoversBar, onTop, graphicsLayer, backgroundGraphicsLayer, backgroundGraphicsLayerSize, density) + } else { + this.desktopBlurredModifier(keyboardInset, blurRadius, keyboardCoversBar, onTop, graphicsLayer, backgroundGraphicsLayer, backgroundGraphicsLayerSize, density) + } +} + +// this is more performant version than for Android but can't be used on desktop because on first frame it shows transparent view +// which is very noticeable on desktop and unnoticeable on Android +private fun Modifier.androidBlurredModifier( + keyboardInset: WindowInsets, + blurRadius: Int, + keyboardCoversBar: Boolean, + onTop: Boolean, + graphicsLayer: GraphicsLayer, + backgroundGraphicsLayer: GraphicsLayer, + backgroundGraphicsLayerSize: State, + density: Density +): Modifier = this + .graphicsLayer { + renderEffect = if (blurRadius > 0) BlurEffect(blurRadius.dp.toPx(), blurRadius.dp.toPx()) else null + clip = blurRadius > 0 + } + .graphicsLayer { + if (!onTop) { + val bgSize = when { + backgroundGraphicsLayerSize.value.height == 0 && backgroundGraphicsLayer.size.height != 0 -> backgroundGraphicsLayer.size.height + backgroundGraphicsLayerSize.value.height == 0 -> graphicsLayer.size.height + else -> backgroundGraphicsLayerSize.value.height + } + val keyboardHeightCovered = if (!keyboardCoversBar) keyboardInset.getBottom(density) else 0 + translationY = -bgSize + size.height + keyboardHeightCovered + } + } + .drawBehind { + drawRect(Color.Black) + if (onTop) { + clipRect { + if (backgroundGraphicsLayer.size != IntSize.Zero) { + drawLayer(backgroundGraphicsLayer) + } else { + drawRect(CurrentColors.value.colors.background, size = Size(graphicsLayer.size.width.toFloat(), graphicsLayer.size.height.toFloat())) + } + drawLayer(graphicsLayer) + } + } else { + if (backgroundGraphicsLayer.size != IntSize.Zero) { + drawLayer(backgroundGraphicsLayer) + } else { + drawRect(CurrentColors.value.colors.background, size = Size(graphicsLayer.size.width.toFloat(), graphicsLayer.size.height.toFloat())) + } + drawLayer(graphicsLayer) + } + } + .graphicsLayer { + if (!onTop) { + val bgSize = when { + backgroundGraphicsLayerSize.value.height == 0 && backgroundGraphicsLayer.size.height != 0 -> backgroundGraphicsLayer.size.height + backgroundGraphicsLayerSize.value.height == 0 -> graphicsLayer.size.height + else -> backgroundGraphicsLayerSize.value.height + } + val keyboardHeightCovered = if (!keyboardCoversBar) keyboardInset.getBottom(density) else 0 + translationY -= -bgSize + size.height + keyboardHeightCovered + } + } + +private fun Modifier.desktopBlurredModifier( + keyboardInset: WindowInsets, + blurRadius: State, + keyboardCoversBar: Boolean, + onTop: Boolean, + graphicsLayer: GraphicsLayer, + backgroundGraphicsLayer: GraphicsLayer, + backgroundGraphicsLayerSize: State, + density: Density +): Modifier = this + .graphicsLayer { + renderEffect = if (blurRadius.value > 0) BlurEffect(blurRadius.value.dp.toPx(), blurRadius.value.dp.toPx()) else null + clip = blurRadius.value > 0 + } + .drawBehind { + drawRect(Color.Black) + if (onTop) { + clipRect { + if (backgroundGraphicsLayer.size != IntSize.Zero) { + drawLayer(backgroundGraphicsLayer) + } else { + drawRect(CurrentColors.value.colors.background, size = Size(graphicsLayer.size.width.toFloat(), graphicsLayer.size.height.toFloat())) + } + drawLayer(graphicsLayer) + } + } else { + val bgSize = when { + backgroundGraphicsLayerSize.value.height == 0 && backgroundGraphicsLayer.size.height != 0 -> backgroundGraphicsLayer.size.height + backgroundGraphicsLayerSize.value.height == 0 -> graphicsLayer.size.height + else -> backgroundGraphicsLayerSize.value.height + } + val keyboardHeightCovered = if (!keyboardCoversBar) keyboardInset.getBottom(density) else 0 + translate(top = -bgSize + size.height + keyboardHeightCovered) { + if (backgroundGraphicsLayer.size != IntSize.Zero) { + drawLayer(backgroundGraphicsLayer) + } else { + drawRect(CurrentColors.value.colors.background, size = Size(graphicsLayer.size.width.toFloat(), graphicsLayer.size.height.toFloat())) + } + drawLayer(graphicsLayer) + } + } + } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatWallpaper.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatWallpaper.kt index 2941b748c7..c1a76d7bf8 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatWallpaper.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatWallpaper.kt @@ -1,11 +1,13 @@ package chat.simplex.common.views.helpers +import androidx.compose.runtime.* import androidx.compose.ui.draw.CacheDrawScope import androidx.compose.ui.draw.DrawResult import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.drawscope.* +import androidx.compose.ui.graphics.layer.GraphicsLayer import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.* import chat.simplex.common.model.ChatController.appPrefs @@ -381,7 +383,14 @@ private fun drawToBitmap(image: ImageBitmap, imageScale: Float, tint: Color, siz return bitmap } -fun CacheDrawScope.chatViewBackground(image: ImageBitmap, imageType: WallpaperType, background: Color, tint: Color): DrawResult { +fun CacheDrawScope.chatViewBackground( + image: ImageBitmap, + imageType: WallpaperType, + background: Color, + tint: Color, + graphicsLayerSize: MutableState? = null, + backgroundGraphicsLayer: GraphicsLayer? = null +): DrawResult { val imageScale = if (imageType is WallpaperType.Preset) { (imageType.scale ?: 1f) * imageType.predefinedImageScale } else if (imageType is WallpaperType.Image && imageType.scaleType == WallpaperScaleType.REPEAT) { @@ -396,53 +405,55 @@ fun CacheDrawScope.chatViewBackground(image: ImageBitmap, imageType: WallpaperTy } return onDrawBehind { - val quality = if (appPlatform.isAndroid) FilterQuality.High else FilterQuality.Low - drawRect(background) - when (imageType) { - is WallpaperType.Preset -> drawImage(image) - is WallpaperType.Image -> when (val scaleType = imageType.scaleType ?: WallpaperScaleType.FILL) { - WallpaperScaleType.REPEAT -> drawImage(image) - WallpaperScaleType.FILL, WallpaperScaleType.FIT -> { - clipRect { - val scale = scaleType.contentScale.computeScaleFactor(Size(image.width.toFloat(), image.height.toFloat()), Size(size.width, size.height)) - val scaledWidth = (image.width * scale.scaleX).roundToInt() - val scaledHeight = (image.height * scale.scaleY).roundToInt() - // Large image will cause freeze - if (image.width > 4320 || image.height > 4320) return@clipRect + copyBackgroundToAppBar(graphicsLayerSize, backgroundGraphicsLayer) { + val quality = if (appPlatform.isAndroid) FilterQuality.High else FilterQuality.Low + drawRect(background) + when (imageType) { + is WallpaperType.Preset -> drawImage(image) + is WallpaperType.Image -> when (val scaleType = imageType.scaleType ?: WallpaperScaleType.FILL) { + WallpaperScaleType.REPEAT -> drawImage(image) + WallpaperScaleType.FILL, WallpaperScaleType.FIT -> { + clipRect { + val scale = scaleType.contentScale.computeScaleFactor(Size(image.width.toFloat(), image.height.toFloat()), Size(size.width, size.height)) + val scaledWidth = (image.width * scale.scaleX).roundToInt() + val scaledHeight = (image.height * scale.scaleY).roundToInt() + // Large image will cause freeze + if (image.width > 4320 || image.height > 4320) return@clipRect - drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality) - if (scaleType == WallpaperScaleType.FIT) { - if (scaledWidth < size.width) { - // has black lines at left and right sides - var x = (size.width - scaledWidth) / 2 - while (x > 0) { - drawImage(image, dstOffset = IntOffset(x = (x - scaledWidth).roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality) - x -= scaledWidth - } - x = size.width - (size.width - scaledWidth) / 2 - while (x < size.width) { - drawImage(image, dstOffset = IntOffset(x = x.roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality) - x += scaledWidth - } - } else { - // has black lines at top and bottom sides - var y = (size.height - scaledHeight) / 2 - while (y > 0) { - drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = (y - scaledHeight).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality) - y -= scaledHeight - } - y = size.height - (size.height - scaledHeight) / 2 - while (y < size.height) { - drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = y.roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality) - y += scaledHeight + drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality) + if (scaleType == WallpaperScaleType.FIT) { + if (scaledWidth < size.width) { + // has black lines at left and right sides + var x = (size.width - scaledWidth) / 2 + while (x > 0) { + drawImage(image, dstOffset = IntOffset(x = (x - scaledWidth).roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality) + x -= scaledWidth + } + x = size.width - (size.width - scaledWidth) / 2 + while (x < size.width) { + drawImage(image, dstOffset = IntOffset(x = x.roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality) + x += scaledWidth + } + } else { + // has black lines at top and bottom sides + var y = (size.height - scaledHeight) / 2 + while (y > 0) { + drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = (y - scaledHeight).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality) + y -= scaledHeight + } + y = size.height - (size.height - scaledHeight) / 2 + while (y < size.height) { + drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = y.roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality) + y += scaledHeight + } } } } + drawRect(tint) } - drawRect(tint) } + is WallpaperType.Empty -> {} } - is WallpaperType.Empty -> {} } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChooseAttachmentView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChooseAttachmentView.kt index aa3c4560ea..33cf7c2263 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChooseAttachmentView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChooseAttachmentView.kt @@ -19,6 +19,8 @@ fun ChooseAttachmentView(attachmentOption: MutableState, hide Box( modifier = Modifier .fillMaxWidth() + .navigationBarsPadding() + .imePadding() .wrapContentHeight() .onFocusChanged { focusState -> if (!focusState.hasFocus) hide() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CloseSheetBar.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CloseSheetBar.kt deleted file mode 100644 index 104c05309c..0000000000 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CloseSheetBar.kt +++ /dev/null @@ -1,181 +0,0 @@ -package chat.simplex.common.views.helpers - -import androidx.compose.foundation.layout.* -import androidx.compose.material.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.desktop.ui.tooling.preview.Preview -import androidx.compose.foundation.background -import androidx.compose.ui.draw.* -import androidx.compose.ui.graphics.* -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.* -import chat.simplex.common.platform.appPlatform -import chat.simplex.common.ui.theme.* -import chat.simplex.common.views.chatlist.DevicePill -import chat.simplex.res.MR -import dev.icerock.moko.resources.compose.painterResource -import kotlin.math.absoluteValue - -@Composable -fun CloseSheetBar(close: (() -> Unit)?, showClose: Boolean = true, tintColor: Color = if (close != null) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, arrangement: Arrangement.Vertical = Arrangement.Top, closeBarTitle: String? = null, barPaddingValues: PaddingValues = PaddingValues(horizontal = AppBarHorizontalPadding), endButtons: @Composable RowScope.() -> Unit = {}) { - var rowModifier = Modifier - .fillMaxWidth() - .height(AppBarHeight * fontSizeSqrtMultiplier) - val themeBackgroundMix = MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.97f) - if (!closeBarTitle.isNullOrEmpty()) { - rowModifier = rowModifier.background(themeBackgroundMix) - } - val handler = LocalAppBarHandler.current - val connection = LocalAppBarHandler.current?.connection - val title = remember(handler?.title?.value) { handler?.title ?: mutableStateOf("") } - - Column( - verticalArrangement = arrangement, - modifier = Modifier - .fillMaxWidth() - .heightIn(min = AppBarHeight * fontSizeSqrtMultiplier) - .drawWithCache { - val backgroundColor = if (appPlatform.isDesktop && connection != null) themeBackgroundMix.copy(alpha = topTitleAlpha(connection)) else Color.Transparent - onDrawBehind { - if (appPlatform.isDesktop) { - drawRect(backgroundColor) - } - } - } - ) { - Row( - modifier = Modifier.padding(barPaddingValues), - content = { - Row( - rowModifier, - verticalAlignment = Alignment.CenterVertically - ) { - if (showClose) { - NavigationButtonBack(tintColor = tintColor, onButtonClicked = close) - } else { - Spacer(Modifier) - } - if (!closeBarTitle.isNullOrEmpty()) { - Row( - Modifier.weight(1f), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - closeBarTitle, - fontWeight = FontWeight.SemiBold, - maxLines = 1 - ) - } - } else if (title.value.isNotEmpty() && connection != null) { - Row( - Modifier - .padding(start = if (showClose) 0.dp else DEFAULT_PADDING_HALF) - .weight(1f) // hides the title if something wants full width (eg, search field in chat profiles screen) - .graphicsLayer { - alpha = topTitleAlpha((connection)) - } - .padding(start = 4.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - title.value, - fontWeight = FontWeight.SemiBold, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } - } else { - Spacer(Modifier.weight(1f)) - } - Row { - endButtons() - } - } - } - ) - if (closeBarTitle.isNullOrEmpty() && title.value.isNotEmpty() && connection != null) { - Divider( - Modifier - .graphicsLayer { - alpha = topTitleAlpha(connection) - } - ) - } - } -} - -@Composable -fun AppBarTitle(title: String, hostDevice: Pair? = null, withPadding: Boolean = true, bottomPadding: Dp = DEFAULT_PADDING * 1.5f + 8.dp) { - val handler = LocalAppBarHandler.current - val connection = handler?.connection - LaunchedEffect(title) { - handler?.title?.value = title - } - val theme = CurrentColors.collectAsState() - val titleColor = MaterialTheme.appColors.title - val brush = if (theme.value.base == DefaultTheme.SIMPLEX) - Brush.linearGradient(listOf(titleColor.darker(0.2f), titleColor.lighter(0.35f)), Offset(0f, Float.POSITIVE_INFINITY), Offset(Float.POSITIVE_INFINITY, 0f)) - else // color is not updated when changing themes if I pass null here - Brush.linearGradient(listOf(titleColor, titleColor), Offset(0f, Float.POSITIVE_INFINITY), Offset(Float.POSITIVE_INFINITY, 0f)) - Column { - Text( - title, - Modifier - .padding(start = if (withPadding) DEFAULT_PADDING else 0.dp, top = DEFAULT_PADDING_HALF, end = if (withPadding) DEFAULT_PADDING else 0.dp,) - .graphicsLayer { - alpha = bottomTitleAlpha(connection) - }, - overflow = TextOverflow.Ellipsis, - style = MaterialTheme.typography.h1.copy(brush = brush), - color = MaterialTheme.colors.primaryVariant, - textAlign = TextAlign.Start - ) - if (hostDevice != null) { - Box(Modifier.padding(start = if (withPadding) DEFAULT_PADDING else 0.dp, end = if (withPadding) DEFAULT_PADDING else 0.dp).graphicsLayer { - alpha = bottomTitleAlpha(connection) - }) { - HostDeviceTitle(hostDevice) - } - } - Spacer(Modifier.height(bottomPadding)) - } -} - -private fun topTitleAlpha(connection: CollapsingAppBarNestedScrollConnection) = - if (connection.appBarOffset.absoluteValue < AppBarHandler.appBarMaxHeightPx / 3) 0f - else ((-connection.appBarOffset * 1.5f) / (AppBarHandler.appBarMaxHeightPx)).coerceIn(0f, 1f) - -private fun bottomTitleAlpha(connection: CollapsingAppBarNestedScrollConnection?) = - if ((connection?.appBarOffset ?: 0f).absoluteValue < AppBarHandler.appBarMaxHeightPx / 3) 1f - else ((AppBarHandler.appBarMaxHeightPx) + (connection?.appBarOffset ?: 0f) / 1.5f).coerceAtLeast(0f) / AppBarHandler.appBarMaxHeightPx - -@Composable -private fun HostDeviceTitle(hostDevice: Pair, extraPadding: Boolean = false) { - Row(Modifier.fillMaxWidth().padding(top = 5.dp, bottom = if (extraPadding) DEFAULT_PADDING * 2 else DEFAULT_PADDING_HALF), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start) { - DevicePill( - active = true, - onClick = {}, - actionButtonVisible = false, - icon = painterResource(if (hostDevice.first == null) MR.images.ic_desktop else MR.images.ic_smartphone_300), - text = hostDevice.second - ) - } -} - -@Preview/*( - uiMode = Configuration.UI_MODE_NIGHT_YES, - showBackground = true, - name = "Dark Mode" -)*/ -@Composable -fun PreviewCloseSheetBar() { - SimpleXTheme { - CloseSheetBar(close = {}) - } -} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CollapsingAppBar.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CollapsingAppBar.kt index 4410f7ada5..50942169b3 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CollapsingAppBar.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CollapsingAppBar.kt @@ -3,15 +3,67 @@ package chat.simplex.common.views.helpers import androidx.compose.foundation.ScrollState import androidx.compose.foundation.lazy.LazyListState import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.* import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.layer.GraphicsLayer +import androidx.compose.ui.graphics.layer.drawLayer +import androidx.compose.ui.graphics.rememberGraphicsLayer import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollSource -import androidx.compose.ui.unit.Velocity +import androidx.compose.ui.unit.IntSize +import chat.simplex.common.model.ChatController.appPrefs val LocalAppBarHandler: ProvidableCompositionLocal = staticCompositionLocalOf { null } +@Composable +fun rememberAppBarHandler(key1: Any? = null, key2: Any? = null, keyboardCoversBar: Boolean = true): AppBarHandler { + val graphicsLayer = rememberGraphicsLayer() + val backgroundGraphicsLayer = rememberGraphicsLayer() + return remember(key1, key2) { AppBarHandler(graphicsLayer, backgroundGraphicsLayer, keyboardCoversBar) } +} + +@Composable +fun adjustAppBarHandler(handler: AppBarHandler): AppBarHandler { + val graphicsLayer = rememberGraphicsLayer() + val backgroundGraphicsLayer = rememberGraphicsLayer() + if (handler.graphicsLayer == null || handler.graphicsLayer?.isReleased == true || handler.backgroundGraphicsLayer?.isReleased == true) { + handler.graphicsLayer = graphicsLayer + handler.backgroundGraphicsLayer = backgroundGraphicsLayer + } + return handler +} + +fun Modifier.copyViewToAppBar(blurRadius: Int, graphicsLayer: GraphicsLayer?): Modifier { + return if (blurRadius > 0 && graphicsLayer != null) { + this.drawWithContent { + graphicsLayer.record { + this@drawWithContent.drawContent() + } + drawLayer(graphicsLayer) + } + } else this +} + +fun DrawScope.copyBackgroundToAppBar(graphicsLayerSize: MutableState?, backgroundGraphicsLayer: GraphicsLayer?, scope: DrawScope.() -> Unit) { + val blurRadius = appPrefs.appearanceBarsBlurRadius.get() + if (blurRadius > 0 && graphicsLayerSize != null && backgroundGraphicsLayer != null) { + graphicsLayerSize.value = backgroundGraphicsLayer.size + backgroundGraphicsLayer.record { + scope() + } + drawLayer(backgroundGraphicsLayer) + } else { + scope() + } +} + @Stable class AppBarHandler( + var graphicsLayer: GraphicsLayer?, + var backgroundGraphicsLayer: GraphicsLayer?, + val keyboardCoversBar: Boolean = true, listState: LazyListState = LazyListState(0, 0), scrollState: ScrollState = ScrollState(initial = 0) ) { @@ -24,6 +76,8 @@ class AppBarHandler( val connection = CollapsingAppBarNestedScrollConnection() + val backgroundGraphicsLayerSize: MutableState = mutableStateOf(IntSize.Zero) + companion object { var appBarMaxHeightPx: Int = 0 } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultDropdownMenu.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultDropdownMenu.kt index 267fc86462..1f00af2809 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultDropdownMenu.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultDropdownMenu.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.unit.dp @Composable fun DefaultDropdownMenu( showMenu: MutableState, + modifier: Modifier = Modifier, offset: DpOffset = DpOffset(0.dp, 0.dp), dropdownMenuItems: (@Composable () -> Unit)? ) { @@ -23,7 +24,7 @@ fun DefaultDropdownMenu( DropdownMenu( expanded = showMenu.value, onDismissRequest = { showMenu.value = false }, - modifier = Modifier + modifier = modifier .widthIn(min = 250.dp) .background(MaterialTheme.colors.surface) .padding(vertical = 4.dp), diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultTopAppBar.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultTopAppBar.kt index 28e9a997ae..cf0c5f7e96 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultTopAppBar.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultTopAppBar.kt @@ -3,44 +3,120 @@ package chat.simplex.common.views.helpers import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.material.* -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.* import androidx.compose.ui.graphics.* -import androidx.compose.ui.unit.Dp +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.* import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource -import androidx.compose.ui.unit.dp +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.chat.item.CenteredRowLayout import chat.simplex.res.MR +import kotlin.math.absoluteValue @Composable -fun DefaultTopAppBar( +fun DefaultAppBar( navigationButton: (@Composable RowScope.() -> Unit)? = null, - title: (@Composable () -> Unit)?, + title: (@Composable () -> Unit)? = null, + fixedTitleText: String? = null, onTitleClick: (() -> Unit)? = null, - showSearch: Boolean, - onSearchValueChanged: (String) -> Unit, - buttons: List<@Composable RowScope.() -> Unit> = emptyList(), + onTop: Boolean, + showSearch: Boolean = false, + searchAlwaysVisible: Boolean = false, + onSearchValueChanged: (String) -> Unit = {}, + buttons: @Composable RowScope.() -> Unit = {}, ) { // If I just disable clickable modifier when don't need it, it will stop passing clicks to search. Replacing the whole modifier val modifier = if (!showSearch) { Modifier.clickable(enabled = onTitleClick != null, onClick = onTitleClick ?: { }) - } else Modifier + } else Modifier.imePadding() - TopAppBar( - modifier = modifier, - title = { - if (!showSearch) { - title?.invoke() - } else { - SearchTextField(Modifier.fillMaxWidth(), alwaysVisible = false, onValueChange = onSearchValueChanged) + val themeBackgroundMix = MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.97f) + val prefAlpha = remember { appPrefs.inAppBarsAlpha.state } + val handler = LocalAppBarHandler.current + val connection = LocalAppBarHandler.current?.connection + val titleText = remember(handler?.title?.value, fixedTitleText) { + if (fixedTitleText != null) { + mutableStateOf(fixedTitleText) + } else { + handler?.title ?: mutableStateOf("") + } + } + val keyboardInset = WindowInsets.ime + Box(modifier) { + val density = LocalDensity.current + val blurRadius = remember { appPrefs.appearanceBarsBlurRadius.state } + Box(Modifier + .matchParentSize() + .blurredBackgroundModifier(keyboardInset, handler, blurRadius, prefAlpha, handler?.keyboardCoversBar == true, onTop, density) + .drawWithCache { + // store it as a variable, don't put it inside if without holding it here. Compiler don't see it changes otherwise + val alpha = prefAlpha.value + val backgroundColor = if (title != null || fixedTitleText != null || connection == null || !onTop) { + themeBackgroundMix.copy(alpha) + } else { + themeBackgroundMix.copy(topTitleAlpha(false, connection)) + } + onDrawBehind { + drawRect(backgroundColor) + } } - }, - backgroundColor = MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.97f), - navigationIcon = navigationButton, - buttons = if (!showSearch) buttons else emptyList(), - centered = !showSearch, + ) + Box( + Modifier + .fillMaxWidth() + .then(if (!onTop) Modifier.navigationBarsPadding() else Modifier) + .heightIn(min = AppBarHeight * fontSizeSqrtMultiplier) + ) { + AppBar( + title = { + if (showSearch) { + SearchTextField(Modifier.fillMaxWidth(), alwaysVisible = searchAlwaysVisible, reducedCloseButtonPadding = 12.dp, onValueChange = onSearchValueChanged) + } else if (title != null) { + title() + } else if (titleText.value.isNotEmpty() && connection != null) { + Row( + Modifier + .graphicsLayer { + alpha = if (fixedTitleText != null) 1f else topTitleAlpha(true, connection) + } + ) { + Text( + titleText.value, + fontWeight = FontWeight.SemiBold, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + }, + navigationIcon = navigationButton, + buttons = if (!showSearch) buttons else {{}}, + centered = !showSearch && (title != null || !onTop), + onTop = onTop, + ) + AppBarDivider(onTop, title != null || fixedTitleText != null, connection) + } + } +} + + +@Composable +fun CallAppBar( + title: @Composable () -> Unit, + onBack: () -> Unit +) { + AppBar( + title, + navigationIcon = { NavigationButtonBack(tintColor = Color(0xFFFFFFD8), onButtonClicked = onBack) }, + centered = false, + onTop = true ) } @@ -83,58 +159,107 @@ fun NavigationButtonMenu(onButtonClicked: () -> Unit) { } @Composable -private fun TopAppBar( +private fun BoxScope.AppBarDivider(onTop: Boolean, fixedAlpha: Boolean, connection: CollapsingAppBarNestedScrollConnection?) { + if (connection != null) { + Divider( + Modifier + .align(if (onTop) Alignment.BottomStart else Alignment.TopStart) + .graphicsLayer { + alpha = if (!onTop || fixedAlpha) 1f else topTitleAlpha(false, connection, 1f) + } + ) + } else { + Divider(Modifier.align(if (onTop) Alignment.BottomStart else Alignment.TopStart)) + } +} + +@Composable +private fun AppBar( title: @Composable () -> Unit, modifier: Modifier = Modifier, navigationIcon: @Composable (RowScope.() -> Unit)? = null, - buttons: List<@Composable RowScope.() -> Unit> = emptyList(), - backgroundColor: Color = MaterialTheme.colors.primarySurface, + buttons: @Composable RowScope.() -> Unit = {}, centered: Boolean, + onTop: Boolean, ) { - Box( - modifier - .fillMaxWidth() - .height(AppBarHeight * fontSizeSqrtMultiplier) - .background(backgroundColor) - .padding(horizontal = 4.dp), - contentAlignment = Alignment.CenterStart, + val adjustedModifier = modifier + .then(if (onTop) Modifier.statusBarsPadding() else Modifier) + .height(AppBarHeight * fontSizeSqrtMultiplier) + .fillMaxWidth() + .padding(horizontal = AppBarHorizontalPadding) + if (centered) { + AppBarCenterAligned(adjustedModifier, title, navigationIcon, buttons) + } else { + AppBarStartAligned(adjustedModifier, title, navigationIcon, buttons) + } +} + +@Composable +private fun AppBarStartAligned( + modifier: Modifier, + title: @Composable () -> Unit, + navigationIcon: @Composable (RowScope.() -> Unit)? = null, + buttons: @Composable RowScope.() -> Unit +) { + Row( + modifier, + verticalAlignment = Alignment.CenterVertically ) { if (navigationIcon != null) { - Row( - Modifier - .fillMaxHeight() - .width(TitleInsetWithIcon - AppBarHorizontalPadding), - verticalAlignment = Alignment.CenterVertically, - content = navigationIcon - ) + navigationIcon() + Spacer(Modifier.width(AppBarHorizontalPadding)) + } else { + Spacer(Modifier.width(DEFAULT_PADDING)) + } + Row(Modifier + .weight(1f) + .padding(end = DEFAULT_PADDING_HALF) + ) { + title() } Row( - Modifier - .fillMaxHeight() - .fillMaxWidth(), horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically, ) { - buttons.forEach { it() } - } - val startPadding = if (navigationIcon != null) TitleInsetWithIcon else TitleInsetWithoutIcon - val endPadding = (buttons.size * 50f).dp - Box( - Modifier - .fillMaxWidth() - .padding( - start = if (centered) kotlin.math.max(startPadding.value, endPadding.value).dp else startPadding, - end = if (centered) kotlin.math.max(startPadding.value, endPadding.value).dp else endPadding - ), - contentAlignment = Alignment.Center - ) { - title() + buttons() } } } +@Composable +private fun AppBarCenterAligned( + modifier: Modifier, + title: @Composable () -> Unit, + navigationIcon: @Composable (RowScope.() -> Unit)? = null, + buttons: @Composable RowScope.() -> Unit, +) { + CenteredRowLayout(modifier) { + if (navigationIcon != null) { + Row( + Modifier.padding(end = AppBarHorizontalPadding), + verticalAlignment = Alignment.CenterVertically, + content = navigationIcon + ) + } else { + Spacer(Modifier) + } + Row( + Modifier.padding(end = DEFAULT_PADDING_HALF) + ) { + title() + } + Row( + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically, + ) { + buttons() + } + } +} + +private fun topTitleAlpha(text: Boolean, connection: CollapsingAppBarNestedScrollConnection, alpha: Float = appPrefs.inAppBarsAlpha.get()) = + if (connection.appBarOffset.absoluteValue < AppBarHandler.appBarMaxHeightPx / 3) 0f + else ((-connection.appBarOffset * 1.5f) / (AppBarHandler.appBarMaxHeightPx)).coerceIn(0f, if (text) 1f else alpha) + val AppBarHeight = 56.dp -val AppBarHorizontalPadding = 4.dp -val BottomAppBarHeight = 60.dp -private val TitleInsetWithoutIcon = DEFAULT_PADDING - AppBarHorizontalPadding -val TitleInsetWithIcon = 72.dp +val AppBarHorizontalPadding = 2.dp diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt index 4c35e72701..c181f74e99 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt @@ -6,12 +6,14 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.* +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatModel import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.chatlist.StatusBarBackground import kotlinx.coroutines.flow.MutableStateFlow import java.util.concurrent.atomic.AtomicBoolean import kotlin.math.min @@ -21,24 +23,40 @@ import kotlin.math.sqrt fun ModalView( close: () -> Unit, showClose: Boolean = true, + showAppBar: Boolean = true, enableClose: Boolean = true, - background: Color = MaterialTheme.colors.background, + background: Color = Color.Unspecified, modifier: Modifier = Modifier, - closeOnTop: Boolean = true, + showSearch: Boolean = false, + searchAlwaysVisible: Boolean = false, + onSearchValueChanged: (String) -> Unit = {}, endButtons: @Composable RowScope.() -> Unit = {}, - content: @Composable () -> Unit, + content: @Composable BoxScope.() -> Unit, ) { - if (showClose) { + if (showClose && showAppBar) { BackHandler(enabled = enableClose, onBack = close) } + val oneHandUI = remember { appPrefs.oneHandUI.state } Surface(Modifier.fillMaxSize(), contentColor = LocalContentColor.current) { - Column(if (background != MaterialTheme.colors.background) Modifier.background(background) else Modifier.themedBackground()) { - if (closeOnTop) { - CloseSheetBar(if (enableClose) close else null, showClose, endButtons = endButtons) - } + Box(if (background != Color.Unspecified) Modifier.background(background) else Modifier.themedBackground(bgLayerSize = LocalAppBarHandler.current?.backgroundGraphicsLayerSize, bgLayer = LocalAppBarHandler.current?.backgroundGraphicsLayer)) { Box(modifier = modifier) { content() } + if (showAppBar) { + if (oneHandUI.value) { + StatusBarBackground() + } + Box(Modifier.align(if (oneHandUI.value) Alignment.BottomStart else Alignment.TopStart)) { + DefaultAppBar( + navigationButton = if (showClose) {{ NavigationButtonBack(onButtonClicked = if (enableClose) close else null) }} else null, + onTop = !oneHandUI.value, + showSearch = showSearch, + searchAlwaysVisible = searchAlwaysVisible, + onSearchValueChanged = onSearchValueChanged, + buttons = endButtons + ) + } + } } } } @@ -47,7 +65,7 @@ enum class ModalPlacement { START, CENTER, END, FULLSCREEN } -class ModalData() { +class ModalData(val keyboardCoversBar: Boolean = true) { private val state = mutableMapOf>() fun stateGetOrPut (key: String, default: () -> T): MutableState = state.getOrPut(key) { mutableStateOf(default() as Any) } as MutableState @@ -55,7 +73,7 @@ class ModalData() { fun stateGetOrPutNullable (key: String, default: () -> T?): MutableState = state.getOrPut(key) { mutableStateOf(default() as Any?) } as MutableState - val appBarHandler = AppBarHandler() + val appBarHandler = AppBarHandler(null, null, keyboardCoversBar = keyboardCoversBar) } class ModalManager(private val placement: ModalPlacement? = null) { @@ -69,23 +87,21 @@ class ModalManager(private val placement: ModalPlacement? = null) { private var passcodeView: MutableStateFlow<(@Composable (close: () -> Unit) -> Unit)?> = MutableStateFlow(null) private var onTimePasscodeView: MutableStateFlow<(@Composable (close: () -> Unit) -> Unit)?> = MutableStateFlow(null) - fun showModal(settings: Boolean = false, showClose: Boolean = true, closeOnTop: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.() -> Unit) { - val data = ModalData() + fun showModal(settings: Boolean = false, showClose: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.() -> Unit) { showCustomModal { close -> - ModalView(close, showClose = showClose, closeOnTop = closeOnTop, endButtons = endButtons, content = { data.content() }) + ModalView(close, showClose = showClose, endButtons = endButtons, content = { content() }) } } - fun showModalCloseable(settings: Boolean = false, showClose: Boolean = true, closeOnTop: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.(close: () -> Unit) -> Unit) { - val data = ModalData() + fun showModalCloseable(settings: Boolean = false, showClose: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.(close: () -> Unit) -> Unit) { showCustomModal { close -> - ModalView(close, showClose = showClose, endButtons = endButtons, closeOnTop = closeOnTop, content = { data.content(close) }) + ModalView(close, showClose = showClose, endButtons = endButtons, content = { content(close) }) } } - fun showCustomModal(animated: Boolean = true, modal: @Composable ModalData.(close: () -> Unit) -> Unit) { + fun showCustomModal(animated: Boolean = true, keyboardCoversBar: Boolean = true, modal: @Composable ModalData.(close: () -> Unit) -> Unit) { Log.d(TAG, "ModalManager.showCustomModal") - val data = ModalData() + val data = ModalData(keyboardCoversBar = keyboardCoversBar) // Means, animation is in progress or not started yet. Do not wait until animation finishes, just remove all from screen. // This is useful when invoking close() and ShowCustomModal one after another without delay. Otherwise, screen will hold prev view if (toRemove.isNotEmpty()) { @@ -146,9 +162,7 @@ class ModalManager(private val placement: ModalPlacement? = null) { // Without animation if (modalCount.value > 0 && modalViews.lastOrNull()?.first == false) { modalViews.lastOrNull()?.let { - CompositionLocalProvider( - LocalAppBarHandler provides it.second.appBarHandler - ) { + CompositionLocalProvider(LocalAppBarHandler provides adjustAppBarHandler(it.second.appBarHandler)) { it.third(it.second, ::closeModal) } } @@ -164,9 +178,7 @@ class ModalManager(private val placement: ModalPlacement? = null) { } ) { modalViews.getOrNull(it - 1)?.let { - CompositionLocalProvider( - LocalAppBarHandler provides it.second.appBarHandler - ) { + CompositionLocalProvider(LocalAppBarHandler provides adjustAppBarHandler(it.second.appBarHandler)) { it.third(it.second, ::closeModal) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/SearchTextField.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/SearchTextField.kt index 60dceab4ad..7124f34ac0 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/SearchTextField.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/SearchTextField.kt @@ -2,7 +2,7 @@ package chat.simplex.common.views.helpers import androidx.compose.foundation.background import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.ZeroCornerSize import androidx.compose.foundation.text.* import androidx.compose.material.* @@ -18,12 +18,9 @@ import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalSoftwareKeyboardController import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.* import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp +import androidx.compose.ui.unit.* import chat.simplex.common.platform.* import chat.simplex.res.MR import kotlinx.coroutines.delay @@ -38,6 +35,7 @@ fun SearchTextField( placeholder: String = stringResource(MR.strings.search_verb), enabled: Boolean = true, trailingContent: @Composable (() -> Unit)? = null, + reducedCloseButtonPadding: Dp = 0.dp, onValueChange: (String) -> Unit ) { val focusRequester = remember { FocusRequester() } @@ -81,15 +79,20 @@ fun SearchTextField( ) val shape = MaterialTheme.shapes.small.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize) val interactionSource = remember { MutableInteractionSource() } + val textStyle = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground) + // sizing is done differently on Android and desktop in order to have the same height of search and compose view on desktop + // see PlatformTextField.desktop + SendMsgView + val padding = if (appPlatform.isAndroid) PaddingValues() else PaddingValues(top = 3.dp, bottom = 4.dp) BasicTextField( value = searchText.value, modifier = modifier .background(colors.backgroundColor(enabled).value, shape) .indicatorLine(enabled, false, interactionSource, colors) .focusRequester(focusRequester) + .padding(padding) .defaultMinSize( minWidth = TextFieldDefaults.MinWidth, - minHeight = TextFieldDefaults.MinHeight + minHeight = if (appPlatform.isAndroid) TextFieldDefaults.MinHeight else 0.dp ), onValueChange = { searchText.value = it @@ -100,18 +103,14 @@ fun SearchTextField( visualTransformation = VisualTransformation.None, keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), singleLine = true, - textStyle = TextStyle( - color = MaterialTheme.colors.onBackground, - fontWeight = FontWeight.Normal, - fontSize = 15.sp - ), + textStyle = textStyle, interactionSource = interactionSource, decorationBox = @Composable { innerTextField -> TextFieldDefaults.TextFieldDecorationBox( value = searchText.value.text, innerTextField = innerTextField, placeholder = { - Text(placeholder, maxLines = 1, overflow = TextOverflow.Ellipsis) + Text(placeholder, style = textStyle.copy(color = MaterialTheme.colors.secondary), maxLines = 1, overflow = TextOverflow.Ellipsis) }, trailingIcon = if (searchText.value.text.isNotEmpty()) {{ IconButton({ @@ -121,7 +120,7 @@ fun SearchTextField( } searchText.value = TextFieldValue(""); onValueChange("") - }) { + }, Modifier.offset(x = reducedCloseButtonPadding)) { Icon(painterResource(MR.images.ic_close), stringResource(MR.strings.icon_descr_close_button), tint = MaterialTheme.colors.primary,) } }} else trailingContent, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/TextEditor.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/TextEditor.kt index ab7e562697..da16e2b7e7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/TextEditor.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/TextEditor.kt @@ -57,7 +57,6 @@ fun TextEditor( ) { val textFieldModifier = modifier .fillMaxWidth() - .navigationBarsWithImePadding() .onFocusChanged { focused = it.isFocused } .padding(10.dp) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ThemeModeEditor.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ThemeModeEditor.kt index a77290d90f..d7cdf0e2e3 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ThemeModeEditor.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ThemeModeEditor.kt @@ -32,10 +32,7 @@ fun ModalData.UserWallpaperEditor( globalThemeUsed: MutableState, save: suspend (applyToMode: DefaultThemeMode?, ThemeModeOverride?) -> Unit ) { - ColumnWithScrollBar( - Modifier - .fillMaxSize() - ) { + ColumnWithScrollBar { val applyToMode = remember { stateGetOrPutNullable("applyToMode") { applyToMode } } var showMore by remember { stateGetOrPut("showMore") { false } } val themeModeOverride = remember { stateGetOrPut("themeModeOverride") { theme } } @@ -231,10 +228,7 @@ fun ModalData.ChatWallpaperEditor( globalThemeUsed: MutableState, save: suspend (applyToMode: DefaultThemeMode?, ThemeModeOverride?) -> Unit ) { - ColumnWithScrollBar( - Modifier - .fillMaxSize() - ) { + ColumnWithScrollBar { val applyToMode = remember { stateGetOrPutNullable("applyToMode") { applyToMode } } var showMore by remember { stateGetOrPut("showMore") { false } } val themeModeOverride = remember { stateGetOrPut("themeModeOverride") { theme } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateFromDevice.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateFromDevice.kt index 4cc7899cc8..e2ee8878c8 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateFromDevice.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateFromDevice.kt @@ -149,9 +149,7 @@ private fun MigrateFromDeviceLayout( ) { val tempDatabaseFile = rememberSaveable { mutableStateOf(fileForTemporaryDatabase()) } - ColumnWithScrollBar( - Modifier.fillMaxSize(), maxIntrinsicSize = true - ) { + ColumnWithScrollBar(maxIntrinsicSize = true) { AppBarTitle(stringResource(MR.strings.migrate_from_device_title)) SectionByState(migrationState, tempDatabaseFile.value, chatReceiver) SectionBottomSpacer() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateToDevice.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateToDevice.kt index 415f5cdd57..90f8593c4a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateToDevice.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateToDevice.kt @@ -162,9 +162,7 @@ private fun ModalData.MigrateToDeviceLayout( close: () -> Unit, ) { val tempDatabaseFile = rememberSaveable { mutableStateOf(fileForTemporaryDatabase()) } - ColumnWithScrollBar( - Modifier.fillMaxSize(), maxIntrinsicSize = true - ) { + ColumnWithScrollBar(maxIntrinsicSize = true) { AppBarTitle(stringResource(MR.strings.migrate_to_device_title)) SectionByState(migrationState, tempDatabaseFile.value, chatReceiver, close) SectionBottomSpacer() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddContactLearnMore.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddContactLearnMore.kt index da59050a3a..077abd1b98 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddContactLearnMore.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddContactLearnMore.kt @@ -15,9 +15,7 @@ import chat.simplex.res.MR @Composable fun AddContactLearnMore(close: () -> Unit) { - ColumnWithScrollBar( - Modifier.padding(horizontal = DEFAULT_PADDING), - ) { + ColumnWithScrollBar(Modifier.padding(horizontal = DEFAULT_PADDING)) { AppBarTitle(stringResource(MR.strings.one_time_link), withPadding = false) ReadableText(MR.strings.scan_qr_to_connect_to_contact) ReadableText(MR.strings.if_you_cant_meet_in_person) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt index c430a62340..e1d3d6541a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt @@ -84,10 +84,9 @@ fun AddGroupLayout( val focusRequester = remember { FocusRequester() } val incognito = remember { mutableStateOf(incognitoPref.get()) } - ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { ModalBottomSheetLayout( scrimColor = Color.Black.copy(alpha = 0.12F), - modifier = Modifier.navigationBarsWithImePadding(), + modifier = Modifier.imePadding(), sheetContent = { GetImageBottomSheet( chosenImage, @@ -100,11 +99,7 @@ fun AddGroupLayout( sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp) ) { ModalView(close = close) { - ColumnWithScrollBar( - Modifier - .fillMaxSize() - .padding(horizontal = DEFAULT_PADDING) - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.create_secret_group_title), hostDevice(rhId)) Box( Modifier @@ -122,7 +117,7 @@ fun AddGroupLayout( } } } - Row(Modifier.padding(bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + Row(Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Text( stringResource(MR.strings.group_display_name_field), fontSize = 16.sp @@ -134,7 +129,9 @@ fun AddGroupLayout( } } } - ProfileNameField(displayName, "", { isValidDisplayName(it.trim()) }, focusRequester) + Box(Modifier.padding(horizontal = DEFAULT_PADDING)) { + ProfileNameField(displayName, "", { isValidDisplayName(it.trim()) }, focusRequester) + } Spacer(Modifier.height(8.dp)) SettingsActionItem( @@ -170,7 +167,6 @@ fun AddGroupLayout( } } } - } } fun canCreateProfile(displayName: String): Boolean = displayName.trim().isNotEmpty() && isValidDisplayName(displayName.trim()) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ContactConnectionInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ContactConnectionInfoView.kt index 64ff7e4f40..1623f8510d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ContactConnectionInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ContactConnectionInfoView.kt @@ -89,7 +89,7 @@ private fun ContactConnectionInfoLayout( SettingsActionItemWithContent( icon = painterResource(MR.images.ic_theater_comedy_filled), text = null, - click = { ModalManager.start.showModal { IncognitoView() } }, + click = { ModalManager.end.showModal { IncognitoView() } }, iconColor = Indigo, extraPadding = false ) { @@ -105,9 +105,7 @@ private fun ContactConnectionInfoLayout( } } - ColumnWithScrollBar( - Modifier, - ) { + ColumnWithScrollBar { AppBarTitle( stringResource( if (contactConnection.initiated) MR.strings.you_invited_a_contact diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt index 1a3ea10806..02996381f8 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt @@ -1,9 +1,7 @@ package chat.simplex.common.views.newchat -import SectionDivider import SectionDividerSpaced import SectionItemView -import SectionSpacer import SectionView import TextIconSpaced import androidx.compose.desktop.ui.tooling.preview.Preview @@ -14,8 +12,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier +import androidx.compose.ui.* import androidx.compose.ui.focus.* import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.painter.Painter @@ -32,56 +29,43 @@ import chat.simplex.common.model.* import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* -import chat.simplex.common.views.chatlist.ScrollDirection +import chat.simplex.common.views.chat.topPaddingToContent +import chat.simplex.common.views.chatlist.* import chat.simplex.common.views.contacts.* import chat.simplex.common.views.helpers.* import chat.simplex.res.MR import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter -import java.net.URI @Composable fun ModalData.NewChatSheet(rh: RemoteHostInfo?, close: () -> Unit) { val oneHandUI = remember { appPrefs.oneHandUI.state } - val keyboardState by getKeyboardState() - val showToolbarInOneHandUI = remember { derivedStateOf { keyboardState == KeyboardState.Closed && oneHandUI.value } } - Scaffold( - bottomBar = { - if (showToolbarInOneHandUI.value) { - Column { - Divider() - CloseSheetBar( - close = close, - showClose = true, - endButtons = { Spacer(Modifier.minimumInteractiveComponentSize()) }, - arrangement = Arrangement.Bottom, - closeBarTitle = generalGetString(MR.strings.new_message), - barPaddingValues = PaddingValues(horizontal = 0.dp) - ) - } - } + Box { + val closeAll = { ModalManager.start.closeModals() } + + Column(modifier = Modifier.fillMaxSize()) { + NewChatSheetLayout( + addContact = { + ModalManager.start.showModalCloseable(endButtons = { AddContactLearnMoreButton() }) { _ -> NewChatView(chatModel.currentRemoteHost.value, NewChatOption.INVITE, close = closeAll) } + }, + scanPaste = { + ModalManager.start.showModalCloseable(endButtons = { AddContactLearnMoreButton() }) { _ -> NewChatView(chatModel.currentRemoteHost.value, NewChatOption.CONNECT, showQRCodeScanner = appPlatform.isAndroid, close = closeAll) } + }, + createGroup = { + ModalManager.start.showCustomModal { close -> AddGroupView(chatModel, chatModel.currentRemoteHost.value, close, closeAll) } + }, + rh = rh, + close = close + ) } - ) { - Column( - modifier = Modifier.fillMaxSize().padding(it) - ) { - val closeAll = { ModalManager.start.closeModals() } - - Column(modifier = Modifier.fillMaxSize()) { - NewChatSheetLayout( - addContact = { - ModalManager.start.showModalCloseable(endButtons = { AddContactLearnMoreButton() }) { _ -> NewChatView(chatModel.currentRemoteHost.value, NewChatOption.INVITE, close = closeAll ) } - }, - scanPaste = { - ModalManager.start.showModalCloseable(endButtons = { AddContactLearnMoreButton() }) { _ -> NewChatView(chatModel.currentRemoteHost.value, NewChatOption.CONNECT, showQRCodeScanner = appPlatform.isAndroid, close = closeAll) } - }, - createGroup = { - ModalManager.start.showCustomModal { close -> AddGroupView(chatModel, chatModel.currentRemoteHost.value, close, closeAll) } - }, - rh = rh, - close = close + if (oneHandUI.value) { + Column(Modifier.align(Alignment.BottomCenter)) { + DefaultAppBar( + navigationButton = { NavigationButtonBack(onButtonClicked = close) }, + fixedTitleText = generalGetString(MR.strings.new_message), + onTop = false, ) } } @@ -187,168 +171,258 @@ private fun ModalData.NewChatSheetLayout( derivedStateOf { filterContactTypes(chatModel.chats.value, deletedContactTypes) } } - LazyColumnWithScrollBar( - Modifier.fillMaxSize(), - listState, - reverseLayout = oneHandUI.value - ) { - if (!oneHandUI.value) { - item { - Box(contentAlignment = Alignment.Center) { - val bottomPadding = DEFAULT_PADDING - AppBarTitle( - stringResource(MR.strings.new_message), - hostDevice(rh?.remoteHostId), - bottomPadding = bottomPadding + val actionButtonsOriginal = listOf( + Triple( + painterResource(MR.images.ic_add_link), + stringResource(MR.strings.add_contact_tab), + addContact, + ), + Triple( + painterResource(MR.images.ic_qr_code), + if (appPlatform.isAndroid) stringResource(MR.strings.scan_paste_link) else stringResource(MR.strings.paste_link), + scanPaste, + ), + Triple( + painterResource(MR.images.ic_group), + stringResource(MR.strings.create_group_button), + createGroup, + ) + ) + + @Composable + fun DeletedChatsItem(actionButtons: List Unit>>) { + if (searchText.value.text.isEmpty()) { + Spacer(Modifier.padding(bottom = 27.dp)) + } + + if (searchText.value.text.isEmpty()) { + Row { + SectionView { + actionButtons.map { + NewChatButton( + icon = it.first, + text = it.second, + click = it.third, + ) + } + } + } + if (deletedChats.isNotEmpty()) { + SectionDividerSpaced(maxBottomPadding = false) + SectionView { + SectionItemView( + click = { + ModalManager.start.showCustomModal { closeDeletedChats -> + ModalView( + close = closeDeletedChats, + showAppBar = !oneHandUI.value, + ) { + if (oneHandUI.value) { + BackHandler(onBack = closeDeletedChats) + } + DeletedContactsView(rh = rh, closeDeletedChats = closeDeletedChats, close = { + ModalManager.start.closeModals() + }) + } + } + } + ) { + Icon( + painterResource(MR.images.ic_inventory_2), + contentDescription = stringResource(MR.strings.deleted_chats), + tint = MaterialTheme.colors.secondary, + ) + TextIconSpaced(false) + Text(text = stringResource(MR.strings.deleted_chats), color = MaterialTheme.colors.onBackground) + } + } + } + } + } + + @Composable + fun NoFilteredContactsItem() { + if (filteredContactChats.isEmpty() && allChats.isNotEmpty()) { + Column(sectionModifier.fillMaxSize().padding(DEFAULT_PADDING)) { + Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { + Text( + generalGetString(MR.strings.no_filtered_contacts), + color = MaterialTheme.colors.secondary ) } } } - stickyHeader { - Column( - Modifier - .offset { - val y = if (searchText.value.text.isEmpty()) { - val offsetMultiplier = if (oneHandUI.value) 1 else -1 + } - if ( - (oneHandUI.value && scrollDirection == ScrollDirection.Up) || - (appPlatform.isAndroid && keyboardState == KeyboardState.Opened) - ) { - 0 - } else if (oneHandUI.value && listState.firstVisibleItemIndex == 0) { - listState.firstVisibleItemScrollOffset - } else if (!oneHandUI.value && listState.firstVisibleItemIndex == 0) { - 0 - } else if (!oneHandUI.value && listState.firstVisibleItemIndex == 1) { - -listState.firstVisibleItemScrollOffset + @Composable + fun OneHandLazyColumn() { + val blankSpaceSize = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + AppBarHeight * fontSizeSqrtMultiplier + LazyColumnWithScrollBar( + state = listState, + reverseLayout = oneHandUI.value + ) { + item { Spacer(Modifier.height(blankSpaceSize)) } + stickyHeader { + val scrolledSomething by remember { derivedStateOf { listState.firstVisibleItemScrollOffset > 0 || listState.firstVisibleItemIndex > 0 } } + Column( + Modifier + .zIndex(1f) + .offset { + val y = if (searchText.value.text.isNotEmpty() || (appPlatform.isAndroid && keyboardState == KeyboardState.Opened)) { + if (listState.firstVisibleItemIndex == 0) -minOf(listState.firstVisibleItemScrollOffset, blankSpaceSize.roundToPx()) + else -blankSpaceSize.roundToPx() } else { - offsetMultiplier * 1000 - } - } else { - 0 - } - IntOffset(0, y) - } - .background(MaterialTheme.colors.background) - ) { - Divider() - ContactsSearchBar( - listState = listState, - searchText = searchText, - searchShowingSimplexLink = searchShowingSimplexLink, - searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink, - close = close, - ) - if (!oneHandUI.value) { - Divider() - } - } - } - item { - if (searchText.value.text.isEmpty()) { - Spacer(Modifier.padding(bottom = 27.dp)) - } - - val actionButtonsOriginal = listOf( - Triple( - painterResource(MR.images.ic_add_link), - stringResource(MR.strings.add_contact_tab), - addContact, - ), - Triple( - painterResource(MR.images.ic_qr_code), - if (appPlatform.isAndroid) stringResource(MR.strings.scan_paste_link) else stringResource(MR.strings.paste_link), - scanPaste, - ), - Triple( - painterResource(MR.images.ic_group), - stringResource(MR.strings.create_group_button), - createGroup, - ) - ) - - val actionButtons by remember(oneHandUI.value) { - derivedStateOf { - if (oneHandUI.value) actionButtonsOriginal.asReversed() else actionButtonsOriginal - } - } - - if (searchText.value.text.isEmpty()) { - Row { - SectionView { - actionButtons.map { - NewChatButton( - icon = it.first, - text = it.second, - click = it.third, - ) - } - } - } - if (deletedChats.isNotEmpty()) { - SectionDividerSpaced(maxBottomPadding = false) - SectionView { - SectionItemView( - click = { - ModalManager.start.showCustomModal { closeDeletedChats -> - ModalView( - close = closeDeletedChats, - closeOnTop = !oneHandUI.value, - ) { - DeletedContactsView(rh = rh, closeDeletedChats = closeDeletedChats, close = { - ModalManager.start.closeModals() - }) - } + when (listState.firstVisibleItemIndex) { + 0 -> 0 + 1 -> listState.firstVisibleItemScrollOffset + else -> 1000 } } - ) { - Icon( - painterResource(MR.images.ic_inventory_2), - contentDescription = stringResource(MR.strings.deleted_chats), - tint = MaterialTheme.colors.secondary, - ) - TextIconSpaced(false) - Text(text = stringResource(MR.strings.deleted_chats), color = MaterialTheme.colors.onBackground) + IntOffset(0, y) } + // show background when something is scrolled because otherwise the bar is transparent. + // not using background always because of gradient in SimpleX theme + .background( + if (scrolledSomething && (keyboardState == KeyboardState.Opened || searchText.value.text.isNotEmpty())) { + MaterialTheme.colors.background + } else { + Color.Unspecified + } + ) + ) { + Divider() + Column(Modifier.consumeWindowInsets(WindowInsets.navigationBars).consumeWindowInsets(PaddingValues(bottom = AppBarHeight * fontSizeSqrtMultiplier))) { + ContactsSearchBar( + listState = listState, + searchText = searchText, + searchShowingSimplexLink = searchShowingSimplexLink, + searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink, + close = close, + ) + Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime)) } } } - } - - item { - if (filteredContactChats.isNotEmpty() && searchText.value.text.isEmpty()) { - if (!oneHandUI.value) { - SectionDividerSpaced() - SectionView(stringResource(MR.strings.contact_list_header_title).uppercase(), headerBottomPadding = DEFAULT_PADDING_HALF) {} - } else { + item { + DeletedChatsItem(actionButtonsOriginal.asReversed()) + } + item { + if (filteredContactChats.isNotEmpty() && searchText.value.text.isEmpty()) { SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = false) SectionView(stringResource(MR.strings.contact_list_header_title).uppercase(), headerBottomPadding = DEFAULT_PADDING_HALF) {} Spacer(Modifier.height(DEFAULT_PADDING_HALF)) } } - } - - item { - if (filteredContactChats.isEmpty() && allChats.isNotEmpty()) { - Column(sectionModifier.fillMaxSize().padding(DEFAULT_PADDING)) { - Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { - Text( - generalGetString(MR.strings.no_filtered_contacts), - color = MaterialTheme.colors.secondary - ) + item { + NoFilteredContactsItem() + } + itemsIndexed(filteredContactChats) { index, chat -> + val nextChatSelected = remember(chat.id, filteredContactChats) { + derivedStateOf { + chatModel.chatId.value != null && filteredContactChats.getOrNull(index + 1)?.id == chatModel.chatId.value } } + ContactListNavLinkView(chat, nextChatSelected, showDeletedChatIcon = true) } - } - - itemsIndexed(filteredContactChats) { index, chat -> - val nextChatSelected = remember(chat.id, filteredContactChats) { - derivedStateOf { - chatModel.chatId.value != null && filteredContactChats.getOrNull(index + 1)?.id == chatModel.chatId.value + if (appPlatform.isAndroid) { + item { + Spacer(Modifier.windowInsetsTopHeight(WindowInsets.statusBars)) } } - ContactListNavLinkView(chat, nextChatSelected, showDeletedChatIcon = true) + } + } + + @Composable + fun NonOneHandLazyColumn() { + val blankSpaceSize = topPaddingToContent() + LazyColumnWithScrollBar( + Modifier.imePadding(), + state = listState, + reverseLayout = false + ) { + item { + Box(Modifier.padding(top = blankSpaceSize)) { + AppBarTitle( + stringResource(MR.strings.new_message), + hostDevice(rh?.remoteHostId), + bottomPadding = DEFAULT_PADDING + ) + } + } + stickyHeader { + val scrolledSomething by remember { derivedStateOf { listState.firstVisibleItemScrollOffset > 0 || listState.firstVisibleItemIndex > 0 } } + Column( + Modifier + .zIndex(1f) + .offset { + val y = if (searchText.value.text.isNotEmpty() || (appPlatform.isAndroid && keyboardState == KeyboardState.Opened)) { + if (listState.firstVisibleItemIndex == 0) (listState.firstVisibleItemScrollOffset - (listState.layoutInfo.visibleItemsInfo[0].size - blankSpaceSize.roundToPx())).coerceAtLeast(0) + else blankSpaceSize.roundToPx() + } else { + when (listState.firstVisibleItemIndex) { + 0 -> 0 + 1 -> -listState.firstVisibleItemScrollOffset + else -> -1000 + } + } + IntOffset(0, y) + } + // show background when something is scrolled because otherwise the bar is transparent. + // not using background always because of gradient in SimpleX theme + .background( + if (scrolledSomething && (keyboardState == KeyboardState.Opened || searchText.value.text.isNotEmpty())) { + MaterialTheme.colors.background + } else { + Color.Unspecified + } + ) + ) { + Divider() + ContactsSearchBar( + listState = listState, + searchText = searchText, + searchShowingSimplexLink = searchShowingSimplexLink, + searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink, + close = close, + ) + Divider() + } + } + item { + DeletedChatsItem(actionButtonsOriginal) + } + item { + if (filteredContactChats.isNotEmpty() && searchText.value.text.isEmpty()) { + SectionDividerSpaced() + SectionView(stringResource(MR.strings.contact_list_header_title).uppercase(), headerBottomPadding = DEFAULT_PADDING_HALF) {} + } + } + item { + NoFilteredContactsItem() + } + itemsIndexed(filteredContactChats) { index, chat -> + val nextChatSelected = remember(chat.id, filteredContactChats) { + derivedStateOf { + chatModel.chatId.value != null && filteredContactChats.getOrNull(index + 1)?.id == chatModel.chatId.value + } + } + ContactListNavLinkView(chat, nextChatSelected, showDeletedChatIcon = true) + } + if (appPlatform.isAndroid) { + item { + Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars)) + } + } + } + } + + Box { + if (oneHandUI.value) { + OneHandLazyColumn() + StatusBarBackground() + } else { + NonOneHandLazyColumn() + NavigationBarBackground(oneHandUI.value, true) } } } @@ -554,26 +628,7 @@ private fun contactTypesSearchTargets(baseContactTypes: List, searc @Composable private fun ModalData.DeletedContactsView(rh: RemoteHostInfo?, closeDeletedChats: () -> Unit, close: () -> Unit) { val oneHandUI = remember { appPrefs.oneHandUI.state } - val keyboardState by getKeyboardState() - val showToolbarInOneHandUI = remember { derivedStateOf { keyboardState == KeyboardState.Closed && oneHandUI.value } } - - Scaffold( - bottomBar = { - if (showToolbarInOneHandUI.value) { - Column { - Divider() - CloseSheetBar( - close = closeDeletedChats, - showClose = true, - endButtons = { Spacer(Modifier.minimumInteractiveComponentSize()) }, - arrangement = Arrangement.Bottom, - closeBarTitle = generalGetString(MR.strings.deleted_chats), - barPaddingValues = PaddingValues(horizontal = 0.dp) - ) - } - } - } - ) { contentPadding -> + Box { val listState = remember { appBarHandler.listState } val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) } val searchShowingSimplexLink = remember { mutableStateOf(false) } @@ -590,57 +645,93 @@ private fun ModalData.DeletedContactsView(rh: RemoteHostInfo?, closeDeletedChats contactChats = allChats ) - LazyColumnWithScrollBar( - Modifier.fillMaxSize(), - contentPadding = contentPadding, - reverseLayout = oneHandUI.value, - ) { - item { - if (!oneHandUI.value) { - Box(contentAlignment = Alignment.Center) { - val bottomPadding = DEFAULT_PADDING - AppBarTitle( - stringResource(MR.strings.deleted_chats), - hostDevice(rh?.remoteHostId), - bottomPadding = bottomPadding - ) - } - } - } - item { - if (!oneHandUI.value) { - Divider() - } - ContactsSearchBar( - listState = listState, - searchText = searchText, - searchShowingSimplexLink = searchShowingSimplexLink, - searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink, - close = close, - ) - Divider() - } - - item { - if (filteredContactChats.isEmpty() && allChats.isNotEmpty()) { - Column(Modifier.fillMaxSize().padding(DEFAULT_PADDING)) { - Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { - Text( - generalGetString(MR.strings.no_filtered_contacts), - color = MaterialTheme.colors.secondary, + Box { + val topPaddingToContent = topPaddingToContent() + LazyColumnWithScrollBar( + if (!oneHandUI.value) Modifier.imePadding() else Modifier, + contentPadding = PaddingValues( + top = if (!oneHandUI.value) topPaddingToContent else 0.dp, + bottom = if (oneHandUI.value) WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + AppBarHeight * fontSizeSqrtMultiplier else 0.dp + ), + reverseLayout = oneHandUI.value, + ) { + item { + if (!oneHandUI.value) { + Box(contentAlignment = Alignment.Center) { + val bottomPadding = DEFAULT_PADDING + AppBarTitle( + stringResource(MR.strings.deleted_chats), + hostDevice(rh?.remoteHostId), + bottomPadding = bottomPadding ) } } } - } + item { + if (!oneHandUI.value) { + Divider() + ContactsSearchBar( + listState = listState, + searchText = searchText, + searchShowingSimplexLink = searchShowingSimplexLink, + searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink, + close = close, + ) + } else { + Column(Modifier.consumeWindowInsets(WindowInsets.navigationBars).consumeWindowInsets(PaddingValues(bottom = AppBarHeight))) { + ContactsSearchBar( + listState = listState, + searchText = searchText, + searchShowingSimplexLink = searchShowingSimplexLink, + searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink, + close = close, + ) + Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime)) + } + } + Divider() + } - itemsIndexed(filteredContactChats) { index, chat -> - val nextChatSelected = remember(chat.id, filteredContactChats) { - derivedStateOf { - chatModel.chatId.value != null && filteredContactChats.getOrNull(index + 1)?.id == chatModel.chatId.value + item { + if (filteredContactChats.isEmpty() && allChats.isNotEmpty()) { + Column(Modifier.fillMaxSize().padding(DEFAULT_PADDING)) { + Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { + Text( + generalGetString(MR.strings.no_filtered_contacts), + color = MaterialTheme.colors.secondary, + ) + } + } } } - ContactListNavLinkView(chat, nextChatSelected, showDeletedChatIcon = false) + + itemsIndexed(filteredContactChats) { index, chat -> + val nextChatSelected = remember(chat.id, filteredContactChats) { + derivedStateOf { + chatModel.chatId.value != null && filteredContactChats.getOrNull(index + 1)?.id == chatModel.chatId.value + } + } + ContactListNavLinkView(chat, nextChatSelected, showDeletedChatIcon = false) + } + if (appPlatform.isAndroid) { + item { + Spacer(if (oneHandUI.value) Modifier.windowInsetsTopHeight(WindowInsets.statusBars) else Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars)) + } + } + } + if (oneHandUI.value) { + StatusBarBackground() + } else { + NavigationBarBackground(oneHandUI.value, true) + } + } + if (oneHandUI.value) { + Column(Modifier.align(Alignment.BottomCenter)) { + DefaultAppBar( + navigationButton = { NavigationButtonBack(onButtonClicked = closeDeletedChats) }, + fixedTitleText = generalGetString(MR.strings.deleted_chats), + onTop = false, + ) } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt index 5298e11e75..61403e07a4 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt @@ -29,10 +29,12 @@ import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.unit.sp import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.chat.topPaddingToContent import chat.simplex.common.views.helpers.* import chat.simplex.common.views.usersettings.* import chat.simplex.res.MR @@ -398,8 +400,12 @@ fun ActiveProfilePicker( .fillMaxSize() .alpha(if (progressByTimeout) 0.6f else 1f) ) { - LazyColumnWithScrollBar(userScrollEnabled = !switchingProfile.value) { + LazyColumnWithScrollBar(Modifier.padding(top = topPaddingToContent()), userScrollEnabled = !switchingProfile.value) { item { + val oneHandUI = remember { appPrefs.oneHandUI.state } + if (oneHandUI.value) { + Spacer(Modifier.padding(top = DEFAULT_PADDING + 5.dp)) + } AppBarTitle(stringResource(MR.strings.select_chat_profile), hostDevice(rhId), bottomPadding = DEFAULT_PADDING) } val activeProfile = filteredProfiles.firstOrNull { it.activeUser } @@ -434,6 +440,9 @@ fun ActiveProfilePicker( ProfilePickerUserOption(p) } } + item { + Spacer(Modifier.imePadding().padding(bottom = DEFAULT_BOTTOM_PADDING)) + } } } if (progressByTimeout) { @@ -472,13 +481,13 @@ private fun InviteView(rhId: Long?, connReqInvitation: String, contactConnection end = 16.dp ), click = { - ModalManager.start.showCustomModal { close -> + ModalManager.start.showCustomModal(keyboardCoversBar = false) { close -> val search = rememberSaveable { mutableStateOf("") } ModalView( { close() }, - endButtons = { - SearchTextField(Modifier.fillMaxWidth(), placeholder = stringResource(MR.strings.search_verb), alwaysVisible = true) { search.value = it } - }, + showSearch = true, + searchAlwaysVisible = true, + onSearchValueChanged = { search.value = it }, content = { ActiveProfilePicker( search = search, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/CreateSimpleXAddress.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/CreateSimpleXAddress.kt index 20a7ada3aa..28ad0fdb7b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/CreateSimpleXAddress.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/CreateSimpleXAddress.kt @@ -76,15 +76,9 @@ private fun CreateSimpleXAddressLayout( createAddress: () -> Unit, nextStep: () -> Unit, ) { - val handler = remember { AppBarHandler() } - CompositionLocalProvider( - LocalAppBarHandler provides handler - ) { + CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) { ModalView({}, showClose = false) { ColumnWithScrollBar( - Modifier - .fillMaxSize() - .themedBackground(), horizontalAlignment = Alignment.CenterHorizontally, ) { AppBarTitle(stringResource(MR.strings.simplex_address)) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/HowItWorks.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/HowItWorks.kt index 9c7e2bdce7..98e8ec971d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/HowItWorks.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/HowItWorks.kt @@ -23,11 +23,7 @@ import dev.icerock.moko.resources.StringResource @Composable fun HowItWorks(user: User?, onboardingStage: SharedPreference? = null) { - ColumnWithScrollBar( - Modifier - .fillMaxWidth() - .padding(DEFAULT_PADDING), - ) { + ColumnWithScrollBar(Modifier.padding(DEFAULT_PADDING)) { AppBarTitle(stringResource(MR.strings.how_simplex_works), withPadding = false) ReadableText(MR.strings.many_people_asked_how_can_it_deliver) ReadableText(MR.strings.to_protect_privacy_simplex_has_ids_for_queues) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/LinkAMobileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/LinkAMobileView.kt index f0e34218d1..9e48f4b2bd 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/LinkAMobileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/LinkAMobileView.kt @@ -7,21 +7,16 @@ import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.dp import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatModel -import chat.simplex.common.platform.BackHandler import chat.simplex.common.platform.chatModel import chat.simplex.common.ui.theme.DEFAULT_PADDING -import chat.simplex.common.ui.theme.themedBackground import chat.simplex.common.views.helpers.* import chat.simplex.common.views.remote.AddingMobileDevice import chat.simplex.common.views.remote.DeviceNameField import chat.simplex.common.views.usersettings.PreferenceToggle import chat.simplex.res.MR -import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource @Composable @@ -59,34 +54,32 @@ private fun LinkAMobileLayout( staleQrCode: MutableState, updateDeviceName: (String) -> Unit, ) { - Column(Modifier.themedBackground()) { - CloseSheetBar(close = { - appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) - }) - BackHandler(onBack = { - appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) - }) - AppBarTitle(stringResource(if (remember { chatModel.remoteHosts }.isEmpty()) MR.strings.link_a_mobile else MR.strings.linked_mobiles)) - Row(Modifier.weight(1f).padding(horizontal = DEFAULT_PADDING * 2), verticalAlignment = Alignment.CenterVertically) { - Column( - Modifier.weight(0.3f), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - SectionView(generalGetString(MR.strings.this_device_name).uppercase()) { - DeviceNameField(deviceName.value ?: "") { updateDeviceName(it) } - SectionTextFooter(generalGetString(MR.strings.this_device_name_shared_with_mobile)) - PreferenceToggle(stringResource(MR.strings.multicast_discoverable_via_local_network), checked = remember { ChatModel.controller.appPrefs.offerRemoteMulticast.state }.value) { - ChatModel.controller.appPrefs.offerRemoteMulticast.set(it) + ModalView({ appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) }) { + Column(Modifier.fillMaxSize().padding(top = AppBarHeight * fontSizeSqrtMultiplier)) { + Box(Modifier.align(Alignment.CenterHorizontally)) { + AppBarTitle(stringResource(if (remember { chatModel.remoteHosts }.isEmpty()) MR.strings.link_a_mobile else MR.strings.linked_mobiles)) + } + Row(Modifier.weight(1f).padding(horizontal = DEFAULT_PADDING * 2), verticalAlignment = Alignment.CenterVertically) { + Column( + Modifier.weight(0.3f), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + SectionView(generalGetString(MR.strings.this_device_name).uppercase()) { + DeviceNameField(deviceName.value ?: "") { updateDeviceName(it) } + SectionTextFooter(generalGetString(MR.strings.this_device_name_shared_with_mobile)) + PreferenceToggle(stringResource(MR.strings.multicast_discoverable_via_local_network), checked = remember { ChatModel.controller.appPrefs.offerRemoteMulticast.state }.value) { + ChatModel.controller.appPrefs.offerRemoteMulticast.set(it) + } } } - } - Box(Modifier.weight(0.7f)) { - AddingMobileDevice(false, staleQrCode, connecting) { - // currentRemoteHost will be set instantly but remoteHosts may be delayed - if (chatModel.remoteHosts.isEmpty() && chatModel.currentRemoteHost.value == null) { - chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) - } else { - chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.OnboardingComplete) + Box(Modifier.weight(0.7f)) { + AddingMobileDevice(false, staleQrCode, connecting) { + // currentRemoteHost will be set instantly but remoteHosts may be delayed + if (chatModel.remoteHosts.isEmpty() && chatModel.currentRemoteHost.value == null) { + chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) + } else { + chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.OnboardingComplete) + } } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetNotificationsMode.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetNotificationsMode.kt index 1903b3cf81..e480d4330b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetNotificationsMode.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetNotificationsMode.kt @@ -25,16 +25,9 @@ import chat.simplex.res.MR @Composable fun SetNotificationsMode(m: ChatModel) { - val handler = remember { AppBarHandler() } - CompositionLocalProvider( - LocalAppBarHandler provides handler - ) { + CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) { ModalView({}, showClose = false) { - ColumnWithScrollBar( - modifier = Modifier - .fillMaxSize() - .themedBackground() - ) { + ColumnWithScrollBar(Modifier.themedBackground(bgLayerSize = LocalAppBarHandler.current?.backgroundGraphicsLayerSize, bgLayer = LocalAppBarHandler.current?.backgroundGraphicsLayer)) { Box(Modifier.align(Alignment.CenterHorizontally)) { AppBarTitle(stringResource(MR.strings.onboarding_notifications_mode_title)) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetupDatabasePassphrase.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetupDatabasePassphrase.kt index 858ca68af3..d0a3e601d2 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetupDatabasePassphrase.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetupDatabasePassphrase.kt @@ -104,13 +104,10 @@ private fun SetupDatabasePassphraseLayout( onConfirmEncrypt: () -> Unit, nextStep: () -> Unit, ) { - val handler = remember { AppBarHandler() } - CompositionLocalProvider( - LocalAppBarHandler provides handler - ) { + CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) { ModalView({}, showClose = false) { ColumnWithScrollBar( - Modifier.fillMaxSize().themedBackground().padding(bottom = DEFAULT_PADDING * 2), + Modifier.themedBackground(bgLayerSize = LocalAppBarHandler.current?.backgroundGraphicsLayerSize, bgLayer = LocalAppBarHandler.current?.backgroundGraphicsLayer).padding(bottom = DEFAULT_PADDING * 2), horizontalAlignment = Alignment.CenterHorizontally, ) { AppBarTitle(stringResource(MR.strings.setup_database_passphrase)) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt index c176950902..e43404cb07 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt @@ -31,15 +31,17 @@ import dev.icerock.moko.resources.StringResource @Composable fun SimpleXInfo(chatModel: ChatModel, onboarding: Boolean = true) { if (onboarding) { - ModalView({}, showClose = false, endButtons = { - IconButton({ ModalManager.fullscreen.showModal { HowItWorks(chatModel.currentUser.value, null) }}) { - Icon(painterResource(MR.images.ic_info), null, Modifier.size(28.dp), tint = MaterialTheme.colors.primary) + CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) { + ModalView({}, showClose = false, endButtons = { + IconButton({ ModalManager.fullscreen.showModal { HowItWorks(chatModel.currentUser.value, null) } }) { + Icon(painterResource(MR.images.ic_info), null, Modifier.size(28.dp), tint = MaterialTheme.colors.primary) + } + }) { + SimpleXInfoLayout( + user = chatModel.currentUser.value, + onboardingStage = chatModel.controller.appPrefs.onboardingStage + ) } - }) { - SimpleXInfoLayout( - user = chatModel.currentUser.value, - onboardingStage = chatModel.controller.appPrefs.onboardingStage - ) } } else { SimpleXInfoLayout( @@ -56,7 +58,6 @@ fun SimpleXInfoLayout( ) { ColumnWithScrollBar( Modifier - .fillMaxSize() .padding(horizontal = DEFAULT_PADDING), horizontalAlignment = Alignment.CenterHorizontally ) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt index 703f3b8915..bdbef3b654 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt @@ -119,11 +119,10 @@ fun WhatsNewView(viaSettings: Boolean = false, close: () -> Unit) { ModalView(close = close) { ColumnWithScrollBar( Modifier - .fillMaxSize() .padding(horizontal = DEFAULT_PADDING), verticalArrangement = Arrangement.spacedBy(DEFAULT_PADDING.times(0.75f)) ) { - AppBarTitle(String.format(generalGetString(MR.strings.new_in_version), v.version), bottomPadding = DEFAULT_PADDING) + AppBarTitle(String.format(generalGetString(MR.strings.new_in_version), v.version), withPadding = false, bottomPadding = DEFAULT_PADDING) v.features.forEach { feature -> if (feature.show) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectDesktopView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectDesktopView.kt index eb7fd7b6b5..c3eed3118e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectDesktopView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectDesktopView.kt @@ -74,9 +74,7 @@ private fun ConnectDesktopLayout(deviceName: String, close: () -> Unit) { val sessionAddress = remember { mutableStateOf("") } val remoteCtrls = remember { mutableStateListOf() } val session = remember { chatModel.remoteCtrlSession }.value - ColumnWithScrollBar( - Modifier.fillMaxWidth(), - ) { + ColumnWithScrollBar { val discovery = if (session == null) null else session.sessionState is UIRemoteCtrlSessionState.Searching if (discovery == true || (discovery == null && !showConnectScreen.value)) { SearchingDesktop(deviceName, remoteCtrls) @@ -408,9 +406,7 @@ private fun DesktopAddressView(sessionAddress: MutableState) { @Composable private fun LinkedDesktopsView(remoteCtrls: SnapshotStateList) { - ColumnWithScrollBar( - Modifier.fillMaxWidth(), - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.linked_desktops)) SectionView(stringResource(MR.strings.desktop_devices).uppercase()) { remoteCtrls.forEach { rc -> diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt index 92503f273e..e727b94781 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt @@ -89,7 +89,7 @@ fun ConnectMobileLayout( connectDesktop: () -> Unit, deleteHost: (RemoteHostInfo) -> Unit, ) { - ColumnWithScrollBar(Modifier.fillMaxWidth()) { + ColumnWithScrollBar { AppBarTitle(stringResource(if (remember { chatModel.remoteHosts }.isEmpty()) MR.strings.link_a_mobile else MR.strings.linked_mobiles)) SectionView(generalGetString(MR.strings.this_device_name).uppercase()) { DeviceNameField(deviceName.value ?: "") { updateDeviceName(it) } @@ -176,7 +176,15 @@ private fun ConnectMobileViewLayout( refreshQrCode: () -> Unit = {}, UnderQrLayout: @Composable () -> Unit = {}, ) { - ColumnWithScrollBar(Modifier.fillMaxWidth()) { + @Composable + fun ScrollableLayout(content: @Composable ColumnScope.() -> Unit) { + if (LocalAppBarHandler.current != null) { + ColumnWithScrollBar(content = content) + } else { + ColumnWithScrollBarNoAppBar(content = content) + } + } + ScrollableLayout { if (title != null) { AppBarTitle(title) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/AdvancedNetworkSettings.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/AdvancedNetworkSettings.kt index 35e0a3c6d8..5757b5d1f4 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/AdvancedNetworkSettings.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/AdvancedNetworkSettings.kt @@ -202,10 +202,7 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> U ) { val secondsLabel = stringResource(MR.strings.network_option_seconds_label) - ColumnWithScrollBar( - Modifier - .fillMaxWidth(), - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.network_settings_title)) if (currentRemoteHost == null) { @@ -328,9 +325,7 @@ private fun SMPProxyModePicker( icon = painterResource(MR.images.ic_settings_ethernet), onSelected = { showModal { - ColumnWithScrollBar( - Modifier.fillMaxWidth(), - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.network_smp_proxy_mode_private_routing)) SectionViewSelectableCards(null, smpProxyMode, values, updateSMPProxyMode) } @@ -365,9 +360,7 @@ private fun SMPProxyFallbackPicker( enabled = enabled, onSelected = { showModal { - ColumnWithScrollBar( - Modifier.fillMaxWidth(), - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.network_smp_proxy_fallback_allow_downgrade)) SectionViewSelectableCards(null, smpProxyFallback, values, updateSMPProxyFallback) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Appearance.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Appearance.kt index f2cd26803b..b4fead6692 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Appearance.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Appearance.kt @@ -4,9 +4,11 @@ import SectionBottomSpacer import SectionDividerSpaced import SectionItemView import SectionItemViewSpaceBetween +import SectionItemViewWithoutMinPadding import SectionSpacer import SectionView import androidx.compose.foundation.* +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.grid.* import androidx.compose.foundation.shape.CircleShape @@ -39,6 +41,7 @@ import chat.simplex.common.views.chat.item.msgTailWidthDp import chat.simplex.res.MR import com.godaddy.android.colorpicker.ClassicColorPicker import com.godaddy.android.colorpicker.HsvColor +import dev.icerock.moko.resources.StringResource import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.datetime.Clock @@ -86,27 +89,114 @@ object AppearanceScope { } @Composable - fun MessageShapeSection() { - SectionView(stringResource(MR.strings.settings_section_title_message_shape).uppercase(), contentPadding = PaddingValues()) { - Row(modifier = Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING + 4.dp ) ,verticalAlignment = Alignment.CenterVertically) { - Text(stringResource(MR.strings.settings_message_shape_corner), color = colors.onBackground) - Spacer(Modifier.width(10.dp)) - Slider( - remember { appPreferences.chatItemRoundness.state }.value, - valueRange = 0f..1f, - steps = 20, - onValueChange = { - val diff = it % 0.05f - appPreferences.chatItemRoundness.set(it + (if (diff >= 0.025f) -diff + 0.05f else -diff)) - saveThemeToDatabase(null) - }, - colors = SliderDefaults.colors( - activeTickColor = Color.Transparent, - inactiveTickColor = Color.Transparent, + fun AppToolbarsSection() { + BoxWithConstraints { + SectionView(stringResource(MR.strings.appearance_app_toolbars).uppercase()) { + SectionItemViewWithoutMinPadding { + Box(Modifier.weight(1f)) { + Text( + stringResource(MR.strings.appearance_in_app_bars_alpha), + Modifier.clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { + appPrefs.inAppBarsAlpha.set(appPrefs.inAppBarsDefaultAlpha) + }, + maxLines = 1 + ) + } + Spacer(Modifier.padding(end = 10.dp)) + Slider( + (1 - remember { appPrefs.inAppBarsAlpha.state }.value).coerceIn(0f, 0.5f), + onValueChange = { + val diff = it % 0.025f + appPrefs.inAppBarsAlpha.set(1f - (String.format(Locale.US, "%.3f", it + (if (diff >= 0.0125f) -diff + 0.025f else -diff)).toFloatOrNull() ?: 1f)) + }, + Modifier.widthIn(max = (this@BoxWithConstraints.maxWidth - DEFAULT_PADDING * 2) * 0.618f), + valueRange = 0f..0.5f, + steps = 21, + colors = SliderDefaults.colors( + activeTickColor = Color.Transparent, + inactiveTickColor = Color.Transparent, + ) ) - ) + } + // In Android in OneHandUI there is a problem with setting initial value of blur if it was 0 before entering the screen. + // So doing in two steps works ok + fun saveBlur(value: Int) { + val oneHandUI = appPrefs.oneHandUI.get() + val pref = appPrefs.appearanceBarsBlurRadius + if (appPlatform.isAndroid && oneHandUI && pref.get() == 0) { + pref.set(if (value > 2) value - 1 else value + 1) + withApi { + delay(50) + pref.set(value) + } + } else { + pref.set(value) + } + } + val blur = remember { appPrefs.appearanceBarsBlurRadius.state } + if (appPrefs.deviceSupportsBlur || blur.value > 0) { + SectionItemViewWithoutMinPadding { + Box(Modifier.weight(1f)) { + Text( + stringResource(MR.strings.appearance_bars_blur_radius), + Modifier.clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { + saveBlur(50) + }, + maxLines = 1 + ) + } + Spacer(Modifier.padding(end = 10.dp)) + Slider( + blur.value.toFloat() / 100f, + onValueChange = { + val diff = it % 0.05f + saveBlur(((String.format(Locale.US, "%.2f", it + (if (diff >= 0.025f) -diff + 0.05f else -diff)).toFloatOrNull() ?: 1f) * 100).toInt()) + }, + Modifier.widthIn(max = (this@BoxWithConstraints.maxWidth - DEFAULT_PADDING * 2) * 0.618f), + valueRange = 0f..1f, + steps = 21, + colors = SliderDefaults.colors( + activeTickColor = Color.Transparent, + inactiveTickColor = Color.Transparent, + ) + ) + } + } + } + } + } + + @Composable + fun MessageShapeSection() { + BoxWithConstraints { + SectionView(stringResource(MR.strings.settings_section_title_message_shape).uppercase()) { + SectionItemViewWithoutMinPadding { + Text(stringResource(MR.strings.settings_message_shape_corner), Modifier.weight(1f)) + Spacer(Modifier.width(10.dp)) + Slider( + remember { appPreferences.chatItemRoundness.state }.value, + onValueChange = { + val diff = it % 0.05f + appPreferences.chatItemRoundness.set(it + (if (diff >= 0.025f) -diff + 0.05f else -diff)) + saveThemeToDatabase(null) + }, + Modifier.widthIn(max = (this@BoxWithConstraints.maxWidth - DEFAULT_PADDING * 2) * 0.618f), + valueRange = 0f..1f, + steps = 20, + colors = SliderDefaults.colors( + activeTickColor = Color.Transparent, + inactiveTickColor = Color.Transparent, + ) + ) + } + SettingsPreferenceItem(icon = null, stringResource(MR.strings.settings_message_shape_tail), appPreferences.chatItemTail) } - SettingsPreferenceItem(icon = null, stringResource(MR.strings.settings_message_shape_tail), appPreferences.chatItemTail) } } @@ -115,7 +205,7 @@ object AppearanceScope { val localFontScale = remember { mutableStateOf(appPrefs.fontScale.get()) } SectionView(stringResource(MR.strings.appearance_font_size).uppercase(), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) { Row(Modifier.padding(top = 10.dp), verticalAlignment = Alignment.CenterVertically) { - Box(Modifier.size(60.dp) + Box(Modifier.size(50.dp) .background(MaterialTheme.colors.surface, RoundedCornerShape(percent = 22)) .clip(RoundedCornerShape(percent = 22)) .clickable { @@ -129,7 +219,7 @@ object AppearanceScope { Text("Aa", color = if (localFontScale.value == 1f) MaterialTheme.colors.primary else MaterialTheme.colors.onBackground) } } - Spacer(Modifier.width(10.dp)) + Spacer(Modifier.width(15.dp)) // Text("${(localFontScale.value * 100).roundToInt()}%", Modifier.width(70.dp), textAlign = TextAlign.Center, fontSize = 12.sp) if (appPlatform.isAndroid) { Slider( @@ -185,7 +275,7 @@ object AppearanceScope { Column(Modifier .drawWithCache { if (wallpaperImage != null && wallpaperType != null && backgroundColor != null && tintColor != null) { - chatViewBackground(wallpaperImage, wallpaperType, backgroundColor, tintColor) + chatViewBackground(wallpaperImage, wallpaperType, backgroundColor, tintColor, null, null) } else { onDrawBehind { drawRect(themeBackgroundColor) @@ -514,9 +604,7 @@ object AppearanceScope { @Composable fun CustomizeThemeView(onChooseType: (WallpaperType?) -> Unit) { - ColumnWithScrollBar( - Modifier.fillMaxWidth(), - ) { + ColumnWithScrollBar { val currentTheme by CurrentColors.collectAsState() AppBarTitle(stringResource(MR.strings.customize_theme_title)) @@ -909,10 +997,7 @@ object AppearanceScope { currentColors: () -> ThemeManager.ActiveTheme, onColorChange: (Color?) -> Unit, ) { - ColumnWithScrollBar( - Modifier - .fillMaxWidth() - ) { + ColumnWithScrollBar(Modifier.imePadding()) { AppBarTitle(name.text) val supportedLiveChange = name in listOf(ThemeColor.SECONDARY, ThemeColor.BACKGROUND, ThemeColor.SURFACE, ThemeColor.RECEIVED_MESSAGE, ThemeColor.SENT_MESSAGE, ThemeColor.SENT_QUOTE, ThemeColor.WALLPAPER_BACKGROUND, ThemeColor.WALLPAPER_TINT) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/CallSettings.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/CallSettings.kt index 468a192f09..cb36e4ae1a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/CallSettings.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/CallSettings.kt @@ -36,7 +36,7 @@ fun CallSettingsLayout( callOnLockScreen: SharedPreference, editIceServers: () -> Unit, ) { - ColumnWithScrollBar(Modifier.fillMaxWidth()) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.your_calls)) val lockCallState = remember { mutableStateOf(callOnLockScreen.get()) } SectionView(stringResource(MR.strings.settings_section_title_settings)) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt index 2123d98f41..87770e9ffd 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt @@ -22,12 +22,10 @@ import chat.simplex.common.views.helpers.* import chat.simplex.res.MR @Composable -fun DeveloperView( - m: ChatModel, - showCustomModal: (@Composable ModalData.(ChatModel, () -> Unit) -> Unit) -> (() -> Unit), - withAuth: (title: String, desc: String, block: () -> Unit) -> Unit +fun DeveloperView(withAuth: (title: String, desc: String, block: () -> Unit) -> Unit ) { - ColumnWithScrollBar(Modifier.fillMaxWidth()) { + val m = chatModel + ColumnWithScrollBar { val uriHandler = LocalUriHandler.current AppBarTitle(stringResource(MR.strings.settings_developer_tools)) val developerTools = m.controller.appPrefs.developerTools @@ -35,7 +33,7 @@ fun DeveloperView( val unchangedHints = mutableStateOf(unchangedHintPreferences()) SectionView { InstallTerminalAppItem(uriHandler) - ChatConsoleItem { withAuth(generalGetString(MR.strings.auth_open_chat_console), generalGetString(MR.strings.auth_log_in_using_credential), showCustomModal { it, close -> TerminalView(false, close) }) } + ChatConsoleItem { withAuth(generalGetString(MR.strings.auth_open_chat_console), generalGetString(MR.strings.auth_log_in_using_credential)) { ModalManager.start.showModalCloseable { TerminalView(false) } } } ResetHintsItem(unchangedHints) SettingsPreferenceItem(painterResource(MR.images.ic_code), stringResource(MR.strings.show_developer_options), developerTools) SectionTextFooter( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/HelpView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/HelpView.kt index c2bf69bc0e..aaaef31583 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/HelpView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/HelpView.kt @@ -21,11 +21,7 @@ fun HelpView(userDisplayName: String) { @Composable fun HelpLayout(userDisplayName: String) { - ColumnWithScrollBar( - Modifier - .fillMaxWidth() - .padding(horizontal = DEFAULT_PADDING), - ){ + ColumnWithScrollBar(Modifier.padding(horizontal = DEFAULT_PADDING)){ AppBarTitle(String.format(stringResource(MR.strings.personal_welcome), userDisplayName), withPadding = false) ChatHelpView() } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/HiddenProfileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/HiddenProfileView.kt index e5116f9149..55bd796a3b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/HiddenProfileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/HiddenProfileView.kt @@ -56,10 +56,7 @@ private fun HiddenProfileLayout( user: User, saveProfilePassword: (String) -> Unit ) { - ColumnWithScrollBar( - Modifier - .fillMaxWidth(), - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.hide_profile)) SectionView(contentPadding = PaddingValues(start = 8.dp, end = DEFAULT_PADDING)) { UserProfileRow(user) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt index dc3def3884..2c4870b121 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt @@ -109,7 +109,7 @@ fun NetworkAndServersView() { toggleSocksProxy: (Boolean) -> Unit, ) { val m = chatModel - ColumnWithScrollBar(Modifier.fillMaxWidth()) { + ColumnWithScrollBar { val showModal = { it: @Composable ModalData.() -> Unit -> ModalManager.start.showModal(content = it) } val showCustomModal = { it: @Composable (close: () -> Unit) -> Unit -> ModalManager.start.showCustomModal { close -> it(close) }} @@ -304,10 +304,7 @@ fun SocksProxySettings( } }, ) { - ColumnWithScrollBar( - Modifier - .fillMaxWidth() - ) { + ColumnWithScrollBar { AppBarTitle(generalGetString(MR.strings.network_socks_proxy_settings)) SectionView(stringResource(MR.strings.network_socks_proxy).uppercase()) { Column(Modifier.padding(horizontal = DEFAULT_PADDING)) { @@ -479,9 +476,7 @@ fun SessionModePicker( icon = painterResource(MR.images.ic_safety_divider), onSelected = { showModal { - ColumnWithScrollBar( - Modifier.fillMaxWidth(), - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.network_session_mode_transport_isolation)) SectionViewSelectable(null, sessionMode, values, updateSessionMode) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NotificationsSettingsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NotificationsSettingsView.kt index 515d73a426..60bde83c17 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NotificationsSettingsView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NotificationsSettingsView.kt @@ -56,9 +56,7 @@ fun NotificationsSettingsLayout( val modes = remember { notificationModes() } val previewModes = remember { notificationPreviewModes() } - ColumnWithScrollBar( - Modifier.fillMaxWidth(), - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.notifications)) SectionView(null) { if (appPlatform == AppPlatform.ANDROID) { @@ -90,9 +88,7 @@ fun NotificationsModeView( onNotificationsModeSelected: (NotificationsMode) -> Unit, ) { val modes = remember { notificationModes() } - ColumnWithScrollBar( - Modifier.fillMaxWidth(), - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.settings_notifications_mode_title).lowercase().capitalize(Locale.current)) SectionViewSelectable(null, notificationsMode, modes, onNotificationsModeSelected) } @@ -104,9 +100,7 @@ fun NotificationPreviewView( onNotificationPreviewModeSelected: (NotificationPreviewMode) -> Unit, ) { val previewModes = remember { notificationPreviewModes() } - ColumnWithScrollBar( - Modifier.fillMaxWidth(), - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.settings_notification_preview_title)) SectionViewSelectable(null, notificationPreviewMode, previewModes, onNotificationPreviewModeSelected) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Preferences.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Preferences.kt index 96a0bdcda3..bc27773ca6 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Preferences.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Preferences.kt @@ -66,9 +66,7 @@ private fun PreferencesLayout( reset: () -> Unit, savePrefs: () -> Unit, ) { - ColumnWithScrollBar( - Modifier.fillMaxWidth(), - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.your_preferences)) val timedMessages = remember(preferences) { mutableStateOf(preferences.timedMessages.allow) } TimedMessagesFeatureSection(timedMessages) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt index abf318390f..9ec2d29843 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt @@ -55,9 +55,7 @@ fun PrivacySettingsView( showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), setPerformLA: (Boolean) -> Unit ) { - ColumnWithScrollBar( - Modifier.fillMaxWidth(), - ) { + ColumnWithScrollBar { val simplexLinkMode = chatModel.controller.appPrefs.simplexLinkMode AppBarTitle(stringResource(MR.strings.your_privacy)) PrivacyDeviceSection(showSettingsModal, setPerformLA) @@ -514,9 +512,7 @@ fun SimplexLockView( } } - ColumnWithScrollBar( - Modifier.fillMaxWidth(), - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.chat_lock)) SectionView { EnableLock(remember { appPrefs.performLA.state }) { performLAToggle -> diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ProtocolServerView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ProtocolServerView.kt index 3a1a1cb8f3..be566e6c5a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ProtocolServerView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ProtocolServerView.kt @@ -75,10 +75,7 @@ private fun ProtocolServerLayout( onUpdate: (ServerCfg) -> Unit, onDelete: () -> Unit, ) { - ColumnWithScrollBar( - Modifier - .fillMaxWidth() - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(if (server.preset) MR.strings.smp_servers_preset_server else MR.strings.smp_servers_your_server)) if (server.preset) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ProtocolServersView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ProtocolServersView.kt index 5d5f1d039a..f5e3cda2c7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ProtocolServersView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ProtocolServersView.kt @@ -192,10 +192,7 @@ private fun ProtocolServersLayout( saveSMPServers: () -> Unit, showServer: (ServerCfg) -> Unit, ) { - ColumnWithScrollBar( - Modifier - .fillMaxWidth() - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(if (serverProtocol == ServerProtocol.SMP) MR.strings.your_SMP_servers else MR.strings.your_XFTP_servers)) val configuredServers = servers.filter { it.preset || it.enabled } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ScanProtocolServer.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ScanProtocolServer.kt index 7c2c578d6a..966f44cac7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ScanProtocolServer.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ScanProtocolServer.kt @@ -7,6 +7,7 @@ import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.unit.dp import chat.simplex.common.model.ServerAddress.Companion.parseServerAddress import chat.simplex.common.model.ServerCfg +import chat.simplex.common.platform.ColumnWithScrollBar import chat.simplex.common.ui.theme.DEFAULT_PADDING import chat.simplex.common.views.helpers.* import chat.simplex.common.views.newchat.QRCodeScanner @@ -17,10 +18,7 @@ expect fun ScanProtocolServer(rhId: Long?, onNext: (ServerCfg) -> Unit) @Composable fun ScanProtocolServerLayout(rhId: Long?, onNext: (ServerCfg) -> Unit) { - Column( - Modifier - .fillMaxSize() - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.smp_servers_scan_qr)) QRCodeScanner { text -> val res = parseServerAddress(text) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SetDeliveryReceiptsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SetDeliveryReceiptsView.kt index 0229e7da2a..ef4acdeac6 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SetDeliveryReceiptsView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SetDeliveryReceiptsView.kt @@ -74,10 +74,7 @@ private fun SetDeliveryReceiptsLayout( userCount: Int, ) { Box(Modifier.padding(top = DEFAULT_PADDING)) { - ColumnWithScrollBar( - Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { + ColumnWithScrollBar(horizontalAlignment = Alignment.CenterHorizontally) { AppBarTitle(stringResource(MR.strings.delivery_receipts_title)) Spacer(Modifier.weight(1f)) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt index bb4a0b61b0..78c5e3b212 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt @@ -39,7 +39,6 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, close: ( val user = chatModel.currentUser.value val stopped = chatModel.chatRunning.value == false SettingsLayout( - profile = user?.profile, stopped, chatModel.chatDbEncrypted.value == true, remember { chatModel.controller.appPrefs.storeDBPassphrase.state }.value, @@ -53,9 +52,9 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, close: ( val search = rememberSaveable { mutableStateOf("") } ModalView( { close() }, - endButtons = { - SearchTextField(Modifier.fillMaxWidth(), placeholder = stringResource(MR.strings.search_verb), alwaysVisible = true) { search.value = it } - }, + showSearch = true, + searchAlwaysVisible = true, + onSearchValueChanged = { search.value = it }, content = { modalView(chatModel, search) }) } }, @@ -80,7 +79,6 @@ val simplexTeamUri = @Composable fun SettingsLayout( - profile: LocalProfile?, stopped: Boolean, encrypted: Boolean, passphraseSaved: Boolean, @@ -94,18 +92,12 @@ fun SettingsLayout( showVersion: () -> Unit, withAuth: (title: String, desc: String, block: () -> Unit) -> Unit, ) { - val scope = rememberCoroutineScope() val view = LocalMultiplatformView() LaunchedEffect(Unit) { hideKeyboard(view) } - val theme = CurrentColors.collectAsState() val uriHandler = LocalUriHandler.current - ColumnWithScrollBar( - Modifier - .fillMaxSize() - .themedBackground(theme.value.base) - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.your_settings)) SectionView(stringResource(MR.strings.settings_section_title_settings)) { @@ -142,7 +134,7 @@ fun SettingsLayout( } SectionDividerSpaced() - SettingsSectionApp(showSettingsModal, showCustomModal, showVersion, withAuth) + SettingsSectionApp(showSettingsModal, showVersion, withAuth) SectionBottomSpacer() } } @@ -150,7 +142,6 @@ fun SettingsLayout( @Composable expect fun SettingsSectionApp( showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), - showCustomModal: (@Composable ModalData.(ChatModel, () -> Unit) -> Unit) -> (() -> Unit), showVersion: () -> Unit, withAuth: (title: String, desc: String, block: () -> Unit) -> Unit ) @@ -488,7 +479,6 @@ private fun runAuth(title: String, desc: String, onFinish: (success: Boolean) -> fun PreviewSettingsLayout() { SimpleXTheme { SettingsLayout( - profile = LocalProfile.sampleData, stopped = false, encrypted = false, passphraseSaved = false, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressLearnMore.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressLearnMore.kt index 1ac0cd7ecd..6d6b72d2d1 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressLearnMore.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressLearnMore.kt @@ -13,11 +13,7 @@ import chat.simplex.res.MR @Composable fun UserAddressLearnMore() { - ColumnWithScrollBar( - Modifier - .fillMaxHeight() - .padding(horizontal = DEFAULT_PADDING) - ) { + ColumnWithScrollBar(Modifier .padding(horizontal = DEFAULT_PADDING)) { AppBarTitle(stringResource(MR.strings.simplex_address), withPadding = false) ReadableText(MR.strings.you_can_share_your_address) ReadableText(MR.strings.you_wont_lose_your_contacts_if_delete_address) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfileView.kt index 10acaffe1a..90122bd29d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfileView.kt @@ -71,10 +71,8 @@ fun UserProfileLayout( val keyboardState by getKeyboardState() var savedKeyboardState by remember { mutableStateOf(keyboardState) } val focusRequester = remember { FocusRequester() } - ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { ModalBottomSheetLayout( scrimColor = Color.Black.copy(alpha = 0.12F), - modifier = Modifier.navigationBarsWithImePadding(), sheetContent = { GetImageBottomSheet( chosenImage, @@ -90,7 +88,6 @@ fun UserProfileLayout( displayName.value == profile.displayName && fullName.value == profile.fullName && profile.image == profileImage.value - val closeWithAlert = { if (dataUnchanged || !canSaveProfile(displayName.value, profile)) { close() @@ -103,7 +100,7 @@ fun UserProfileLayout( Modifier .padding(horizontal = DEFAULT_PADDING), ) { - AppBarTitle(stringResource(MR.strings.your_current_profile)) + AppBarTitle(stringResource(MR.strings.your_current_profile), withPadding = false) ReadableText(generalGetString(MR.strings.your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it), TextAlign.Center) Column( Modifier @@ -170,7 +167,6 @@ fun UserProfileLayout( } } } - } } @Composable diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfilesView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfilesView.kt index dcf8351166..fa9e709d4b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfilesView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfilesView.kt @@ -151,10 +151,7 @@ private fun UserProfilesLayout( unmuteUser: (User) -> Unit, showHiddenProfile: (User) -> Unit, ) { - ColumnWithScrollBar( - Modifier - .fillMaxWidth() - ) { + ColumnWithScrollBar { if (profileHidden.value) { SectionView { SettingsActionItem(painterResource(MR.images.ic_lock_open_right), stringResource(MR.strings.enter_password_to_show), click = { @@ -252,10 +249,7 @@ enum class UserProfileAction { @Composable private fun ProfileActionView(action: UserProfileAction, user: User, doAction: (String) -> Unit) { - ColumnWithScrollBar( - Modifier - .fillMaxWidth() - ) { + ColumnWithScrollBar { val actionPassword = rememberSaveable { mutableStateOf("") } val passwordValid by remember { derivedStateOf { actionPassword.value == actionPassword.value.trim() } } val actionEnabled by remember { derivedStateOf { actionPassword.value != "" && passwordValid && correctPassword(user, actionPassword.value) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/VersionInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/VersionInfoView.kt index 06a4762210..52addd146b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/VersionInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/VersionInfoView.kt @@ -1,6 +1,5 @@ package chat.simplex.common.views.usersettings -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.material.Text import androidx.compose.runtime.Composable @@ -8,6 +7,7 @@ import androidx.compose.ui.Modifier import dev.icerock.moko.resources.compose.stringResource import chat.simplex.common.BuildConfigCommon import chat.simplex.common.model.CoreVersionInfo +import chat.simplex.common.platform.ColumnWithScrollBar import chat.simplex.common.platform.appPlatform import chat.simplex.common.ui.theme.DEFAULT_PADDING import chat.simplex.common.views.helpers.AppBarTitle @@ -15,7 +15,7 @@ import chat.simplex.res.MR @Composable fun VersionInfoView(info: CoreVersionInfo) { - Column( + ColumnWithScrollBar( Modifier.padding(horizontal = DEFAULT_PADDING), ) { AppBarTitle(stringResource(MR.strings.app_version_title), withPadding = false) diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index fd6988d5e8..1ab7e3aed2 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -1755,6 +1755,9 @@ Remove image Font size Zoom + App toolbars + Transparency + Blur System mode diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt index 1fb739946c..25d85a6b7d 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt @@ -198,12 +198,17 @@ private fun ApplicationScope.AppWindow(closedByError: MutableState) { val cWindowState = rememberWindowState(placement = WindowPlacement.Floating, width = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier, height = 768.dp) Window(state = cWindowState, onCloseRequest = { hiddenUntilRestart = true }, title = stringResource(MR.strings.chat_console)) { + val data = remember { ModalData() } SimpleXTheme { - TerminalView(true) { hiddenUntilRestart = true } - ModalManager.floatingTerminal.showInView() - DisposableEffect(Unit) { - onDispose { - ModalManager.floatingTerminal.closeModals() + CompositionLocalProvider(LocalAppBarHandler provides data.appBarHandler) { + ModalView({ hiddenUntilRestart = true }) { + TerminalView(true) + } + ModalManager.floatingTerminal.showInView() + DisposableEffect(Unit) { + onDispose { + ModalManager.floatingTerminal.closeModals() + } } } } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Modifier.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Modifier.desktop.kt index 150885cbc8..b090e301d5 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Modifier.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Modifier.desktop.kt @@ -15,17 +15,6 @@ import java.awt.image.BufferedImage import java.io.File import java.net.URI -actual fun Modifier.navigationBarsWithImePadding(): Modifier = this - -@Composable -actual fun ProvideWindowInsets( - consumeWindowInsets: Boolean, - windowInsetsAnimationsEnabled: Boolean, - content: @Composable () -> Unit -) { - content() -} - @Composable actual fun Modifier.desktopOnExternalDrag( enabled: Boolean, diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/PlatformTextField.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/PlatformTextField.desktop.kt index e37e99f3e9..e7bcf4802a 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/PlatformTextField.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/PlatformTextField.desktop.kt @@ -1,10 +1,12 @@ package chat.simplex.common.platform +import androidx.compose.foundation.background import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.* +import androidx.compose.material.TextFieldDefaults.textFieldWithLabelPadding import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -113,7 +115,9 @@ actual fun PlatformTextField( autoCorrectEnabled = true ), modifier = Modifier - .padding(vertical = 4.dp) + .padding(start = startPadding, end = endPadding) + .offset(y = (-5).dp) + .fillMaxWidth() .focusRequester(focusRequester) .onPreviewKeyEvent { if ((it.key == Key.Enter || it.key == Key.NumPadEnter) && it.type == KeyEventType.KeyDown) { @@ -177,30 +181,24 @@ actual fun PlatformTextField( }, cursorBrush = SolidColor(MaterialTheme.colors.secondary), decorationBox = { innerTextField -> - Row(verticalAlignment = Alignment.Bottom) { CompositionLocalProvider( LocalLayoutDirection provides if (isRtlByCharacters) LayoutDirection.Rtl else LocalLayoutDirection.current ) { - Column(Modifier.weight(1f).padding(start = startPadding, end = endPadding)) { - Spacer(Modifier.height(8.dp)) - TextFieldDefaults.TextFieldDecorationBox( - value = textFieldValue.text, - innerTextField = innerTextField, - placeholder = { Text(placeholder, style = textStyle.value.copy(color = MaterialTheme.colors.secondary)) }, - singleLine = false, - enabled = true, - isError = false, - trailingIcon = null, - interactionSource = remember { MutableInteractionSource() }, - contentPadding = PaddingValues(), - visualTransformation = VisualTransformation.None, - colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.Unspecified) - ) - Spacer(Modifier.height(10.dp)) - } + TextFieldDefaults.TextFieldDecorationBox( + value = textFieldValue.text, + innerTextField = innerTextField, + placeholder = { Text(placeholder, style = textStyle.value.copy(color = MaterialTheme.colors.secondary)) }, + singleLine = false, + enabled = true, + isError = false, + trailingIcon = null, + interactionSource = remember { MutableInteractionSource() }, + contentPadding = textFieldWithLabelPadding(start = 0.dp, end = 0.dp), + visualTransformation = VisualTransformation.None, + colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.Unspecified) + ) } - } - }, + } ) showDeleteTextButton.value = cs.message.split("\n").size >= 4 && !cs.inProgress if (composeState.value.preview is ComposePreview.VoicePreview) { diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/ScrollableColumn.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/ScrollableColumn.desktop.kt index e6b26f9290..a294f1cc60 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/ScrollableColumn.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/ScrollableColumn.desktop.kt @@ -15,11 +15,14 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.pointer.* +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import chat.simplex.common.model.ChatController.appPrefs +import chat.simplex.common.ui.theme.DEFAULT_PADDING import chat.simplex.common.views.helpers.* import kotlinx.coroutines.* import kotlinx.coroutines.flow.filter -import kotlin.math.absoluteValue +import kotlin.math.* @Composable actual fun LazyColumnWithScrollBar( @@ -31,6 +34,78 @@ actual fun LazyColumnWithScrollBar( horizontalAlignment: Alignment.Horizontal, flingBehavior: FlingBehavior, userScrollEnabled: Boolean, + additionalBarOffset: State?, + fillMaxSize: Boolean, + content: LazyListScope.() -> Unit +) { + val handler = LocalAppBarHandler.current + require(handler != null) { "Using LazyColumnWithScrollBar and without AppBarHandler is an error. Use LazyColumnWithScrollBarNoAppBar instead" } + + val scope = rememberCoroutineScope() + val scrollBarAlpha = remember { Animatable(0f) } + val scrollJob: MutableState = remember { mutableStateOf(Job()) } + val scrollModifier = remember { + Modifier + .pointerInput(Unit) { + detectCursorMove { + scope.launch { + scrollBarAlpha.animateTo(1f) + } + scrollJob.value.cancel() + scrollJob.value = scope.launch { + delay(1000L) + scrollBarAlpha.animateTo(0f) + } + } + } + } + val state = state ?: handler.listState + val connection = handler.connection + // When scroll bar is dragging, there is no scroll event in nested scroll modifier. So, listen for changes on lazy column state + // (only first visible row is useful because LazyColumn doesn't have absolute scroll position, only relative to row) + val scrollBarDraggingState = remember { mutableStateOf(false) } + LaunchedEffect(Unit) { + if (reverseLayout) { + snapshotFlow { state.layoutInfo.visibleItemsInfo.lastOrNull()?.offset ?: 0 } + .collect { scrollPosition -> + connection.appBarOffset = if (state.layoutInfo.visibleItemsInfo.lastOrNull()?.index == state.layoutInfo.totalItemsCount - 1) { + state.layoutInfo.viewportEndOffset - scrollPosition.toFloat() - state.layoutInfo.afterContentPadding + } else { + // show always when last item is not visible + -1000f + } + //Log.d(TAG, "Scrolling position changed from $offset to ${connection.appBarOffset}") + } + } else { + snapshotFlow { state.firstVisibleItemScrollOffset } + .filter { state.firstVisibleItemIndex == 0 } + .collect { scrollPosition -> + val offset = connection.appBarOffset + if ((offset + scrollPosition + state.layoutInfo.afterContentPadding).absoluteValue > 1 || scrollBarDraggingState.value) { + connection.appBarOffset = -scrollPosition.toFloat() + //Log.d(TAG, "Scrolling position changed from $offset to ${connection.appBarOffset}") + } + } + } + } + val modifier = if (fillMaxSize) Modifier.fillMaxSize().then(modifier) else modifier + Box(Modifier.copyViewToAppBar(remember { appPrefs.appearanceBarsBlurRadius.state }.value, LocalAppBarHandler.current?.graphicsLayer).nestedScroll(connection)) { + LazyColumn(modifier.then(scrollModifier), state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled, content) + ScrollBar(reverseLayout, state, scrollBarAlpha, scrollJob, scrollBarDraggingState, additionalBarOffset) + } +} + +@Composable +actual fun LazyColumnWithScrollBarNoAppBar( + modifier: Modifier, + state: LazyListState?, + contentPadding: PaddingValues, + reverseLayout: Boolean, + verticalArrangement: Arrangement.Vertical, + horizontalAlignment: Alignment.Horizontal, + flingBehavior: FlingBehavior, + userScrollEnabled: Boolean, + additionalBarOffset: State?, content: LazyListScope.() -> Unit ) { val scope = rememberCoroutineScope() @@ -51,32 +126,110 @@ actual fun LazyColumnWithScrollBar( } } } - val state = state ?: LocalAppBarHandler.current?.listState ?: rememberLazyListState() - val connection = LocalAppBarHandler.current?.connection + val state = state ?: rememberLazyListState() // When scroll bar is dragging, there is no scroll event in nested scroll modifier. So, listen for changes on lazy column state // (only first visible row is useful because LazyColumn doesn't have absolute scroll position, only relative to row) val scrollBarDraggingState = remember { mutableStateOf(false) } - LaunchedEffect(Unit) { - snapshotFlow { state.firstVisibleItemScrollOffset } - .filter { state.firstVisibleItemIndex == 0 } - .collect { scrollPosition -> - val offset = connection?.appBarOffset - if (offset != null && ((offset + scrollPosition).absoluteValue > 1 || scrollBarDraggingState.value)) { - connection.appBarOffset = -scrollPosition.toFloat() -// Log.d(TAG, "Scrolling position changed from $offset to ${connection.appBarOffset}") - } - } - } - Box(if (connection != null) Modifier.nestedScroll(connection) else Modifier) { + Box { LazyColumn(modifier.then(scrollModifier), state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled, content) - Box(Modifier.fillMaxSize(), contentAlignment = Alignment.CenterEnd) { - DesktopScrollBar(rememberScrollbarAdapter(state), Modifier.fillMaxHeight(), scrollBarAlpha, scrollJob, reverseLayout, scrollBarDraggingState) - } + ScrollBar(reverseLayout, state, scrollBarAlpha, scrollJob, scrollBarDraggingState, additionalBarOffset) + } +} + +@Composable +private fun ScrollBar( + reverseLayout: Boolean, + state: LazyListState, + scrollBarAlpha: Animatable, + scrollJob: MutableState, + scrollBarDraggingState: MutableState, + additionalBarHeight: State? +) { + val oneHandUI = remember { appPrefs.oneHandUI.state } + val padding = if (additionalBarHeight != null) { + PaddingValues(top = if (oneHandUI.value) 0.dp else AppBarHeight * fontSizeSqrtMultiplier, bottom = additionalBarHeight.value) + } else if (reverseLayout) { + PaddingValues(bottom = AppBarHeight * fontSizeSqrtMultiplier) + } else { + PaddingValues(top = if (oneHandUI.value) 0.dp else AppBarHeight * fontSizeSqrtMultiplier) + } + Box(Modifier.fillMaxSize().padding(padding), contentAlignment = Alignment.CenterEnd) { + DesktopScrollBar(rememberScrollbarAdapter(state), Modifier.fillMaxHeight(), scrollBarAlpha, scrollJob, reverseLayout, scrollBarDraggingState) } } @Composable actual fun ColumnWithScrollBar( + modifier: Modifier, + verticalArrangement: Arrangement.Vertical, + horizontalAlignment: Alignment.Horizontal, + state: ScrollState?, + maxIntrinsicSize: Boolean, + fillMaxSize: Boolean, + content: @Composable() (ColumnScope.() -> Unit) +) { + val handler = LocalAppBarHandler.current + require(handler != null) { "Using ColumnWithScrollBar and without AppBarHandler is an error. Use ColumnWithScrollBarNoAppBar instead" } + + val scope = rememberCoroutineScope() + val scrollBarAlpha = remember { Animatable(0f) } + val scrollJob: MutableState = remember { mutableStateOf(Job()) } + val scrollModifier = remember { + Modifier + .pointerInput(Unit) { + detectCursorMove { + scope.launch { + scrollBarAlpha.animateTo(1f) + } + scrollJob.value.cancel() + scrollJob.value = scope.launch { + delay(1000L) + scrollBarAlpha.animateTo(0f) + } + } + } + } + val state = state ?: handler.scrollState + val connection = handler.connection + // When scroll bar is dragging, there is no scroll event in nested scroll modifier. So, listen for changes on column state + // (exact scroll position is available but in Int, not Float) + val scrollBarDraggingState = remember { mutableStateOf(false) } + LaunchedEffect(Unit) { + snapshotFlow { state.value } + .collect { scrollPosition -> + val offset = connection.appBarOffset + if ((offset + scrollPosition).absoluteValue > 1 || scrollBarDraggingState.value) { + connection.appBarOffset = -scrollPosition.toFloat() +// Log.d(TAG, "Scrolling position changed from $offset to ${connection.appBarOffset}") + } + } + } + val modifier = if (fillMaxSize) Modifier.fillMaxSize().then(modifier) else modifier + Box(Modifier.nestedScroll(connection)) { + val oneHandUI = remember { appPrefs.oneHandUI.state } + val padding = if (oneHandUI.value) PaddingValues(bottom = AppBarHeight * fontSizeSqrtMultiplier) else PaddingValues(top = AppBarHeight * fontSizeSqrtMultiplier) + Column( + if (maxIntrinsicSize) { + modifier.copyViewToAppBar(remember { appPrefs.appearanceBarsBlurRadius.state }.value, LocalAppBarHandler.current?.graphicsLayer).verticalScroll(state).height(IntrinsicSize.Max).then(scrollModifier) + } else { + modifier.then(scrollModifier).copyViewToAppBar(remember { appPrefs.appearanceBarsBlurRadius.state }.value, LocalAppBarHandler.current?.graphicsLayer).verticalScroll(state) + }, + verticalArrangement, horizontalAlignment + ) { + Spacer(if (oneHandUI.value) Modifier.padding(top = DEFAULT_PADDING + 5.dp) else Modifier.padding(padding)) + content() + if (oneHandUI.value) { + Spacer(Modifier.padding(padding)) + } + } + Box(Modifier.fillMaxSize().padding(padding), contentAlignment = Alignment.CenterEnd) { + DesktopScrollBar(rememberScrollbarAdapter(state), Modifier.fillMaxHeight(), scrollBarAlpha, scrollJob, false, scrollBarDraggingState) + } + } +} + +@Composable +actual fun ColumnWithScrollBarNoAppBar( modifier: Modifier, verticalArrangement: Arrangement.Vertical, horizontalAlignment: Alignment.Horizontal, @@ -102,29 +255,20 @@ actual fun ColumnWithScrollBar( } } } - val state = state ?: LocalAppBarHandler.current?.scrollState ?: rememberScrollState() - val connection = LocalAppBarHandler.current?.connection + val state = state ?: rememberScrollState() // When scroll bar is dragging, there is no scroll event in nested scroll modifier. So, listen for changes on column state // (exact scroll position is available but in Int, not Float) val scrollBarDraggingState = remember { mutableStateOf(false) } - LaunchedEffect(Unit) { - snapshotFlow { state.value } - .collect { scrollPosition -> - val offset = connection?.appBarOffset - if (offset != null && ((offset + scrollPosition).absoluteValue > 1 || scrollBarDraggingState.value)) { - connection.appBarOffset = -scrollPosition.toFloat() -// Log.d(TAG, "Scrolling position changed from $offset to ${connection.appBarOffset}") - } - } - } - Box(if (connection != null) Modifier.nestedScroll(connection) else Modifier) { + Box { Column( if (maxIntrinsicSize) { modifier.verticalScroll(state).height(IntrinsicSize.Max).then(scrollModifier) } else { - modifier.verticalScroll(state).then(scrollModifier) + modifier.then(scrollModifier).verticalScroll(state) }, - verticalArrangement, horizontalAlignment, content) + verticalArrangement, horizontalAlignment) { + content() + } Box(Modifier.fillMaxSize(), contentAlignment = Alignment.CenterEnd) { DesktopScrollBar(rememberScrollbarAdapter(state), Modifier.fillMaxHeight(), scrollBarAlpha, scrollJob, false, scrollBarDraggingState) } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.desktop.kt index bf2e118cd1..a1df7091d6 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.desktop.kt @@ -1,86 +1,155 @@ package chat.simplex.common.views.chatlist import androidx.compose.foundation.* +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Icon -import androidx.compose.material.MaterialTheme +import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.dp +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.call.Call -import chat.simplex.common.views.call.CallMediaType import chat.simplex.common.views.chat.item.ItemAction import chat.simplex.common.views.helpers.* import chat.simplex.res.MR import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource -import kotlinx.coroutines.flow.MutableStateFlow @Composable actual fun ActiveCallInteractiveArea(call: Call) { val showMenu = remember { mutableStateOf(false) } - CompositionLocalProvider( - LocalIndication provides NoIndication + val oneHandUI = remember { appPrefs.oneHandUI.state } + if (oneHandUI.value) { + ActiveCallInteractiveAreaOneHand(call, showMenu) + } else { + CompositionLocalProvider( + LocalIndication provides NoIndication + ) { + ActiveCallInteractiveAreaNonOneHand(call, showMenu) + } + } +} + +@Composable +private fun ActiveCallInteractiveAreaOneHand(call: Call, showMenu: MutableState) { + Box( + Modifier + .minimumInteractiveComponentSize() + .combinedClickable(onClick = { + val chat = chatModel.getChat(call.contact.id) + if (chat != null) { + withBGApi { + openChat(chat.remoteHostId, chat.chatInfo, chatModel) + } + } + }, + onLongClick = { showMenu.value = true }, + role = Role.Button, + interactionSource = remember { MutableInteractionSource() }, + indication = remember { ripple(bounded = false, radius = 24.dp) } + ) + .onRightClick { showMenu.value = true }, + contentAlignment = Alignment.Center + ) { + ProfileImage( + image = call.contact.profile.image, + size = 37.dp * fontSizeSqrtMultiplier, + color = MaterialTheme.colors.secondaryVariant.mixWith(MaterialTheme.colors.onBackground, 0.97f) + ) + Box( + Modifier.offset(x = 1.dp, y = (-1).dp).background(SimplexGreen, CircleShape).padding(3.dp) + .align(Alignment.TopEnd) + ) { + if (call.hasVideo) { + Icon( + painterResource(MR.images.ic_videocam_filled), + stringResource(MR.strings.icon_descr_video_call), + Modifier.size(12.dp), + tint = Color.White + ) + } else { + Icon( + painterResource(MR.images.ic_call_filled), + stringResource(MR.strings.icon_descr_audio_call), + Modifier.size(12.dp), + tint = Color.White + ) + } + } + DefaultDropdownMenu(showMenu) { + ItemAction( + stringResource(MR.strings.icon_descr_hang_up), + painterResource(MR.images.ic_call_end_filled), + color = MaterialTheme.colors.error, + onClick = { + withBGApi { chatModel.callManager.endCall(call) } + showMenu.value = false + }) + } + } +} + +@Composable +private fun ActiveCallInteractiveAreaNonOneHand(call: Call, showMenu: MutableState) { + Box( + Modifier + .fillMaxSize(), + contentAlignment = Alignment.BottomEnd ) { Box( Modifier - .fillMaxSize(), - contentAlignment = Alignment.BottomEnd - ) { - Box( - Modifier - .padding(end = 15.dp, bottom = 92.dp) - .size(67.dp) - .combinedClickable(onClick = { - val chat = chatModel.getChat(call.contact.id) - if (chat != null) { - withBGApi { - openChat(chat.remoteHostId, chat.chatInfo, chatModel) - } + .padding(end = 15.dp, bottom = 92.dp) + .size(67.dp) + .combinedClickable(onClick = { + val chat = chatModel.getChat(call.contact.id) + if (chat != null) { + withBGApi { + openChat(chat.remoteHostId, chat.chatInfo, chatModel) } - }, - onLongClick = { showMenu.value = true }) - .onRightClick { showMenu.value = true }, - contentAlignment = Alignment.Center - ) { - Box(Modifier.background(MaterialTheme.colors.background, CircleShape)) { - ProfileImageForActiveCall(size = 56.dp, image = call.contact.profile.image) - } - Box( - Modifier.padding().background(SimplexGreen, CircleShape).padding(4.dp) - .align(Alignment.TopEnd) - ) { - if (call.hasVideo) { - Icon( - painterResource(MR.images.ic_videocam_filled), - stringResource(MR.strings.icon_descr_video_call), - Modifier.size(18.dp), - tint = Color.White - ) - } else { - Icon( - painterResource(MR.images.ic_call_filled), - stringResource(MR.strings.icon_descr_audio_call), - Modifier.size(18.dp), - tint = Color.White - ) } + }, + onLongClick = { showMenu.value = true }) + .onRightClick { showMenu.value = true }, + contentAlignment = Alignment.Center + ) { + Box(Modifier.background(MaterialTheme.colors.background, CircleShape)) { + ProfileImageForActiveCall(size = 56.dp, image = call.contact.profile.image) + } + Box( + Modifier.padding().background(SimplexGreen, CircleShape).padding(4.dp) + .align(Alignment.TopEnd) + ) { + if (call.hasVideo) { + Icon( + painterResource(MR.images.ic_videocam_filled), + stringResource(MR.strings.icon_descr_video_call), + Modifier.size(18.dp), + tint = Color.White + ) + } else { + Icon( + painterResource(MR.images.ic_call_filled), + stringResource(MR.strings.icon_descr_audio_call), + Modifier.size(18.dp), + tint = Color.White + ) } - DefaultDropdownMenu(showMenu) { - ItemAction( - stringResource(MR.strings.icon_descr_hang_up), - painterResource(MR.images.ic_call_end_filled), - color = MaterialTheme.colors.error, - onClick = { - withBGApi { chatModel.callManager.endCall(call) } - showMenu.value = false - }) - } + } + DefaultDropdownMenu(showMenu) { + ItemAction( + stringResource(MR.strings.icon_descr_hang_up), + painterResource(MR.images.ic_call_end_filled), + color = MaterialTheme.colors.error, + onClick = { + withBGApi { chatModel.callManager.endCall(call) } + showMenu.value = false + }) } } } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.desktop.kt index d9b53b9485..3855835ab6 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.desktop.kt @@ -10,11 +10,13 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.User import chat.simplex.common.model.UserInfo import chat.simplex.common.platform.* @@ -25,6 +27,7 @@ import kotlinx.coroutines.flow.MutableStateFlow @Composable actual fun UserPickerUsersSection( users: List, + iconColor: Color, stopped: Boolean, onUserClicked: (user: User) -> Unit, ) { @@ -37,7 +40,7 @@ actual fun UserPickerUsersSection( .padding(horizontal = horizontalPadding) .height((55.dp + 16.sp.toDp()) * rowsToDisplay + (if (rowsToDisplay > 1) DEFAULT_PADDING else 0.dp)) ) { - ColumnWithScrollBar( + ColumnWithScrollBarNoAppBar( verticalArrangement = Arrangement.spacedBy(DEFAULT_PADDING) ) { val spaceBetween = (((DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier) - (horizontalPadding)) - (65.dp * 5)) / 5 @@ -57,7 +60,7 @@ actual fun UserPickerUsersSection( ) { val user = u.user Box { - ProfileImage(size = 55.dp, image = user.profile.image, color = MaterialTheme.colors.secondaryVariant) + ProfileImage(size = 55.dp, image = user.profile.image, color = iconColor) if (u.unreadCount > 0 && !user.activeUser) { unreadBadge(u.unreadCount, user.showNtfs, true) @@ -95,7 +98,8 @@ actual fun PlatformUserPicker(modifier: Modifier, pickerState: MutableStateFlow< .clickable(interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = { pickerState.value = AnimatedViewState.HIDING }), contentAlignment = Alignment.TopStart ) { - ColumnWithScrollBar(modifier) { + val oneHandUI = remember { appPrefs.oneHandUI.state } + ColumnWithScrollBarNoAppBar(modifier.align(if (oneHandUI.value) Alignment.BottomCenter else Alignment.TopCenter)) { content() } } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/Appearance.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/Appearance.desktop.kt index 244504f4c7..91ff8831ce 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/Appearance.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/Appearance.desktop.kt @@ -2,6 +2,7 @@ package chat.simplex.common.views.usersettings import SectionBottomSpacer import SectionDividerSpaced +import SectionSpacer import SectionView import androidx.compose.foundation.* import androidx.compose.foundation.layout.* @@ -39,9 +40,7 @@ fun AppearanceScope.AppearanceLayout( languagePref: SharedPreference, systemDarkTheme: SharedPreference, ) { - ColumnWithScrollBar( - Modifier.fillMaxWidth(), - ) { + ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.appearance_settings)) SectionView(stringResource(MR.strings.settings_section_title_language), contentPadding = PaddingValues()) { val state = rememberSaveable { mutableStateOf(languagePref.get() ?: "system") } @@ -58,10 +57,14 @@ fun AppearanceScope.AppearanceLayout( } } } + SettingsPreferenceItem(icon = null, stringResource(MR.strings.one_hand_ui), ChatModel.controller.appPrefs.oneHandUI) } SectionDividerSpaced() ThemesSection(systemDarkTheme) + SectionDividerSpaced() + AppToolbarsSection() + SectionDividerSpaced() MessageShapeSection() @@ -83,7 +86,7 @@ fun DensityScaleSection() { val localDensityScale = remember { mutableStateOf(appPrefs.densityScale.get()) } SectionView(stringResource(MR.strings.appearance_zoom).uppercase(), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) { Row(Modifier.padding(top = 10.dp), verticalAlignment = Alignment.CenterVertically) { - Box(Modifier.size(60.dp) + Box(Modifier.size(50.dp) .background(MaterialTheme.colors.surface, RoundedCornerShape(percent = 22)) .clip(RoundedCornerShape(percent = 22)) .clickable { @@ -101,7 +104,7 @@ fun DensityScaleSection() { ) } } - Spacer(Modifier.width(10.dp)) + Spacer(Modifier.width(15.dp)) Slider( localDensityScale.value, valueRange = 1f..2f, diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.desktop.kt index ee8ae93de5..5b4a044df3 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.desktop.kt @@ -18,12 +18,11 @@ import dev.icerock.moko.resources.compose.stringResource @Composable actual fun SettingsSectionApp( showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), - showCustomModal: (@Composable ModalData.(ChatModel, () -> Unit) -> Unit) -> (() -> Unit), showVersion: () -> Unit, withAuth: (title: String, desc: String, block: () -> Unit) -> Unit ) { SectionView(stringResource(MR.strings.settings_section_title_app)) { - SettingsActionItem(painterResource(MR.images.ic_code), stringResource(MR.strings.settings_developer_tools), showSettingsModal { DeveloperView(it, showCustomModal, withAuth) }) + SettingsActionItem(painterResource(MR.images.ic_code), stringResource(MR.strings.settings_developer_tools), showSettingsModal { DeveloperView(withAuth) }) val selectedChannel = remember { appPrefs.appUpdateChannel.state } val values = AppUpdatesChannel.entries.map { it to it.text } ExposedDropDownSettingRow(stringResource(MR.strings.app_check_for_updates), values, selectedChannel) { From 3c8c9d8b524482903a819861f032d896e43b8024 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sat, 2 Nov 2024 13:43:45 +0000 Subject: [PATCH 005/167] website: update jobs page --- docs/JOIN_TEAM.md | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/docs/JOIN_TEAM.md b/docs/JOIN_TEAM.md index 26502a05af..c72a75cfec 100644 --- a/docs/JOIN_TEAM.md +++ b/docs/JOIN_TEAM.md @@ -8,25 +8,23 @@ layout: layouts/jobs.html SimpleX Chat Ltd is a seed stage startup with a lot of user growth in 2022-2023, and a lot of exciting technical and product problems to solve to grow faster. -We currently have 4 full-time people in the team - all engineers, including the founder. - -We want to add up to 3 people to the team. +We currently have 6 full-time people in the team. +We want to add 2 people to the team. ## Who we are looking for -### Product/UI designer +### Web designer & developer for a website contract -You will be designing the user experience and the interface of both the app and the website in collaboration with the team. +You will work with the founder and a product marketing expert to convert the stories we want to tell our current and prospective users into interactive experiences. -The current focus of the app is privacy and security, but we hope to have the design that would support the feeling of psychological safety, enabling people to achieve the results in the smallest amount of time. +You are an expert in creating interactive web experiences: +- 15+ years of web development and design experience. +- Passionate about communications, privacy and data ownership. +- Competent using PhotoShop, 3D modelling, etc. +- Competent in Web tech, including JavaScript, animations, etc. -You are an experienced and innovative product designer with: -- 8+ years of user experience and visual design. -- Expertise in typography and high sensitivity to colors. -- Exceptional precision and attention to details. -- Strong opinions (weakly held). -- A strong empathy. +We will NOT consider agencies or groups – it must be one person working on the project. ### Application Haskell engineer @@ -34,13 +32,12 @@ You will work with the Haskell core of the client applications and with the netw You are an expert in language models, databases and Haskell: - expert knowledge of SQL. -- Haskell exception handling, concurrency, STM, type systems. -- 8y+ of software engineering experience in complex projects, +- Haskell strictness, exceptions, [concurrency](https://simonmar.github.io/pages/pcph.html), STM, [type systems](https://thinkingwithtypes.com). +- 15y+ of software engineering experience in complex projects. - deep understanding of the common programming principles: - data structures, bits and bytes, text encoding. - - software design and algorithms. - - concurrency. - - networking. + - [functional software design](https://mitp-content-server.mit.edu/books/content/sectbyfn/books_pres_0/6515/sicp.zip/index.html) and algorithms. + - protocols and networking. ## About you @@ -48,6 +45,7 @@ You are an expert in language models, databases and Haskell: - already use SimpleX Chat to communicate with friends/family or participate in public SimpleX Chat groups. - passionate about privacy, security and communications. - interested to make contributions to SimpleX Chat open-source project in your free time before we hire you, as an extended test. + - you founded (and probably failed) at least one startup, or spent more time working for yourself than being employed. - **Exceptionally pragmatic, very fast and customer-focussed**: - care about the customers (aka users) and about the product we build much more than about the code quality, technology stack, etc. From ceb17b23b42dddae68f81598602942612fc4ee23 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Sat, 2 Nov 2024 15:28:41 +0000 Subject: [PATCH 006/167] bumped haskell.nix (#5134) Co-authored-by: Moritz Angermann --- flake.lock | 111 +++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 82 insertions(+), 29 deletions(-) diff --git a/flake.lock b/flake.lock index a11e01683e..eac7357cd8 100644 --- a/flake.lock +++ b/flake.lock @@ -156,11 +156,11 @@ "ghc98X": { "flake": false, "locked": { - "lastModified": 1696643148, - "narHash": "sha256-E02DfgISH7EvvNAu0BHiPvl1E5FGMDi0pWdNZtIBC9I=", + "lastModified": 1715066704, + "narHash": "sha256-F0EVR8x/fcpj1st+hz96Wdsz5uwVIOziGKAwRxLOYJw=", "ref": "ghc-9.8", - "rev": "443e870d977b1ab6fc05f47a9a17bc49296adbd6", - "revCount": 61642, + "rev": "78a253543d466ac511a1664a3e6aff032ca684d5", + "revCount": 61757, "submodules": true, "type": "git", "url": "https://gitlab.haskell.org/ghc/ghc" @@ -175,11 +175,11 @@ "ghc99": { "flake": false, "locked": { - "lastModified": 1697054644, - "narHash": "sha256-kKarOuXUaAH3QWv7ASx+gGFMHaHKe0pK5Zu37ky2AL4=", + "lastModified": 1726585445, + "narHash": "sha256-IdwQBex4boY6s0Plj5+ixf36rfYSUyMdTWrztKvZH30=", "ref": "refs/heads/master", - "rev": "f383a242c76f90bcca8a4d7ee001dcb49c172a9a", - "revCount": 62040, + "rev": "7fd9e5e29ab54eb406880077463e8552e2ddd39a", + "revCount": 67238, "submodules": true, "type": "git", "url": "https://gitlab.haskell.org/ghc/ghc" @@ -225,6 +225,8 @@ "hls-2.2": "hls-2.2", "hls-2.3": "hls-2.3", "hls-2.4": "hls-2.4", + "hls-2.5": "hls-2.5", + "hls-2.6": "hls-2.6", "hpc-coveralls": "hpc-coveralls", "hydra": "hydra", "iserv-proxy": "iserv-proxy", @@ -238,16 +240,17 @@ "nixpkgs-2205": "nixpkgs-2205", "nixpkgs-2211": "nixpkgs-2211", "nixpkgs-2305": "nixpkgs-2305", + "nixpkgs-2311": "nixpkgs-2311", "nixpkgs-unstable": "nixpkgs-unstable", "old-ghc-nix": "old-ghc-nix", "stackage": "stackage" }, "locked": { - "lastModified": 1701163700, - "narHash": "sha256-sOrewUS3LnzV09nGr7+3R6Q6zsgU4smJc61QsHq+4DE=", + "lastModified": 1705833500, + "narHash": "sha256-rUIr6JNbCedt1g4gVYVvE9t0oFU6FUspCA0DS5cA8Bg=", "owner": "input-output-hk", "repo": "haskell.nix", - "rev": "2808bfe3e62e9eb4ee8974cd623a00e1611f302b", + "rev": "d0c35e75cbbc6858770af42ac32b0b85495fbd71", "type": "github" }, "original": { @@ -328,16 +331,50 @@ "hls-2.4": { "flake": false, "locked": { - "lastModified": 1696939266, - "narHash": "sha256-VOMf5+kyOeOmfXTHlv4LNFJuDGa7G3pDnOxtzYR40IU=", + "lastModified": 1699862708, + "narHash": "sha256-YHXSkdz53zd0fYGIYOgLt6HrA0eaRJi9mXVqDgmvrjk=", "owner": "haskell", "repo": "haskell-language-server", - "rev": "362fdd1293efb4b82410b676ab1273479f6d17ee", + "rev": "54507ef7e85fa8e9d0eb9a669832a3287ffccd57", "type": "github" }, "original": { "owner": "haskell", - "ref": "2.4.0.0", + "ref": "2.4.0.1", + "repo": "haskell-language-server", + "type": "github" + } + }, + "hls-2.5": { + "flake": false, + "locked": { + "lastModified": 1701080174, + "narHash": "sha256-fyiR9TaHGJIIR0UmcCb73Xv9TJq3ht2ioxQ2mT7kVdc=", + "owner": "haskell", + "repo": "haskell-language-server", + "rev": "27f8c3d3892e38edaef5bea3870161815c4d014c", + "type": "github" + }, + "original": { + "owner": "haskell", + "ref": "2.5.0.0", + "repo": "haskell-language-server", + "type": "github" + } + }, + "hls-2.6": { + "flake": false, + "locked": { + "lastModified": 1705325287, + "narHash": "sha256-+P87oLdlPyMw8Mgoul7HMWdEvWP/fNlo8jyNtwME8E8=", + "owner": "haskell", + "repo": "haskell-language-server", + "rev": "6e0b342fa0327e628610f2711f8c3e4eaaa08b1e", + "type": "github" + }, + "original": { + "owner": "haskell", + "ref": "2.6.0.0", "repo": "haskell-language-server", "type": "github" } @@ -384,11 +421,11 @@ "iserv-proxy": { "flake": false, "locked": { - "lastModified": 1691634696, - "narHash": "sha256-MZH2NznKC/gbgBu8NgIibtSUZeJ00HTLJ0PlWKCBHb0=", + "lastModified": 1707968597, + "narHash": "sha256-C53NqToxl+n9s1pQ0iLtiH6P5vX3rM+NW/mFt4Ykpsk=", "ref": "hkm/remote-iserv", - "rev": "43a979272d9addc29fbffc2e8542c5d96e993d73", - "revCount": 14, + "rev": "1b7f8aeb37bbc7c00f04e44d9379aa15a4409e8b", + "revCount": 18, "type": "git", "url": "https://gitlab.haskell.org/hamishmack/iserv-proxy.git" }, @@ -552,11 +589,11 @@ }, "nixpkgs-2305": { "locked": { - "lastModified": 1695416179, - "narHash": "sha256-610o1+pwbSu+QuF3GE0NU5xQdTHM3t9wyYhB9l94Cd8=", + "lastModified": 1705033721, + "narHash": "sha256-K5eJHmL1/kev6WuqyqqbS1cdNnSidIZ3jeqJ7GbrYnQ=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "715d72e967ec1dd5ecc71290ee072bcaf5181ed6", + "rev": "a1982c92d8980a0114372973cbdfe0a307f1bdea", "type": "github" }, "original": { @@ -566,6 +603,22 @@ "type": "github" } }, + "nixpkgs-2311": { + "locked": { + "lastModified": 1719957072, + "narHash": "sha256-gvFhEf5nszouwLAkT9nWsDzocUTqLWHuL++dvNjMp9I=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "7144d6241f02d171d25fba3edeaf15e0f2592105", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-23.11-darwin", + "repo": "nixpkgs", + "type": "github" + } + }, "nixpkgs-lib": { "locked": { "dir": "lib", @@ -602,17 +655,17 @@ }, "nixpkgs-unstable": { "locked": { - "lastModified": 1695318763, - "narHash": "sha256-FHVPDRP2AfvsxAdc+AsgFJevMz5VBmnZglFUMlxBkcY=", + "lastModified": 1694822471, + "narHash": "sha256-6fSDCj++lZVMZlyqOe9SIOL8tYSBz1bI8acwovRwoX8=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "e12483116b3b51a185a33a272bf351e357ba9a99", + "rev": "47585496bcb13fb72e4a90daeea2f434e2501998", "type": "github" }, "original": { "owner": "NixOS", - "ref": "nixpkgs-unstable", "repo": "nixpkgs", + "rev": "47585496bcb13fb72e4a90daeea2f434e2501998", "type": "github" } }, @@ -664,11 +717,11 @@ "stackage": { "flake": false, "locked": { - "lastModified": 1699834215, - "narHash": "sha256-g/JKy0BCvJaxPuYDl3QVc4OY8cFEomgG+hW/eEV470M=", + "lastModified": 1726532152, + "narHash": "sha256-LRXbVY3M2S8uQWdwd2zZrsnVPEvt2GxaHGoy8EFFdJA=", "owner": "input-output-hk", "repo": "stackage.nix", - "rev": "47aacd04abcce6bad57f43cbbbd133538380248e", + "rev": "c77b3530cebad603812cb111c6f64968c2d2337d", "type": "github" }, "original": { From 165143a1112308c035ac00ed669b96b60599aa1c Mon Sep 17 00:00:00 2001 From: Alexander Bondarenko <486682+dpwiz@users.noreply.github.com> Date: Sat, 2 Nov 2024 19:51:11 +0200 Subject: [PATCH 007/167] Use simplexmq with client_library flag (#5133) * Use simplexmq with client_library flag * fix server config for mq master * simplexmq --------- Co-authored-by: Evgeny Poberezkin --- Dockerfile | 2 +- cabal.project | 2 +- flake.nix | 7 +++++++ scripts/desktop/build-lib-linux.sh | 2 +- scripts/desktop/build-lib-mac.sh | 2 +- scripts/desktop/build-lib-windows.sh | 2 +- scripts/nix/sha256map.nix | 2 +- tests/ChatClient.hs | 4 ++++ 8 files changed, 17 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index 6c60195f97..7b9641777a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,7 +29,7 @@ RUN cp ./scripts/cabal.project.local.linux ./cabal.project.local # Compile simplex-chat RUN cabal update -RUN cabal build exe:simplex-chat +RUN cabal build exe:simplex-chat --constraint 'simplexmq +client_library' # Strip the binary from debug symbols to reduce size RUN bin=$(find /project/dist-newstyle -name "simplex-chat" -type f -executable) && \ diff --git a/cabal.project b/cabal.project index e98f8122d0..c9b8b11722 100644 --- a/cabal.project +++ b/cabal.project @@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: a8471eed5be93e7c3741aa4742b24193c9a2d6f5 + tag: ffecf200d4874dfa34f6d15b269964c0115a54ca source-repository-package type: git diff --git a/flake.nix b/flake.nix index e8ff779a87..1a1043c5f2 100644 --- a/flake.nix +++ b/flake.nix @@ -198,6 +198,7 @@ packages.direct-sqlcipher.components.library.libs = pkgs.lib.mkForce [ pkgs.pkgsCross.mingwW64.openssl ]; + packages.simplexmq.flags.client_library = true; packages.simplexmq.components.library.libs = pkgs.lib.mkForce [ pkgs.pkgsCross.mingwW64.openssl ]; @@ -335,6 +336,7 @@ packages.direct-sqlcipher.patches = [ ./scripts/nix/direct-sqlcipher-android-log.patch ]; + packages.simplexmq.flags.client_library = true; packages.simplexmq.components.library.libs = pkgs.lib.mkForce [ (android32Pkgs.openssl.override { static = true; enableKTLS = false; }) ]; @@ -443,6 +445,7 @@ packages.direct-sqlcipher.patches = [ ./scripts/nix/direct-sqlcipher-android-log.patch ]; + packages.simplexmq.flags.client_library = true; packages.simplexmq.components.library.libs = pkgs.lib.mkForce [ (androidPkgs.openssl.override { static = true; }) ]; @@ -547,6 +550,7 @@ packages.simplexmq.flags.swift = true; packages.direct-sqlcipher.flags.commoncrypto = true; packages.entropy.flags.DoNotGetEntropy = true; + packages.simplexmq.flags.client_library = true; packages.simplexmq.components.library.libs = pkgs.lib.mkForce [ # TODO: have a cross override for iOS, that sets this. ((pkgs.openssl.override { static = true; }).overrideDerivation (old: { CFLAGS = "-mcpu=apple-a7 -march=armv8-a+norcpc" ;})) @@ -561,6 +565,7 @@ extra-modules = [{ packages.direct-sqlcipher.flags.commoncrypto = true; packages.entropy.flags.DoNotGetEntropy = true; + packages.simplexmq.flags.client_library = true; packages.simplexmq.components.library.libs = pkgs.lib.mkForce [ ((pkgs.openssl.override { static = true; }).overrideDerivation (old: { CFLAGS = "-mcpu=apple-a7 -march=armv8-a+norcpc" ;})) ]; @@ -578,6 +583,7 @@ packages.simplexmq.flags.swift = true; packages.direct-sqlcipher.flags.commoncrypto = true; packages.entropy.flags.DoNotGetEntropy = true; + packages.simplexmq.flags.client_library = true; packages.simplexmq.components.library.libs = pkgs.lib.mkForce [ (pkgs.openssl.override { static = true; }) ]; @@ -591,6 +597,7 @@ extra-modules = [{ packages.direct-sqlcipher.flags.commoncrypto = true; packages.entropy.flags.DoNotGetEntropy = true; + packages.simplexmq.flags.client_library = true; packages.simplexmq.components.library.libs = pkgs.lib.mkForce [ (pkgs.openssl.override { static = true; }) ]; diff --git a/scripts/desktop/build-lib-linux.sh b/scripts/desktop/build-lib-linux.sh index da645c6e86..80ae9fa82e 100755 --- a/scripts/desktop/build-lib-linux.sh +++ b/scripts/desktop/build-lib-linux.sh @@ -25,7 +25,7 @@ for elem in "${exports[@]}"; do count=$(grep -R "$elem$" libsimplex.dll.def | wc for elem in "${exports[@]}"; do count=$(grep -R "\"$elem\"" flake.nix | wc -l); if [ $count -ne 2 ]; then echo Wrong exports in flake.nix. Add \"$elem\" in two places of the file; exit 1; fi ; done rm -rf $BUILD_DIR -cabal build lib:simplex-chat --ghc-options='-optl-Wl,-rpath,$ORIGIN -flink-rts -threaded' +cabal build lib:simplex-chat --ghc-options='-optl-Wl,-rpath,$ORIGIN -flink-rts -threaded' --constraint 'simplexmq +client_library' cd $BUILD_DIR/build #patchelf --add-needed libHSrts_thr-ghc${GHC_VERSION}.so libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so #patchelf --add-rpath '$ORIGIN' libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so diff --git a/scripts/desktop/build-lib-mac.sh b/scripts/desktop/build-lib-mac.sh index 2b0fd5376f..9d7d5031a0 100755 --- a/scripts/desktop/build-lib-mac.sh +++ b/scripts/desktop/build-lib-mac.sh @@ -24,7 +24,7 @@ for elem in "${exports[@]}"; do count=$(grep -R "$elem$" libsimplex.dll.def | wc for elem in "${exports[@]}"; do count=$(grep -R "\"$elem\"" flake.nix | wc -l); if [ $count -ne 2 ]; then echo Wrong exports in flake.nix. Add \"$elem\" in two places of the file; exit 1; fi ; done rm -rf $BUILD_DIR -cabal build lib:simplex-chat lib:simplex-chat --ghc-options="-optl-Wl,-rpath,@loader_path -optl-Wl,-L$GHC_LIBS_DIR/$ARCH-osx-ghc-$GHC_VERSION -optl-lHSrts_thr-ghc$GHC_VERSION -optl-lffi" +cabal build lib:simplex-chat lib:simplex-chat --ghc-options="-optl-Wl,-rpath,@loader_path -optl-Wl,-L$GHC_LIBS_DIR/$ARCH-osx-ghc-$GHC_VERSION -optl-lHSrts_thr-ghc$GHC_VERSION -optl-lffi" --constraint 'simplexmq +client_library' cd $BUILD_DIR/build mkdir deps 2> /dev/null || true diff --git a/scripts/desktop/build-lib-windows.sh b/scripts/desktop/build-lib-windows.sh index 72de53854f..0e96a42e86 100755 --- a/scripts/desktop/build-lib-windows.sh +++ b/scripts/desktop/build-lib-windows.sh @@ -51,7 +51,7 @@ echo " ghc-options: -shared -threaded -optl-L$openssl_windows_style_path -opt # Very important! Without it the build fails on linking step since the linker can't find exported symbols. # It looks like GHC bug because with such random path the build ends successfully sed -i "s/ld.lld.exe/abracadabra.exe/" `ghc --print-libdir`/settings -cabal build lib:simplex-chat +cabal build lib:simplex-chat --constraint 'simplexmq +client_library' rm -rf apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ rm -rf apps/multiplatform/desktop/build/cmake diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 8f53d078dc..8de91675e3 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."a8471eed5be93e7c3741aa4742b24193c9a2d6f5" = "093i40api0dp7rvw6f1f3pww3q5iv6mvbj577nlxp3qqcbvyh6fs"; + "https://github.com/simplex-chat/simplexmq.git"."ffecf200d4874dfa34f6d15b269964c0115a54ca" = "0kb8hq37fc5g198wq7dswnlwjzk67q8rrzil2dii5lc6xfr47jbs"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index 36bdf92dbf..75b85d7a5f 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -51,6 +51,7 @@ import qualified Simplex.Messaging.Crypto.Ratchet as CR import Simplex.Messaging.Protocol (srvHostnamesSMPClientVersion) import Simplex.Messaging.Server (runSMPServerBlocking) import Simplex.Messaging.Server.Env.STM +import Simplex.Messaging.Server.MsgStore.Types (AMSType (..), SMSType (..)) import Simplex.Messaging.Transport import Simplex.Messaging.Transport.Server (ServerCredentials (..), defaultTransportServerConfig) import Simplex.Messaging.Version @@ -424,6 +425,9 @@ smpServerCfg = tbqSize = 1, -- serverTbqSize = 1, msgQueueQuota = 16, + msgStoreType = AMSType SMSMemory, + maxJournalMsgCount = 1000, + maxJournalStateLines = 1000, queueIdBytes = 12, msgIdBytes = 6, storeLogFile = Nothing, From 7a741e7ac4ce945cf8bda920153169b6c2d8c051 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sat, 2 Nov 2024 20:03:27 +0000 Subject: [PATCH 008/167] ios: update core library --- apps/ios/SimpleX.xcodeproj/project.pbxproj | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 2b1160061c..cd146d4292 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -149,9 +149,9 @@ 6419EC582AB97507004A607A /* CIMemberCreatedContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6419EC572AB97507004A607A /* CIMemberCreatedContactView.swift */; }; 6432857C2925443C00FBE5C8 /* GroupPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */; }; 643B3B452CCBEB080083A2CF /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643B3B402CCBEB080083A2CF /* libgmpxx.a */; }; - 643B3B462CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643B3B412CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9-ghc9.6.3.a */; }; + 643B3B462CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643B3B412CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5-ghc9.6.3.a */; }; 643B3B472CCBEB080083A2CF /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643B3B422CCBEB080083A2CF /* libffi.a */; }; - 643B3B482CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643B3B432CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9.a */; }; + 643B3B482CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643B3B432CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5.a */; }; 643B3B492CCBEB080083A2CF /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643B3B442CCBEB080083A2CF /* libgmp.a */; }; 6440CA00288857A10062C672 /* CIEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440C9FF288857A10062C672 /* CIEventView.swift */; }; 6440CA03288AECA70062C672 /* AddGroupMembersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440CA02288AECA70062C672 /* AddGroupMembersView.swift */; }; @@ -492,9 +492,9 @@ 6419EC572AB97507004A607A /* CIMemberCreatedContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIMemberCreatedContactView.swift; sourceTree = ""; }; 6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupPreferencesView.swift; sourceTree = ""; }; 643B3B402CCBEB080083A2CF /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libgmpxx.a; path = Libraries/libgmpxx.a; sourceTree = ""; }; - 643B3B412CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9-ghc9.6.3.a"; path = "Libraries/libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9-ghc9.6.3.a"; sourceTree = ""; }; + 643B3B412CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5-ghc9.6.3.a"; path = "Libraries/libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5-ghc9.6.3.a"; sourceTree = ""; }; 643B3B422CCBEB080083A2CF /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libffi.a; path = Libraries/libffi.a; sourceTree = ""; }; - 643B3B432CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9.a"; path = "Libraries/libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9.a"; sourceTree = ""; }; + 643B3B432CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5.a"; path = "Libraries/libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5.a"; sourceTree = ""; }; 643B3B442CCBEB080083A2CF /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libgmp.a; path = Libraries/libgmp.a; sourceTree = ""; }; 6440C9FF288857A10062C672 /* CIEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIEventView.swift; sourceTree = ""; }; 6440CA02288AECA70062C672 /* AddGroupMembersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGroupMembersView.swift; sourceTree = ""; }; @@ -663,8 +663,8 @@ 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */, - 643B3B482CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9.a in Frameworks */, - 643B3B462CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9-ghc9.6.3.a in Frameworks */, + 643B3B482CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5.a in Frameworks */, + 643B3B462CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5-ghc9.6.3.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -815,8 +815,8 @@ 643B3B422CCBEB080083A2CF /* libffi.a */, 643B3B442CCBEB080083A2CF /* libgmp.a */, 643B3B402CCBEB080083A2CF /* libgmpxx.a */, - 643B3B412CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9-ghc9.6.3.a */, - 643B3B432CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-KwHIA7FZqPI5ZTCAoi00n9.a */, + 643B3B412CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5-ghc9.6.3.a */, + 643B3B432CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5.a */, 5CA059C2279559F40002BEB4 /* Shared */, 5CDCAD462818589900503DA2 /* SimpleX NSE */, CEE723A82C3BD3D70009AE93 /* SimpleX SE */, From 97df069730e2d63b3eb7b644b127a7e3cc7b03ed Mon Sep 17 00:00:00 2001 From: Evgeny Date: Mon, 4 Nov 2024 13:28:57 +0000 Subject: [PATCH 009/167] core: add support for server operators (#4961) * core: add support for server operators * migration * update schema and queries, rfc * add usage conditions tables * core: server operators new apis draft * update * conditions * update * add get conditions api * add get conditions API * WIP * compiles * fix schema * core: ui logic in types (#5139) * update --------- Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> --- cabal.project | 2 +- docs/rfcs/2024-10-27-server-operators.md | 24 ++++ package.yaml | 1 + scripts/nix/sha256map.nix | 2 +- simplex-chat.cabal | 10 ++ src/Simplex/Chat.hs | 50 +++++++- src/Simplex/Chat/Controller.hs | 25 +++- .../Migrations/M20241027_server_operators.hs | 70 +++++++++++ src/Simplex/Chat/Migrations/chat_schema.sql | 39 +++++++ src/Simplex/Chat/Operators.hs | 110 ++++++++++++++++++ src/Simplex/Chat/Operators/Conditions.hs | 19 +++ src/Simplex/Chat/Store/Migrations.hs | 4 +- src/Simplex/Chat/Store/Profiles.hs | 77 ++++++++++-- src/Simplex/Chat/Terminal.hs | 8 +- src/Simplex/Chat/View.hs | 24 ++-- tests/ChatClient.hs | 7 +- tests/RandomServers.hs | 4 +- 17 files changed, 440 insertions(+), 36 deletions(-) create mode 100644 docs/rfcs/2024-10-27-server-operators.md create mode 100644 src/Simplex/Chat/Migrations/M20241027_server_operators.hs create mode 100644 src/Simplex/Chat/Operators.hs create mode 100644 src/Simplex/Chat/Operators/Conditions.hs diff --git a/cabal.project b/cabal.project index c9b8b11722..61ce04a569 100644 --- a/cabal.project +++ b/cabal.project @@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: ffecf200d4874dfa34f6d15b269964c0115a54ca + tag: ff05a465ee15ac7ae2c14a9fb703a18564950631 source-repository-package type: git diff --git a/docs/rfcs/2024-10-27-server-operators.md b/docs/rfcs/2024-10-27-server-operators.md new file mode 100644 index 0000000000..5456d28f08 --- /dev/null +++ b/docs/rfcs/2024-10-27-server-operators.md @@ -0,0 +1,24 @@ +# Server operators + +## Problem + +All preconfigured servers operated by a single company create a risk that user connections can be analysed by aggregating transport information from these servers. + +The solution is to have more than one operator servers pre-configured in the app. + +For operators to be protected from any violations of rights of other users or third parties by the users who use servers of these operators, the users have to explicitely accept conditions of use with the operator, in the same way they accept conditions of use with SimpleX Chat Ltd by downloading the app. + +## Solution + +Allow to assign operators to servers, both with preconfigured operators and servers, and with user-defined operators. Agent added support for server roles, chat app could: +- allow assigning server roles only on the operator level. +- only on server level. +- on both, with server roles overriding operator roles (that would require a different type for server for chat app). + +For simplicity of both UX and logic it is probably better to allow assigning roles only on operators' level, and servers without set operators can be used for both roles. + +For agreements, it is sufficient to record the signatures of these agreements on users' devices, together with the copy of signed agreement (or its hash and version) in a separate table. The terms themselves could be: +- included in the app - either in code or in migration. +- referenced with a stable link to a particular commit. + +The first solution seems better, as it avoids any third party dependency, and the agreement size is relatively small (~31kb), to reduce size we can store it compressed. diff --git a/package.yaml b/package.yaml index 94dc13ad2e..2fc50a3532 100644 --- a/package.yaml +++ b/package.yaml @@ -29,6 +29,7 @@ dependencies: - email-validate == 2.3.* - exceptions == 0.10.* - filepath == 1.4.* + - file-embed == 0.0.15.* - http-types == 0.12.* - http2 >= 4.2.2 && < 4.3 - memory == 0.18.* diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 8de91675e3..3e0f103641 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."ffecf200d4874dfa34f6d15b269964c0115a54ca" = "0kb8hq37fc5g198wq7dswnlwjzk67q8rrzil2dii5lc6xfr47jbs"; + "https://github.com/simplex-chat/simplexmq.git"."ff05a465ee15ac7ae2c14a9fb703a18564950631" = "1gv4nwqzbqkj7y3ffkiwkr4qwv52vdzppsds5vsfqaayl14rzmgp"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 96d16f5004..c7d603457c 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -150,10 +150,13 @@ library Simplex.Chat.Migrations.M20240920_user_order Simplex.Chat.Migrations.M20241008_indexes Simplex.Chat.Migrations.M20241010_contact_requests_contact_id + Simplex.Chat.Migrations.M20241027_server_operators Simplex.Chat.Mobile Simplex.Chat.Mobile.File Simplex.Chat.Mobile.Shared Simplex.Chat.Mobile.WebRTC + Simplex.Chat.Operators + Simplex.Chat.Operators.Conditions Simplex.Chat.Options Simplex.Chat.ProfileGenerator Simplex.Chat.Protocol @@ -213,6 +216,7 @@ library , directory ==1.3.* , email-validate ==2.3.* , exceptions ==0.10.* + , file-embed ==0.0.15.* , filepath ==1.4.* , http-types ==0.12.* , http2 >=4.2.2 && <4.3 @@ -276,6 +280,7 @@ executable simplex-bot , directory ==1.3.* , email-validate ==2.3.* , exceptions ==0.10.* + , file-embed ==0.0.15.* , filepath ==1.4.* , http-types ==0.12.* , http2 >=4.2.2 && <4.3 @@ -340,6 +345,7 @@ executable simplex-bot-advanced , directory ==1.3.* , email-validate ==2.3.* , exceptions ==0.10.* + , file-embed ==0.0.15.* , filepath ==1.4.* , http-types ==0.12.* , http2 >=4.2.2 && <4.3 @@ -407,6 +413,7 @@ executable simplex-broadcast-bot , directory ==1.3.* , email-validate ==2.3.* , exceptions ==0.10.* + , file-embed ==0.0.15.* , filepath ==1.4.* , http-types ==0.12.* , http2 >=4.2.2 && <4.3 @@ -472,6 +479,7 @@ executable simplex-chat , directory ==1.3.* , email-validate ==2.3.* , exceptions ==0.10.* + , file-embed ==0.0.15.* , filepath ==1.4.* , http-types ==0.12.* , http2 >=4.2.2 && <4.3 @@ -543,6 +551,7 @@ executable simplex-directory-service , directory ==1.3.* , email-validate ==2.3.* , exceptions ==0.10.* + , file-embed ==0.0.15.* , filepath ==1.4.* , http-types ==0.12.* , http2 >=4.2.2 && <4.3 @@ -642,6 +651,7 @@ test-suite simplex-chat-test , directory ==1.3.* , email-validate ==2.3.* , exceptions ==0.10.* + , file-embed ==0.0.15.* , filepath ==1.4.* , generic-random ==1.5.* , http-types ==0.12.* diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 885d4303c8..380f6c5d24 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -67,6 +67,7 @@ import Simplex.Chat.Messages import Simplex.Chat.Messages.Batch (MsgBatch (..), batchMessages) import Simplex.Chat.Messages.CIContent import Simplex.Chat.Messages.CIContent.Events +import Simplex.Chat.Operators import Simplex.Chat.Options import Simplex.Chat.ProfileGenerator (generateRandomProfile) import Simplex.Chat.Protocol @@ -97,7 +98,7 @@ import qualified Simplex.FileTransfer.Transport as XFTP import Simplex.FileTransfer.Types (FileErrorType (..), RcvFileId, SndFileId) import Simplex.Messaging.Agent as Agent import Simplex.Messaging.Agent.Client (SubInfo (..), agentClientStore, getAgentQueuesInfo, getAgentWorkersDetails, getAgentWorkersSummary, getFastNetworkConfig, ipAddressProtected, withLockMap) -import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), ServerCfg (..), createAgentStore, defaultAgentConfig, enabledServerCfg, presetServerCfg) +import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), OperatorId, ServerCfg (..), allRoles, createAgentStore, defaultAgentConfig, enabledServerCfg, presetServerCfg) import Simplex.Messaging.Agent.Lock (withLock) import Simplex.Messaging.Agent.Protocol import qualified Simplex.Messaging.Agent.Protocol as AP (AgentErrorType (..)) @@ -152,7 +153,7 @@ defaultChatConfig = { smp = _defaultSMPServers, useSMP = 4, ntf = _defaultNtfServers, - xftp = L.map (presetServerCfg True) defaultXFTPServers, + xftp = L.map (presetServerCfg True allRoles operatorSimpleXChat) defaultXFTPServers, useXFTP = L.length defaultXFTPServers, netCfg = defaultNetworkConfig }, @@ -181,7 +182,7 @@ _defaultSMPServers :: NonEmpty (ServerCfg 'PSMP) _defaultSMPServers = L.fromList $ map - (presetServerCfg True) + (presetServerCfg True allRoles operatorSimpleXChat) [ "smp://0YuTwO05YJWS8rkjn9eLJDjQhFKvIYd8d4xG8X1blIU=@smp8.simplex.im,beccx4yfxxbvyhqypaavemqurytl6hozr47wfc7uuecacjqdvwpw2xid.onion", "smp://SkIkI6EPd2D63F4xFKfHk7I1UGZVNn6k1QWZ5rcyr6w=@smp9.simplex.im,jssqzccmrcws6bhmn77vgmhfjmhwlyr3u7puw4erkyoosywgl67slqqd.onion", "smp://6iIcWT_dF2zN_w5xzZEY7HI2Prbh3ldP07YTyDexPjE=@smp10.simplex.im,rb2pbttocvnbrngnwziclp2f4ckjq65kebafws6g4hy22cdaiv5dwjqd.onion", @@ -195,12 +196,15 @@ _defaultSMPServers = "smp://N_McQS3F9TGoh4ER0QstUf55kGnNSd-wXfNPZ7HukcM=@smp19.simplex.im,i53bbtoqhlc365k6kxzwdp5w3cdt433s7bwh3y32rcbml2vztiyyz5id.onion" ] <> map - (presetServerCfg False) + (presetServerCfg False allRoles operatorSimpleXChat) [ "smp://u2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU=@smp4.simplex.im,o5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion", "smp://hpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg=@smp5.simplex.im,jjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion", "smp://PQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo=@smp6.simplex.im,bylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion" ] +operatorSimpleXChat :: Maybe OperatorId +operatorSimpleXChat = Just 1 + _defaultNtfServers :: [NtfServer] _defaultNtfServers = [ "ntf://FB-Uop7RTaZZEG0ZLD2CIaTjsPh-Fw0zFAnb7QyA8Ks=@ntf2.simplex.im,5ex3mupcazy3zlky64ab27phjhijpemsiby33qzq3pliejipbtx5xgad.onion" @@ -1484,8 +1488,11 @@ processChatCommand' vr = \case pure $ CRConnNtfMessages ntfMsgs APIGetUserProtoServers userId (AProtocolType p) -> withUserId userId $ \user -> withServerProtocol p $ do cfg@ChatConfig {defaultServers} <- asks config - servers <- withFastStore' (`getProtocolServers` user) - pure $ CRUserProtoServers user $ AUPS $ UserProtoServers p (useServers cfg p servers) (cfgServers p defaultServers) + srvs <- withFastStore' (`getProtocolServers` user) + ts <- liftIO getCurrentTime + operators <- withFastStore' $ \db -> getServerOperators db ts + let servers = AUPS $ UserProtoServers p (useServers cfg p srvs) (cfgServers p defaultServers) + pure $ CRUserProtoServers {user, servers, operators} GetUserProtoServers aProtocol -> withUser $ \User {userId} -> processChatCommand $ APIGetUserProtoServers userId aProtocol APISetUserProtoServers userId (APSC p (ProtoServersConfig servers)) @@ -1501,6 +1508,37 @@ processChatCommand' vr = \case lift $ CRServerTestResult user srv <$> withAgent' (\a -> testProtocolServer a (aUserId user) server) TestProtoServer srv -> withUser $ \User {userId} -> processChatCommand $ APITestProtoServer userId srv + APIGetServerOperators -> pure $ chatCmdError Nothing "not supported" + APISetServerOperators _operators -> pure $ chatCmdError Nothing "not supported" + APIGetUserServers userId -> withUserId userId $ \user -> + pure $ chatCmdError (Just user) "not supported" + APISetUserServers userId _userServers -> withUserId userId $ \user -> + pure $ chatCmdError (Just user) "not supported" + APIValidateServers _userServers -> + -- response is CRUserServersValidation + pure $ chatCmdError Nothing "not supported" + APIGetUsageConditions -> do + -- TODO + -- get current conditions + -- get latest accepted conditions (from operators) + ts <- liftIO getCurrentTime + let usageConditions = + UsageConditions + { conditionsId = 1, + conditionsCommit = "abc", + notifiedAt = Nothing, + createdAt = ts + } + pure + CRUsageConditions + { usageConditions = usageConditions, + conditionsText = usageConditionsText, + acceptedConditions = Nothing + } + APISetConditionsNotified _conditionsId -> do + pure $ chatCmdError Nothing "not supported" + APIAcceptConditions _conditionsId _opIds -> + pure $ chatCmdError Nothing "not supported" APISetChatItemTTL userId newTTL_ -> withUserId userId $ \user -> checkStoreNotChanged $ withChatLock "setChatItemTTL" $ do diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index b39b4d7456..bd2cee3e50 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -57,6 +57,7 @@ import Simplex.Chat.Call import Simplex.Chat.Markdown (MarkdownList) import Simplex.Chat.Messages import Simplex.Chat.Messages.CIContent +import Simplex.Chat.Operators import Simplex.Chat.Protocol import Simplex.Chat.Remote.AppVersion import Simplex.Chat.Remote.Types @@ -70,7 +71,7 @@ import Simplex.Chat.Util (liftIOEither) import Simplex.FileTransfer.Description (FileDescriptionURI) import Simplex.Messaging.Agent (AgentClient, SubscriptionsInfo) import Simplex.Messaging.Agent.Client (AgentLocks, AgentQueuesInfo (..), AgentWorkersDetails (..), AgentWorkersSummary (..), ProtocolTestFailure, SMPServerSubs, ServerQueueInfo, UserNetworkInfo) -import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, NetworkConfig, ServerCfg) +import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, NetworkConfig, OperatorId, ServerCfg) import Simplex.Messaging.Agent.Lock import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation, SQLiteStore, UpMigration, withTransaction, withTransactionPriority) @@ -352,6 +353,14 @@ data ChatCommand | SetUserProtoServers AProtoServersConfig | APITestProtoServer UserId AProtoServerWithAuth | TestProtoServer AProtoServerWithAuth + | APIGetServerOperators + | APISetServerOperators (NonEmpty (OperatorId, Bool)) + | APIGetUserServers UserId + | APISetUserServers UserId (NonEmpty UserServers) + | APIValidateServers (NonEmpty UserServers) -- response is CRUserServersValidation + | APIGetUsageConditions + | APISetConditionsNotified Int64 + | APIAcceptConditions Int64 (NonEmpty OperatorId) | APISetChatItemTTL UserId (Maybe Int64) | SetChatItemTTL (Maybe Int64) | APIGetChatItemTTL UserId @@ -577,8 +586,12 @@ data ChatResponse | CRChatItemInfo {user :: User, chatItem :: AChatItem, chatItemInfo :: ChatItemInfo} | CRChatItemId User (Maybe ChatItemId) | CRApiParsedMarkdown {formattedText :: Maybe MarkdownList} - | CRUserProtoServers {user :: User, servers :: AUserProtoServers} + | CRUserProtoServers {user :: User, servers :: AUserProtoServers, operators :: [ServerOperator]} | CRServerTestResult {user :: User, testServer :: AProtoServerWithAuth, testFailure :: Maybe ProtocolTestFailure} + | CRServerOperators {operators :: [ServerOperator], conditionsAction :: UsageConditionsAction} + | CRUserServers {userServers :: [UserServers]} + | CRUserServersValidation {serverErrors :: [UserServersError]} + | CRUsageConditions {usageConditions :: UsageConditions, conditionsText :: Text, acceptedConditions :: Maybe UsageConditions} | CRChatItemTTL {user :: User, chatItemTTL :: Maybe Int64} | CRNetworkConfig {networkConfig :: NetworkConfig} | CRContactInfo {user :: User, contact :: Contact, connectionStats_ :: Maybe ConnectionStats, customUserProfile :: Maybe Profile} @@ -948,6 +961,12 @@ data AProtoServersConfig = forall p. ProtocolTypeI p => APSC (SProtocolType p) ( deriving instance Show AProtoServersConfig +data UserServersError + = USEStorageMissing + | USEProxyMissing + | USEDuplicate {server :: AProtoServerWithAuth} + deriving (Show) + data UserProtoServers p = UserProtoServers { serverProtocol :: SProtocolType p, protoServers :: NonEmpty (ServerCfg p), @@ -1526,6 +1545,8 @@ $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "DB") ''DatabaseError) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "Chat") ''ChatError) +$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "USE") ''UserServersError) + $(JQ.deriveJSON defaultJSON ''AppFilePathsConfig) $(JQ.deriveJSON defaultJSON ''ContactSubStatus) diff --git a/src/Simplex/Chat/Migrations/M20241027_server_operators.hs b/src/Simplex/Chat/Migrations/M20241027_server_operators.hs new file mode 100644 index 0000000000..bc9f40bddf --- /dev/null +++ b/src/Simplex/Chat/Migrations/M20241027_server_operators.hs @@ -0,0 +1,70 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Migrations.M20241027_server_operators where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20241027_server_operators :: Query +m20241027_server_operators = + [sql| +CREATE TABLE server_operators ( + server_operator_id INTEGER PRIMARY KEY AUTOINCREMENT, + server_operator_tag TEXT, + trade_name TEXT NOT NULL, + legal_name TEXT, + server_domains TEXT, + enabled INTEGER NOT NULL DEFAULT 1, + role_storage INTEGER NOT NULL DEFAULT 1, + role_proxy INTEGER NOT NULL DEFAULT 1, + accepted_conditions_commit TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +ALTER TABLE protocol_servers ADD COLUMN server_operator_id INTEGER REFERENCES server_operators ON DELETE SET NULL; + +CREATE TABLE usage_conditions ( + usage_conditions_id INTEGER PRIMARY KEY AUTOINCREMENT, + conditions_commit TEXT NOT NULL UNIQUE, + notified_at TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE TABLE operator_usage_conditions ( + operator_usage_conditions_id INTEGER PRIMARY KEY AUTOINCREMENT, + server_operator_id INTEGER REFERENCES server_operators (server_operator_id) ON DELETE SET NULL ON UPDATE CASCADE, + server_operator_tag TEXT, + conditions_commit TEXT NOT NULL, + accepted_at TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE INDEX idx_protocol_servers_server_operator_id ON protocol_servers(server_operator_id); +CREATE INDEX idx_operator_usage_conditions_server_operator_id ON operator_usage_conditions(server_operator_id); +CREATE UNIQUE INDEX idx_operator_usage_conditions_conditions_commit ON operator_usage_conditions(server_operator_id, conditions_commit); + +INSERT INTO server_operators + (server_operator_id, server_operator_tag, trade_name, legal_name, server_domains, enabled) + VALUES (1, 'simplex', 'SimpleX Chat', 'SimpleX Chat Ltd', 'simplex.im', 1); +INSERT INTO server_operators + (server_operator_id, server_operator_tag, trade_name, legal_name, server_domains, enabled) + VALUES (2, 'xyz', 'XYZ', 'XYZ Ltd', 'xyz.com', 0); + +-- UPDATE protocol_servers SET server_operator_id = 1 WHERE host LIKE "%.simplex.im" OR host LIKE "%.simplex.im,%"; +|] + +down_m20241027_server_operators :: Query +down_m20241027_server_operators = + [sql| +DROP INDEX idx_operator_usage_conditions_conditions_commit; +DROP INDEX idx_operator_usage_conditions_server_operator_id; +DROP INDEX idx_protocol_servers_server_operator_id; + +ALTER TABLE protocol_servers DROP COLUMN server_operator_id; + +DROP TABLE operator_usage_conditions; +DROP TABLE usage_conditions; +DROP TABLE server_operators; +|] diff --git a/src/Simplex/Chat/Migrations/chat_schema.sql b/src/Simplex/Chat/Migrations/chat_schema.sql index 2619a5c4e5..07c363eda9 100644 --- a/src/Simplex/Chat/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Migrations/chat_schema.sql @@ -450,6 +450,7 @@ CREATE TABLE IF NOT EXISTS "protocol_servers"( created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')), protocol TEXT NOT NULL DEFAULT 'smp', + server_operator_id INTEGER REFERENCES server_operators ON DELETE SET NULL, UNIQUE(user_id, host, port) ); CREATE TABLE xftp_file_descriptions( @@ -589,6 +590,34 @@ CREATE TABLE note_folders( unread_chat INTEGER NOT NULL DEFAULT 0 ); CREATE TABLE app_settings(app_settings TEXT NOT NULL); +CREATE TABLE server_operators( + server_operator_id INTEGER PRIMARY KEY AUTOINCREMENT, + server_operator_tag TEXT, + trade_name TEXT NOT NULL, + legal_name TEXT, + server_domains TEXT, + enabled INTEGER NOT NULL DEFAULT 1, + role_storage INTEGER NOT NULL DEFAULT 1, + role_proxy INTEGER NOT NULL DEFAULT 1, + accepted_conditions_commit TEXT, + created_at TEXT NOT NULL DEFAULT(datetime('now')), + updated_at TEXT NOT NULL DEFAULT(datetime('now')) +); +CREATE TABLE usage_conditions( + usage_conditions_id INTEGER PRIMARY KEY AUTOINCREMENT, + conditions_commit TEXT NOT NULL UNIQUE, + notified_at TEXT, + created_at TEXT NOT NULL DEFAULT(datetime('now')), + updated_at TEXT NOT NULL DEFAULT(datetime('now')) +); +CREATE TABLE operator_usage_conditions( + operator_usage_conditions_id INTEGER PRIMARY KEY AUTOINCREMENT, + server_operator_id INTEGER REFERENCES server_operators(server_operator_id) ON DELETE SET NULL ON UPDATE CASCADE, + server_operator_tag TEXT, + conditions_commit TEXT NOT NULL, + accepted_at TEXT, + created_at TEXT NOT NULL DEFAULT(datetime('now')) +); CREATE INDEX contact_profiles_index ON contact_profiles( display_name, full_name @@ -890,3 +919,13 @@ CREATE INDEX idx_received_probes_group_member_id on received_probes( group_member_id ); CREATE INDEX idx_contact_requests_contact_id ON contact_requests(contact_id); +CREATE INDEX idx_protocol_servers_server_operator_id ON protocol_servers( + server_operator_id +); +CREATE INDEX idx_operator_usage_conditions_server_operator_id ON operator_usage_conditions( + server_operator_id +); +CREATE UNIQUE INDEX idx_operator_usage_conditions_conditions_commit ON operator_usage_conditions( + server_operator_id, + conditions_commit +); diff --git a/src/Simplex/Chat/Operators.hs b/src/Simplex/Chat/Operators.hs new file mode 100644 index 0000000000..9a2dac0b1b --- /dev/null +++ b/src/Simplex/Chat/Operators.hs @@ -0,0 +1,110 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TemplateHaskell #-} + +module Simplex.Chat.Operators where + +import Data.Aeson (FromJSON (..), ToJSON (..)) +import qualified Data.Aeson as J +import qualified Data.Aeson.Encoding as JE +import qualified Data.Aeson.TH as JQ +import Data.FileEmbed +import Data.Int (Int64) +import Data.List.NonEmpty (NonEmpty) +import Data.Text (Text) +import Data.Time.Clock (UTCTime) +import Database.SQLite.Simple.FromField (FromField (..)) +import Database.SQLite.Simple.ToField (ToField (..)) +import Language.Haskell.TH.Syntax (lift) +import Simplex.Chat.Operators.Conditions +import Simplex.Chat.Types.Util (textParseJSON) +import Simplex.Messaging.Agent.Env.SQLite (OperatorId, ServerRoles) +import Simplex.Messaging.Encoding.String +import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, fromTextField_, sumTypeJSON) +import Simplex.Messaging.Protocol (ProtoServerWithAuth, ProtocolType (..)) +import Simplex.Messaging.Util (safeDecodeUtf8) + +usageConditionsCommit :: Text +usageConditionsCommit = "165143a1112308c035ac00ed669b96b60599aa1c" + +usageConditionsText :: Text +usageConditionsText = + $( let s = $(embedFile =<< makeRelativeToProject "PRIVACY.md") + in [|stripFrontMatter (safeDecodeUtf8 $(lift s))|] + ) + +data OperatorTag = OTSimplex | OTXyz + deriving (Show) + +instance FromField OperatorTag where fromField = fromTextField_ textDecode + +instance ToField OperatorTag where toField = toField . textEncode + +instance FromJSON OperatorTag where + parseJSON = textParseJSON "OperatorTag" + +instance ToJSON OperatorTag where + toJSON = J.String . textEncode + toEncoding = JE.text . textEncode + +instance TextEncoding OperatorTag where + textDecode = \case + "simplex" -> Just OTSimplex + "xyz" -> Just OTXyz + _ -> Nothing + textEncode = \case + OTSimplex -> "simplex" + OTXyz -> "xyz" + +data UsageConditions = UsageConditions + { conditionsId :: Int64, + conditionsCommit :: Text, + notifiedAt :: Maybe UTCTime, + createdAt :: UTCTime + } + deriving (Show) + +data UsageConditionsAction + = UCAReview {operators :: [ServerOperator], deadline :: Maybe UTCTime, showNotice :: Bool} + | UCAAccepted {operators :: [ServerOperator]} + deriving (Show) + +-- TODO UI logic +usageConditionsAction :: UsageConditionsAction +usageConditionsAction = UCAAccepted [] + +data ConditionsAcceptance + = CAAccepted {acceptedAt :: UTCTime} + | CARequired {deadline :: Maybe UTCTime} + deriving (Show) + +data ServerOperator = ServerOperator + { operatorId :: OperatorId, + operatorTag :: Maybe OperatorTag, + tradeName :: Text, + legalName :: Maybe Text, + serverDomains :: [Text], + acceptedConditions :: ConditionsAcceptance, + enabled :: Bool, + roles :: ServerRoles + } + deriving (Show) + +data UserServers = UserServers + { operator :: ServerOperator, + smpServers :: NonEmpty (ProtoServerWithAuth 'PSMP), + xftpServers :: NonEmpty (ProtoServerWithAuth 'PXFTP) + } + deriving (Show) + +$(JQ.deriveJSON defaultJSON ''UsageConditions) + +$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CA") ''ConditionsAcceptance) + +$(JQ.deriveJSON defaultJSON ''ServerOperator) + +$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "UCA") ''UsageConditionsAction) + +$(JQ.deriveJSON defaultJSON ''UserServers) diff --git a/src/Simplex/Chat/Operators/Conditions.hs b/src/Simplex/Chat/Operators/Conditions.hs new file mode 100644 index 0000000000..55cf8b658d --- /dev/null +++ b/src/Simplex/Chat/Operators/Conditions.hs @@ -0,0 +1,19 @@ +{-# LANGUAGE OverloadedStrings #-} + +module Simplex.Chat.Operators.Conditions where + +import Data.Char (isSpace) +import Data.Text (Text) +import qualified Data.Text as T + +stripFrontMatter :: Text -> Text +stripFrontMatter = + T.unlines + . dropWhile ("# " `T.isPrefixOf`) -- strip title + . dropWhile (T.all isSpace) + . dropWhile fm + . (\ls -> let ls' = dropWhile (not . fm) ls in if null ls' then ls else ls') + . dropWhile fm + . T.lines + where + fm = ("---" `T.isPrefixOf`) diff --git a/src/Simplex/Chat/Store/Migrations.hs b/src/Simplex/Chat/Store/Migrations.hs index e2d12e78d7..e33f2336cc 100644 --- a/src/Simplex/Chat/Store/Migrations.hs +++ b/src/Simplex/Chat/Store/Migrations.hs @@ -114,6 +114,7 @@ import Simplex.Chat.Migrations.M20240827_calls_uuid import Simplex.Chat.Migrations.M20240920_user_order import Simplex.Chat.Migrations.M20241008_indexes import Simplex.Chat.Migrations.M20241010_contact_requests_contact_id +import Simplex.Chat.Migrations.M20241027_server_operators import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..)) schemaMigrations :: [(String, Query, Maybe Query)] @@ -227,7 +228,8 @@ schemaMigrations = ("20240827_calls_uuid", m20240827_calls_uuid, Just down_m20240827_calls_uuid), ("20240920_user_order", m20240920_user_order, Just down_m20240920_user_order), ("20241008_indexes", m20241008_indexes, Just down_m20241008_indexes), - ("20241010_contact_requests_contact_id", m20241010_contact_requests_contact_id, Just down_m20241010_contact_requests_contact_id) + ("20241010_contact_requests_contact_id", m20241010_contact_requests_contact_id, Just down_m20241010_contact_requests_contact_id), + ("20241027_server_operators", m20241027_server_operators, Just down_m20241027_server_operators) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs index fb9774a54e..fe2cc737fb 100644 --- a/src/Simplex/Chat/Store/Profiles.hs +++ b/src/Simplex/Chat/Store/Profiles.hs @@ -47,7 +47,9 @@ module Simplex.Chat.Store.Profiles getContactWithoutConnViaAddress, updateUserAddressAutoAccept, getProtocolServers, + -- overwriteOperatorsAndServers, overwriteProtocolServers, + getServerOperators, createCall, deleteCalls, getCalls, @@ -76,6 +78,7 @@ import Database.SQLite.Simple (NamedParam (..), Only (..), (:.) (..)) import Database.SQLite.Simple.QQ (sql) import Simplex.Chat.Call import Simplex.Chat.Messages +import Simplex.Chat.Operators import Simplex.Chat.Protocol import Simplex.Chat.Store.Direct import Simplex.Chat.Store.Shared @@ -83,7 +86,7 @@ import Simplex.Chat.Types import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Shared import Simplex.Chat.Types.UITheme -import Simplex.Messaging.Agent.Env.SQLite (ServerCfg (..)) +import Simplex.Messaging.Agent.Env.SQLite (OperatorId, ServerCfg (..), ServerRoles (..)) import Simplex.Messaging.Agent.Protocol (ACorrId, ConnId, UserId) import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow) import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB @@ -521,20 +524,25 @@ getProtocolServers db User {userId} = <$> DB.query db [sql| - SELECT host, port, key_hash, basic_auth, preset, tested, enabled - FROM protocol_servers - WHERE user_id = ? AND protocol = ?; + SELECT s.host, s.port, s.key_hash, s.basic_auth, s.server_operator_id, s.preset, s.tested, s.enabled, o.role_storage, o.role_proxy + FROM protocol_servers s + LEFT JOIN server_operators o USING (server_operator_id) + WHERE s.user_id = ? AND s.protocol = ? |] (userId, decodeLatin1 $ strEncode protocol) where protocol = protocolTypeI @p - toServerCfg :: (NonEmpty TransportHost, String, C.KeyHash, Maybe Text, Bool, Maybe Bool, Bool) -> ServerCfg p - toServerCfg (host, port, keyHash, auth_, preset, tested, enabled) = + toServerCfg :: (NonEmpty TransportHost, String, C.KeyHash, Maybe Text, Maybe OperatorId, Bool, Maybe Bool, Bool, Maybe Bool, Maybe Bool) -> ServerCfg p + toServerCfg (host, port, keyHash, auth_, operator, preset, tested, enabled, storage_, proxy_) = let server = ProtoServerWithAuth (ProtocolServer protocol host port keyHash) (BasicAuth . encodeUtf8 <$> auth_) - in ServerCfg {server, preset, tested, enabled} + roles = ServerRoles {storage = fromMaybe True storage_, proxy = fromMaybe True proxy_} + in ServerCfg {server, operator, preset, tested, enabled, roles} -overwriteProtocolServers :: forall p. ProtocolTypeI p => DB.Connection -> User -> [ServerCfg p] -> ExceptT StoreError IO () +-- overwriteOperatorsAndServers :: forall p. ProtocolTypeI p => DB.Connection -> User -> Maybe [ServerOperator] -> [ServerCfg p] -> ExceptT StoreError IO [ServerCfg p] +-- overwriteOperatorsAndServers db user@User {userId} operators_ servers = do +overwriteProtocolServers :: forall p. ProtocolTypeI p => DB.Connection -> User -> [ServerCfg p] -> ExceptT StoreError IO () overwriteProtocolServers db User {userId} servers = + -- liftIO $ mapM_ (updateServerOperators_ db) operators_ checkConstraint SEUniqueID . ExceptT $ do currentTs <- getCurrentTime DB.execute db "DELETE FROM protocol_servers WHERE user_id = ? AND protocol = ? " (userId, protocol) @@ -549,9 +557,62 @@ overwriteProtocolServers db User {userId} servers = |] ((protocol, host, port, keyHash, safeDecodeUtf8 . unBasicAuth <$> auth_) :. (preset, tested, enabled, userId, currentTs, currentTs)) pure $ Right () + -- Right <$> getProtocolServers db user where protocol = decodeLatin1 $ strEncode $ protocolTypeI @p +getServerOperators :: DB.Connection -> UTCTime -> IO [ServerOperator] +getServerOperators db ts = + map toOperator + <$> DB.query_ + db + [sql| + SELECT server_operator_id, server_operator_tag, trade_name, legal_name, server_domains, enabled, role_storage, role_proxy + FROM server_operators; + |] + where + -- TODO get conditions state + toOperator (operatorId, operatorTag, tradeName, legalName, domains, enabled, storage, proxy) = + let roles = ServerRoles {storage, proxy} + in ServerOperator {operatorId, operatorTag, tradeName, legalName, serverDomains = [domains], acceptedConditions = CAAccepted ts, enabled, roles} + +-- updateServerOperators_ :: DB.Connection -> [ServerOperator] -> IO [ServerOperator] +-- updateServerOperators_ db operators = do +-- DB.execute_ db "DELETE FROM server_operators WHERE preset = 0" +-- let (existing, new) = partition (isJust . operatorId) operators +-- existing' <- mapM (\op -> upsertExisting op $> op) existing +-- new' <- mapM insertNew new +-- pure $ existing' <> new' +-- where +-- upsertExisting ServerOperator {operatorId, name, preset, enabled, roles = ServerRoles {storage, proxy}} +-- | preset = +-- DB.execute +-- db +-- [sql| +-- UPDATE server_operators +-- SET enabled = ?, role_storage = ?, role_proxy = ? +-- WHERE server_operator_id = ? +-- |] +-- (enabled, storage, proxy, operatorId) +-- | otherwise = +-- DB.execute +-- db +-- [sql| +-- INSERT INTO server_operators (server_operator_id, name, preset, enabled, role_storage, role_proxy) +-- VALUES (?,?,?,?,?,?) +-- |] +-- (operatorId, name, preset, enabled, storage, proxy) +-- insertNew op@ServerOperator {name, preset, enabled, roles = ServerRoles {storage, proxy}} = do +-- DB.execute +-- db +-- [sql| +-- INSERT INTO server_operators (name, preset, enabled, role_storage, role_proxy) +-- VALUES (?,?,?,?,?) +-- |] +-- (name, preset, enabled, storage, proxy) +-- opId <- insertedRowId db +-- pure op {operatorId = Just opId} + createCall :: DB.Connection -> User -> Call -> UTCTime -> IO () createCall db user@User {userId} Call {contactId, callId, callUUID, chatItemId, callState} callTs = do currentTs <- getCurrentTime diff --git a/src/Simplex/Chat/Terminal.hs b/src/Simplex/Chat/Terminal.hs index 5cc695db04..e38a34d45f 100644 --- a/src/Simplex/Chat/Terminal.hs +++ b/src/Simplex/Chat/Terminal.hs @@ -13,7 +13,7 @@ import qualified Data.Text as T import Data.Text.Encoding (encodeUtf8) import Database.SQLite.Simple (SQLError (..)) import qualified Database.SQLite.Simple as DB -import Simplex.Chat (defaultChatConfig) +import Simplex.Chat (defaultChatConfig, operatorSimpleXChat) import Simplex.Chat.Controller import Simplex.Chat.Core import Simplex.Chat.Help (chatWelcome) @@ -21,7 +21,7 @@ import Simplex.Chat.Options import Simplex.Chat.Terminal.Input import Simplex.Chat.Terminal.Output import Simplex.FileTransfer.Client.Presets (defaultXFTPServers) -import Simplex.Messaging.Agent.Env.SQLite (presetServerCfg) +import Simplex.Messaging.Agent.Env.SQLite (allRoles, presetServerCfg) import Simplex.Messaging.Client (NetworkConfig (..), SMPProxyFallback (..), SMPProxyMode (..), defaultNetworkConfig) import Simplex.Messaging.Util (raceAny_) import System.IO (hFlush, hSetEcho, stdin, stdout) @@ -34,14 +34,14 @@ terminalChatConfig = { smp = L.fromList $ map - (presetServerCfg True) + (presetServerCfg True allRoles operatorSimpleXChat) [ "smp://u2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU=@smp4.simplex.im,o5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion", "smp://hpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg=@smp5.simplex.im,jjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion", "smp://PQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo=@smp6.simplex.im,bylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion" ], useSMP = 3, ntf = ["ntf://FB-Uop7RTaZZEG0ZLD2CIaTjsPh-Fw0zFAnb7QyA8Ks=@ntf2.simplex.im,ntg7jdjy2i3qbib3sykiho3enekwiaqg3icctliqhtqcg6jmoh6cxiad.onion"], - xftp = L.map (presetServerCfg True) defaultXFTPServers, + xftp = L.map (presetServerCfg True allRoles operatorSimpleXChat) defaultXFTPServers, useXFTP = L.length defaultXFTPServers, netCfg = defaultNetworkConfig diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index ade36476c7..c53e5a2749 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -19,7 +19,7 @@ import qualified Data.ByteString.Lazy.Char8 as LB import Data.Char (isSpace, toUpper) import Data.Function (on) import Data.Int (Int64) -import Data.List (groupBy, intercalate, intersperse, partition, sortOn) +import Data.List (foldl', groupBy, intercalate, intersperse, partition, sortOn) import Data.List.NonEmpty (NonEmpty (..)) import qualified Data.List.NonEmpty as L import Data.Map.Strict (Map) @@ -42,6 +42,7 @@ import Simplex.Chat.Help import Simplex.Chat.Markdown import Simplex.Chat.Messages hiding (NewChatItem (..)) import Simplex.Chat.Messages.CIContent +import Simplex.Chat.Operators import Simplex.Chat.Protocol import Simplex.Chat.Remote.AppVersion (AppVersion (..), pattern AppVersionRange) import Simplex.Chat.Remote.Types @@ -95,8 +96,12 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe CRChats chats -> viewChats ts tz chats CRApiChat u chat -> ttyUser u $ if testView then testViewChat chat else [viewJSON chat] CRApiParsedMarkdown ft -> [viewJSON ft] - CRUserProtoServers u userServers -> ttyUser u $ viewUserServers userServers testView + CRUserProtoServers u userServers operators -> ttyUser u $ viewUserServers userServers operators testView CRServerTestResult u srv testFailure -> ttyUser u $ viewServerTestResult srv testFailure + CRServerOperators {} -> [] + CRUserServers {} -> [] + CRUserServersValidation _ -> [] + CRUsageConditions {} -> [] CRChatItemTTL u ttl -> ttyUser u $ viewChatItemTTL ttl CRNetworkConfig cfg -> viewNetworkConfig cfg CRContactInfo u ct cStats customUserProfile -> ttyUser u $ viewContactInfo ct cStats customUserProfile @@ -1209,8 +1214,8 @@ viewUserPrivacy User {userId} User {userId = userId', localDisplayName = n', sho "profile is " <> if isJust viewPwdHash then "hidden" else "visible" ] -viewUserServers :: AUserProtoServers -> Bool -> [StyledString] -viewUserServers (AUPS UserProtoServers {serverProtocol = p, protoServers, presetServers}) testView = +viewUserServers :: AUserProtoServers -> [ServerOperator] -> Bool -> [StyledString] +viewUserServers (AUPS UserProtoServers {serverProtocol = p, protoServers, presetServers}) operators testView = customServers <> if testView then [] @@ -1228,8 +1233,8 @@ viewUserServers (AUPS UserProtoServers {serverProtocol = p, protoServers, preset pName = protocolName p customServers = if null protoServers - then ("no " <> pName <> " servers saved, using presets: ") : viewServers presetServers - else viewServers protoServers + then ("no " <> pName <> " servers saved, using presets: ") : viewServers operators presetServers + else viewServers operators protoServers protocolName :: ProtocolTypeI p => SProtocolType p -> StyledString protocolName = plain . map toUpper . T.unpack . decodeLatin1 . strEncode @@ -1326,8 +1331,11 @@ viewConnectionStats ConnectionStats {rcvQueuesInfo, sndQueuesInfo} = ["receiving messages via: " <> viewRcvQueuesInfo rcvQueuesInfo | not $ null rcvQueuesInfo] <> ["sending messages via: " <> viewSndQueuesInfo sndQueuesInfo | not $ null sndQueuesInfo] -viewServers :: ProtocolTypeI p => NonEmpty (ServerCfg p) -> [StyledString] -viewServers = map (plain . B.unpack . strEncode . (\ServerCfg {server} -> server)) . L.toList +viewServers :: ProtocolTypeI p => [ServerOperator] -> NonEmpty (ServerCfg p) -> [StyledString] +viewServers operators = map (plain . (\ServerCfg {server, operator} -> B.unpack (strEncode server) <> viewOperator operator)) . L.toList + where + ops :: Map (Maybe Int64) Text = foldl' (\m ServerOperator {operatorId, tradeName} -> M.insert (Just operatorId) tradeName m) M.empty operators + viewOperator = maybe "" $ \op -> " (operator " <> maybe (show op) T.unpack (M.lookup (Just op) ops) <> ")" viewRcvQueuesInfo :: [RcvQueueInfo] -> StyledString viewRcvQueuesInfo = plain . intercalate ", " . map showQueueInfo diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index 75b85d7a5f..d435af186e 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -423,11 +423,10 @@ smpServerCfg = ServerConfig { transports = [(serverPort, transport @TLS, False)], tbqSize = 1, - -- serverTbqSize = 1, - msgQueueQuota = 16, msgStoreType = AMSType SMSMemory, - maxJournalMsgCount = 1000, - maxJournalStateLines = 1000, + msgQueueQuota = 16, + maxJournalMsgCount = 24, + maxJournalStateLines = 4, queueIdBytes = 12, msgIdBytes = 6, storeLogFile = Nothing, diff --git a/tests/RandomServers.hs b/tests/RandomServers.hs index 0c6baa71bb..e0b1939c9e 100644 --- a/tests/RandomServers.hs +++ b/tests/RandomServers.hs @@ -9,7 +9,7 @@ import Control.Monad (replicateM) import qualified Data.List.NonEmpty as L import Simplex.Chat (cfgServers, cfgServersToUse, defaultChatConfig, randomServers) import Simplex.Chat.Controller (ChatConfig (..)) -import Simplex.Messaging.Agent.Env.SQLite (ServerCfg (..)) +import Simplex.Messaging.Agent.Env.SQLite (ServerCfg (..), ServerRoles (..)) import Simplex.Messaging.Protocol (ProtoServerWithAuth (..), SProtocolType (..), UserProtocol) import Test.Hspec @@ -18,6 +18,8 @@ randomServersTests = describe "choosig random servers" $ do it "should choose 4 random SMP servers and keep the rest disabled" testRandomSMPServers it "should keep all 6 XFTP servers" testRandomXFTPServers +deriving instance Eq ServerRoles + deriving instance Eq (ServerCfg p) testRandomSMPServers :: IO () From bdaec30fa084e4c18964035d432abc42a55288a7 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Mon, 4 Nov 2024 21:11:03 +0400 Subject: [PATCH 010/167] core: getServerOperators, getUserServers, getUsageConditions apis wip (#5141) --- src/Simplex/Chat.hs | 31 +++++++------ src/Simplex/Chat/Controller.hs | 2 +- src/Simplex/Chat/Operators.hs | 38 ++++++++++++---- src/Simplex/Chat/Store/Profiles.hs | 73 ++++++++++++++++++++++++------ src/Simplex/Chat/Store/Shared.hs | 1 + 5 files changed, 107 insertions(+), 38 deletions(-) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 380f6c5d24..bd165ea5e6 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -1489,8 +1489,7 @@ processChatCommand' vr = \case APIGetUserProtoServers userId (AProtocolType p) -> withUserId userId $ \user -> withServerProtocol p $ do cfg@ChatConfig {defaultServers} <- asks config srvs <- withFastStore' (`getProtocolServers` user) - ts <- liftIO getCurrentTime - operators <- withFastStore' $ \db -> getServerOperators db ts + operators <- withFastStore $ \db -> getServerOperators db let servers = AUPS $ UserProtoServers p (useServers cfg p srvs) (cfgServers p defaultServers) pure $ CRUserProtoServers {user, servers, operators} GetUserProtoServers aProtocol -> withUser $ \User {userId} -> @@ -1508,27 +1507,31 @@ processChatCommand' vr = \case lift $ CRServerTestResult user srv <$> withAgent' (\a -> testProtocolServer a (aUserId user) server) TestProtoServer srv -> withUser $ \User {userId} -> processChatCommand $ APITestProtoServer userId srv - APIGetServerOperators -> pure $ chatCmdError Nothing "not supported" + APIGetServerOperators -> do + operators <- withFastStore $ \db -> getServerOperators db + let conditionsAction = usageConditionsAction operators + pure $ CRServerOperators operators conditionsAction APISetServerOperators _operators -> pure $ chatCmdError Nothing "not supported" - APIGetUserServers userId -> withUserId userId $ \user -> - pure $ chatCmdError (Just user) "not supported" + APIGetUserServers userId -> withUserId userId $ \user -> do + (operators, smpServers, xftpServers) <- withFastStore $ \db -> do + operators <- getServerOperators db + smpServers <- liftIO $ getServers db user SPSMP + xftpServers <- liftIO $ getServers db user SPXFTP + pure (operators, smpServers, xftpServers) + let userServers = groupByOperator operators smpServers xftpServers + pure $ CRUserServers user userServers + where + getServers :: (ProtocolTypeI p) => DB.Connection -> User -> SProtocolType p -> IO [ServerCfg p] + getServers db user _p = getProtocolServers db user APISetUserServers userId _userServers -> withUserId userId $ \user -> pure $ chatCmdError (Just user) "not supported" APIValidateServers _userServers -> -- response is CRUserServersValidation pure $ chatCmdError Nothing "not supported" APIGetUsageConditions -> do + usageConditions <- withFastStore $ \db -> getCurrentUsageConditions db -- TODO - -- get current conditions -- get latest accepted conditions (from operators) - ts <- liftIO getCurrentTime - let usageConditions = - UsageConditions - { conditionsId = 1, - conditionsCommit = "abc", - notifiedAt = Nothing, - createdAt = ts - } pure CRUsageConditions { usageConditions = usageConditions, diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index bd2cee3e50..2cb8e0cd42 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -589,7 +589,7 @@ data ChatResponse | CRUserProtoServers {user :: User, servers :: AUserProtoServers, operators :: [ServerOperator]} | CRServerTestResult {user :: User, testServer :: AProtoServerWithAuth, testFailure :: Maybe ProtocolTestFailure} | CRServerOperators {operators :: [ServerOperator], conditionsAction :: UsageConditionsAction} - | CRUserServers {userServers :: [UserServers]} + | CRUserServers {user :: User, userServers :: [UserServers]} | CRUserServersValidation {serverErrors :: [UserServersError]} | CRUsageConditions {usageConditions :: UsageConditions, conditionsText :: Text, acceptedConditions :: Maybe UsageConditions} | CRChatItemTTL {user :: User, chatItemTTL :: Maybe Int64} diff --git a/src/Simplex/Chat/Operators.hs b/src/Simplex/Chat/Operators.hs index 9a2dac0b1b..ff110e2ada 100644 --- a/src/Simplex/Chat/Operators.hs +++ b/src/Simplex/Chat/Operators.hs @@ -1,6 +1,7 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE LambdaCase #-} +{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TemplateHaskell #-} @@ -12,7 +13,9 @@ import qualified Data.Aeson.Encoding as JE import qualified Data.Aeson.TH as JQ import Data.FileEmbed import Data.Int (Int64) -import Data.List.NonEmpty (NonEmpty) +import Data.Map.Strict (Map) +import qualified Data.Map.Strict as M +import Data.Maybe (fromMaybe) import Data.Text (Text) import Data.Time.Clock (UTCTime) import Database.SQLite.Simple.FromField (FromField (..)) @@ -20,10 +23,10 @@ import Database.SQLite.Simple.ToField (ToField (..)) import Language.Haskell.TH.Syntax (lift) import Simplex.Chat.Operators.Conditions import Simplex.Chat.Types.Util (textParseJSON) -import Simplex.Messaging.Agent.Env.SQLite (OperatorId, ServerRoles) +import Simplex.Messaging.Agent.Env.SQLite (OperatorId, ServerCfg (..), ServerRoles) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, fromTextField_, sumTypeJSON) -import Simplex.Messaging.Protocol (ProtoServerWithAuth, ProtocolType (..)) +import Simplex.Messaging.Protocol (ProtocolType (..)) import Simplex.Messaging.Util (safeDecodeUtf8) usageConditionsCommit :: Text @@ -72,8 +75,8 @@ data UsageConditionsAction deriving (Show) -- TODO UI logic -usageConditionsAction :: UsageConditionsAction -usageConditionsAction = UCAAccepted [] +usageConditionsAction :: [ServerOperator] -> UsageConditionsAction +usageConditionsAction _operators = UCAAccepted [] data ConditionsAcceptance = CAAccepted {acceptedAt :: UTCTime} @@ -93,12 +96,31 @@ data ServerOperator = ServerOperator deriving (Show) data UserServers = UserServers - { operator :: ServerOperator, - smpServers :: NonEmpty (ProtoServerWithAuth 'PSMP), - xftpServers :: NonEmpty (ProtoServerWithAuth 'PXFTP) + { operator :: Maybe ServerOperator, + smpServers :: [ServerCfg 'PSMP], + xftpServers :: [ServerCfg 'PXFTP] } deriving (Show) +groupByOperator :: [ServerOperator] -> [ServerCfg 'PSMP] -> [ServerCfg 'PXFTP] -> [UserServers] +groupByOperator srvOperators smpSrvs xftpSrvs = + map createOperatorServers (M.toList combinedMap) + where + srvOperatorId :: ServerCfg p -> Maybe Int64 + srvOperatorId ServerCfg {operator} = operator + operatorMap :: Map (Maybe Int64) (Maybe ServerOperator) + operatorMap = M.fromList [(Just (operatorId op), Just op) | op <- srvOperators] `M.union` M.singleton Nothing Nothing + initialMap :: Map (Maybe Int64) ([ServerCfg 'PSMP], [ServerCfg 'PXFTP]) + initialMap = M.fromList [(key, ([], [])) | key <- M.keys operatorMap] + smpsMap = foldr (\server acc -> M.adjust (\(smps, xftps) -> (server : smps, xftps)) (srvOperatorId server) acc) initialMap smpSrvs + combinedMap = foldr (\server acc -> M.adjust (\(smps, xftps) -> (smps, server : xftps)) (srvOperatorId server) acc) smpsMap xftpSrvs + createOperatorServers (key, (groupedSmps, groupedXftps)) = + UserServers + { operator = fromMaybe Nothing (M.lookup key operatorMap), + smpServers = groupedSmps, + xftpServers = groupedXftps + } + $(JQ.deriveJSON defaultJSON ''UsageConditions) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CA") ''ConditionsAcceptance) diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs index fe2cc737fb..d6627505f3 100644 --- a/src/Simplex/Chat/Store/Profiles.hs +++ b/src/Simplex/Chat/Store/Profiles.hs @@ -50,6 +50,7 @@ module Simplex.Chat.Store.Profiles -- overwriteOperatorsAndServers, overwriteProtocolServers, getServerOperators, + getCurrentUsageConditions, createCall, deleteCalls, getCalls, @@ -73,7 +74,8 @@ import qualified Data.List.NonEmpty as L import Data.Maybe (fromMaybe) import Data.Text (Text) import Data.Text.Encoding (decodeLatin1, encodeUtf8) -import Data.Time.Clock (UTCTime (..), getCurrentTime) +import Data.Time (addUTCTime) +import Data.Time.Clock (UTCTime (..), getCurrentTime, nominalDay) import Database.SQLite.Simple (NamedParam (..), Only (..), (:.) (..)) import Database.SQLite.Simple.QQ (sql) import Simplex.Chat.Call @@ -540,7 +542,7 @@ getProtocolServers db User {userId} = -- overwriteOperatorsAndServers :: forall p. ProtocolTypeI p => DB.Connection -> User -> Maybe [ServerOperator] -> [ServerCfg p] -> ExceptT StoreError IO [ServerCfg p] -- overwriteOperatorsAndServers db user@User {userId} operators_ servers = do -overwriteProtocolServers :: forall p. ProtocolTypeI p => DB.Connection -> User -> [ServerCfg p] -> ExceptT StoreError IO () +overwriteProtocolServers :: forall p. ProtocolTypeI p => DB.Connection -> User -> [ServerCfg p] -> ExceptT StoreError IO () overwriteProtocolServers db User {userId} servers = -- liftIO $ mapM_ (updateServerOperators_ db) operators_ checkConstraint SEUniqueID . ExceptT $ do @@ -556,25 +558,66 @@ overwriteProtocolServers db User {userId} servers = VALUES (?,?,?,?,?,?,?,?,?,?,?) |] ((protocol, host, port, keyHash, safeDecodeUtf8 . unBasicAuth <$> auth_) :. (preset, tested, enabled, userId, currentTs, currentTs)) - pure $ Right () -- Right <$> getProtocolServers db user + pure $ Right () where protocol = decodeLatin1 $ strEncode $ protocolTypeI @p -getServerOperators :: DB.Connection -> UTCTime -> IO [ServerOperator] -getServerOperators db ts = - map toOperator - <$> DB.query_ - db - [sql| - SELECT server_operator_id, server_operator_tag, trade_name, legal_name, server_domains, enabled, role_storage, role_proxy - FROM server_operators; +getServerOperators :: DB.Connection -> ExceptT StoreError IO [ServerOperator] +getServerOperators db = do + conditions <- getCurrentUsageConditions db + liftIO $ + map (toOperator conditions) + <$> DB.query_ + db + [sql| + SELECT + so.server_operator_id, so.server_operator_tag, so.trade_name, so.legal_name, + so.server_domains, so.enabled, so.role_storage, so.role_proxy, + LastOperatorConditions.conditions_commit, LastOperatorConditions.accepted_at + FROM server_operators so + LEFT JOIN ( + SELECT server_operator_id, conditions_commit, accepted_at, MAX(operator_usage_conditions_id) + FROM operator_usage_conditions + GROUP BY server_operator_id + ) LastOperatorConditions ON LastOperatorConditions.server_operator_id = so.server_operator_id |] where - -- TODO get conditions state - toOperator (operatorId, operatorTag, tradeName, legalName, domains, enabled, storage, proxy) = - let roles = ServerRoles {storage, proxy} - in ServerOperator {operatorId, operatorTag, tradeName, legalName, serverDomains = [domains], acceptedConditions = CAAccepted ts, enabled, roles} + toOperator :: + UsageConditions -> + ( (OperatorId, Maybe OperatorTag, Text, Maybe Text, Text, Bool, Bool, Bool) + :. (Maybe Text, Maybe UTCTime) + ) -> + ServerOperator + toOperator + UsageConditions {conditionsCommit, createdAt} + ( (operatorId, operatorTag, tradeName, legalName, domains, enabled, storage, proxy) + :. (operatorConditionsCommit_, acceptedAt_) + ) = + let roles = ServerRoles {storage, proxy} + acceptedConditions = case (operatorConditionsCommit_, acceptedAt_) of + (Nothing, _) -> CARequired Nothing + (Just operatorConditionsCommit, Just acceptedAt) + | conditionsCommit == operatorConditionsCommit -> CAAccepted acceptedAt + _ -> CARequired (Just $ conditionsDeadline createdAt) + in ServerOperator {operatorId, operatorTag, tradeName, legalName, serverDomains = [domains], acceptedConditions, enabled, roles} + conditionsDeadline :: UTCTime -> UTCTime + conditionsDeadline = addUTCTime (31 * nominalDay) + +getCurrentUsageConditions :: DB.Connection -> ExceptT StoreError IO UsageConditions +getCurrentUsageConditions db = + ExceptT . firstRow toUsageConditions SEUsageConditionsNotFound $ + DB.query_ + db + [sql| + SELECT usage_conditions_id, conditions_commit, notified_at, created_at + FROM usage_conditions + ORDER BY usage_conditions_id DESC LIMIT 1 + |] + +toUsageConditions :: (Int64, Text, Maybe UTCTime, UTCTime) -> UsageConditions +toUsageConditions (conditionsId, conditionsCommit, notifiedAt, createdAt) = + UsageConditions {conditionsId, conditionsCommit, notifiedAt, createdAt} -- updateServerOperators_ :: DB.Connection -> [ServerOperator] -> IO [ServerOperator] -- updateServerOperators_ db operators = do diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs index f9a8685ec8..083079e2ea 100644 --- a/src/Simplex/Chat/Store/Shared.hs +++ b/src/Simplex/Chat/Store/Shared.hs @@ -127,6 +127,7 @@ data StoreError | SERemoteCtrlNotFound {remoteCtrlId :: RemoteCtrlId} | SERemoteCtrlDuplicateCA | SEProhibitedDeleteUser {userId :: UserId, contactId :: ContactId} + | SEUsageConditionsNotFound deriving (Show, Exception) $(J.deriveJSON (sumTypeJSON $ dropPrefix "SE") ''StoreError) From 3b0205b25f5a3377d0b5c6162f21d6cc82b4565a Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Tue, 5 Nov 2024 14:15:20 +0400 Subject: [PATCH 011/167] core: setServerOperators, getUsageConditions api wip (#5145) --- src/Simplex/Chat.hs | 16 ++-- src/Simplex/Chat/Controller.hs | 2 +- .../Migrations/M20241027_server_operators.hs | 10 +- src/Simplex/Chat/Migrations/chat_schema.sql | 2 +- src/Simplex/Chat/Operators.hs | 15 ++- src/Simplex/Chat/Store/Profiles.hs | 91 ++++++++++++++++--- 6 files changed, 105 insertions(+), 31 deletions(-) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index bd165ea5e6..b083134e2c 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -1511,7 +1511,10 @@ processChatCommand' vr = \case operators <- withFastStore $ \db -> getServerOperators db let conditionsAction = usageConditionsAction operators pure $ CRServerOperators operators conditionsAction - APISetServerOperators _operators -> pure $ chatCmdError Nothing "not supported" + APISetServerOperators operatorsEnabled -> do + operators <- withFastStore $ \db -> setServerOperators db operatorsEnabled + let conditionsAction = usageConditionsAction operators + pure $ CRServerOperators operators conditionsAction APIGetUserServers userId -> withUserId userId $ \user -> do (operators, smpServers, xftpServers) <- withFastStore $ \db -> do operators <- getServerOperators db @@ -1529,14 +1532,15 @@ processChatCommand' vr = \case -- response is CRUserServersValidation pure $ chatCmdError Nothing "not supported" APIGetUsageConditions -> do - usageConditions <- withFastStore $ \db -> getCurrentUsageConditions db - -- TODO - -- get latest accepted conditions (from operators) + (usageConditions, acceptedConditions) <- withFastStore $ \db -> do + usageConditions <- getCurrentUsageConditions db + acceptedConditions <- getLatestAcceptedConditions db + pure (usageConditions, acceptedConditions) pure CRUsageConditions - { usageConditions = usageConditions, + { usageConditions, conditionsText = usageConditionsText, - acceptedConditions = Nothing + acceptedConditions } APISetConditionsNotified _conditionsId -> do pure $ chatCmdError Nothing "not supported" diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 2cb8e0cd42..81e7a9980b 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -354,7 +354,7 @@ data ChatCommand | APITestProtoServer UserId AProtoServerWithAuth | TestProtoServer AProtoServerWithAuth | APIGetServerOperators - | APISetServerOperators (NonEmpty (OperatorId, Bool)) + | APISetServerOperators (NonEmpty OperatorEnabled) | APIGetUserServers UserId | APISetUserServers UserId (NonEmpty UserServers) | APIValidateServers (NonEmpty UserServers) -- response is CRUserServersValidation diff --git a/src/Simplex/Chat/Migrations/M20241027_server_operators.hs b/src/Simplex/Chat/Migrations/M20241027_server_operators.hs index bc9f40bddf..fc0ca21e54 100644 --- a/src/Simplex/Chat/Migrations/M20241027_server_operators.hs +++ b/src/Simplex/Chat/Migrations/M20241027_server_operators.hs @@ -11,13 +11,13 @@ m20241027_server_operators = CREATE TABLE server_operators ( server_operator_id INTEGER PRIMARY KEY AUTOINCREMENT, server_operator_tag TEXT, + app_vendor INTEGER NOT NULL, trade_name TEXT NOT NULL, legal_name TEXT, server_domains TEXT, enabled INTEGER NOT NULL DEFAULT 1, role_storage INTEGER NOT NULL DEFAULT 1, role_proxy INTEGER NOT NULL DEFAULT 1, - accepted_conditions_commit TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')) ); @@ -46,11 +46,11 @@ CREATE INDEX idx_operator_usage_conditions_server_operator_id ON operator_usage_ CREATE UNIQUE INDEX idx_operator_usage_conditions_conditions_commit ON operator_usage_conditions(server_operator_id, conditions_commit); INSERT INTO server_operators - (server_operator_id, server_operator_tag, trade_name, legal_name, server_domains, enabled) - VALUES (1, 'simplex', 'SimpleX Chat', 'SimpleX Chat Ltd', 'simplex.im', 1); + (server_operator_id, server_operator_tag, app_vendor, trade_name, legal_name, server_domains, enabled) + VALUES (1, 'simplex', 1, 'SimpleX Chat', 'SimpleX Chat Ltd', 'simplex.im', 1); INSERT INTO server_operators - (server_operator_id, server_operator_tag, trade_name, legal_name, server_domains, enabled) - VALUES (2, 'xyz', 'XYZ', 'XYZ Ltd', 'xyz.com', 0); + (server_operator_id, server_operator_tag, app_vendor, trade_name, legal_name, server_domains, enabled) + VALUES (2, 'xyz', 0, 'XYZ', 'XYZ Ltd', 'xyz.com', 0); -- UPDATE protocol_servers SET server_operator_id = 1 WHERE host LIKE "%.simplex.im" OR host LIKE "%.simplex.im,%"; |] diff --git a/src/Simplex/Chat/Migrations/chat_schema.sql b/src/Simplex/Chat/Migrations/chat_schema.sql index 07c363eda9..1541f36b60 100644 --- a/src/Simplex/Chat/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Migrations/chat_schema.sql @@ -593,13 +593,13 @@ CREATE TABLE app_settings(app_settings TEXT NOT NULL); CREATE TABLE server_operators( server_operator_id INTEGER PRIMARY KEY AUTOINCREMENT, server_operator_tag TEXT, + app_vendor INTEGER NOT NULL, trade_name TEXT NOT NULL, legal_name TEXT, server_domains TEXT, enabled INTEGER NOT NULL DEFAULT 1, role_storage INTEGER NOT NULL DEFAULT 1, role_proxy INTEGER NOT NULL DEFAULT 1, - accepted_conditions_commit TEXT, created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')) ); diff --git a/src/Simplex/Chat/Operators.hs b/src/Simplex/Chat/Operators.hs index ff110e2ada..6fc5663085 100644 --- a/src/Simplex/Chat/Operators.hs +++ b/src/Simplex/Chat/Operators.hs @@ -79,7 +79,7 @@ usageConditionsAction :: [ServerOperator] -> UsageConditionsAction usageConditionsAction _operators = UCAAccepted [] data ConditionsAcceptance - = CAAccepted {acceptedAt :: UTCTime} + = CAAccepted {acceptedAt :: Maybe UTCTime} | CARequired {deadline :: Maybe UTCTime} deriving (Show) @@ -89,7 +89,14 @@ data ServerOperator = ServerOperator tradeName :: Text, legalName :: Maybe Text, serverDomains :: [Text], - acceptedConditions :: ConditionsAcceptance, + conditionsAcceptance :: ConditionsAcceptance, + enabled :: Bool, + roles :: ServerRoles + } + deriving (Show) + +data OperatorEnabled = OperatorEnabled + { operatorId :: OperatorId, enabled :: Bool, roles :: ServerRoles } @@ -106,10 +113,10 @@ groupByOperator :: [ServerOperator] -> [ServerCfg 'PSMP] -> [ServerCfg 'PXFTP] - groupByOperator srvOperators smpSrvs xftpSrvs = map createOperatorServers (M.toList combinedMap) where - srvOperatorId :: ServerCfg p -> Maybe Int64 srvOperatorId ServerCfg {operator} = operator + opId ServerOperator {operatorId} = operatorId operatorMap :: Map (Maybe Int64) (Maybe ServerOperator) - operatorMap = M.fromList [(Just (operatorId op), Just op) | op <- srvOperators] `M.union` M.singleton Nothing Nothing + operatorMap = M.fromList [(Just (opId op), Just op) | op <- srvOperators] `M.union` M.singleton Nothing Nothing initialMap :: Map (Maybe Int64) ([ServerCfg 'PSMP], [ServerCfg 'PXFTP]) initialMap = M.fromList [(key, ([], [])) | key <- M.keys operatorMap] smpsMap = foldr (\server acc -> M.adjust (\(smps, xftps) -> (server : smps, xftps)) (srvOperatorId server) acc) initialMap smpSrvs diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs index d6627505f3..259d08d9ad 100644 --- a/src/Simplex/Chat/Store/Profiles.hs +++ b/src/Simplex/Chat/Store/Profiles.hs @@ -50,7 +50,9 @@ module Simplex.Chat.Store.Profiles -- overwriteOperatorsAndServers, overwriteProtocolServers, getServerOperators, + setServerOperators, getCurrentUsageConditions, + getLatestAcceptedConditions, createCall, deleteCalls, getCalls, @@ -72,7 +74,7 @@ import Data.Int (Int64) import Data.List.NonEmpty (NonEmpty) import qualified Data.List.NonEmpty as L import Data.Maybe (fromMaybe) -import Data.Text (Text) +import Data.Text (Text, splitOn) import Data.Text.Encoding (decodeLatin1, encodeUtf8) import Data.Time (addUTCTime) import Data.Time.Clock (UTCTime (..), getCurrentTime, nominalDay) @@ -565,44 +567,80 @@ overwriteProtocolServers db User {userId} servers = getServerOperators :: DB.Connection -> ExceptT StoreError IO [ServerOperator] getServerOperators db = do - conditions <- getCurrentUsageConditions db + now <- liftIO getCurrentTime + currentConditions <- getCurrentUsageConditions db + latestAcceptedConditions <- getLatestAcceptedConditions db liftIO $ - map (toOperator conditions) + map (toOperator now currentConditions latestAcceptedConditions) <$> DB.query_ db [sql| SELECT so.server_operator_id, so.server_operator_tag, so.trade_name, so.legal_name, so.server_domains, so.enabled, so.role_storage, so.role_proxy, - LastOperatorConditions.conditions_commit, LastOperatorConditions.accepted_at + AcceptedConditions.conditions_commit, AcceptedConditions.accepted_at FROM server_operators so LEFT JOIN ( SELECT server_operator_id, conditions_commit, accepted_at, MAX(operator_usage_conditions_id) FROM operator_usage_conditions GROUP BY server_operator_id - ) LastOperatorConditions ON LastOperatorConditions.server_operator_id = so.server_operator_id + ) AcceptedConditions ON AcceptedConditions.server_operator_id = so.server_operator_id |] where toOperator :: + UTCTime -> UsageConditions -> + Maybe UsageConditions -> ( (OperatorId, Maybe OperatorTag, Text, Maybe Text, Text, Bool, Bool, Bool) :. (Maybe Text, Maybe UTCTime) ) -> ServerOperator toOperator - UsageConditions {conditionsCommit, createdAt} + now + UsageConditions {conditionsCommit = currentCommit, createdAt, notifiedAt} + latestAcceptedConditions_ ( (operatorId, operatorTag, tradeName, legalName, domains, enabled, storage, proxy) - :. (operatorConditionsCommit_, acceptedAt_) + :. (operatorCommit_, acceptedAt_) ) = let roles = ServerRoles {storage, proxy} - acceptedConditions = case (operatorConditionsCommit_, acceptedAt_) of + serverDomains = splitOn "," domains + conditionsAcceptance = case (latestAcceptedConditions_, operatorCommit_) of + -- no conditions were ever accepted for any operator(s) + -- (shouldn't happen as there should always be record for SimpleX Chat) (Nothing, _) -> CARequired Nothing - (Just operatorConditionsCommit, Just acceptedAt) - | conditionsCommit == operatorConditionsCommit -> CAAccepted acceptedAt - _ -> CARequired (Just $ conditionsDeadline createdAt) - in ServerOperator {operatorId, operatorTag, tradeName, legalName, serverDomains = [domains], acceptedConditions, enabled, roles} - conditionsDeadline :: UTCTime -> UTCTime - conditionsDeadline = addUTCTime (31 * nominalDay) + -- no conditions were ever accepted for this operator + (_, Nothing) -> CARequired Nothing + (Just UsageConditions {conditionsCommit = latestAcceptedCommit}, Just operatorCommit) + | latestAcceptedCommit == currentCommit -> + if operatorCommit == latestAcceptedCommit + then -- current conditions were accepted for operator + CAAccepted acceptedAt_ + else -- current conditions were NOT accepted for operator, but were accepted for other operator(s) + CARequired Nothing + | otherwise -> + if operatorCommit == latestAcceptedCommit + then -- new conditions available, latest accepted conditions were accepted for operator + conditionsRequiredOrDeadline createdAt (fromMaybe now notifiedAt) + else -- new conditions available, latest accepted conditions were NOT accepted for operator (were accepted for other operator(s)) + CARequired Nothing + in ServerOperator {operatorId, operatorTag, tradeName, legalName, serverDomains, conditionsAcceptance, enabled, roles} + conditionsRequiredOrDeadline :: UTCTime -> UTCTime -> ConditionsAcceptance + conditionsRequiredOrDeadline createdAt notifiedAtOrNow = + if notifiedAtOrNow < addUTCTime (14 * nominalDay) createdAt + then CARequired (Just $ conditionsDeadline notifiedAtOrNow) + else CARequired Nothing + where + conditionsDeadline :: UTCTime -> UTCTime + conditionsDeadline = addUTCTime (31 * nominalDay) + +setServerOperators :: DB.Connection -> NonEmpty OperatorEnabled -> ExceptT StoreError IO [ServerOperator] +setServerOperators db operatorsEnabled = do + liftIO $ forM_ operatorsEnabled $ \OperatorEnabled {operatorId, enabled, roles = ServerRoles {storage, proxy}} -> + DB.execute + db + "UPDATE server_operators SET enabled = ?, role_storage = ?, role_proxy = ? WHERE server_operator_id = ?" + (enabled, storage, proxy, operatorId) + getServerOperators db getCurrentUsageConditions :: DB.Connection -> ExceptT StoreError IO UsageConditions getCurrentUsageConditions db = @@ -619,6 +657,31 @@ toUsageConditions :: (Int64, Text, Maybe UTCTime, UTCTime) -> UsageConditions toUsageConditions (conditionsId, conditionsCommit, notifiedAt, createdAt) = UsageConditions {conditionsId, conditionsCommit, notifiedAt, createdAt} +getLatestAcceptedConditions :: DB.Connection -> ExceptT StoreError IO (Maybe UsageConditions) +getLatestAcceptedConditions db = do + (latestAcceptedCommit_ :: Maybe Text) <- + liftIO $ + maybeFirstRow fromOnly $ + DB.query_ + db + [sql| + SELECT conditions_commit + FROM operator_usage_conditions + WHERE conditions_accepted = 1 + ORDER BY accepted_at DESC + LIMIT 1 + |] + forM latestAcceptedCommit_ $ \latestAcceptedCommit -> + ExceptT . firstRow toUsageConditions SEUsageConditionsNotFound $ + DB.query + db + [sql| + SELECT usage_conditions_id, conditions_commit, notified_at, created_at + FROM usage_conditions + WHERE conditions_commit = ? + |] + (Only latestAcceptedCommit) + -- updateServerOperators_ :: DB.Connection -> [ServerOperator] -> IO [ServerOperator] -- updateServerOperators_ db operators = do -- DB.execute_ db "DELETE FROM server_operators WHERE preset = 0" From 2da89c2cf1d155e228979c6460c5dddb0cca73c3 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Tue, 5 Nov 2024 21:40:33 +0400 Subject: [PATCH 012/167] core: setConditionsNotified, acceptConditions, setUserServers, validateServers apis wip (#5147) --- src/Simplex/Chat.hs | 39 +++++---- src/Simplex/Chat/Controller.hs | 14 +--- src/Simplex/Chat/Operators.hs | 65 +++++++++++++-- src/Simplex/Chat/Store/Profiles.hs | 127 ++++++++++++++++++++++------- 4 files changed, 181 insertions(+), 64 deletions(-) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index b083134e2c..69b78ba9d4 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -1489,7 +1489,7 @@ processChatCommand' vr = \case APIGetUserProtoServers userId (AProtocolType p) -> withUserId userId $ \user -> withServerProtocol p $ do cfg@ChatConfig {defaultServers} <- asks config srvs <- withFastStore' (`getProtocolServers` user) - operators <- withFastStore $ \db -> getServerOperators db + (operators, _) <- withFastStore $ \db -> getServerOperators db let servers = AUPS $ UserProtoServers p (useServers cfg p srvs) (cfgServers p defaultServers) pure $ CRUserProtoServers {user, servers, operators} GetUserProtoServers aProtocol -> withUser $ \User {userId} -> @@ -1508,44 +1508,51 @@ processChatCommand' vr = \case TestProtoServer srv -> withUser $ \User {userId} -> processChatCommand $ APITestProtoServer userId srv APIGetServerOperators -> do - operators <- withFastStore $ \db -> getServerOperators db - let conditionsAction = usageConditionsAction operators + (operators, conditionsAction) <- withFastStore $ \db -> getServerOperators db pure $ CRServerOperators operators conditionsAction APISetServerOperators operatorsEnabled -> do - operators <- withFastStore $ \db -> setServerOperators db operatorsEnabled - let conditionsAction = usageConditionsAction operators + (operators, conditionsAction) <- withFastStore $ \db -> setServerOperators db operatorsEnabled pure $ CRServerOperators operators conditionsAction APIGetUserServers userId -> withUserId userId $ \user -> do (operators, smpServers, xftpServers) <- withFastStore $ \db -> do - operators <- getServerOperators db + (operators, _) <- getServerOperators db smpServers <- liftIO $ getServers db user SPSMP xftpServers <- liftIO $ getServers db user SPXFTP pure (operators, smpServers, xftpServers) let userServers = groupByOperator operators smpServers xftpServers pure $ CRUserServers user userServers where - getServers :: (ProtocolTypeI p) => DB.Connection -> User -> SProtocolType p -> IO [ServerCfg p] + getServers :: ProtocolTypeI p => DB.Connection -> User -> SProtocolType p -> IO [ServerCfg p] getServers db user _p = getProtocolServers db user - APISetUserServers userId _userServers -> withUserId userId $ \user -> - pure $ chatCmdError (Just user) "not supported" - APIValidateServers _userServers -> - -- response is CRUserServersValidation - pure $ chatCmdError Nothing "not supported" + APISetUserServers userId userServers -> withUserId userId $ \user -> do + let errors = validateUserServers userServers + unless (null errors) $ throwChatError (CECommandError $ "user servers validation error(s): " <> show errors) + withFastStore $ \db -> setUserServers db user userServers + -- TODO set protocol servers for agent + ok_ + APIValidateServers userServers -> do + let errors = validateUserServers userServers + pure $ CRUserServersValidation errors APIGetUsageConditions -> do (usageConditions, acceptedConditions) <- withFastStore $ \db -> do usageConditions <- getCurrentUsageConditions db acceptedConditions <- getLatestAcceptedConditions db pure (usageConditions, acceptedConditions) + -- TODO if db commit is different from source commit, conditionsText should be nothing in response pure CRUsageConditions { usageConditions, conditionsText = usageConditionsText, acceptedConditions } - APISetConditionsNotified _conditionsId -> do - pure $ chatCmdError Nothing "not supported" - APIAcceptConditions _conditionsId _opIds -> - pure $ chatCmdError Nothing "not supported" + APISetConditionsNotified conditionsId -> do + currentTs <- liftIO getCurrentTime + withFastStore' $ \db -> setConditionsNotified db conditionsId currentTs + ok_ + APIAcceptConditions conditionsId operators -> do + currentTs <- liftIO getCurrentTime + (operators', conditionsAction) <- withFastStore $ \db -> acceptConditions db conditionsId operators currentTs + pure $ CRServerOperators operators' conditionsAction APISetChatItemTTL userId newTTL_ -> withUserId userId $ \user -> checkStoreNotChanged $ withChatLock "setChatItemTTL" $ do diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 81e7a9980b..cbfa0969d4 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -71,7 +71,7 @@ import Simplex.Chat.Util (liftIOEither) import Simplex.FileTransfer.Description (FileDescriptionURI) import Simplex.Messaging.Agent (AgentClient, SubscriptionsInfo) import Simplex.Messaging.Agent.Client (AgentLocks, AgentQueuesInfo (..), AgentWorkersDetails (..), AgentWorkersSummary (..), ProtocolTestFailure, SMPServerSubs, ServerQueueInfo, UserNetworkInfo) -import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, NetworkConfig, OperatorId, ServerCfg) +import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, NetworkConfig, ServerCfg) import Simplex.Messaging.Agent.Lock import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation, SQLiteStore, UpMigration, withTransaction, withTransactionPriority) @@ -360,7 +360,7 @@ data ChatCommand | APIValidateServers (NonEmpty UserServers) -- response is CRUserServersValidation | APIGetUsageConditions | APISetConditionsNotified Int64 - | APIAcceptConditions Int64 (NonEmpty OperatorId) + | APIAcceptConditions Int64 (NonEmpty ServerOperator) | APISetChatItemTTL UserId (Maybe Int64) | SetChatItemTTL (Maybe Int64) | APIGetChatItemTTL UserId @@ -588,7 +588,7 @@ data ChatResponse | CRApiParsedMarkdown {formattedText :: Maybe MarkdownList} | CRUserProtoServers {user :: User, servers :: AUserProtoServers, operators :: [ServerOperator]} | CRServerTestResult {user :: User, testServer :: AProtoServerWithAuth, testFailure :: Maybe ProtocolTestFailure} - | CRServerOperators {operators :: [ServerOperator], conditionsAction :: UsageConditionsAction} + | CRServerOperators {operators :: [ServerOperator], conditionsAction :: Maybe UsageConditionsAction} | CRUserServers {user :: User, userServers :: [UserServers]} | CRUserServersValidation {serverErrors :: [UserServersError]} | CRUsageConditions {usageConditions :: UsageConditions, conditionsText :: Text, acceptedConditions :: Maybe UsageConditions} @@ -961,12 +961,6 @@ data AProtoServersConfig = forall p. ProtocolTypeI p => APSC (SProtocolType p) ( deriving instance Show AProtoServersConfig -data UserServersError - = USEStorageMissing - | USEProxyMissing - | USEDuplicate {server :: AProtoServerWithAuth} - deriving (Show) - data UserProtoServers p = UserProtoServers { serverProtocol :: SProtocolType p, protoServers :: NonEmpty (ServerCfg p), @@ -1545,8 +1539,6 @@ $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "DB") ''DatabaseError) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "Chat") ''ChatError) -$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "USE") ''UserServersError) - $(JQ.deriveJSON defaultJSON ''AppFilePathsConfig) $(JQ.deriveJSON defaultJSON ''ContactSubStatus) diff --git a/src/Simplex/Chat/Operators.hs b/src/Simplex/Chat/Operators.hs index 6fc5663085..5e32807ddc 100644 --- a/src/Simplex/Chat/Operators.hs +++ b/src/Simplex/Chat/Operators.hs @@ -13,20 +13,22 @@ import qualified Data.Aeson.Encoding as JE import qualified Data.Aeson.TH as JQ import Data.FileEmbed import Data.Int (Int64) +import Data.List.NonEmpty (NonEmpty) import Data.Map.Strict (Map) import qualified Data.Map.Strict as M -import Data.Maybe (fromMaybe) +import Data.Maybe (fromMaybe, isNothing) import Data.Text (Text) -import Data.Time.Clock (UTCTime) +import Data.Time (addUTCTime) +import Data.Time.Clock (UTCTime, nominalDay) import Database.SQLite.Simple.FromField (FromField (..)) import Database.SQLite.Simple.ToField (ToField (..)) import Language.Haskell.TH.Syntax (lift) import Simplex.Chat.Operators.Conditions import Simplex.Chat.Types.Util (textParseJSON) -import Simplex.Messaging.Agent.Env.SQLite (OperatorId, ServerCfg (..), ServerRoles) +import Simplex.Messaging.Agent.Env.SQLite (OperatorId, ServerCfg (..), ServerRoles (..)) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, fromTextField_, sumTypeJSON) -import Simplex.Messaging.Protocol (ProtocolType (..)) +import Simplex.Messaging.Protocol (AProtoServerWithAuth, ProtocolType (..)) import Simplex.Messaging.Util (safeDecodeUtf8) usageConditionsCommit :: Text @@ -74,9 +76,30 @@ data UsageConditionsAction | UCAAccepted {operators :: [ServerOperator]} deriving (Show) --- TODO UI logic -usageConditionsAction :: [ServerOperator] -> UsageConditionsAction -usageConditionsAction _operators = UCAAccepted [] +usageConditionsAction :: [ServerOperator] -> UsageConditions -> UTCTime -> Maybe UsageConditionsAction +usageConditionsAction operators UsageConditions {createdAt, notifiedAt} now = do + let enabledOperators = filter (\ServerOperator {enabled} -> enabled) operators + if null enabledOperators + then Nothing + else + if all conditionsAccepted enabledOperators + then + let acceptedForOperators = filter conditionsAccepted operators + in Just $ UCAAccepted acceptedForOperators + else + let acceptForOperators = filter (not . conditionsAccepted) enabledOperators + deadline = conditionsRequiredOrDeadline createdAt (fromMaybe now notifiedAt) + showNotice = isNothing notifiedAt + in Just $ UCAReview acceptForOperators deadline showNotice + +conditionsRequiredOrDeadline :: UTCTime -> UTCTime -> Maybe UTCTime +conditionsRequiredOrDeadline createdAt notifiedAtOrNow = + if notifiedAtOrNow < addUTCTime (14 * nominalDay) createdAt + then Just $ conditionsDeadline notifiedAtOrNow + else Nothing -- required + where + conditionsDeadline :: UTCTime -> UTCTime + conditionsDeadline = addUTCTime (31 * nominalDay) data ConditionsAcceptance = CAAccepted {acceptedAt :: Maybe UTCTime} @@ -95,6 +118,11 @@ data ServerOperator = ServerOperator } deriving (Show) +conditionsAccepted :: ServerOperator -> Bool +conditionsAccepted ServerOperator {conditionsAcceptance} = case conditionsAcceptance of + CAAccepted {} -> True + _ -> False + data OperatorEnabled = OperatorEnabled { operatorId :: OperatorId, enabled :: Bool, @@ -128,6 +156,27 @@ groupByOperator srvOperators smpSrvs xftpSrvs = xftpServers = groupedXftps } +data UserServersError + = USEStorageMissing + | USEProxyMissing + | USEDuplicate {server :: AProtoServerWithAuth} + deriving (Show) + +validateUserServers :: NonEmpty UserServers -> [UserServersError] +validateUserServers userServers = + let storageMissing_ = if any (canUseForRole storage) userServers then [] else [USEStorageMissing] + proxyMissing_ = if any (canUseForRole proxy) userServers then [] else [USEProxyMissing] + -- TODO duplicate errors + -- allSMPServers = + -- map (\ServerCfg {server} -> server) $ + -- concatMap (\UserServers {smpServers} -> smpServers) userServers + in storageMissing_ <> proxyMissing_ -- <> duplicateErrors + where + canUseForRole :: (ServerRoles -> Bool) -> UserServers -> Bool + canUseForRole roleSel UserServers {operator, smpServers, xftpServers} = case operator of + Just ServerOperator {roles} -> roleSel roles + Nothing -> not (null smpServers) && not (null xftpServers) + $(JQ.deriveJSON defaultJSON ''UsageConditions) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CA") ''ConditionsAcceptance) @@ -137,3 +186,5 @@ $(JQ.deriveJSON defaultJSON ''ServerOperator) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "UCA") ''UsageConditionsAction) $(JQ.deriveJSON defaultJSON ''UserServers) + +$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "USE") ''UserServersError) diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs index 259d08d9ad..f4f574c3d7 100644 --- a/src/Simplex/Chat/Store/Profiles.hs +++ b/src/Simplex/Chat/Store/Profiles.hs @@ -53,6 +53,9 @@ module Simplex.Chat.Store.Profiles setServerOperators, getCurrentUsageConditions, getLatestAcceptedConditions, + setConditionsNotified, + acceptConditions, + setUserServers, createCall, deleteCalls, getCalls, @@ -76,8 +79,7 @@ import qualified Data.List.NonEmpty as L import Data.Maybe (fromMaybe) import Data.Text (Text, splitOn) import Data.Text.Encoding (decodeLatin1, encodeUtf8) -import Data.Time (addUTCTime) -import Data.Time.Clock (UTCTime (..), getCurrentTime, nominalDay) +import Data.Time.Clock (UTCTime (..), getCurrentTime) import Database.SQLite.Simple (NamedParam (..), Only (..), (:.) (..)) import Database.SQLite.Simple.QQ (sql) import Simplex.Chat.Call @@ -542,6 +544,7 @@ getProtocolServers db User {userId} = roles = ServerRoles {storage = fromMaybe True storage_, proxy = fromMaybe True proxy_} in ServerCfg {server, operator, preset, tested, enabled, roles} +-- TODO remove -- overwriteOperatorsAndServers :: forall p. ProtocolTypeI p => DB.Connection -> User -> Maybe [ServerOperator] -> [ServerCfg p] -> ExceptT StoreError IO [ServerCfg p] -- overwriteOperatorsAndServers db user@User {userId} operators_ servers = do overwriteProtocolServers :: forall p. ProtocolTypeI p => DB.Connection -> User -> [ServerCfg p] -> ExceptT StoreError IO () @@ -565,27 +568,29 @@ overwriteProtocolServers db User {userId} servers = where protocol = decodeLatin1 $ strEncode $ protocolTypeI @p -getServerOperators :: DB.Connection -> ExceptT StoreError IO [ServerOperator] +getServerOperators :: DB.Connection -> ExceptT StoreError IO ([ServerOperator], Maybe UsageConditionsAction) getServerOperators db = do now <- liftIO getCurrentTime currentConditions <- getCurrentUsageConditions db latestAcceptedConditions <- getLatestAcceptedConditions db - liftIO $ - map (toOperator now currentConditions latestAcceptedConditions) - <$> DB.query_ - db - [sql| - SELECT - so.server_operator_id, so.server_operator_tag, so.trade_name, so.legal_name, - so.server_domains, so.enabled, so.role_storage, so.role_proxy, - AcceptedConditions.conditions_commit, AcceptedConditions.accepted_at - FROM server_operators so - LEFT JOIN ( - SELECT server_operator_id, conditions_commit, accepted_at, MAX(operator_usage_conditions_id) - FROM operator_usage_conditions - GROUP BY server_operator_id - ) AcceptedConditions ON AcceptedConditions.server_operator_id = so.server_operator_id - |] + operators <- + liftIO $ + map (toOperator now currentConditions latestAcceptedConditions) + <$> DB.query_ + db + [sql| + SELECT + so.server_operator_id, so.server_operator_tag, so.trade_name, so.legal_name, + so.server_domains, so.enabled, so.role_storage, so.role_proxy, + AcceptedConditions.conditions_commit, AcceptedConditions.accepted_at + FROM server_operators so + LEFT JOIN ( + SELECT server_operator_id, conditions_commit, accepted_at, MAX(operator_usage_conditions_id) + FROM operator_usage_conditions + GROUP BY server_operator_id + ) AcceptedConditions ON AcceptedConditions.server_operator_id = so.server_operator_id + |] + pure (operators, usageConditionsAction operators currentConditions now) where toOperator :: UTCTime -> @@ -620,20 +625,12 @@ getServerOperators db = do | otherwise -> if operatorCommit == latestAcceptedCommit then -- new conditions available, latest accepted conditions were accepted for operator - conditionsRequiredOrDeadline createdAt (fromMaybe now notifiedAt) + CARequired $ conditionsRequiredOrDeadline createdAt (fromMaybe now notifiedAt) else -- new conditions available, latest accepted conditions were NOT accepted for operator (were accepted for other operator(s)) CARequired Nothing in ServerOperator {operatorId, operatorTag, tradeName, legalName, serverDomains, conditionsAcceptance, enabled, roles} - conditionsRequiredOrDeadline :: UTCTime -> UTCTime -> ConditionsAcceptance - conditionsRequiredOrDeadline createdAt notifiedAtOrNow = - if notifiedAtOrNow < addUTCTime (14 * nominalDay) createdAt - then CARequired (Just $ conditionsDeadline notifiedAtOrNow) - else CARequired Nothing - where - conditionsDeadline :: UTCTime -> UTCTime - conditionsDeadline = addUTCTime (31 * nominalDay) -setServerOperators :: DB.Connection -> NonEmpty OperatorEnabled -> ExceptT StoreError IO [ServerOperator] +setServerOperators :: DB.Connection -> NonEmpty OperatorEnabled -> ExceptT StoreError IO ([ServerOperator], Maybe UsageConditionsAction) setServerOperators db operatorsEnabled = do liftIO $ forM_ operatorsEnabled $ \OperatorEnabled {operatorId, enabled, roles = ServerRoles {storage, proxy}} -> DB.execute @@ -667,7 +664,6 @@ getLatestAcceptedConditions db = do [sql| SELECT conditions_commit FROM operator_usage_conditions - WHERE conditions_accepted = 1 ORDER BY accepted_at DESC LIMIT 1 |] @@ -682,6 +678,77 @@ getLatestAcceptedConditions db = do |] (Only latestAcceptedCommit) +setConditionsNotified :: DB.Connection -> Int64 -> UTCTime -> IO () +setConditionsNotified db conditionsId notifiedAt = + DB.execute db "UPDATE usage_conditions SET notified_at = ? WHERE usage_conditions_id = ?" (notifiedAt, conditionsId) + +acceptConditions :: DB.Connection -> Int64 -> NonEmpty ServerOperator -> UTCTime -> ExceptT StoreError IO ([ServerOperator], Maybe UsageConditionsAction) +acceptConditions db conditionsId operators acceptedAt = do + UsageConditions {conditionsCommit} <- getUsageConditionsById_ db conditionsId + liftIO $ forM_ operators $ \ServerOperator {operatorId, operatorTag} -> + DB.execute + db + [sql| + INSERT INTO operator_usage_conditions + (server_operator_id, server_operator_tag, conditions_commit, accepted_at) + VALUES (?,?,?,?) + |] + (operatorId, operatorTag, conditionsCommit, acceptedAt) + getServerOperators db + +getUsageConditionsById_ :: DB.Connection -> Int64 -> ExceptT StoreError IO UsageConditions +getUsageConditionsById_ db conditionsId = + ExceptT . firstRow toUsageConditions SEUsageConditionsNotFound $ + DB.query + db + [sql| + SELECT usage_conditions_id, conditions_commit, notified_at, created_at + FROM usage_conditions + WHERE usage_conditions_id = ? + |] + (Only conditionsId) + +setUserServers :: DB.Connection -> User -> NonEmpty UserServers -> ExceptT StoreError IO () +setUserServers db User {userId} userServers = do + currentTs <- liftIO getCurrentTime + forM_ userServers $ do + \UserServers {operator, smpServers, xftpServers} -> do + forM_ operator $ \op -> liftIO $ updateOperator currentTs op + overwriteServers currentTs operator smpServers + overwriteServers currentTs operator xftpServers + where + updateOperator :: UTCTime -> ServerOperator -> IO () + updateOperator currentTs ServerOperator {operatorId, enabled, roles = ServerRoles {storage, proxy}} = + DB.execute + db + [sql| + UPDATE server_operators + SET enabled = ?, role_storage = ?, role_proxy = ?, updated_at = ? + WHERE server_operator_id = ? + |] + (enabled, storage, proxy, operatorId, currentTs) + overwriteServers :: forall p. ProtocolTypeI p => UTCTime -> Maybe ServerOperator -> [ServerCfg p] -> ExceptT StoreError IO () + overwriteServers currentTs serverOperator servers = + checkConstraint SEUniqueID . ExceptT $ do + case serverOperator of + Nothing -> + DB.execute db "DELETE FROM protocol_servers WHERE user_id = ? AND server_operator_id IS NULL AND protocol = ?" (userId, protocol) + Just ServerOperator {operatorId} -> + DB.execute db "DELETE FROM protocol_servers WHERE user_id = ? AND server_operator_id = ? AND protocol = ?" (userId, operatorId, protocol) + forM_ servers $ \ServerCfg {server, operator, preset, tested, enabled} -> do + let ProtoServerWithAuth ProtocolServer {host, port, keyHash} auth_ = server + DB.execute + db + [sql| + INSERT INTO protocol_servers + (protocol, host, port, key_hash, basic_auth, operator, preset, tested, enabled, user_id, created_at, updated_at) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?) + |] + ((protocol, host, port, keyHash, safeDecodeUtf8 . unBasicAuth <$> auth_, operator) :. (preset, tested, enabled, userId, currentTs, currentTs)) + pure $ Right () + where + protocol = decodeLatin1 $ strEncode $ protocolTypeI @p + -- updateServerOperators_ :: DB.Connection -> [ServerOperator] -> IO [ServerOperator] -- updateServerOperators_ db operators = do -- DB.execute_ db "DELETE FROM server_operators WHERE preset = 0" From 8396e70e7b82b111f2d2e156d10a92dea4883319 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Wed, 6 Nov 2024 16:13:08 +0400 Subject: [PATCH 013/167] core: validate servers - find servers with duplicate hosts (#5150) --- src/Simplex/Chat/Operators.hs | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/Simplex/Chat/Operators.hs b/src/Simplex/Chat/Operators.hs index 5e32807ddc..cedc3ca6d1 100644 --- a/src/Simplex/Chat/Operators.hs +++ b/src/Simplex/Chat/Operators.hs @@ -14,6 +14,7 @@ import qualified Data.Aeson.TH as JQ import Data.FileEmbed import Data.Int (Int64) import Data.List.NonEmpty (NonEmpty) +import qualified Data.List.NonEmpty as L import Data.Map.Strict (Map) import qualified Data.Map.Strict as M import Data.Maybe (fromMaybe, isNothing) @@ -28,7 +29,7 @@ import Simplex.Chat.Types.Util (textParseJSON) import Simplex.Messaging.Agent.Env.SQLite (OperatorId, ServerCfg (..), ServerRoles (..)) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, fromTextField_, sumTypeJSON) -import Simplex.Messaging.Protocol (AProtoServerWithAuth, ProtocolType (..)) +import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), ProtoServerWithAuth (..), ProtocolServer (..), ProtocolType (..), SProtocolType (..)) import Simplex.Messaging.Util (safeDecodeUtf8) usageConditionsCommit :: Text @@ -159,23 +160,34 @@ groupByOperator srvOperators smpSrvs xftpSrvs = data UserServersError = USEStorageMissing | USEProxyMissing - | USEDuplicate {server :: AProtoServerWithAuth} + | USEDuplicateSMP {server :: AProtoServerWithAuth} + | USEDuplicateXFTP {server :: AProtoServerWithAuth} deriving (Show) validateUserServers :: NonEmpty UserServers -> [UserServersError] validateUserServers userServers = let storageMissing_ = if any (canUseForRole storage) userServers then [] else [USEStorageMissing] proxyMissing_ = if any (canUseForRole proxy) userServers then [] else [USEProxyMissing] - -- TODO duplicate errors - -- allSMPServers = - -- map (\ServerCfg {server} -> server) $ - -- concatMap (\UserServers {smpServers} -> smpServers) userServers - in storageMissing_ <> proxyMissing_ -- <> duplicateErrors + + allSMPServers = map (\ServerCfg {server} -> server) $ concatMap (\UserServers {smpServers} -> smpServers) userServers + duplicateSMPServers = findDuplicatesByHost allSMPServers + duplicateSMPErrors = map (USEDuplicateSMP . AProtoServerWithAuth SPSMP) duplicateSMPServers + + allXFTPServers = map (\ServerCfg {server} -> server) $ concatMap (\UserServers {xftpServers} -> xftpServers) userServers + duplicateXFTPServers = findDuplicatesByHost allXFTPServers + duplicateXFTPErrors = map (USEDuplicateXFTP . AProtoServerWithAuth SPXFTP) duplicateXFTPServers + in storageMissing_ <> proxyMissing_ <> duplicateSMPErrors <> duplicateXFTPErrors where canUseForRole :: (ServerRoles -> Bool) -> UserServers -> Bool canUseForRole roleSel UserServers {operator, smpServers, xftpServers} = case operator of Just ServerOperator {roles} -> roleSel roles Nothing -> not (null smpServers) && not (null xftpServers) + findDuplicatesByHost :: [ProtoServerWithAuth p] -> [ProtoServerWithAuth p] + findDuplicatesByHost servers = + let allHosts = concatMap (L.toList . host . protoServer) servers + hostCounts = M.fromListWith (+) [(host, 1 :: Int) | host <- allHosts] + duplicateHosts = M.keys $ M.filter (> 1) hostCounts + in filter (\srv -> any (`elem` duplicateHosts) (L.toList $ host . protoServer $ srv)) servers $(JQ.deriveJSON defaultJSON ''UsageConditions) From ef0f21a11c082c9f3b9b5bc77d2d70c6bd9ee5ce Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Fri, 8 Nov 2024 14:45:00 +0400 Subject: [PATCH 014/167] core: operator apis commands (#5155) --- src/Simplex/Chat.hs | 8 ++++++++ src/Simplex/Chat/Operators.hs | 2 ++ 2 files changed, 10 insertions(+) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 69b78ba9d4..dad3a6f813 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -8140,6 +8140,14 @@ chatCommandP = "/_servers " *> (APIGetUserProtoServers <$> A.decimal <* A.space <*> strP), "/smp" $> GetUserProtoServers (AProtocolType SPSMP), "/xftp" $> GetUserProtoServers (AProtocolType SPXFTP), + "/_operators" $> APIGetServerOperators, + "/_operators " *> (APISetServerOperators <$> jsonP), + "/_user_servers " *> (APIGetUserServers <$> A.decimal), + "/_user_servers " *> (APISetUserServers <$> A.decimal <* A.space <*> jsonP), + "/_validate_servers " *> (APIValidateServers <$> jsonP), + "/_conditions" $> APIGetUsageConditions, + "/_conditions_notified " *> (APISetConditionsNotified <$> A.decimal), + "/_accept_conditions " *> (APIAcceptConditions <$> A.decimal <* A.space <*> jsonP), "/_ttl " *> (APISetChatItemTTL <$> A.decimal <* A.space <*> ciTTLDecimal), "/ttl " *> (SetChatItemTTL <$> ciTTL), "/_ttl " *> (APIGetChatItemTTL <$> A.decimal), diff --git a/src/Simplex/Chat/Operators.hs b/src/Simplex/Chat/Operators.hs index cedc3ca6d1..b3f92caaf9 100644 --- a/src/Simplex/Chat/Operators.hs +++ b/src/Simplex/Chat/Operators.hs @@ -195,6 +195,8 @@ $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CA") ''ConditionsAcceptance) $(JQ.deriveJSON defaultJSON ''ServerOperator) +$(JQ.deriveJSON defaultJSON ''OperatorEnabled) + $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "UCA") ''UsageConditionsAction) $(JQ.deriveJSON defaultJSON ''UserServers) From 2d588949b1f397d683dbca195ca3330983fef0e4 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Sun, 10 Nov 2024 15:21:33 +0000 Subject: [PATCH 015/167] directory service: additional commands (#5159) * directory service: additional commands * notify superusers * 48 hours * replace T.elem --- apps/simplex-bot-advanced/Main.hs | 10 +- .../src/Broadcast/Bot.hs | 5 +- .../src/Broadcast/Options.hs | 9 +- .../src/Directory/Events.hs | 57 ++-- .../src/Directory/Options.hs | 14 +- .../src/Directory/Service.hs | 269 +++++++++++------- src/Simplex/Chat/Bot.hs | 16 +- src/Simplex/Chat/Bot/KnownContacts.hs | 4 +- tests/Bots/DirectoryTests.hs | 39 ++- 9 files changed, 259 insertions(+), 164 deletions(-) diff --git a/apps/simplex-bot-advanced/Main.hs b/apps/simplex-bot-advanced/Main.hs index 4733dafb79..cedbd4fe34 100644 --- a/apps/simplex-bot-advanced/Main.hs +++ b/apps/simplex-bot-advanced/Main.hs @@ -9,6 +9,7 @@ module Main where import Control.Concurrent.Async import Control.Concurrent.STM import Control.Monad +import Data.Text (Text) import qualified Data.Text as T import Simplex.Chat.Bot import Simplex.Chat.Controller @@ -18,6 +19,7 @@ import Simplex.Chat.Messages.CIContent import Simplex.Chat.Options import Simplex.Chat.Terminal (terminalChatConfig) import Simplex.Chat.Types +import Simplex.Messaging.Util (tshow) import System.Directory (getAppUserDataDirectory) import Text.Read @@ -34,7 +36,7 @@ welcomeGetOpts = do putStrLn $ "db: " <> dbFilePrefix <> "_chat.db, " <> dbFilePrefix <> "_agent.db" pure opts -welcomeMessage :: String +welcomeMessage :: Text welcomeMessage = "Hello! I am a simple squaring bot.\nIf you send me a number, I will calculate its square" mySquaringBot :: User -> ChatController -> IO () @@ -47,10 +49,10 @@ mySquaringBot _user cc = do contactConnected contact sendMessage cc contact welcomeMessage CRNewChatItems {chatItems = (AChatItem _ SMDRcv (DirectChat contact) ChatItem {content = mc@CIRcvMsgContent {}}) : _} -> do - let msg = T.unpack $ ciContentToText mc - number_ = readMaybe msg :: Maybe Integer + let msg = ciContentToText mc + number_ = readMaybe (T.unpack msg) :: Maybe Integer sendMessage cc contact $ case number_ of - Just n -> msg <> " * " <> msg <> " = " <> show (n * n) + Just n -> msg <> " * " <> msg <> " = " <> tshow (n * n) _ -> "\"" <> msg <> "\" is not a number" _ -> pure () where diff --git a/apps/simplex-broadcast-bot/src/Broadcast/Bot.hs b/apps/simplex-broadcast-bot/src/Broadcast/Bot.hs index da021ee0b5..c526d64886 100644 --- a/apps/simplex-broadcast-bot/src/Broadcast/Bot.hs +++ b/apps/simplex-broadcast-bot/src/Broadcast/Bot.hs @@ -21,6 +21,7 @@ import Simplex.Chat.Messages.CIContent import Simplex.Chat.Options import Simplex.Chat.Protocol (MsgContent (..)) import Simplex.Chat.Types +import Simplex.Messaging.Util (tshow) import System.Directory (getAppUserDataDirectory) welcomeGetOpts :: IO BroadcastBotOpts @@ -48,14 +49,14 @@ broadcastBot BroadcastBotOpts {publishers, welcomeMessage, prohibitedMessage} _u CRContactsList _ cts -> void . forkIO $ do let cts' = filter broadcastTo cts forM_ cts' $ \ct' -> sendComposedMessage cc ct' Nothing mc - sendReply $ "Forwarded to " <> show (length cts') <> " contact(s)" + sendReply $ "Forwarded to " <> tshow (length cts') <> " contact(s)" r -> putStrLn $ "Error getting contacts list: " <> show r else sendReply "!1 Message is not supported!" | otherwise -> do sendReply prohibitedMessage deleteMessage cc ct $ chatItemId' ci where - sendReply = sendComposedMessage cc ct (Just $ chatItemId' ci) . textMsgContent + sendReply = sendComposedMessage cc ct (Just $ chatItemId' ci) . MCText publisher = KnownContact {contactId = contactId' ct, localDisplayName = localDisplayName' ct} allowContent = \case MCText _ -> True diff --git a/apps/simplex-broadcast-bot/src/Broadcast/Options.hs b/apps/simplex-broadcast-bot/src/Broadcast/Options.hs index 57986874aa..5bc4ffef25 100644 --- a/apps/simplex-broadcast-bot/src/Broadcast/Options.hs +++ b/apps/simplex-broadcast-bot/src/Broadcast/Options.hs @@ -7,6 +7,7 @@ module Broadcast.Options where import Data.Maybe (fromMaybe) +import Data.Text (Text) import Options.Applicative import Simplex.Chat.Bot.KnownContacts import Simplex.Chat.Controller (updateStr, versionNumber, versionString) @@ -15,14 +16,14 @@ import Simplex.Chat.Options (ChatCmdLog (..), ChatOpts (..), CoreChatOpts, coreC data BroadcastBotOpts = BroadcastBotOpts { coreOptions :: CoreChatOpts, publishers :: [KnownContact], - welcomeMessage :: String, - prohibitedMessage :: String + welcomeMessage :: Text, + prohibitedMessage :: Text } -defaultWelcomeMessage :: [KnownContact] -> String +defaultWelcomeMessage :: [KnownContact] -> Text defaultWelcomeMessage ps = "Hello! I am a broadcast bot.\nI broadcast messages to all connected users from " <> knownContactNames ps <> "." -defaultProhibitedMessage :: [KnownContact] -> String +defaultProhibitedMessage :: [KnownContact] -> Text defaultProhibitedMessage ps = "Sorry, only these users can broadcast messages: " <> knownContactNames ps <> ". Your message is deleted." broadcastBotOpts :: FilePath -> FilePath -> Parser BroadcastBotOpts diff --git a/apps/simplex-directory-service/src/Directory/Events.hs b/apps/simplex-directory-service/src/Directory/Events.hs index 3119815d7b..ce165a1344 100644 --- a/apps/simplex-directory-service/src/Directory/Events.hs +++ b/apps/simplex-directory-service/src/Directory/Events.hs @@ -89,10 +89,11 @@ crDirectoryEvent = \case CRChatErrors {chatErrors} -> Just $ DELogChatResponse $ "chat errors: " <> T.intercalate ", " (map tshow chatErrors) _ -> Nothing -data DirectoryRole = DRUser | DRSuperUser +data DirectoryRole = DRUser | DRAdmin | DRSuperUser data SDirectoryRole (r :: DirectoryRole) where SDRUser :: SDirectoryRole 'DRUser + SDRAdmin :: SDirectoryRole 'DRAdmin SDRSuperUser :: SDirectoryRole 'DRSuperUser deriving instance Show (SDirectoryRole r) @@ -107,12 +108,14 @@ data DirectoryCmdTag (r :: DirectoryRole) where DCListUserGroups_ :: DirectoryCmdTag 'DRUser DCDeleteGroup_ :: DirectoryCmdTag 'DRUser DCSetRole_ :: DirectoryCmdTag 'DRUser - DCApproveGroup_ :: DirectoryCmdTag 'DRSuperUser - DCRejectGroup_ :: DirectoryCmdTag 'DRSuperUser - DCSuspendGroup_ :: DirectoryCmdTag 'DRSuperUser - DCResumeGroup_ :: DirectoryCmdTag 'DRSuperUser - DCListLastGroups_ :: DirectoryCmdTag 'DRSuperUser - DCListPendingGroups_ :: DirectoryCmdTag 'DRSuperUser + DCApproveGroup_ :: DirectoryCmdTag 'DRAdmin + DCRejectGroup_ :: DirectoryCmdTag 'DRAdmin + DCSuspendGroup_ :: DirectoryCmdTag 'DRAdmin + DCResumeGroup_ :: DirectoryCmdTag 'DRAdmin + DCListLastGroups_ :: DirectoryCmdTag 'DRAdmin + DCListPendingGroups_ :: DirectoryCmdTag 'DRAdmin + DCShowGroupLink_ :: DirectoryCmdTag 'DRAdmin + DCSendToGroupOwner_ :: DirectoryCmdTag 'DRAdmin DCExecuteCommand_ :: DirectoryCmdTag 'DRSuperUser deriving instance Show (DirectoryCmdTag r) @@ -130,12 +133,14 @@ data DirectoryCmd (r :: DirectoryRole) where DCListUserGroups :: DirectoryCmd 'DRUser DCDeleteGroup :: UserGroupRegId -> GroupName -> DirectoryCmd 'DRUser DCSetRole :: GroupId -> GroupName -> GroupMemberRole -> DirectoryCmd 'DRUser - DCApproveGroup :: {groupId :: GroupId, displayName :: GroupName, groupApprovalId :: GroupApprovalId} -> DirectoryCmd 'DRSuperUser - DCRejectGroup :: GroupId -> GroupName -> DirectoryCmd 'DRSuperUser - DCSuspendGroup :: GroupId -> GroupName -> DirectoryCmd 'DRSuperUser - DCResumeGroup :: GroupId -> GroupName -> DirectoryCmd 'DRSuperUser - DCListLastGroups :: Int -> DirectoryCmd 'DRSuperUser - DCListPendingGroups :: Int -> DirectoryCmd 'DRSuperUser + DCApproveGroup :: {groupId :: GroupId, displayName :: GroupName, groupApprovalId :: GroupApprovalId} -> DirectoryCmd 'DRAdmin + DCRejectGroup :: GroupId -> GroupName -> DirectoryCmd 'DRAdmin + DCSuspendGroup :: GroupId -> GroupName -> DirectoryCmd 'DRAdmin + DCResumeGroup :: GroupId -> GroupName -> DirectoryCmd 'DRAdmin + DCListLastGroups :: Int -> DirectoryCmd 'DRAdmin + DCListPendingGroups :: Int -> DirectoryCmd 'DRAdmin + DCShowGroupLink :: GroupId -> GroupName -> DirectoryCmd 'DRAdmin + DCSendToGroupOwner :: GroupId -> GroupName -> Text -> DirectoryCmd 'DRAdmin DCExecuteCommand :: String -> DirectoryCmd 'DRSuperUser DCUnknownCommand :: DirectoryCmd 'DRUser DCCommandError :: DirectoryCmdTag r -> DirectoryCmd r @@ -168,17 +173,20 @@ directoryCmdP = "ls" -> u DCListUserGroups_ "delete" -> u DCDeleteGroup_ "role" -> u DCSetRole_ - "approve" -> su DCApproveGroup_ - "reject" -> su DCRejectGroup_ - "suspend" -> su DCSuspendGroup_ - "resume" -> su DCResumeGroup_ - "last" -> su DCListLastGroups_ - "pending" -> su DCListPendingGroups_ + "approve" -> au DCApproveGroup_ + "reject" -> au DCRejectGroup_ + "suspend" -> au DCSuspendGroup_ + "resume" -> au DCResumeGroup_ + "last" -> au DCListLastGroups_ + "pending" -> au DCListPendingGroups_ + "link" -> au DCShowGroupLink_ + "owner" -> au DCSendToGroupOwner_ "exec" -> su DCExecuteCommand_ "x" -> su DCExecuteCommand_ _ -> fail "bad command tag" where u = pure . ADCT SDRUser + au = pure . ADCT SDRAdmin su = pure . ADCT SDRSuperUser cmdP :: DirectoryCmdTag r -> Parser (DirectoryCmd r) cmdP = \case @@ -203,6 +211,11 @@ directoryCmdP = DCResumeGroup_ -> gc DCResumeGroup DCListLastGroups_ -> DCListLastGroups <$> (A.space *> A.decimal <|> pure 10) DCListPendingGroups_ -> DCListPendingGroups <$> (A.space *> A.decimal <|> pure 10) + DCShowGroupLink_ -> gc DCShowGroupLink + DCSendToGroupOwner_ -> do + (groupId, displayName) <- gc (,) + msg <- A.space *> A.takeText + pure $ DCSendToGroupOwner groupId displayName msg DCExecuteCommand_ -> DCExecuteCommand . T.unpack <$> (A.space *> A.takeText) where gc f = f <$> (A.space *> A.decimal <* A.char ':') <*> displayNameP @@ -213,8 +226,8 @@ directoryCmdP = quoted c = A.char c *> takeNameTill (== c) <* A.char c refChar c = c > ' ' && c /= '#' && c /= '@' -viewName :: String -> String -viewName n = if ' ' `elem` n then "'" <> n <> "'" else n +viewName :: Text -> Text +viewName n = if any (== ' ') (T.unpack n) then "'" <> n <> "'" else n directoryCmdTag :: DirectoryCmd r -> Text directoryCmdTag = \case @@ -234,6 +247,8 @@ directoryCmdTag = \case DCResumeGroup {} -> "resume" DCListLastGroups _ -> "last" DCListPendingGroups _ -> "pending" + DCShowGroupLink {} -> "link" + DCSendToGroupOwner {} -> "owner" DCExecuteCommand _ -> "exec" DCUnknownCommand -> "unknown" DCCommandError _ -> "error" diff --git a/apps/simplex-directory-service/src/Directory/Options.hs b/apps/simplex-directory-service/src/Directory/Options.hs index 0d64064d7d..7f02a580e6 100644 --- a/apps/simplex-directory-service/src/Directory/Options.hs +++ b/apps/simplex-directory-service/src/Directory/Options.hs @@ -11,6 +11,7 @@ module Directory.Options ) where +import qualified Data.Text as T import Options.Applicative import Simplex.Chat.Bot.KnownContacts import Simplex.Chat.Controller (updateStr, versionNumber, versionString) @@ -18,9 +19,10 @@ import Simplex.Chat.Options (ChatOpts (..), ChatCmdLog (..), CoreChatOpts, coreC data DirectoryOpts = DirectoryOpts { coreOptions :: CoreChatOpts, + adminUsers :: [KnownContact], superUsers :: [KnownContact], directoryLog :: Maybe FilePath, - serviceName :: String, + serviceName :: T.Text, searchResults :: Int, testing :: Bool } @@ -28,6 +30,13 @@ data DirectoryOpts = DirectoryOpts directoryOpts :: FilePath -> FilePath -> Parser DirectoryOpts directoryOpts appDir defaultDbFileName = do coreOptions <- coreChatOptsP appDir defaultDbFileName + adminUsers <- + option + parseKnownContacts + ( long "admin-users" + <> metavar "ADMIN_USERS" + <> help "Comma-separated list of admin-users in the format CONTACT_ID:DISPLAY_NAME who will be allowed to manage the directory" + ) superUsers <- option parseKnownContacts @@ -52,9 +61,10 @@ directoryOpts appDir defaultDbFileName = do pure DirectoryOpts { coreOptions, + adminUsers, superUsers, directoryLog, - serviceName, + serviceName = T.pack serviceName, searchResults = 10, testing = False } diff --git a/apps/simplex-directory-service/src/Directory/Service.hs b/apps/simplex-directory-service/src/Directory/Service.hs index ba03642a28..c1012f2a0a 100644 --- a/apps/simplex-directory-service/src/Directory/Service.hs +++ b/apps/simplex-directory-service/src/Directory/Service.hs @@ -17,13 +17,11 @@ import Control.Concurrent.Async import Control.Concurrent.STM import Control.Logger.Simple import Control.Monad -import qualified Data.ByteString.Char8 as B import Data.Maybe (fromMaybe, maybeToList) import Data.Set (Set) import qualified Data.Set as S import Data.Text (Text) import qualified Data.Text as T -import Data.Text.Encoding (decodeLatin1) import Data.Time.Clock (diffUTCTime, getCurrentTime) import Data.Time.LocalTime (getCurrentTimeZone) import Directory.Events @@ -37,6 +35,7 @@ import Simplex.Chat.Core import Simplex.Chat.Messages import Simplex.Chat.Options import Simplex.Chat.Protocol (MsgContent (..)) +import Simplex.Chat.Store.Shared (StoreError (..)) import Simplex.Chat.Types import Simplex.Chat.Types.Shared import Simplex.Chat.View (serializeChatResponse, simplexChatContact, viewContactName, viewGroupName) @@ -79,7 +78,7 @@ welcomeGetOpts = do pure opts directoryService :: DirectoryStore -> DirectoryOpts -> User -> ChatController -> IO () -directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testing} user@User {userId} cc = do +directoryService st DirectoryOpts {adminUsers, superUsers, serviceName, searchResults, testing} user@User {userId} cc = do initializeBotAddress' (not testing) cc env <- newServiceState race_ (forever $ void getLine) . forever $ do @@ -102,6 +101,7 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi logInfo $ "command received " <> directoryCmdTag cmd case sUser of SDRUser -> deUserCommand env ct ciId cmd + SDRAdmin -> deAdminCommand ct ciId cmd SDRSuperUser -> deSuperUserCommand ct ciId cmd DELogChatResponse r -> logInfo r where @@ -118,9 +118,9 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi userGroupReference gr GroupInfo {groupProfile = GroupProfile {displayName}} = userGroupReference' gr displayName userGroupReference' GroupReg {userGroupRegId} displayName = groupReference' userGroupRegId displayName groupReference GroupInfo {groupId, groupProfile = GroupProfile {displayName}} = groupReference' groupId displayName - groupReference' groupId displayName = "ID " <> show groupId <> " (" <> T.unpack displayName <> ")" + groupReference' groupId displayName = "ID " <> tshow groupId <> " (" <> displayName <> ")" groupAlreadyListed GroupInfo {groupProfile = GroupProfile {displayName, fullName}} = - T.unpack $ "The group " <> displayName <> " (" <> fullName <> ") is already listed in the directory, please choose another name." + "The group " <> displayName <> " (" <> fullName <> ") is already listed in the directory, please choose another name." getGroups :: Text -> IO (Maybe [(GroupInfo, GroupSummary)]) getGroups = getGroups_ . Just @@ -151,7 +151,7 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi processInvitation ct g@GroupInfo {groupId, groupProfile = GroupProfile {displayName}} = do void $ addGroupReg st ct g GRSProposed r <- sendChatCmd cc $ APIJoinGroup groupId - sendMessage cc ct $ T.unpack $ case r of + sendMessage cc ct $ case r of CRUserAcceptedGroupSent {} -> "Joining the group " <> displayName <> "…" _ -> "Error joining group " <> displayName <> ", please re-send the invitation!" @@ -179,10 +179,10 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi where askConfirmation = do ugrId <- addGroupReg st ct g GRSPendingConfirmation - sendMessage cc ct $ T.unpack $ "The group " <> displayName <> " (" <> fullName <> ") is already submitted to the directory.\nTo confirm the registration, please send:" - sendMessage cc ct $ "/confirm " <> show ugrId <> ":" <> viewName (T.unpack displayName) + sendMessage cc ct $ "The group " <> displayName <> " (" <> fullName <> ") is already submitted to the directory.\nTo confirm the registration, please send:" + sendMessage cc ct $ "/confirm " <> tshow ugrId <> ":" <> viewName displayName - badRolesMsg :: GroupRolesStatus -> Maybe String + badRolesMsg :: GroupRolesStatus -> Maybe Text badRolesMsg = \case GRSOk -> Nothing GRSServiceNotAdmin -> Just "You must grant directory service *admin* role to register the group" @@ -218,7 +218,7 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi when (ctId `isOwner` gr) $ do setGroupRegOwner st gr owner let GroupInfo {groupId, groupProfile = GroupProfile {displayName}} = g - notifyOwner gr $ T.unpack $ "Joined the group " <> displayName <> ", creating the link…" + notifyOwner gr $ "Joined the group " <> displayName <> ", creating the link…" sendChatCmd cc (APICreateGroupLink groupId GRMember) >>= \case CRGroupLinkCreated {connReqContact} -> do setGroupStatus st gr GRSPendingUpdate @@ -227,7 +227,7 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi "Created the public link to join the group via this directory service that is always online.\n\n\ \Please add it to the group welcome message.\n\ \For example, add:" - notifyOwner gr $ "Link to join the group " <> T.unpack displayName <> ": " <> B.unpack (strEncode $ simplexChatContact connReqContact) + notifyOwner gr $ "Link to join the group " <> displayName <> ": " <> strEncodeTxt (simplexChatContact connReqContact) CRChatCmdError _ (ChatError e) -> case e of CEGroupUserRole {} -> notifyOwner gr "Failed creating group link, as service is no longer an admin." CEGroupMemberUserRemoved -> notifyOwner gr "Failed creating group link, as service is removed from the group." @@ -256,7 +256,7 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi GPHasServiceLink -> when (ctId `isOwner` gr) $ groupLinkAdded gr GPServiceLinkError -> do when (ctId `isOwner` gr) $ notifyOwner gr $ "Error: " <> serviceName <> " has no group link for " <> userGroupRef <> ". Please report the error to the developers." - logError $ "Error: no group link for " <> T.pack userGroupRef + logError $ "Error: no group link for " <> userGroupRef GRSPendingApproval n -> processProfileChange gr $ n + 1 GRSActive -> processProfileChange gr 1 GRSSuspended -> processProfileChange gr 1 @@ -277,7 +277,7 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi _ -> do let gaId = 1 setGroupStatus st gr $ GRSPendingApproval gaId - notifyOwner gr $ "Thank you! The group link for " <> userGroupReference gr toGroup <> " is added to the welcome message.\nYou will be notified once the group is added to the directory - it may take up to 24 hours." + notifyOwner gr $ "Thank you! The group link for " <> userGroupReference gr toGroup <> " is added to the welcome message.\nYou will be notified once the group is added to the directory - it may take up to 48 hours." checkRolesSendToApprove gr gaId processProfileChange gr n' = do setGroupStatus st gr GRSPendingUpdate @@ -299,13 +299,13 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi notifyOwner gr $ "The group " <> userGroupRef <> " is updated!\nIt is hidden from the directory until approved." notifySuperUsers $ "The group " <> groupRef <> " is updated." checkRolesSendToApprove gr n' - GPServiceLinkError -> logError $ "Error: no group link for " <> T.pack groupRef <> " pending approval." + GPServiceLinkError -> logError $ "Error: no group link for " <> groupRef <> " pending approval." groupProfileUpdate = profileUpdate <$> sendChatCmd cc (APIGetGroupLink groupId) where profileUpdate = \case CRGroupLink {connReqContact} -> - let groupLink1 = safeDecodeUtf8 $ strEncode connReqContact - groupLink2 = safeDecodeUtf8 $ strEncode $ simplexChatContact connReqContact + let groupLink1 = strEncodeTxt connReqContact + groupLink2 = strEncodeTxt $ simplexChatContact connReqContact hadLinkBefore = groupLink1 `isInfix` description p || groupLink2 `isInfix` description p hasLinkNow = groupLink1 `isInfix` description p' || groupLink2 `isInfix` description p' in if @@ -331,7 +331,7 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi msg = maybe (MCText text) (\image -> MCImage {text, image}) image' withSuperUsers $ \cId -> do sendComposedMessage' cc cId Nothing msg - sendMessage' cc cId $ "/approve " <> show dbGroupId <> ":" <> viewName (T.unpack displayName) <> " " <> show gaId + sendMessage' cc cId $ "/approve " <> tshow dbGroupId <> ":" <> viewName displayName <> " " <> tshow gaId deContactRoleChanged :: GroupInfo -> ContactId -> GroupMemberRole -> IO () deContactRoleChanged g@GroupInfo {membership = GroupMember {memberRole = serviceRole}} ctId contactRole = do @@ -356,7 +356,7 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi where rStatus = groupRolesStatus contactRole serviceRole groupRef = groupReference g - ctRole = "*" <> B.unpack (strEncode contactRole) <> "*" + ctRole = "*" <> strEncodeTxt contactRole <> "*" suCtRole = "(user role is set to " <> ctRole <> ")." deServiceRoleChanged :: GroupInfo -> GroupMemberRole -> IO () @@ -382,7 +382,7 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi _ -> pure () where groupRef = groupReference g - srvRole = "*" <> B.unpack (strEncode serviceRole) <> "*" + srvRole = "*" <> strEncodeTxt serviceRole <> "*" suSrvRole = "(" <> serviceName <> " role is changed to " <> srvRole <> ")." whenContactIsOwner gr action = getGroupMember gr @@ -426,7 +426,7 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi <> serviceName <> " bot will create a public group link for the new members to join even when you are offline.\n\ \3. You will then need to add this link to the group welcome message.\n\ - \4. Once the link is added, service admins will approve the group (it can take up to 24 hours), and everybody will be able to find it in directory.\n\n\ + \4. Once the link is added, service admins will approve the group (it can take up to 48 hours), and everybody will be able to find it in directory.\n\n\ \Start from inviting the bot to your group as admin - it will guide you through the process" DCSearchGroup s -> withFoundListedGroups (Just s) $ sendSearchResults s DCSearchNext -> @@ -448,44 +448,47 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi DCRecentGroups -> withFoundListedGroups Nothing $ sendAllGroups takeRecent "the most recent" STRecent DCSubmitGroup _link -> pure () DCConfirmDuplicateGroup ugrId gName -> - withUserGroupReg ugrId gName $ \gr g@GroupInfo {groupProfile = GroupProfile {displayName}} -> + withUserGroupReg ugrId gName $ \g@GroupInfo {groupProfile = GroupProfile {displayName}} gr -> readTVarIO (groupRegStatus gr) >>= \case GRSPendingConfirmation -> getDuplicateGroup g >>= \case Nothing -> sendMessage cc ct "Error: getDuplicateGroup. Please notify the developers." Just DGReserved -> sendMessage cc ct $ groupAlreadyListed g _ -> processInvitation ct g - _ -> sendReply $ "Error: the group ID " <> show ugrId <> " (" <> T.unpack displayName <> ") is not pending confirmation." + _ -> sendReply $ "Error: the group ID " <> tshow ugrId <> " (" <> displayName <> ") is not pending confirmation." DCListUserGroups -> atomically (getUserGroupRegs st $ contactId' ct) >>= \grs -> do - sendReply $ show (length grs) <> " registered group(s)" + sendReply $ tshow (length grs) <> " registered group(s)" void . forkIO $ forM_ (reverse grs) $ \gr@GroupReg {userGroupRegId} -> sendGroupInfo ct gr userGroupRegId Nothing DCDeleteGroup ugrId gName -> - withUserGroupReg ugrId gName $ \gr GroupInfo {groupProfile = GroupProfile {displayName}} -> do + withUserGroupReg ugrId gName $ \GroupInfo {groupProfile = GroupProfile {displayName}} gr -> do delGroupReg st gr - sendReply $ T.unpack $ "Your group " <> displayName <> " is deleted from the directory" - DCSetRole ugrId gName mRole -> - withUserGroupReg ugrId gName $ \_gr GroupInfo {groupId, groupProfile = GroupProfile {displayName}} -> do - gLink_ <- setGroupLinkRole cc groupId mRole - sendReply $ T.unpack $ case gLink_ of - Nothing -> "Error: the initial member role for the group " <> displayName <> " was NOT upgated" - Just gLink -> - ("The initial member role for the group " <> displayName <> " is set to *" <> decodeLatin1 (strEncode mRole) <> "*\n\n") - <> ("*Please note*: it applies only to members joining via this link: " <> safeDecodeUtf8 (strEncode $ simplexChatContact gLink)) + sendReply $ "Your group " <> displayName <> " is deleted from the directory" + DCSetRole gId gName mRole -> + (if isAdmin then withGroupAndReg sendReply else withUserGroupReg) gId gName $ + \GroupInfo {groupId, groupProfile = GroupProfile {displayName}} _gr -> do + gLink_ <- setGroupLinkRole cc groupId mRole + sendReply $ case gLink_ of + Nothing -> "Error: the initial member role for the group " <> displayName <> " was NOT upgated" + Just gLink -> + ("The initial member role for the group " <> displayName <> " is set to *" <> strEncodeTxt mRole <> "*\n\n") + <> ("*Please note*: it applies only to members joining via this link: " <> strEncodeTxt (simplexChatContact gLink)) DCUnknownCommand -> sendReply "Unknown command" - DCCommandError tag -> sendReply $ "Command error: " <> show tag + DCCommandError tag -> sendReply $ "Command error: " <> tshow tag where + knownCt = knownContact ct + isAdmin = knownCt `elem` adminUsers || knownCt `elem` superUsers withUserGroupReg ugrId gName action = atomically (getUserGroupReg st (contactId' ct) ugrId) >>= \case - Nothing -> sendReply $ "Group ID " <> show ugrId <> " not found" + Nothing -> sendReply $ "Group ID " <> tshow ugrId <> " not found" Just gr@GroupReg {dbGroupId} -> do getGroup cc dbGroupId >>= \case - Nothing -> sendReply $ "Group ID " <> show ugrId <> " not found" + Nothing -> sendReply $ "Group ID " <> tshow ugrId <> " not found" Just g@GroupInfo {groupProfile = GroupProfile {displayName}} - | displayName == gName -> action gr g - | otherwise -> sendReply $ "Group ID " <> show ugrId <> " has the display name " <> T.unpack displayName - sendReply = sendComposedMessage cc ct (Just ciId) . textMsgContent + | displayName == gName -> action g gr + | otherwise -> sendReply $ "Group ID " <> tshow ugrId <> " has the display name " <> displayName + sendReply = mkSendReply ct ciId withFoundListedGroups s_ action = getGroups_ s_ >>= \case Just groups -> atomically (filterListedGroups st groups) >>= action @@ -495,8 +498,8 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi gs -> do let gs' = takeTop searchResults gs moreGroups = length gs - length gs' - more = if moreGroups > 0 then ", sending top " <> show (length gs') else "" - sendReply $ "Found " <> show (length gs) <> " group(s)" <> more <> "." + more = if moreGroups > 0 then ", sending top " <> tshow (length gs') else "" + sendReply $ "Found " <> tshow (length gs) <> " group(s)" <> more <> "." updateSearchRequest (STSearch s) $ groupIds gs' sendFoundGroups gs' moreGroups sendAllGroups takeFirst sortName searchType = \case @@ -504,8 +507,8 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi gs -> do let gs' = takeFirst searchResults gs moreGroups = length gs - length gs' - more = if moreGroups > 0 then ", sending " <> sortName <> " " <> show (length gs') else "" - sendReply $ show (length gs) <> " group(s) listed" <> more <> "." + more = if moreGroups > 0 then ", sending " <> sortName <> " " <> tshow (length gs') else "" + sendReply $ tshow (length gs) <> " group(s) listed" <> more <> "." updateSearchRequest searchType $ groupIds gs' sendFoundGroups gs' moreGroups sendNextSearchResults takeFirst SearchRequest {searchType, sentGroups} = \case @@ -516,7 +519,7 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi let gs' = takeFirst searchResults $ filterNotSent sentGroups gs sentGroups' = sentGroups <> groupIds gs' moreGroups = length gs - S.size sentGroups' - sendReply $ "Sending " <> show (length gs') <> " more group(s)." + sendReply $ "Sending " <> tshow (length gs') <> " more group(s)." updateSearchRequest searchType sentGroups' sendFoundGroups gs' moreGroups updateSearchRequest :: SearchType -> Set GroupId -> IO () @@ -527,9 +530,10 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi sendFoundGroups gs moreGroups = void . forkIO $ do forM_ gs $ - \(GroupInfo {groupProfile = p@GroupProfile {image = image_}}, GroupSummary {currentMembers}) -> do + \(GroupInfo {groupId, groupProfile = p@GroupProfile {image = image_}}, GroupSummary {currentMembers}) -> do let membersStr = "_" <> tshow currentMembers <> " members_" - text = groupInfoText p <> "\n" <> membersStr + showId = if isAdmin then tshow groupId <> ". " else "" + text = showId <> groupInfoText p <> "\n" <> membersStr msg = maybe (MCText text) (\image -> MCImage {text, image}) image_ sendComposedMessage cc ct Nothing msg when (moreGroups > 0) $ @@ -537,92 +541,134 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi MCText $ "Send */next* or just *.* for " <> tshow moreGroups <> " more result(s)." - deSuperUserCommand :: Contact -> ChatItemId -> DirectoryCmd 'DRSuperUser -> IO () - deSuperUserCommand ct ciId cmd - | superUser `elem` superUsers = case cmd of + deAdminCommand :: Contact -> ChatItemId -> DirectoryCmd 'DRAdmin -> IO () + deAdminCommand ct ciId cmd + | knownCt `elem` adminUsers || knownCt `elem` superUsers = case cmd of DCApproveGroup {groupId, displayName = n, groupApprovalId} -> - getGroupAndReg groupId n >>= \case - Nothing -> sendReply $ "The group " <> groupRef <> " not found (getGroupAndReg)." - Just (g, gr) -> - readTVarIO (groupRegStatus gr) >>= \case - GRSPendingApproval gaId - | gaId == groupApprovalId -> do - getDuplicateGroup g >>= \case - Nothing -> sendReply "Error: getDuplicateGroup. Please notify the developers." - Just DGReserved -> sendReply $ "The group " <> groupRef <> " is already listed in the directory." - _ -> do - getGroupRolesStatus g gr >>= \case - Just GRSOk -> do - setGroupStatus st gr GRSActive - sendReply "Group approved!" - notifyOwner gr $ "The group " <> userGroupReference' gr n <> " is approved and listed in directory!\nPlease note: if you change the group profile it will be hidden from directory until it is re-approved." - Just GRSServiceNotAdmin -> replyNotApproved serviceNotAdmin - Just GRSContactNotOwner -> replyNotApproved "user is not an owner." - Just GRSBadRoles -> replyNotApproved $ "user is not an owner, " <> serviceNotAdmin - Nothing -> sendReply "Error: getGroupRolesStatus. Please notify the developers." - where - replyNotApproved reason = sendReply $ "Group is not approved: " <> reason - serviceNotAdmin = serviceName <> " is not an admin." - | otherwise -> sendReply "Incorrect approval code" - _ -> sendReply $ "Error: the group " <> groupRef <> " is not pending approval." + withGroupAndReg sendReply groupId n $ \g gr -> + readTVarIO (groupRegStatus gr) >>= \case + GRSPendingApproval gaId + | gaId == groupApprovalId -> do + getDuplicateGroup g >>= \case + Nothing -> sendReply "Error: getDuplicateGroup. Please notify the developers." + Just DGReserved -> sendReply $ "The group " <> groupRef <> " is already listed in the directory." + _ -> do + getGroupRolesStatus g gr >>= \case + Just GRSOk -> do + setGroupStatus st gr GRSActive + let approved = "The group " <> userGroupReference' gr n <> " is approved" + notifyOwner gr $ approved <> " and listed in directory!\nPlease note: if you change the group profile it will be hidden from directory until it is re-approved." + sendReply "Group approved!" + notifyOtherSuperUsers $ approved <> " by " <> viewName (localDisplayName' ct) + Just GRSServiceNotAdmin -> replyNotApproved serviceNotAdmin + Just GRSContactNotOwner -> replyNotApproved "user is not an owner." + Just GRSBadRoles -> replyNotApproved $ "user is not an owner, " <> serviceNotAdmin + Nothing -> sendReply "Error: getGroupRolesStatus. Please notify the developers." + where + replyNotApproved reason = sendReply $ "Group is not approved: " <> reason + serviceNotAdmin = serviceName <> " is not an admin." + | otherwise -> sendReply "Incorrect approval code" + _ -> sendReply $ "Error: the group " <> groupRef <> " is not pending approval." where groupRef = groupReference' groupId n DCRejectGroup _gaId _gName -> pure () DCSuspendGroup groupId gName -> do let groupRef = groupReference' groupId gName - getGroupAndReg groupId gName >>= \case - Nothing -> sendReply $ "The group " <> groupRef <> " not found (getGroupAndReg)." - Just (_, gr) -> - readTVarIO (groupRegStatus gr) >>= \case - GRSActive -> do - setGroupStatus st gr GRSSuspended - notifyOwner gr $ "The group " <> userGroupReference' gr gName <> " is suspended and hidden from directory. Please contact the administrators." - sendReply "Group suspended!" - _ -> sendReply $ "The group " <> groupRef <> " is not active, can't be suspended." + withGroupAndReg sendReply groupId gName $ \_ gr -> + readTVarIO (groupRegStatus gr) >>= \case + GRSActive -> do + setGroupStatus st gr GRSSuspended + let suspended = "The group " <> userGroupReference' gr gName <> " is suspended" + notifyOwner gr $ suspended <> " and hidden from directory. Please contact the administrators." + sendReply "Group suspended!" + notifyOtherSuperUsers $ suspended <> " by " <> viewName (localDisplayName' ct) + _ -> sendReply $ "The group " <> groupRef <> " is not active, can't be suspended." DCResumeGroup groupId gName -> do let groupRef = groupReference' groupId gName - getGroupAndReg groupId gName >>= \case - Nothing -> sendReply $ "The group " <> groupRef <> " not found (getGroupAndReg)." - Just (_, gr) -> - readTVarIO (groupRegStatus gr) >>= \case - GRSSuspended -> do - setGroupStatus st gr GRSActive - notifyOwner gr $ "The group " <> userGroupReference' gr gName <> " is listed in the directory again!" - sendReply "Group listing resumed!" - _ -> sendReply $ "The group " <> groupRef <> " is not suspended, can't be resumed." + withGroupAndReg sendReply groupId gName $ \_ gr -> + readTVarIO (groupRegStatus gr) >>= \case + GRSSuspended -> do + setGroupStatus st gr GRSActive + let groupStr = "The group " <> userGroupReference' gr gName + notifyOwner gr $ groupStr <> " is listed in the directory again!" + sendReply "Group listing resumed!" + notifyOtherSuperUsers $ groupStr <> " listing resumed by " <> viewName (localDisplayName' ct) + _ -> sendReply $ "The group " <> groupRef <> " is not suspended, can't be resumed." DCListLastGroups count -> listGroups count False DCListPendingGroups count -> listGroups count True - DCExecuteCommand cmdStr -> - sendChatCmdStr cc cmdStr >>= \r -> do - ts <- getCurrentTime - tz <- getCurrentTimeZone - sendReply $ serializeChatResponse (Nothing, Just user) ts tz Nothing r - DCCommandError tag -> sendReply $ "Command error: " <> show tag + DCShowGroupLink groupId gName -> do + let groupRef = groupReference' groupId gName + withGroupAndReg sendReply groupId gName $ \_ _ -> + sendChatCmd cc (APIGetGroupLink groupId) >>= \case + CRGroupLink {connReqContact, memberRole} -> + sendReply $ T.unlines + [ "The link to join the group " <> groupRef <> ":", + strEncodeTxt $ simplexChatContact connReqContact, + "New member role: " <> strEncodeTxt memberRole + ] + CRChatCmdError _ (ChatErrorStore (SEGroupLinkNotFound _)) -> + sendReply $ "The group " <> groupRef <> " has no public link." + r -> do + ts <- getCurrentTime + tz <- getCurrentTimeZone + let resp = T.pack $ serializeChatResponse (Nothing, Just user) ts tz Nothing r + sendReply $ "Unexpected error:\n" <> resp + DCSendToGroupOwner groupId gName msg -> do + let groupRef = groupReference' groupId gName + withGroupAndReg sendReply groupId gName $ \_ gr@GroupReg {dbContactId} -> do + notifyOwner gr msg + owner_ <- getContact cc dbContactId + let ownerInfo = "the owner of the group " <> groupRef + ownerName ct' = "@" <> viewName (localDisplayName' ct') <> ", " + sendReply $ "Forwarded to " <> maybe "" ownerName owner_ <> ownerInfo + DCCommandError tag -> sendReply $ "Command error: " <> tshow tag | otherwise = sendReply "You are not allowed to use this command" where - superUser = KnownContact {contactId = contactId' ct, localDisplayName = localDisplayName' ct} - sendReply = sendComposedMessage cc ct (Just ciId) . textMsgContent + knownCt = knownContact ct + sendReply = mkSendReply ct ciId + notifyOtherSuperUsers s = withSuperUsers $ \ctId -> unless (ctId == contactId' ct) $ sendMessage' cc ctId s listGroups count pending = readTVarIO (groupRegs st) >>= \groups -> do grs <- if pending then filterM (fmap pendingApproval . readTVarIO . groupRegStatus) groups else pure groups - sendReply $ show (length grs) <> " registered group(s)" <> (if length grs > count then ", showing the last " <> show count else "") + sendReply $ tshow (length grs) <> " registered group(s)" <> (if length grs > count then ", showing the last " <> tshow count else "") void . forkIO $ forM_ (reverse $ take count grs) $ \gr@GroupReg {dbGroupId, dbContactId} -> do ct_ <- getContact cc dbContactId let ownerStr = "Owner: " <> maybe "getContact error" localDisplayName' ct_ sendGroupInfo ct gr dbGroupId $ Just ownerStr - getGroupAndReg :: GroupId -> GroupName -> IO (Maybe (GroupInfo, GroupReg)) - getGroupAndReg gId gName = - getGroup cc gId - $>>= \g@GroupInfo {groupProfile = GroupProfile {displayName}} -> - if displayName == gName - then - atomically (getGroupReg st gId) - $>>= \gr -> pure $ Just (g, gr) - else pure Nothing + deSuperUserCommand :: Contact -> ChatItemId -> DirectoryCmd 'DRSuperUser -> IO () + deSuperUserCommand ct ciId cmd + | knownContact ct `elem` superUsers = case cmd of + DCExecuteCommand cmdStr -> + sendChatCmdStr cc cmdStr >>= \r -> do + ts <- getCurrentTime + tz <- getCurrentTimeZone + sendReply $ T.pack $ serializeChatResponse (Nothing, Just user) ts tz Nothing r + DCCommandError tag -> sendReply $ "Command error: " <> tshow tag + | otherwise = sendReply "You are not allowed to use this command" + where + sendReply = mkSendReply ct ciId + + knownContact :: Contact -> KnownContact + knownContact ct = KnownContact {contactId = contactId' ct, localDisplayName = localDisplayName' ct} + + mkSendReply :: Contact -> ChatItemId -> Text -> IO () + mkSendReply ct ciId = sendComposedMessage cc ct (Just ciId) . MCText + + withGroupAndReg :: (Text -> IO ()) -> GroupId -> GroupName -> (GroupInfo -> GroupReg -> IO ()) -> IO () + withGroupAndReg sendReply gId gName action = + getGroup cc gId >>= \case + Nothing -> sendReply $ "Group ID " <> tshow gId <> " not found (getGroup)" + Just g@GroupInfo {groupProfile = GroupProfile {displayName}} + | displayName == gName -> + atomically (getGroupReg st gId) >>= \case + Nothing -> sendReply $ "Registration for group ID " <> tshow gId <> " not found (getGroupReg)" + Just gr -> action g gr + | otherwise -> + sendReply $ "Group ID " <> tshow gId <> " has the display name " <> displayName sendGroupInfo :: Contact -> GroupReg -> GroupId -> Maybe Text -> IO () sendGroupInfo ct gr@GroupReg {dbGroupId} useGroupId ownerStr_ = do @@ -668,5 +714,8 @@ setGroupLinkRole cc gId mRole = resp <$> sendChatCmd cc (APIGroupLinkMemberRole CRGroupLink _ _ gLink _ -> Just gLink _ -> Nothing -unexpectedError :: String -> String +unexpectedError :: Text -> Text unexpectedError err = "Unexpected error: " <> err <> ", please notify the developers." + +strEncodeTxt :: StrEncoding a => a -> Text +strEncodeTxt = safeDecodeUtf8 . strEncode diff --git a/src/Simplex/Chat/Bot.hs b/src/Simplex/Chat/Bot.hs index 66479c0ee6..8c0978a98f 100644 --- a/src/Simplex/Chat/Bot.hs +++ b/src/Simplex/Chat/Bot.hs @@ -12,6 +12,7 @@ import Control.Concurrent.STM import Control.Monad import qualified Data.ByteString.Char8 as B import Data.List.NonEmpty (NonEmpty (..)) +import Data.Text (Text) import qualified Data.Text as T import Simplex.Chat.Controller import Simplex.Chat.Core @@ -31,10 +32,10 @@ chatBotRepl welcome answer _user cc = do case resp of CRContactConnected _ contact _ -> do contactConnected contact - void $ sendMessage cc contact welcome + void $ sendMessage cc contact $ T.pack welcome CRNewChatItems {chatItems = (AChatItem _ SMDRcv (DirectChat contact) ChatItem {content = mc@CIRcvMsgContent {}}) : _} -> do let msg = T.unpack $ ciContentToText mc - void $ sendMessage cc contact =<< answer contact msg + void $ sendMessage cc contact . T.pack =<< answer contact msg _ -> pure () where contactConnected Contact {localDisplayName} = putStrLn $ T.unpack localDisplayName <> " connected" @@ -57,11 +58,11 @@ initializeBotAddress' logAddress cc = do when logAddress $ putStrLn $ "Bot's contact address is: " <> B.unpack (strEncode uri) void $ sendChatCmd cc $ AddressAutoAccept $ Just AutoAccept {acceptIncognito = False, autoReply = Nothing} -sendMessage :: ChatController -> Contact -> String -> IO () -sendMessage cc ct = sendComposedMessage cc ct Nothing . textMsgContent +sendMessage :: ChatController -> Contact -> Text -> IO () +sendMessage cc ct = sendComposedMessage cc ct Nothing . MCText -sendMessage' :: ChatController -> ContactId -> String -> IO () -sendMessage' cc ctId = sendComposedMessage' cc ctId Nothing . textMsgContent +sendMessage' :: ChatController -> ContactId -> Text -> IO () +sendMessage' cc ctId = sendComposedMessage' cc ctId Nothing . MCText sendComposedMessage :: ChatController -> Contact -> Maybe ChatItemId -> MsgContent -> IO () sendComposedMessage cc = sendComposedMessage' cc . contactId' @@ -83,9 +84,6 @@ deleteMessage cc ct chatItemId = do contactRef :: Contact -> ChatRef contactRef = ChatRef CTDirect . contactId' -textMsgContent :: String -> MsgContent -textMsgContent = MCText . T.pack - printLog :: ChatController -> ChatLogLevel -> String -> IO () printLog cc level s | logLevel (config cc) <= level = putStrLn s diff --git a/src/Simplex/Chat/Bot/KnownContacts.hs b/src/Simplex/Chat/Bot/KnownContacts.hs index 1ea44d49be..4555bb9fee 100644 --- a/src/Simplex/Chat/Bot/KnownContacts.hs +++ b/src/Simplex/Chat/Bot/KnownContacts.hs @@ -18,8 +18,8 @@ data KnownContact = KnownContact } deriving (Eq) -knownContactNames :: [KnownContact] -> String -knownContactNames = T.unpack . T.intercalate ", " . map (("@" <>) . localDisplayName) +knownContactNames :: [KnownContact] -> Text +knownContactNames = T.intercalate ", " . map (("@" <>) . localDisplayName) parseKnownContacts :: ReadM [KnownContact] parseKnownContacts = eitherReader $ parseAll knownContactsP . encodeUtf8 . T.pack diff --git a/tests/Bots/DirectoryTests.hs b/tests/Bots/DirectoryTests.hs index 3a3e9f889f..c50bb8b02d 100644 --- a/tests/Bots/DirectoryTests.hs +++ b/tests/Bots/DirectoryTests.hs @@ -10,7 +10,8 @@ import ChatTests.Utils import Control.Concurrent (forkIO, killThread, threadDelay) import Control.Exception (finally) import Control.Monad (forM_) -import Directory.Events (viewName) +import qualified Data.Text as T +import qualified Directory.Events as DE import Directory.Options import Directory.Service import Directory.Store @@ -27,7 +28,7 @@ import Test.Hspec hiding (it) directoryServiceTests :: SpecWith FilePath directoryServiceTests = do it "should register group" testDirectoryService - it "should suspend and resume group" testSuspendResume + it "should suspend and resume group, send message to owner" testSuspendResume it "should delete group registration" testDeleteGroup it "should change initial member role" testSetRole it "should join found group via link" testJoinGroup @@ -67,6 +68,7 @@ mkDirectoryOpts :: FilePath -> [KnownContact] -> DirectoryOpts mkDirectoryOpts tmp superUsers = DirectoryOpts { coreOptions = testCoreOpts {dbFilePrefix = tmp serviceDbPrefix}, + adminUsers = [], superUsers, directoryLog = Just $ tmp "directory_service.log", serviceName = "SimpleX-Directory", @@ -77,6 +79,9 @@ mkDirectoryOpts tmp superUsers = serviceDbPrefix :: FilePath serviceDbPrefix = "directory_service" +viewName :: String -> String +viewName = T.unpack . DE.viewName . T.pack + testDirectoryService :: HasCallStack => FilePath -> IO () testDirectoryService tmp = withDirectoryService tmp $ \superUser dsLink -> @@ -111,7 +116,7 @@ testDirectoryService tmp = -- putStrLn "*** update profile so that it has link" updateGroupProfile bob welcomeWithLink bob <# "SimpleX-Directory> Thank you! The group link for ID 1 (PSA) is added to the welcome message." - bob <## "You will be notified once the group is added to the directory - it may take up to 24 hours." + bob <## "You will be notified once the group is added to the directory - it may take up to 48 hours." approvalRequested superUser welcomeWithLink (1 :: Int) -- putStrLn "*** update profile so that it still has link" let welcomeWithLink' = "Welcome! " <> welcomeWithLink @@ -139,7 +144,7 @@ testDirectoryService tmp = -- putStrLn "*** update profile so that it has link again" updateGroupProfile bob welcomeWithLink' bob <# "SimpleX-Directory> Thank you! The group link for ID 1 (PSA) is added to the welcome message." - bob <## "You will be notified once the group is added to the directory - it may take up to 24 hours." + bob <## "You will be notified once the group is added to the directory - it may take up to 48 hours." approvalRequested superUser welcomeWithLink' (1 :: Int) superUser #> "@SimpleX-Directory /pending" superUser <# "SimpleX-Directory> > /pending" @@ -207,6 +212,17 @@ testSuspendResume tmp = superUser <## " Group listing resumed!" bob <# "SimpleX-Directory> The group ID 1 (privacy) is listed in the directory again!" groupFound bob "privacy" + superUser #> "@SimpleX-Directory privacy" + groupFoundN_ (Just 1) 2 superUser "privacy" + superUser #> "@SimpleX-Directory /link 1:privacy" + superUser <# "SimpleX-Directory> > /link 1:privacy" + superUser <## " The link to join the group ID 1 (privacy):" + superUser <##. "https://simplex.chat/contact" + superUser <## "New member role: member" + superUser #> "@SimpleX-Directory /owner 1:privacy hello there" + superUser <# "SimpleX-Directory> > /owner 1:privacy hello there" + superUser <## " Forwarded to @bob, the owner of the group ID 1 (privacy)" + bob <# "SimpleX-Directory> hello there" testDeleteGroup :: HasCallStack => FilePath -> IO () testDeleteGroup tmp = @@ -650,7 +666,7 @@ testRegOwnerRemovedLink tmp = bob <## "description changed to:" bob <## welcomeWithLink bob <# "SimpleX-Directory> Thank you! The group link for ID 1 (privacy) is added to the welcome message." - bob <## "You will be notified once the group is added to the directory - it may take up to 24 hours." + bob <## "You will be notified once the group is added to the directory - it may take up to 48 hours." cath <## "bob updated group #privacy:" cath <## "description changed to:" cath <## welcomeWithLink @@ -692,7 +708,7 @@ testAnotherOwnerRemovedLink tmp = bob <## "description changed to:" bob <## (welcomeWithLink <> " - welcome!") bob <# "SimpleX-Directory> Thank you! The group link for ID 1 (privacy) is added to the welcome message." - bob <## "You will be notified once the group is added to the directory - it may take up to 24 hours." + bob <## "You will be notified once the group is added to the directory - it may take up to 48 hours." cath <## "bob updated group #privacy:" cath <## "description changed to:" cath <## (welcomeWithLink <> " - welcome!") @@ -774,7 +790,7 @@ testDuplicateProhibitWhenUpdated tmp = cath ##> "/gp privacy security Security" cath <## "changed to #security (Security)" cath <# "SimpleX-Directory> Thank you! The group link for ID 2 (security) is added to the welcome message." - cath <## "You will be notified once the group is added to the directory - it may take up to 24 hours." + cath <## "You will be notified once the group is added to the directory - it may take up to 48 hours." notifySuperUser superUser cath "security" "Security" welcomeWithLink' 2 approveRegistration superUser cath "security" 2 groupFound bob "security" @@ -1035,7 +1051,7 @@ updateProfileWithLink u n welcomeWithLink ugId = do u <## "description changed to:" u <## welcomeWithLink u <# ("SimpleX-Directory> Thank you! The group link for ID " <> show ugId <> " (" <> n <> ") is added to the welcome message.") - u <## "You will be notified once the group is added to the directory - it may take up to 24 hours." + u <## "You will be notified once the group is added to the directory - it may take up to 48 hours." notifySuperUser :: TestCC -> TestCC -> String -> String -> String -> Int -> IO () notifySuperUser su u n fn welcomeWithLink gId = do @@ -1112,10 +1128,13 @@ groupFoundN count u name = do groupFoundN' count u name groupFoundN' :: Int -> TestCC -> String -> IO () -groupFoundN' count u name = do +groupFoundN' = groupFoundN_ Nothing + +groupFoundN_ :: Maybe Int -> Int -> TestCC -> String -> IO () +groupFoundN_ shownId_ count u name = do u <# ("SimpleX-Directory> > " <> name) u <## " Found 1 group(s)." - u <#. ("SimpleX-Directory> " <> name) + u <#. ("SimpleX-Directory> " <> maybe "" (\gId -> show gId <> ". ") shownId_ <> name) u <## "Welcome message:" u <##. "Link to join the group " u <## (show count <> " members") From 8af54539f66b94d554ee90d8995c5619f399e993 Mon Sep 17 00:00:00 2001 From: sh <37271604+shumvgolove@users.noreply.github.com> Date: Tue, 12 Nov 2024 10:37:12 +0000 Subject: [PATCH 016/167] docs: add control port section (#5164) * docs: add control port section * docs: apply suggestions --- docs/SERVER.md | 81 +++++++++++++++++++++++++++++++++++++++++++-- docs/XFTP-SERVER.md | 69 +++++++++++++++++++++++++++++++++++++- 2 files changed, 146 insertions(+), 4 deletions(-) diff --git a/docs/SERVER.md b/docs/SERVER.md index ce6c466573..9c3f2f619e 100644 --- a/docs/SERVER.md +++ b/docs/SERVER.md @@ -28,7 +28,8 @@ revision: 12.10.2024 - [Documentation](#documentation) - [SMP server address](#smp-server-address) - [Systemd commands](#systemd-commands) - - [Monitoring](#monitoring) + - [Control port](#control-port) + - [Daily statistics](#daily-statistics) - [Updating your SMP server](#updating-your-smp-server) - [Configuring the app to use the server](#configuring-the-app-to-use-the-server) @@ -1079,7 +1080,81 @@ Nov 23 19:23:21 5588ab759e80 smp-server[30878]: not expiring inactive clients Nov 23 19:23:21 5588ab759e80 smp-server[30878]: creating new queues requires password ``` -#### Monitoring +#### Control port + +Enabling control port in the configuration allows administrator to see information about the smp-server in real-time. Additionally, it allows to delete queues for content moderation and see the debug info about the clients, sockets, etc. Enabling the control port requires setting the `admin` and `user` passwords. + +1. Generate two passwords for each user: + + ```sh + tr -dc A-Za-z0-9 + control_port_user_password: + + [TRANSPORT] + control_port: 5224 + ``` + +3. Restart the server: + + ```sh + systemctl restart smp-server + ``` + +To access the control port, use: + +```sh +nc 127.0.0.1 5224 +``` + +or: + +```sh +telnet 127.0.0.1 5224 +``` + +Upon connecting, the control port should print: + +```sh +SMP server control port +'help' for supported commands +``` + +To authenticate, type the following and hit enter. Change the `my_generated_password` with the `user` or `admin` password from the configuration: + +```sh +auth my_generated_password +``` + +Here's the full list of commands, their descriptions and who can access them. + +| Command | Description | Requires `admin` role | +| ---------------- | ------------------------------------------------------------------------------- | -------------------------- | +| `stats` | Real-time statistics. Fields described in [Daily statistics](#daily-statistics) | - | +| `stats-rts` | GHC/Haskell statistics. Can be enabled with `+RTS -T -RTS` option | - | +| `clients` | Clients information. Useful for debugging. | yes | +| `sockets` | General sockets information. | - | +| `socket-threads` | Thread infomation per socket. Useful for debugging. | yes | +| `threads` | Threads information. Useful for debugging. | yes | +| `server-info` | Aggregated server infomation. | - | +| `delete` | Delete known queue. Useful for content moderation. | - | +| `save` | Save queues/messages from memory. | yes | +| `help` | Help menu. | - | +| `quit` | Exit the control port. | - | + +#### Daily statistics You can enable `smp-server` statistics for `Grafana` dashboard by setting value `on` in `/etc/opt/simplex/smp-server.ini`, under `[STORE_LOG]` section in `log_stats:` field. @@ -1089,7 +1164,7 @@ Logs will be stored as `csv` file in `/var/opt/simplex/smp-server-stats.daily.lo fromTime,qCreated,qSecured,qDeleted,msgSent,msgRecv,dayMsgQueues,weekMsgQueues,monthMsgQueues,msgSentNtf,msgRecvNtf,dayCountNtf,weekCountNtf,monthCountNtf,qCount,msgCount,msgExpired,qDeletedNew,qDeletedSecured,pRelays_pRequests,pRelays_pSuccesses,pRelays_pErrorsConnect,pRelays_pErrorsCompat,pRelays_pErrorsOther,pRelaysOwn_pRequests,pRelaysOwn_pSuccesses,pRelaysOwn_pErrorsConnect,pRelaysOwn_pErrorsCompat,pRelaysOwn_pErrorsOther,pMsgFwds_pRequests,pMsgFwds_pSuccesses,pMsgFwds_pErrorsConnect,pMsgFwds_pErrorsCompat,pMsgFwds_pErrorsOther,pMsgFwdsOwn_pRequests,pMsgFwdsOwn_pSuccesses,pMsgFwdsOwn_pErrorsConnect,pMsgFwdsOwn_pErrorsCompat,pMsgFwdsOwn_pErrorsOther,pMsgFwdsRecv,qSub,qSubAuth,qSubDuplicate,qSubProhibited,msgSentAuth,msgSentQuota,msgSentLarge,msgNtfs,msgNtfNoSub,msgNtfLost,qSubNoMsg,msgRecvGet,msgGet,msgGetNoMsg,msgGetAuth,msgGetDuplicate,msgGetProhibited,psSubDaily,psSubWeekly,psSubMonthly,qCount2,ntfCreated,ntfDeleted,ntfSub,ntfSubAuth,ntfSubDuplicate,ntfCount,qDeletedAllB,qSubAllB,qSubEnd,qSubEndB,ntfDeletedB,ntfSubB,msgNtfsB,msgNtfExpired ``` -#### Fields description +**Fields description** | Field number | Field name | Field Description | | ------------- | ---------------------------- | -------------------------- | diff --git a/docs/XFTP-SERVER.md b/docs/XFTP-SERVER.md index a2eb9816e5..88428a0dc3 100644 --- a/docs/XFTP-SERVER.md +++ b/docs/XFTP-SERVER.md @@ -361,7 +361,74 @@ Feb 27 19:21:11 localhost xftp-server[2350]: Listening on port 443... Feb 27 19:21:11 localhost xftp-server[2350]: [INFO 2023-02-27 19:21:11 +0000 src/Simplex/FileTransfer/Server/Env.hs:85] Total / available storage: 64424509440 / 64424509440 ```` -### Monitoring +### Control port + +Enabling control port in the configuration allows administrator to see information about the smp-server in real-time. Additionally, it allows to delete file chunks for content moderation and see the debug info about the clients, sockets, etc. Enabling the control port requires setting the `admin` and `user` passwords. + +1. Generate two passwords for each user: + + ```sh + tr -dc A-Za-z0-9 + control_port_user_password: + + [TRANSPORT] + control_port: 5224 + ``` + +3. Restart the server: + + ```sh + systemctl restart xftp-server + ``` + +To access the control port, use: + +```sh +nc 127.0.0.1 5224 +``` + +or: + +```sh +telnet 127.0.0.1 5224 +``` + +Upon connecting, the control port should print: + +```sh +XFTP server control port +'help' for supported commands +``` + +To authenticate, type the following and hit enter. Change the `my_generated_password` with the `user` or `admin` password from the configuration: + +```sh +auth my_generated_password +``` + +Here's the full list of commands, their descriptions and who can access them. + +| Command | Description | Requires `admin` role | +| ---------------- | ------------------------------------------------------------------------------- | -------------------------- | +| `stats-rts` | GHC/Haskell statistics. Can be enabled with `+RTS -T -RTS` option | - | +| `delete` | Delete known file chunk. Useful for content moderation. | - | +| `help` | Help menu. | - | +| `quit` | Exit the control port. | - | + +### Daily statistics You can enable `xftp-server` statistics for `Grafana` dashboard by setting value `on` in `/etc/opt/simplex-xftp/file-server.ini`, under `[STORE_LOG]` section in `log_stats:` field. From 15bac88ec99ba09ebc116ee5282bf593608c6218 Mon Sep 17 00:00:00 2001 From: Diogo Date: Wed, 13 Nov 2024 09:27:49 +0000 Subject: [PATCH 017/167] desktop, android: user profiles move auth to change actions, show unread counts (#5171) * auth only on change actions for profiles * show notification count in profiles view * auth to hidde profile * save authorized * refactor and icon fix * keep key --- .../common/views/chatlist/UserPicker.kt | 89 +++++++------ .../views/usersettings/UserProfilesView.kt | 119 ++++++++++-------- .../commonMain/resources/MR/base/strings.xml | 2 +- 3 files changed, 121 insertions(+), 89 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt index 2709c7760b..185ec3925f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt @@ -268,22 +268,32 @@ fun UserPicker( painterResource(MR.images.ic_manage_accounts), stringResource(MR.strings.your_chat_profiles), { - doWithAuth( - generalGetString(MR.strings.auth_open_chat_profiles), - generalGetString(MR.strings.auth_log_in_using_credential) - ) { - ModalManager.start.showCustomModal(keyboardCoversBar = false) { close -> - val search = rememberSaveable { mutableStateOf("") } - val profileHidden = rememberSaveable { mutableStateOf(false) } - ModalView( - { close() }, - showSearch = true, - searchAlwaysVisible = true, - onSearchValueChanged = { - search.value = it - }, - content = { UserProfilesView(chatModel, search, profileHidden) }) - } + ModalManager.start.showCustomModal(keyboardCoversBar = false) { close -> + val search = rememberSaveable { mutableStateOf("") } + val profileHidden = rememberSaveable { mutableStateOf(false) } + val authorized = remember { stateGetOrPut("authorized") { false } } + ModalView( + { close() }, + showSearch = true, + searchAlwaysVisible = true, + onSearchValueChanged = { + search.value = it + }, + content = { + UserProfilesView(chatModel, search, profileHidden) { block -> + if (authorized.value) { + block() + } else { + doWithAuth( + generalGetString(MR.strings.auth_open_chat_profiles), + generalGetString(MR.strings.auth_log_in_using_credential) + ) { + authorized.value = true + block() + } + } + } + }) } }, disabled = stopped @@ -412,26 +422,35 @@ fun UserProfilePickerItem( UserProfileRow(u, enabled) if (u.activeUser) { Icon(painterResource(MR.images.ic_done_filled), null, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground) - } else if (u.hidden) { - Icon(painterResource(MR.images.ic_lock), null, Modifier.size(20.dp), tint = MaterialTheme.colors.secondary) - } else if (unreadCount > 0) { - Box( - contentAlignment = Alignment.Center - ) { - Text( - unreadCountStr(unreadCount), - color = Color.White, - fontSize = 10.sp, - modifier = Modifier - .background(MaterialTheme.colors.primaryVariant, shape = CircleShape) - .padding(2.dp) - .badgeLayout() - ) - } - } else if (!u.showNtfs) { - Icon(painterResource(MR.images.ic_notifications_off), null, Modifier.size(20.dp), tint = MaterialTheme.colors.secondary) } else { - Box(Modifier.size(20.dp)) + Row(verticalAlignment = Alignment.CenterVertically) { + if (unreadCount > 0) { + Box( + contentAlignment = Alignment.Center, + ) { + Text( + unreadCountStr(unreadCount), + color = Color.White, + fontSize = 10.sp, + modifier = Modifier + .background(if (u.showNtfs) MaterialTheme.colors.primaryVariant else MaterialTheme.colors.secondary, shape = CircleShape) + .padding(2.dp) + .badgeLayout() + ) + } + + if (u.hidden) { + Spacer(Modifier.width(8.dp)) + Icon(painterResource(MR.images.ic_lock), null, Modifier.size(20.dp), tint = MaterialTheme.colors.secondary) + } + } else if (u.hidden) { + Icon(painterResource(MR.images.ic_lock), null, Modifier.size(20.dp), tint = MaterialTheme.colors.secondary) + } else if (!u.showNtfs) { + Icon(painterResource(MR.images.ic_notifications_off), null, Modifier.size(20.dp), tint = MaterialTheme.colors.secondary) + } else { + Box(Modifier.size(20.dp)) + } + } } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfilesView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfilesView.kt index fa9e709d4b..ad732cd699 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfilesView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfilesView.kt @@ -36,7 +36,7 @@ import dev.icerock.moko.resources.StringResource import kotlinx.coroutines.* @Composable -fun UserProfilesView(m: ChatModel, search: MutableState, profileHidden: MutableState) { +fun UserProfilesView(m: ChatModel, search: MutableState, profileHidden: MutableState, withAuth: (block: () -> Unit) -> Unit) { val searchTextOrPassword = rememberSaveable { search } val users by remember { derivedStateOf { m.users.map { it.user } } } val filteredUsers by remember { derivedStateOf { filteredUsers(m, searchTextOrPassword.value) } } @@ -48,8 +48,10 @@ fun UserProfilesView(m: ChatModel, search: MutableState, profileHidden: showHiddenProfilesNotice = m.controller.appPrefs.showHiddenProfilesNotice, visibleUsersCount = visibleUsersCount(m), addUser = { - ModalManager.center.showModalCloseable { close -> - CreateProfile(m, close) + withAuth { + ModalManager.center.showModalCloseable { close -> + CreateProfile(m, close) + } } }, activateUser = { user -> @@ -64,68 +66,78 @@ fun UserProfilesView(m: ChatModel, search: MutableState, profileHidden: } }, removeUser = { user -> - val text = buildAnnotatedString { - append(generalGetString(MR.strings.users_delete_all_chats_deleted) + "\n\n" + generalGetString(MR.strings.users_delete_profile_for) + " ") - withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { - append(user.displayName) + withAuth { + val text = buildAnnotatedString { + append(generalGetString(MR.strings.users_delete_all_chats_deleted) + "\n\n" + generalGetString(MR.strings.users_delete_profile_for) + " ") + withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { + append(user.displayName) + } + append(":") } - append(":") - } - AlertManager.shared.showAlertDialogButtonsColumn( - title = generalGetString(MR.strings.users_delete_question), - text = text, - buttons = { - Column { - SectionItemView({ - AlertManager.shared.hideAlert() - removeUser(m, user, users, true, searchTextOrPassword.value.trim()) - }) { - Text(stringResource(MR.strings.users_delete_with_connections), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red) - } - SectionItemView({ - AlertManager.shared.hideAlert() - removeUser(m, user, users, false, searchTextOrPassword.value.trim()) - } - ) { - Text(stringResource(MR.strings.users_delete_data_only), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red) + AlertManager.shared.showAlertDialogButtonsColumn( + title = generalGetString(MR.strings.users_delete_question), + text = text, + buttons = { + Column { + SectionItemView({ + AlertManager.shared.hideAlert() + removeUser(m, user, users, true, searchTextOrPassword.value.trim()) + }) { + Text(stringResource(MR.strings.users_delete_with_connections), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red) + } + SectionItemView({ + AlertManager.shared.hideAlert() + removeUser(m, user, users, false, searchTextOrPassword.value.trim()) + } + ) { + Text(stringResource(MR.strings.users_delete_data_only), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red) + } } } - } - ) + ) + } }, unhideUser = { user -> - if (passwordEntryRequired(user, searchTextOrPassword.value)) { - ModalManager.start.showModalCloseable(true) { close -> - ProfileActionView(UserProfileAction.UNHIDE, user) { pwd -> - withBGApi { - setUserPrivacy(m) { m.controller.apiUnhideUser(user, pwd) } - close() + withAuth { + if (passwordEntryRequired(user, searchTextOrPassword.value)) { + ModalManager.start.showModalCloseable(true) { close -> + ProfileActionView(UserProfileAction.UNHIDE, user) { pwd -> + withBGApi { + setUserPrivacy(m) { m.controller.apiUnhideUser(user, pwd) } + close() + } } } + } else { + withBGApi { setUserPrivacy(m) { m.controller.apiUnhideUser(user, searchTextOrPassword.value.trim()) } } } - } else { - withBGApi { setUserPrivacy(m) { m.controller.apiUnhideUser(user, searchTextOrPassword.value.trim()) } } } }, muteUser = { user -> - withBGApi { - setUserPrivacy(m, onSuccess = { - if (m.controller.appPrefs.showMuteProfileAlert.get()) showMuteProfileAlert(m.controller.appPrefs.showMuteProfileAlert) - }) { m.controller.apiMuteUser(user) } + withAuth { + withBGApi { + setUserPrivacy(m, onSuccess = { + if (m.controller.appPrefs.showMuteProfileAlert.get()) showMuteProfileAlert(m.controller.appPrefs.showMuteProfileAlert) + }) { m.controller.apiMuteUser(user) } + } } }, unmuteUser = { user -> - withBGApi { setUserPrivacy(m) { m.controller.apiUnmuteUser(user) } } + withAuth { + withBGApi { setUserPrivacy(m) { m.controller.apiUnmuteUser(user) } } + } }, showHiddenProfile = { user -> - ModalManager.start.showModalCloseable(true) { close -> - HiddenProfileView(m, user) { - profileHidden.value = true - withBGApi { - delay(10_000) - profileHidden.value = false + withAuth { + ModalManager.start.showModalCloseable(true) { close -> + HiddenProfileView(m, user) { + profileHidden.value = true + withBGApi { + delay(10_000) + profileHidden.value = false + } + close() } - close() } } } @@ -138,7 +150,7 @@ fun UserProfilesView(m: ChatModel, search: MutableState, profileHidden: @Composable private fun UserProfilesLayout( users: List, - filteredUsers: List, + filteredUsers: List, searchTextOrPassword: MutableState, profileHidden: MutableState, visibleUsersCount: Int, @@ -195,7 +207,7 @@ private fun UserProfilesLayout( @Composable private fun UserView( - user: User, + userInfo: UserInfo, visibleUsersCount: Int, activateUser: (User) -> Unit, removeUser: (User) -> Unit, @@ -205,7 +217,8 @@ private fun UserView( showHiddenProfile: (User) -> Unit, ) { val showMenu = remember { mutableStateOf(false) } - UserProfilePickerItem(user, onLongClick = { showMenu.value = true }) { + val user = userInfo.user + UserProfilePickerItem(user, onLongClick = { showMenu.value = true }, unreadCount = userInfo.unreadCount) { activateUser(user) } Box(Modifier.padding(horizontal = DEFAULT_PADDING)) { @@ -290,7 +303,7 @@ private fun ProfileActionView(action: UserProfileAction, user: User, doAction: ( } } -fun filteredUsers(m: ChatModel, searchTextOrPassword: String): List { +fun filteredUsers(m: ChatModel, searchTextOrPassword: String): List { val s = searchTextOrPassword.trim() val lower = s.lowercase() return m.users.filter { u -> @@ -299,7 +312,7 @@ fun filteredUsers(m: ChatModel, searchTextOrPassword: String): List { } else { correctPassword(u.user, s) } - }.map { it.user } + } } private fun visibleUsersCount(m: ChatModel): Int = m.users.filter { u -> !u.user.hidden }.size diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 1ab7e3aed2..0ada4d3095 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -267,7 +267,7 @@ Device authentication is disabled. Turning off SimpleX Lock. Stop chat Open chat console - Open chat profiles + Change chat profiles Open migration screen SimpleX Lock not enabled! You can turn on SimpleX Lock via Settings. From 60c37f0d1d03be234e0f40f97044daa48ae30f99 Mon Sep 17 00:00:00 2001 From: Diogo Date: Wed, 13 Nov 2024 11:41:39 +0000 Subject: [PATCH 018/167] ios: user profiles move auth to change actions, show unread counts (#5170) * ios: user profiles move auth to change actions, show unread count per profile * simpler approach and add profile protection * not show muted icon * refactor * not needed * fix * simpler fix * deadline --------- Co-authored-by: Evgeny Poberezkin --- .../Shared/Views/ChatList/UserPicker.swift | 18 ++- .../Views/UserSettings/UserProfilesView.swift | 128 ++++++++++++------ 2 files changed, 98 insertions(+), 48 deletions(-) diff --git a/apps/ios/Shared/Views/ChatList/UserPicker.swift b/apps/ios/Shared/Views/ChatList/UserPicker.swift index cfcfe851f3..dbe10ad997 100644 --- a/apps/ios/Shared/Views/ChatList/UserPicker.swift +++ b/apps/ios/Shared/Views/ChatList/UserPicker.swift @@ -124,7 +124,7 @@ struct UserPicker: View { ZStack(alignment: .topTrailing) { ProfileImage(imageStr: u.user.image, size: size, color: Color(uiColor: .tertiarySystemGroupedBackground)) if (u.unreadCount > 0) { - unreadBadge(u).offset(x: 4, y: -4) + UnreadBadge(userInfo: u).offset(x: 4, y: -4) } } .padding(.trailing, 6) @@ -169,15 +169,21 @@ struct UserPicker: View { } } } - - private func unreadBadge(_ u: UserInfo) -> some View { +} + +struct UnreadBadge: View { + var userInfo: UserInfo + @EnvironmentObject var theme: AppTheme + @Environment(\.dynamicTypeSize) private var userFont: DynamicTypeSize + + var body: some View { let size = dynamicSize(userFont).chatInfoSize - return unreadCountText(u.unreadCount) - .font(userFont <= .xxxLarge ? .caption : .caption2) + unreadCountText(userInfo.unreadCount) + .font(userFont <= .xxxLarge ? .caption : .caption2) .foregroundColor(.white) .padding(.horizontal, dynamicSize(userFont).unreadPadding) .frame(minWidth: size, minHeight: size) - .background(u.user.showNtfs ? theme.colors.primary : theme.colors.secondary) + .background(userInfo.user.showNtfs ? theme.colors.primary : theme.colors.secondary) .cornerRadius(dynamicSize(userFont).unreadCorner) } } diff --git a/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift b/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift index 330ce56e0b..c3dce183bb 100644 --- a/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift +++ b/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift @@ -21,6 +21,7 @@ struct UserProfilesView: View { @State private var profileHidden = false @State private var profileAction: UserProfileAction? @State private var actionPassword = "" + @State private var navigateToProfileCreate = false var trimmedSearchTextOrPassword: String { searchTextOrPassword.trimmingCharacters(in: .whitespaces)} @@ -55,17 +56,6 @@ struct UserProfilesView: View { } var body: some View { - if authorized { - userProfilesView() - } else { - Button(action: runAuth) { Label("Unlock", systemImage: "lock") } - .onAppear(perform: runAuth) - } - } - - private func runAuth() { authorize(NSLocalizedString("Open user profiles", comment: "authentication reason"), $authorized) } - - private func userProfilesView() -> some View { List { if profileHidden { Button { @@ -77,12 +67,14 @@ struct UserProfilesView: View { Section { let users = filteredUsers() let v = ForEach(users) { u in - userView(u.user) + userView(u) } if #available(iOS 16, *) { v.onDelete { indexSet in if let i = indexSet.first { - confirmDeleteUser(users[i].user) + withAuth { + confirmDeleteUser(users[i].user) + } } } } else { @@ -90,12 +82,22 @@ struct UserProfilesView: View { } if trimmedSearchTextOrPassword == "" { - NavigationLink { - CreateProfile() - } label: { + NavigationLink( + destination: CreateProfile(), + isActive: $navigateToProfileCreate + ) { Label("Add profile", systemImage: "plus") + .frame(maxWidth: .infinity, alignment: .leading) + .frame(height: 38) + .padding(.leading, 16).padding(.vertical, 8).padding(.trailing, 32) + .contentShape(Rectangle()) + .onTapGesture { + withAuth { + self.navigateToProfileCreate = true + } + } + .padding(.leading, -16).padding(.vertical, -8).padding(.trailing, -32) } - .frame(height: 38) } } footer: { Text("Tap to activate profile.") @@ -189,7 +191,25 @@ struct UserProfilesView: View { private var visibleUsersCount: Int { m.users.filter({ u in !u.user.hidden }).count } - + + private func withAuth(_ action: @escaping () -> Void) { + if authorized { + action() + } else { + authenticate( + reason: NSLocalizedString("Change user profiles", comment: "authentication reason") + ) { laResult in + switch laResult { + case .success, .unavailable: + authorized = true + AppSheetState.shared.scenePhaseActive = true + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: action) + case .failed: authorized = false + } + } + } + } + private func correctPassword(_ user: User, _ pwd: String) -> Bool { if let ph = user.viewPwdHash { return pwd != "" && chatPasswordHash(pwd, ph.salt) == ph.hash @@ -213,8 +233,10 @@ struct UserProfilesView: View { passwordField settingsRow("trash", color: theme.colors.secondary) { Button("Delete chat profile", role: .destructive) { - profileAction = nil - Task { await removeUser(user, delSMPQueues, viewPwd: actionPassword) } + withAuth { + profileAction = nil + Task { await removeUser(user, delSMPQueues, viewPwd: actionPassword) } + } } .disabled(!actionEnabled(user)) } @@ -231,8 +253,10 @@ struct UserProfilesView: View { passwordField settingsRow("lock.open", color: theme.colors.secondary) { Button("Unhide chat profile") { - profileAction = nil - setUserPrivacy(user) { try await apiUnhideUser(user.userId, viewPwd: actionPassword) } + withAuth{ + profileAction = nil + setUserPrivacy(user) { try await apiUnhideUser(user.userId, viewPwd: actionPassword) } + } } .disabled(!actionEnabled(user)) } @@ -255,11 +279,13 @@ struct UserProfilesView: View { private func deleteModeButton(_ title: LocalizedStringKey, _ delSMPQueues: Bool) -> some View { Button(title, role: .destructive) { - if let user = userToDelete { - if passwordEntryRequired(user) { - profileAction = .deleteUser(user: user, delSMPQueues: delSMPQueues) - } else { - alert = .deleteUser(user: user, delSMPQueues: delSMPQueues) + withAuth { + if let user = userToDelete { + if passwordEntryRequired(user) { + profileAction = .deleteUser(user: user, delSMPQueues: delSMPQueues) + } else { + alert = .deleteUser(user: user, delSMPQueues: delSMPQueues) + } } } } @@ -301,7 +327,8 @@ struct UserProfilesView: View { } } - @ViewBuilder private func userView(_ user: User) -> some View { + @ViewBuilder private func userView(_ userInfo: UserInfo) -> some View { + let user = userInfo.user let v = Button { Task { do { @@ -319,12 +346,19 @@ struct UserProfilesView: View { Spacer() if user.activeUser { Image(systemName: "checkmark").foregroundColor(theme.colors.onBackground) - } else if user.hidden { - Image(systemName: "lock").foregroundColor(theme.colors.secondary) - } else if !user.showNtfs { - Image(systemName: "speaker.slash").foregroundColor(theme.colors.secondary) } else { - Image(systemName: "checkmark").foregroundColor(.clear) + if userInfo.unreadCount > 0 { + UnreadBadge(userInfo: userInfo) + } + if user.hidden { + Image(systemName: "lock").foregroundColor(theme.colors.secondary) + } else if userInfo.unreadCount == 0 { + if !user.showNtfs { + Image(systemName: "speaker.slash").foregroundColor(theme.colors.secondary) + } else { + Image(systemName: "checkmark").foregroundColor(.clear) + } + } } } } @@ -332,30 +366,38 @@ struct UserProfilesView: View { .swipeActions(edge: .leading, allowsFullSwipe: true) { if user.hidden { Button("Unhide") { - if passwordEntryRequired(user) { - profileAction = .unhideUser(user: user) - } else { - setUserPrivacy(user) { try await apiUnhideUser(user.userId, viewPwd: trimmedSearchTextOrPassword) } + withAuth { + if passwordEntryRequired(user) { + profileAction = .unhideUser(user: user) + } else { + setUserPrivacy(user) { try await apiUnhideUser(user.userId, viewPwd: trimmedSearchTextOrPassword) } + } } } .tint(.green) } else { if visibleUsersCount > 1 { Button("Hide") { - selectedUser = user + withAuth { + selectedUser = user + } } .tint(.gray) } Group { if user.showNtfs { Button("Mute") { - setUserPrivacy(user, successAlert: showMuteProfileAlert ? .muteProfileAlert : nil) { - try await apiMuteUser(user.userId) + withAuth { + setUserPrivacy(user, successAlert: showMuteProfileAlert ? .muteProfileAlert : nil) { + try await apiMuteUser(user.userId) + } } } } else { Button("Unmute") { - setUserPrivacy(user) { try await apiUnmuteUser(user.userId) } + withAuth { + setUserPrivacy(user) { try await apiUnmuteUser(user.userId) } + } } } } @@ -367,7 +409,9 @@ struct UserProfilesView: View { } else { v.swipeActions(edge: .trailing, allowsFullSwipe: true) { Button("Delete", role: .destructive) { - confirmDeleteUser(user) + withAuth { + confirmDeleteUser(user) + } } } } From 4d82209a3ac51689265a654a49715336b0d0409b Mon Sep 17 00:00:00 2001 From: Diogo Date: Thu, 14 Nov 2024 08:34:25 +0000 Subject: [PATCH 019/167] core: pagination API to load items around defined or the earliest unread item (#5100) * core: auto increment chat item ids (#5088) * core: auto increment chat item ids * file name * down name * update schema * ignore down migration on schema dump test * fix testDirectMessageDelete test * fix testNotes test * core: initial api support for items around a given item (#5092) * core: initial api support for items around a given item * implementation and tests for local messages * pass entities down * unused * getAllChatItems implementation and tests * pagination for getting chat and tests * remove unused import * group implementation and tests * refactor * order by created at for local and direct chats * core: initial landing api for chat and gaps (#5104) * initial work on initial param for loading chat * support for initial * controller parse * fixed sqls * refactor names * fix ChatLandingSection serialized type * total accuracy on landing section * descriptive view message * foldr * refactor to make landingSection reusable * refactor: use foldr everywhere * propagate search * Revert "propagate search" This reverts commit 01611fd7197c135639db2a869d96d7621ba093ee. * throw when search is sent for initial * gap size wip (needs testing) * final * remove order by * remove index --------- Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> * core: fix initial api latest chat items ordering (#5151) * core: fix one item missing from latest in initial and wrong check (#5153) * core: fix one item missing from latest in initial and wrong check * final fixes and tests * clearer tests * core: remove gaps and make sure page size is always the same (#5163) * remove gaps * consistent pagination size * proper fix and around fix too * optimize * refactor * core: simplify pagination * core: first unread queries (#5174) * core: pagination nav info (#5175) * core: pagination nav info * wip * rework * rework * group, local * fix * rename * fix tests * just --------- Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Co-authored-by: Evgeny Poberezkin --- .../src/Directory/Service.hs | 4 +- simplex-chat.cabal | 1 + src/Simplex/Chat.hs | 14 +- src/Simplex/Chat/Controller.hs | 4 +- src/Simplex/Chat/Messages.hs | 18 +- .../M20241023_chat_item_autoincrement_id.hs | 34 + src/Simplex/Chat/Migrations/chat_schema.sql | 4 +- src/Simplex/Chat/Store/Messages.hs | 682 +++++++++++++----- src/Simplex/Chat/Store/Migrations.hs | 4 +- src/Simplex/Chat/View.hs | 2 +- tests/ChatTests/Direct.hs | 58 +- tests/ChatTests/Groups.hs | 34 + tests/ChatTests/Local.hs | 6 +- tests/SchemaDump.hs | 4 +- 14 files changed, 665 insertions(+), 204 deletions(-) create mode 100644 src/Simplex/Chat/Migrations/M20241023_chat_item_autoincrement_id.hs diff --git a/apps/simplex-directory-service/src/Directory/Service.hs b/apps/simplex-directory-service/src/Directory/Service.hs index c1012f2a0a..2c18d4df27 100644 --- a/apps/simplex-directory-service/src/Directory/Service.hs +++ b/apps/simplex-directory-service/src/Directory/Service.hs @@ -630,7 +630,7 @@ directoryService st DirectoryOpts {adminUsers, superUsers, serviceName, searchRe listGroups count pending = readTVarIO (groupRegs st) >>= \groups -> do grs <- - if pending + if pending then filterM (fmap pendingApproval . readTVarIO . groupRegStatus) groups else pure groups sendReply $ tshow (length grs) <> " registered group(s)" <> (if length grs > count then ", showing the last " <> tshow count else "") @@ -689,7 +689,7 @@ getContact cc ctId = resp <$> sendChatCmd cc (APIGetChat (ChatRef CTDirect ctId) where resp :: ChatResponse -> Maybe Contact resp = \case - CRApiChat _ (AChat SCTDirect Chat {chatInfo = DirectChat ct}) -> Just ct + CRApiChat _ (AChat SCTDirect Chat {chatInfo = DirectChat ct}) _ -> Just ct _ -> Nothing getGroup :: ChatController -> GroupId -> IO (Maybe GroupInfo) diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 96d16f5004..fb7f32faa5 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -150,6 +150,7 @@ library Simplex.Chat.Migrations.M20240920_user_order Simplex.Chat.Migrations.M20241008_indexes Simplex.Chat.Migrations.M20241010_contact_requests_contact_id + Simplex.Chat.Migrations.M20241023_chat_item_autoincrement_id Simplex.Chat.Mobile Simplex.Chat.Mobile.File Simplex.Chat.Mobile.Shared diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 885d4303c8..b74531b9e1 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -735,14 +735,14 @@ processChatCommand' vr = \case APIGetChat (ChatRef cType cId) pagination search -> withUser $ \user -> case cType of -- TODO optimize queries calculating ChatStats, currently they're disabled CTDirect -> do - directChat <- withFastStore (\db -> getDirectChat db vr user cId pagination search) - pure $ CRApiChat user (AChat SCTDirect directChat) + (directChat, navInfo) <- withFastStore (\db -> getDirectChat db vr user cId pagination search) + pure $ CRApiChat user (AChat SCTDirect directChat) navInfo CTGroup -> do - groupChat <- withFastStore (\db -> getGroupChat db vr user cId pagination search) - pure $ CRApiChat user (AChat SCTGroup groupChat) + (groupChat, navInfo) <- withFastStore (\db -> getGroupChat db vr user cId pagination search) + pure $ CRApiChat user (AChat SCTGroup groupChat) navInfo CTLocal -> do - localChat <- withFastStore (\db -> getLocalChat db user cId pagination search) - pure $ CRApiChat user (AChat SCTLocal localChat) + (localChat, navInfo) <- withFastStore (\db -> getLocalChat db user cId pagination search) + pure $ CRApiChat user (AChat SCTLocal localChat) navInfo CTContactRequest -> pure $ chatCmdError (Just user) "not implemented" CTContactConnection -> pure $ chatCmdError (Just user) "not supported" APIGetChatItems pagination search -> withUser $ \user -> do @@ -8301,6 +8301,8 @@ chatCommandP = (CPLast <$ "count=" <*> A.decimal) <|> (CPAfter <$ "after=" <*> A.decimal <* A.space <* "count=" <*> A.decimal) <|> (CPBefore <$ "before=" <*> A.decimal <* A.space <* "count=" <*> A.decimal) + <|> (CPAround <$ "around=" <*> A.decimal <* A.space <* "count=" <*> A.decimal) + <|> (CPInitial <$ "initial=" <*> A.decimal) paginationByTimeP = (PTLast <$ "count=" <*> A.decimal) <|> (PTAfter <$ "after=" <*> strP <* A.space <* "count=" <*> A.decimal) diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index b39b4d7456..4be6086acb 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -572,7 +572,7 @@ data ChatResponse | CRChatSuspended | CRApiChats {user :: User, chats :: [AChat]} | CRChats {chats :: [AChat]} - | CRApiChat {user :: User, chat :: AChat} + | CRApiChat {user :: User, chat :: AChat, navInfo :: Maybe NavigationInfo} | CRChatItems {user :: User, chatName_ :: Maybe ChatName, chatItems :: [AChatItem]} | CRChatItemInfo {user :: User, chatItem :: AChatItem, chatItemInfo :: ChatItemInfo} | CRChatItemId User (Maybe ChatItemId) @@ -839,6 +839,8 @@ data ChatPagination = CPLast Int | CPAfter ChatItemId Int | CPBefore ChatItemId Int + | CPAround ChatItemId Int + | CPInitial Int deriving (Show) data PaginationByTime diff --git a/src/Simplex/Chat/Messages.hs b/src/Simplex/Chat/Messages.hs index 50e68e5bf4..0e3575b64c 100644 --- a/src/Simplex/Chat/Messages.hs +++ b/src/Simplex/Chat/Messages.hs @@ -227,8 +227,8 @@ data CChatItem c = forall d. MsgDirectionI d => CChatItem (SMsgDirection d) (Cha deriving instance Show (CChatItem c) -cchatItemId :: CChatItem c -> ChatItemId -cchatItemId (CChatItem _ ci) = chatItemId' ci +cChatItemId :: CChatItem c -> ChatItemId +cChatItemId (CChatItem _ ci) = chatItemId' ci chatItemId' :: ChatItem c d -> ChatItemId chatItemId' ChatItem {meta = CIMeta {itemId}} = itemId @@ -239,6 +239,12 @@ chatItemTs (CChatItem _ ci) = chatItemTs' ci chatItemTs' :: ChatItem c d -> UTCTime chatItemTs' ChatItem {meta = CIMeta {itemTs}} = itemTs +ciCreatedAt :: CChatItem c -> UTCTime +ciCreatedAt (CChatItem _ ci) = ciCreatedAt' ci + +ciCreatedAt' :: ChatItem c d -> UTCTime +ciCreatedAt' ChatItem {meta = CIMeta {createdAt}} = createdAt + chatItemTimed :: ChatItem c d -> Maybe CITimed chatItemTimed ChatItem {meta = CIMeta {itemTimed}} = itemTimed @@ -318,6 +324,12 @@ data ChatStats = ChatStats } deriving (Show) +data NavigationInfo = NavigationInfo + { afterUnread :: Int, + afterTotal :: Int + } + deriving (Show) + -- | type to show a mix of messages from multiple chats data AChatItem = forall c d. (ChatTypeI c, MsgDirectionI d) => AChatItem (SChatType c) (SMsgDirection d) (ChatInfo c) (ChatItem c d) @@ -1408,6 +1420,8 @@ $(JQ.deriveJSON defaultJSON ''ChatItemInfo) $(JQ.deriveJSON defaultJSON ''ChatStats) +$(JQ.deriveJSON defaultJSON ''NavigationInfo) + instance ChatTypeI c => ToJSON (Chat c) where toJSON = $(JQ.mkToJSON defaultJSON ''Chat) toEncoding = $(JQ.mkToEncoding defaultJSON ''Chat) diff --git a/src/Simplex/Chat/Migrations/M20241023_chat_item_autoincrement_id.hs b/src/Simplex/Chat/Migrations/M20241023_chat_item_autoincrement_id.hs new file mode 100644 index 0000000000..7f1e272026 --- /dev/null +++ b/src/Simplex/Chat/Migrations/M20241023_chat_item_autoincrement_id.hs @@ -0,0 +1,34 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Migrations.M20241023_chat_item_autoincrement_id where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20241023_chat_item_autoincrement_id :: Query +m20241023_chat_item_autoincrement_id = + [sql| +INSERT INTO sqlite_sequence (name, seq) +SELECT 'chat_items', MAX(ROWID) FROM chat_items; + +PRAGMA writable_schema=1; + +UPDATE sqlite_master SET sql = replace(sql, 'INTEGER PRIMARY KEY', 'INTEGER PRIMARY KEY AUTOINCREMENT') +WHERE name = 'chat_items' AND type = 'table'; + +PRAGMA writable_schema=0; +|] + +down_m20241023_chat_item_autoincrement_id :: Query +down_m20241023_chat_item_autoincrement_id = + [sql| +DELETE FROM sqlite_sequence WHERE name = 'chat_items'; + +PRAGMA writable_schema=1; + +UPDATE sqlite_master +SET sql = replace(sql, 'INTEGER PRIMARY KEY AUTOINCREMENT', 'INTEGER PRIMARY KEY') +WHERE name = 'chat_items' AND type = 'table'; + +PRAGMA writable_schema=0; +|] diff --git a/src/Simplex/Chat/Migrations/chat_schema.sql b/src/Simplex/Chat/Migrations/chat_schema.sql index 2619a5c4e5..f16ca6b870 100644 --- a/src/Simplex/Chat/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Migrations/chat_schema.sql @@ -360,7 +360,7 @@ CREATE TABLE pending_group_messages( updated_at TEXT NOT NULL DEFAULT(datetime('now')) ); CREATE TABLE chat_items( - chat_item_id INTEGER PRIMARY KEY, + chat_item_id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, contact_id INTEGER REFERENCES contacts ON DELETE CASCADE, group_id INTEGER REFERENCES groups ON DELETE CASCADE, @@ -399,6 +399,7 @@ CREATE TABLE chat_items( fwd_from_chat_item_id INTEGER REFERENCES chat_items ON DELETE SET NULL, via_proxy INTEGER ); +CREATE TABLE sqlite_sequence(name,seq); CREATE TABLE chat_item_messages( chat_item_id INTEGER NOT NULL REFERENCES chat_items ON DELETE CASCADE, message_id INTEGER NOT NULL UNIQUE REFERENCES messages ON DELETE CASCADE, @@ -429,7 +430,6 @@ CREATE TABLE commands( created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')) ); -CREATE TABLE sqlite_sequence(name,seq); CREATE TABLE settings( settings_id INTEGER PRIMARY KEY, chat_item_ttl INTEGER, diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs index ad77e6c3f1..ab8a52a98a 100644 --- a/src/Simplex/Chat/Store/Messages.hs +++ b/src/Simplex/Chat/Store/Messages.hs @@ -3,6 +3,7 @@ {-# LANGUAGE GADTs #-} {-# LANGUAGE KindSignatures #-} {-# LANGUAGE LambdaCase #-} +{-# LANGUAGE MultiWayIf #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PatternSynonyms #-} @@ -947,37 +948,41 @@ getContactConnectionChatPreviews_ db User {userId} pagination clq = case clq of aChat = AChat SCTContactConnection $ Chat (ContactConnection conn) [] stats in ACPD SCTContactConnection $ ContactConnectionPD updatedAt aChat -getDirectChat :: DB.Connection -> VersionRangeChat -> User -> Int64 -> ChatPagination -> Maybe String -> ExceptT StoreError IO (Chat 'CTDirect) +getDirectChat :: DB.Connection -> VersionRangeChat -> User -> Int64 -> ChatPagination -> Maybe String -> ExceptT StoreError IO (Chat 'CTDirect, Maybe NavigationInfo) getDirectChat db vr user contactId pagination search_ = do let search = fromMaybe "" search_ ct <- getContact db vr user contactId - liftIO $ case pagination of - CPLast count -> getDirectChatLast_ db user ct count search - CPAfter afterId count -> getDirectChatAfter_ db user ct afterId count search - CPBefore beforeId count -> getDirectChatBefore_ db user ct beforeId count search + case pagination of + CPLast count -> liftIO $ (,Nothing) <$> getDirectChatLast_ db user ct count search + CPAfter afterId count -> (,Nothing) <$> getDirectChatAfter_ db user ct afterId count search + CPBefore beforeId count -> (,Nothing) <$> getDirectChatBefore_ db user ct beforeId count search + CPAround aroundId count -> getDirectChatAround_ db user ct aroundId count search + CPInitial count -> do + unless (null search) $ throwError $ SEInternalError "initial chat pagination doesn't support search" + getDirectChatInitial_ db user ct count -- the last items in reverse order (the last item in the conversation is the first in the returned list) getDirectChatLast_ :: DB.Connection -> User -> Contact -> Int -> String -> IO (Chat 'CTDirect) -getDirectChatLast_ db user@User {userId} ct@Contact {contactId} count search = do +getDirectChatLast_ db user ct count search = do let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False} - chatItemIds <- getDirectChatItemIdsLast_ - currentTs <- getCurrentTime - chatItems <- mapM (safeGetDirectItem db user ct currentTs) chatItemIds - pure $ Chat (DirectChat ct) (reverse chatItems) stats - where - getDirectChatItemIdsLast_ :: IO [ChatItemId] - getDirectChatItemIdsLast_ = - map fromOnly - <$> DB.query - db - [sql| - SELECT chat_item_id - FROM chat_items - WHERE user_id = ? AND contact_id = ? AND item_text LIKE '%' || ? || '%' - ORDER BY created_at DESC, chat_item_id DESC - LIMIT ? - |] - (userId, contactId, search, count) + ciIds <- getDirectChatItemIdsLast_ db user ct count search + ts <- getCurrentTime + cis <- mapM (safeGetDirectItem db user ct ts) ciIds + pure $ Chat (DirectChat ct) (reverse cis) stats + +getDirectChatItemIdsLast_ :: DB.Connection -> User -> Contact -> Int -> String -> IO [ChatItemId] +getDirectChatItemIdsLast_ db User {userId} Contact {contactId} count search = + map fromOnly + <$> DB.query + db + [sql| + SELECT chat_item_id + FROM chat_items + WHERE user_id = ? AND contact_id = ? AND item_text LIKE '%' || ? || '%' + ORDER BY created_at DESC, chat_item_id DESC + LIMIT ? + |] + (userId, contactId, search, count) safeGetDirectItem :: DB.Connection -> User -> Contact -> UTCTime -> ChatItemId -> IO (CChatItem 'CTDirect) safeGetDirectItem db user ct currentTs itemId = @@ -1021,82 +1026,181 @@ getDirectChatItemLast db user@User {userId} contactId = do (userId, contactId) getDirectChatItem db user contactId chatItemId -getDirectChatAfter_ :: DB.Connection -> User -> Contact -> ChatItemId -> Int -> String -> IO (Chat 'CTDirect) -getDirectChatAfter_ db user@User {userId} ct@Contact {contactId} afterChatItemId count search = do +getDirectChatAfter_ :: DB.Connection -> User -> Contact -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTDirect) +getDirectChatAfter_ db user ct@Contact {contactId} afterId count search = do let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False} - chatItemIds <- getDirectChatItemIdsAfter_ - currentTs <- getCurrentTime - chatItems <- mapM (safeGetDirectItem db user ct currentTs) chatItemIds - pure $ Chat (DirectChat ct) chatItems stats + afterCI <- getDirectChatItem db user contactId afterId + ciIds <- liftIO $ getDirectCIsAfter_ db user ct afterCI count search + ts <- liftIO getCurrentTime + cis <- liftIO $ mapM (safeGetDirectItem db user ct ts) ciIds + pure $ Chat (DirectChat ct) cis stats + +getDirectCIsAfter_ :: DB.Connection -> User -> Contact -> CChatItem 'CTDirect -> Int -> String -> IO [ChatItemId] +getDirectCIsAfter_ db User {userId} Contact {contactId} afterCI count search = + map fromOnly + <$> DB.query + db + [sql| + SELECT chat_item_id + FROM chat_items + WHERE user_id = ? AND contact_id = ? AND item_text LIKE '%' || ? || '%' + AND (created_at > ? OR (created_at = ? AND chat_item_id > ?)) + ORDER BY created_at ASC, chat_item_id ASC + LIMIT ? + |] + (userId, contactId, search, ciCreatedAt afterCI, ciCreatedAt afterCI, cChatItemId afterCI, count) + +getDirectChatBefore_ :: DB.Connection -> User -> Contact -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTDirect) +getDirectChatBefore_ db user ct@Contact {contactId} beforeId count search = do + let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False} + beforeCI <- getDirectChatItem db user contactId beforeId + ciIds <- liftIO $ getDirectCIsBefore_ db user ct beforeCI count search + ts <- liftIO getCurrentTime + cis <- liftIO $ mapM (safeGetDirectItem db user ct ts) ciIds + pure $ Chat (DirectChat ct) (reverse cis) stats + +getDirectCIsBefore_ :: DB.Connection -> User -> Contact -> CChatItem 'CTDirect -> Int -> String -> IO [ChatItemId] +getDirectCIsBefore_ db User {userId} Contact {contactId} beforeCI count search = + map fromOnly + <$> DB.query + db + [sql| + SELECT chat_item_id + FROM chat_items + WHERE user_id = ? AND contact_id = ? AND item_text LIKE '%' || ? || '%' + AND (created_at < ? OR (created_at = ? AND chat_item_id < ?)) + ORDER BY created_at DESC, chat_item_id DESC + LIMIT ? + |] + (userId, contactId, search, ciCreatedAt beforeCI, ciCreatedAt beforeCI, cChatItemId beforeCI, count) + +getDirectChatAround_ :: DB.Connection -> User -> Contact -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTDirect, Maybe NavigationInfo) +getDirectChatAround_ db user ct aroundId count search = do + stats <- liftIO $ getContactStats_ db user ct + getDirectChatAround' db user ct aroundId count search stats + +getDirectChatAround' :: DB.Connection -> User -> Contact -> ChatItemId -> Int -> String -> ChatStats -> ExceptT StoreError IO (Chat 'CTDirect, Maybe NavigationInfo) +getDirectChatAround' db user ct@Contact {contactId} aroundId count search stats = do + aroundCI <- getDirectChatItem db user contactId aroundId + beforeIds <- liftIO $ getDirectCIsBefore_ db user ct aroundCI count search + afterIds <- liftIO $ getDirectCIsAfter_ db user ct aroundCI count search + ts <- liftIO getCurrentTime + beforeCIs <- liftIO $ mapM (safeGetDirectItem db user ct ts) beforeIds + afterCIs <- liftIO $ mapM (safeGetDirectItem db user ct ts) afterIds + let cis = reverse beforeCIs <> [aroundCI] <> afterCIs + navInfo <- liftIO $ getNavInfo cis + pure (Chat (DirectChat ct) cis stats, Just navInfo) where - getDirectChatItemIdsAfter_ :: IO [ChatItemId] - getDirectChatItemIdsAfter_ = - map fromOnly + getNavInfo cis_ = case cis_ of + [] -> pure $ NavigationInfo 0 0 + cis -> getContactNavInfo_ db user ct (last cis) + +getDirectChatInitial_ :: DB.Connection -> User -> Contact -> Int -> ExceptT StoreError IO (Chat 'CTDirect, Maybe NavigationInfo) +getDirectChatInitial_ db user ct count = do + liftIO (getContactMinUnreadId_ db user ct) >>= \case + Just minUnreadItemId -> do + unreadCount <- liftIO $ getContactUnreadCount_ db user ct + let stats = ChatStats {unreadCount, minUnreadItemId, unreadChat = False} + getDirectChatAround' db user ct minUnreadItemId count "" stats + Nothing -> liftIO $ (,Just $ NavigationInfo 0 0) <$> getDirectChatLast_ db user ct count "" + +getContactStats_ :: DB.Connection -> User -> Contact -> IO ChatStats +getContactStats_ db user ct = do + minUnreadItemId <- fromMaybe 0 <$> getContactMinUnreadId_ db user ct + unreadCount <- getContactUnreadCount_ db user ct + pure ChatStats {unreadCount, minUnreadItemId, unreadChat = False} + +getContactMinUnreadId_ :: DB.Connection -> User -> Contact -> IO (Maybe ChatItemId) +getContactMinUnreadId_ db User {userId} Contact {contactId} = + fmap join . maybeFirstRow fromOnly $ + DB.query + db + [sql| + SELECT chat_item_id + FROM chat_items + WHERE user_id = ? AND contact_id = ? AND item_status = ? + ORDER BY created_at ASC, chat_item_id ASC + LIMIT 1 + |] + (userId, contactId, CISRcvNew) + +getContactUnreadCount_ :: DB.Connection -> User -> Contact -> IO Int +getContactUnreadCount_ db User {userId} Contact {contactId} = + fromOnly . head + <$> DB.query + db + [sql| + SELECT COUNT(1) + FROM chat_items + WHERE user_id = ? AND contact_id = ? AND item_status = ? + |] + (userId, contactId, CISRcvNew) + +getContactNavInfo_ :: DB.Connection -> User -> Contact -> CChatItem 'CTDirect -> IO NavigationInfo +getContactNavInfo_ db User {userId} Contact {contactId} afterCI = do + afterUnread <- getAfterUnreadCount + afterTotal <- getAfterTotalCount + pure NavigationInfo {afterUnread, afterTotal} + where + getAfterUnreadCount :: IO Int + getAfterUnreadCount = + fromOnly . head <$> DB.query db [sql| - SELECT chat_item_id + SELECT COUNT(1) FROM chat_items - WHERE user_id = ? AND contact_id = ? AND item_text LIKE '%' || ? || '%' - AND chat_item_id > ? - ORDER BY created_at ASC, chat_item_id ASC - LIMIT ? + WHERE user_id = ? AND contact_id = ? AND item_status = ? + AND (created_at > ? OR (created_at = ? AND chat_item_id > ?)) |] - (userId, contactId, search, afterChatItemId, count) - -getDirectChatBefore_ :: DB.Connection -> User -> Contact -> ChatItemId -> Int -> String -> IO (Chat 'CTDirect) -getDirectChatBefore_ db user@User {userId} ct@Contact {contactId} beforeChatItemId count search = do - let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False} - chatItemIds <- getDirectChatItemsIdsBefore_ - currentTs <- getCurrentTime - chatItems <- mapM (safeGetDirectItem db user ct currentTs) chatItemIds - pure $ Chat (DirectChat ct) (reverse chatItems) stats - where - getDirectChatItemsIdsBefore_ :: IO [ChatItemId] - getDirectChatItemsIdsBefore_ = - map fromOnly + (userId, contactId, CISRcvNew, ciCreatedAt afterCI, ciCreatedAt afterCI, cChatItemId afterCI) + getAfterTotalCount :: IO Int + getAfterTotalCount = + fromOnly . head <$> DB.query db [sql| - SELECT chat_item_id + SELECT COUNT(1) FROM chat_items - WHERE user_id = ? AND contact_id = ? AND item_text LIKE '%' || ? || '%' - AND chat_item_id < ? - ORDER BY created_at DESC, chat_item_id DESC - LIMIT ? + WHERE user_id = ? AND contact_id = ? + AND (created_at > ? OR (created_at = ? AND chat_item_id > ?)) |] - (userId, contactId, search, beforeChatItemId, count) + (userId, contactId, ciCreatedAt afterCI, ciCreatedAt afterCI, cChatItemId afterCI) -getGroupChat :: DB.Connection -> VersionRangeChat -> User -> Int64 -> ChatPagination -> Maybe String -> ExceptT StoreError IO (Chat 'CTGroup) +getGroupChat :: DB.Connection -> VersionRangeChat -> User -> Int64 -> ChatPagination -> Maybe String -> ExceptT StoreError IO (Chat 'CTGroup, Maybe NavigationInfo) getGroupChat db vr user groupId pagination search_ = do let search = fromMaybe "" search_ g <- getGroupInfo db vr user groupId case pagination of - CPLast count -> liftIO $ getGroupChatLast_ db user g count search - CPAfter afterId count -> getGroupChatAfter_ db user g afterId count search - CPBefore beforeId count -> getGroupChatBefore_ db user g beforeId count search + CPLast count -> liftIO $ (,Nothing) <$> getGroupChatLast_ db user g count search + CPAfter afterId count -> (,Nothing) <$> getGroupChatAfter_ db user g afterId count search + CPBefore beforeId count -> (,Nothing) <$> getGroupChatBefore_ db user g beforeId count search + CPAround aroundId count -> getGroupChatAround_ db user g aroundId count search + CPInitial count -> do + unless (null search) $ throwError $ SEInternalError "initial chat pagination doesn't support search" + getGroupChatInitial_ db user g count getGroupChatLast_ :: DB.Connection -> User -> GroupInfo -> Int -> String -> IO (Chat 'CTGroup) -getGroupChatLast_ db user@User {userId} g@GroupInfo {groupId} count search = do +getGroupChatLast_ db user g count search = do let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False} - chatItemIds <- getGroupChatItemIdsLast_ - currentTs <- getCurrentTime - chatItems <- mapM (safeGetGroupItem db user g currentTs) chatItemIds - pure $ Chat (GroupChat g) (reverse chatItems) stats - where - getGroupChatItemIdsLast_ :: IO [ChatItemId] - getGroupChatItemIdsLast_ = - map fromOnly - <$> DB.query - db - [sql| - SELECT chat_item_id - FROM chat_items - WHERE user_id = ? AND group_id = ? AND item_text LIKE '%' || ? || '%' - ORDER BY item_ts DESC, chat_item_id DESC - LIMIT ? - |] - (userId, groupId, search, count) + ciIds <- getGroupChatItemIdsLast_ db user g count search + ts <- getCurrentTime + cis <- mapM (safeGetGroupItem db user g ts) ciIds + pure $ Chat (GroupChat g) (reverse cis) stats + +getGroupChatItemIdsLast_ :: DB.Connection -> User -> GroupInfo -> Int -> String -> IO [ChatItemId] +getGroupChatItemIdsLast_ db User {userId} GroupInfo {groupId} count search = + map fromOnly + <$> DB.query + db + [sql| + SELECT chat_item_id + FROM chat_items + WHERE user_id = ? AND group_id = ? AND item_text LIKE '%' || ? || '%' + ORDER BY item_ts DESC, chat_item_id DESC + LIMIT ? + |] + (userId, groupId, search, count) safeGetGroupItem :: DB.Connection -> User -> GroupInfo -> UTCTime -> ChatItemId -> IO (CChatItem 'CTGroup) safeGetGroupItem db user g currentTs itemId = @@ -1141,83 +1245,180 @@ getGroupMemberChatItemLast db user@User {userId} groupId groupMemberId = do getGroupChatItem db user groupId chatItemId getGroupChatAfter_ :: DB.Connection -> User -> GroupInfo -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTGroup) -getGroupChatAfter_ db user@User {userId} g@GroupInfo {groupId} afterChatItemId count search = do +getGroupChatAfter_ db user g@GroupInfo {groupId} afterId count search = do let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False} - afterChatItem <- getGroupChatItem db user groupId afterChatItemId - chatItemIds <- liftIO $ getGroupChatItemIdsAfter_ (chatItemTs afterChatItem) - currentTs <- liftIO getCurrentTime - chatItems <- liftIO $ mapM (safeGetGroupItem db user g currentTs) chatItemIds - pure $ Chat (GroupChat g) chatItems stats - where - getGroupChatItemIdsAfter_ :: UTCTime -> IO [ChatItemId] - getGroupChatItemIdsAfter_ afterChatItemTs = - map fromOnly - <$> DB.query - db - [sql| - SELECT chat_item_id - FROM chat_items - WHERE user_id = ? AND group_id = ? AND item_text LIKE '%' || ? || '%' - AND (item_ts > ? OR (item_ts = ? AND chat_item_id > ?)) - ORDER BY item_ts ASC, chat_item_id ASC - LIMIT ? - |] - (userId, groupId, search, afterChatItemTs, afterChatItemTs, afterChatItemId, count) + afterCI <- getGroupChatItem db user groupId afterId + ciIds <- liftIO $ getGroupCIsAfter_ db user g afterCI count search + ts <- liftIO getCurrentTime + cis <- liftIO $ mapM (safeGetGroupItem db user g ts) ciIds + pure $ Chat (GroupChat g) cis stats + +getGroupCIsAfter_ :: DB.Connection -> User -> GroupInfo -> CChatItem 'CTGroup -> Int -> String -> IO [ChatItemId] +getGroupCIsAfter_ db User {userId} GroupInfo {groupId} afterCI count search = + map fromOnly + <$> DB.query + db + [sql| + SELECT chat_item_id + FROM chat_items + WHERE user_id = ? AND group_id = ? AND item_text LIKE '%' || ? || '%' + AND (item_ts > ? OR (item_ts = ? AND chat_item_id > ?)) + ORDER BY item_ts ASC, chat_item_id ASC + LIMIT ? + |] + (userId, groupId, search, chatItemTs afterCI, chatItemTs afterCI, cChatItemId afterCI, count) getGroupChatBefore_ :: DB.Connection -> User -> GroupInfo -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTGroup) -getGroupChatBefore_ db user@User {userId} g@GroupInfo {groupId} beforeChatItemId count search = do +getGroupChatBefore_ db user g@GroupInfo {groupId} beforeId count search = do let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False} - beforeChatItem <- getGroupChatItem db user groupId beforeChatItemId - chatItemIds <- liftIO $ getGroupChatItemIdsBefore_ (chatItemTs beforeChatItem) - currentTs <- liftIO getCurrentTime - chatItems <- liftIO $ mapM (safeGetGroupItem db user g currentTs) chatItemIds - pure $ Chat (GroupChat g) (reverse chatItems) stats + beforeCI <- getGroupChatItem db user groupId beforeId + ciIds <- liftIO $ getGroupCIsBefore_ db user g beforeCI count search + ts <- liftIO getCurrentTime + cis <- liftIO $ mapM (safeGetGroupItem db user g ts) ciIds + pure $ Chat (GroupChat g) (reverse cis) stats + +getGroupCIsBefore_ :: DB.Connection -> User -> GroupInfo -> CChatItem 'CTGroup -> Int -> String -> IO [ChatItemId] +getGroupCIsBefore_ db User {userId} GroupInfo {groupId} beforeCI count search = + map fromOnly + <$> DB.query + db + [sql| + SELECT chat_item_id + FROM chat_items + WHERE user_id = ? AND group_id = ? AND item_text LIKE '%' || ? || '%' + AND (item_ts < ? OR (item_ts = ? AND chat_item_id < ?)) + ORDER BY item_ts DESC, chat_item_id DESC + LIMIT ? + |] + (userId, groupId, search, chatItemTs beforeCI, chatItemTs beforeCI, cChatItemId beforeCI, count) + +getGroupChatAround_ :: DB.Connection -> User -> GroupInfo -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTGroup, Maybe NavigationInfo) +getGroupChatAround_ db user g aroundId count search = do + stats <- liftIO $ getGroupStats_ db user g + getGroupChatAround' db user g aroundId count search stats + +getGroupChatAround' :: DB.Connection -> User -> GroupInfo -> ChatItemId -> Int -> String -> ChatStats -> ExceptT StoreError IO (Chat 'CTGroup, Maybe NavigationInfo) +getGroupChatAround' db user g@GroupInfo {groupId} aroundId count search stats = do + aroundCI <- getGroupChatItem db user groupId aroundId + beforeIds <- liftIO $ getGroupCIsBefore_ db user g aroundCI count search + afterIds <- liftIO $ getGroupCIsAfter_ db user g aroundCI count search + ts <- liftIO getCurrentTime + beforeCIs <- liftIO $ mapM (safeGetGroupItem db user g ts) beforeIds + afterCIs <- liftIO $ mapM (safeGetGroupItem db user g ts) afterIds + let cis = reverse beforeCIs <> [aroundCI] <> afterCIs + navInfo <- liftIO $ getNavInfo cis + pure (Chat (GroupChat g) cis stats, Just navInfo) where - getGroupChatItemIdsBefore_ :: UTCTime -> IO [ChatItemId] - getGroupChatItemIdsBefore_ beforeChatItemTs = - map fromOnly + getNavInfo cis_ = case cis_ of + [] -> pure $ NavigationInfo 0 0 + cis -> getGroupNavInfo_ db user g (last cis) + +getGroupChatInitial_ :: DB.Connection -> User -> GroupInfo -> Int -> ExceptT StoreError IO (Chat 'CTGroup, Maybe NavigationInfo) +getGroupChatInitial_ db user g count = + liftIO (getGroupMinUnreadId_ db user g) >>= \case + Just minUnreadItemId -> do + unreadCount <- liftIO $ getGroupUnreadCount_ db user g + let stats = ChatStats {unreadCount, minUnreadItemId, unreadChat = False} + getGroupChatAround' db user g minUnreadItemId count "" stats + Nothing -> liftIO $ (,Just $ NavigationInfo 0 0) <$> getGroupChatLast_ db user g count "" + +getGroupStats_ :: DB.Connection -> User -> GroupInfo -> IO ChatStats +getGroupStats_ db user g = do + minUnreadItemId <- fromMaybe 0 <$> getGroupMinUnreadId_ db user g + unreadCount <- getGroupUnreadCount_ db user g + pure ChatStats {unreadCount, minUnreadItemId, unreadChat = False} + +getGroupMinUnreadId_ :: DB.Connection -> User -> GroupInfo -> IO (Maybe ChatItemId) +getGroupMinUnreadId_ db User {userId} GroupInfo {groupId} = + fmap join . maybeFirstRow fromOnly $ + DB.query + db + [sql| + SELECT chat_item_id + FROM chat_items + WHERE user_id = ? AND group_id = ? AND item_status = ? + ORDER BY item_ts ASC, chat_item_id ASC + LIMIT 1 + |] + (userId, groupId, CISRcvNew) + +getGroupUnreadCount_ :: DB.Connection -> User -> GroupInfo -> IO Int +getGroupUnreadCount_ db User {userId} GroupInfo {groupId} = + fromOnly . head + <$> DB.query + db + [sql| + SELECT COUNT(1) + FROM chat_items + WHERE user_id = ? AND group_id = ? AND item_status = ? + |] + (userId, groupId, CISRcvNew) + +getGroupNavInfo_ :: DB.Connection -> User -> GroupInfo -> CChatItem 'CTGroup -> IO NavigationInfo +getGroupNavInfo_ db User {userId} GroupInfo {groupId} afterCI = do + afterUnread <- getAfterUnreadCount + afterTotal <- getAfterTotalCount + pure NavigationInfo {afterUnread, afterTotal} + where + getAfterUnreadCount :: IO Int + getAfterUnreadCount = + fromOnly . head <$> DB.query db [sql| - SELECT chat_item_id + SELECT COUNT(1) FROM chat_items - WHERE user_id = ? AND group_id = ? AND item_text LIKE '%' || ? || '%' - AND (item_ts < ? OR (item_ts = ? AND chat_item_id < ?)) - ORDER BY item_ts DESC, chat_item_id DESC - LIMIT ? + WHERE user_id = ? AND group_id = ? AND item_status = ? + AND (item_ts > ? OR (item_ts = ? AND chat_item_id > ?)) |] - (userId, groupId, search, beforeChatItemTs, beforeChatItemTs, beforeChatItemId, count) + (userId, groupId, CISRcvNew, chatItemTs afterCI, chatItemTs afterCI, cChatItemId afterCI) + getAfterTotalCount :: IO Int + getAfterTotalCount = + fromOnly . head + <$> DB.query + db + [sql| + SELECT COUNT(1) + FROM chat_items + WHERE user_id = ? AND group_id = ? + AND (item_ts > ? OR (item_ts = ? AND chat_item_id > ?)) + |] + (userId, groupId, chatItemTs afterCI, chatItemTs afterCI, cChatItemId afterCI) -getLocalChat :: DB.Connection -> User -> Int64 -> ChatPagination -> Maybe String -> ExceptT StoreError IO (Chat 'CTLocal) +getLocalChat :: DB.Connection -> User -> Int64 -> ChatPagination -> Maybe String -> ExceptT StoreError IO (Chat 'CTLocal, Maybe NavigationInfo) getLocalChat db user folderId pagination search_ = do let search = fromMaybe "" search_ nf <- getNoteFolder db user folderId - liftIO $ case pagination of - CPLast count -> getLocalChatLast_ db user nf count search - CPAfter afterId count -> getLocalChatAfter_ db user nf afterId count search - CPBefore beforeId count -> getLocalChatBefore_ db user nf beforeId count search + case pagination of + CPLast count -> liftIO $ (,Nothing) <$> getLocalChatLast_ db user nf count search + CPAfter afterId count -> (,Nothing) <$> getLocalChatAfter_ db user nf afterId count search + CPBefore beforeId count -> (,Nothing) <$> getLocalChatBefore_ db user nf beforeId count search + CPAround aroundId count -> getLocalChatAround_ db user nf aroundId count search + CPInitial count -> do + unless (null search) $ throwError $ SEInternalError "initial chat pagination doesn't support search" + getLocalChatInitial_ db user nf count getLocalChatLast_ :: DB.Connection -> User -> NoteFolder -> Int -> String -> IO (Chat 'CTLocal) -getLocalChatLast_ db user@User {userId} nf@NoteFolder {noteFolderId} count search = do +getLocalChatLast_ db user nf count search = do let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False} - chatItemIds <- getLocalChatItemIdsLast_ - currentTs <- getCurrentTime - chatItems <- mapM (safeGetLocalItem db user nf currentTs) chatItemIds - pure $ Chat (LocalChat nf) (reverse chatItems) stats - where - getLocalChatItemIdsLast_ :: IO [ChatItemId] - getLocalChatItemIdsLast_ = - map fromOnly - <$> DB.query - db - [sql| - SELECT chat_item_id - FROM chat_items - WHERE user_id = ? AND note_folder_id = ? AND item_text LIKE '%' || ? || '%' - ORDER BY created_at DESC, chat_item_id DESC - LIMIT ? - |] - (userId, noteFolderId, search, count) + ciIds <- getLocalChatItemIdsLast_ db user nf count search + ts <- getCurrentTime + cis <- mapM (safeGetLocalItem db user nf ts) ciIds + pure $ Chat (LocalChat nf) (reverse cis) stats + +getLocalChatItemIdsLast_ :: DB.Connection -> User -> NoteFolder -> Int -> String -> IO [ChatItemId] +getLocalChatItemIdsLast_ db User {userId} NoteFolder {noteFolderId} count search = + map fromOnly + <$> DB.query + db + [sql| + SELECT chat_item_id + FROM chat_items + WHERE user_id = ? AND note_folder_id = ? AND item_text LIKE '%' || ? || '%' + ORDER BY created_at DESC, chat_item_id DESC + LIMIT ? + |] + (userId, noteFolderId, search, count) safeGetLocalItem :: DB.Connection -> User -> NoteFolder -> UTCTime -> ChatItemId -> IO (CChatItem 'CTLocal) safeGetLocalItem db user NoteFolder {noteFolderId} currentTs itemId = @@ -1245,51 +1446,146 @@ safeToLocalItem currentTs itemId = \case file = Nothing } -getLocalChatAfter_ :: DB.Connection -> User -> NoteFolder -> ChatItemId -> Int -> String -> IO (Chat 'CTLocal) -getLocalChatAfter_ db user@User {userId} nf@NoteFolder {noteFolderId} afterChatItemId count search = do +getLocalChatAfter_ :: DB.Connection -> User -> NoteFolder -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTLocal) +getLocalChatAfter_ db user nf@NoteFolder {noteFolderId} afterId count search = do let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False} - chatItemIds <- getLocalChatItemIdsAfter_ - currentTs <- getCurrentTime - chatItems <- mapM (safeGetLocalItem db user nf currentTs) chatItemIds - pure $ Chat (LocalChat nf) chatItems stats - where - getLocalChatItemIdsAfter_ :: IO [ChatItemId] - getLocalChatItemIdsAfter_ = - map fromOnly - <$> DB.query - db - [sql| - SELECT chat_item_id - FROM chat_items - WHERE user_id = ? AND note_folder_id = ? AND item_text LIKE '%' || ? || '%' - AND chat_item_id > ? - ORDER BY created_at ASC, chat_item_id ASC - LIMIT ? - |] - (userId, noteFolderId, search, afterChatItemId, count) + afterCI <- getLocalChatItem db user noteFolderId afterId + ciIds <- liftIO $ getLocalCIsAfter_ db user nf afterCI count search + ts <- liftIO getCurrentTime + cis <- liftIO $ mapM (safeGetLocalItem db user nf ts) ciIds + pure $ Chat (LocalChat nf) cis stats -getLocalChatBefore_ :: DB.Connection -> User -> NoteFolder -> ChatItemId -> Int -> String -> IO (Chat 'CTLocal) -getLocalChatBefore_ db user@User {userId} nf@NoteFolder {noteFolderId} beforeChatItemId count search = do +getLocalCIsAfter_ :: DB.Connection -> User -> NoteFolder -> CChatItem 'CTLocal -> Int -> String -> IO [ChatItemId] +getLocalCIsAfter_ db User {userId} NoteFolder {noteFolderId} afterCI count search = + map fromOnly + <$> DB.query + db + [sql| + SELECT chat_item_id + FROM chat_items + WHERE user_id = ? AND note_folder_id = ? AND item_text LIKE '%' || ? || '%' + AND (created_at > ? OR (created_at = ? AND chat_item_id > ?)) + ORDER BY created_at ASC, chat_item_id ASC + LIMIT ? + |] + (userId, noteFolderId, search, ciCreatedAt afterCI, ciCreatedAt afterCI, cChatItemId afterCI, count) + +getLocalChatBefore_ :: DB.Connection -> User -> NoteFolder -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTLocal) +getLocalChatBefore_ db user nf@NoteFolder {noteFolderId} beforeId count search = do let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False} - chatItemIds <- getLocalChatItemIdsBefore_ - currentTs <- getCurrentTime - chatItems <- mapM (safeGetLocalItem db user nf currentTs) chatItemIds - pure $ Chat (LocalChat nf) (reverse chatItems) stats + beforeCI <- getLocalChatItem db user noteFolderId beforeId + ciIds <- liftIO $ getLocalCIsBefore_ db user nf beforeCI count search + ts <- liftIO getCurrentTime + cis <- liftIO $ mapM (safeGetLocalItem db user nf ts) ciIds + pure $ Chat (LocalChat nf) (reverse cis) stats + +getLocalCIsBefore_ :: DB.Connection -> User -> NoteFolder -> CChatItem 'CTLocal -> Int -> String -> IO [ChatItemId] +getLocalCIsBefore_ db User {userId} NoteFolder {noteFolderId} beforeCI count search = + map fromOnly + <$> DB.query + db + [sql| + SELECT chat_item_id + FROM chat_items + WHERE user_id = ? AND note_folder_id = ? AND item_text LIKE '%' || ? || '%' + AND (created_at < ? OR (created_at = ? AND chat_item_id < ?)) + ORDER BY created_at DESC, chat_item_id DESC + LIMIT ? + |] + (userId, noteFolderId, search, ciCreatedAt beforeCI, ciCreatedAt beforeCI, cChatItemId beforeCI, count) + +getLocalChatAround_ :: DB.Connection -> User -> NoteFolder -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTLocal, Maybe NavigationInfo) +getLocalChatAround_ db user nf aroundId count search = do + stats <- liftIO $ getLocalStats_ db user nf + getLocalChatAround' db user nf aroundId count search stats + +getLocalChatAround' :: DB.Connection -> User -> NoteFolder -> ChatItemId -> Int -> String -> ChatStats -> ExceptT StoreError IO (Chat 'CTLocal, Maybe NavigationInfo) +getLocalChatAround' db user nf@NoteFolder {noteFolderId} aroundId count search stats = do + aroundCI <- getLocalChatItem db user noteFolderId aroundId + beforeIds <- liftIO $ getLocalCIsBefore_ db user nf aroundCI count search + afterIds <- liftIO $ getLocalCIsAfter_ db user nf aroundCI count search + ts <- liftIO getCurrentTime + beforeCIs <- liftIO $ mapM (safeGetLocalItem db user nf ts) beforeIds + afterCIs <- liftIO $ mapM (safeGetLocalItem db user nf ts) afterIds + let cis = reverse beforeCIs <> [aroundCI] <> afterCIs + navInfo <- liftIO $ getNavInfo cis + pure (Chat (LocalChat nf) cis stats, Just navInfo) where - getLocalChatItemIdsBefore_ :: IO [ChatItemId] - getLocalChatItemIdsBefore_ = - map fromOnly + getNavInfo cis_ = case cis_ of + [] -> pure $ NavigationInfo 0 0 + cis -> getLocalNavInfo_ db user nf (last cis) + +getLocalChatInitial_ :: DB.Connection -> User -> NoteFolder -> Int -> ExceptT StoreError IO (Chat 'CTLocal, Maybe NavigationInfo) +getLocalChatInitial_ db user nf count = do + liftIO (getLocalMinUnreadId_ db user nf) >>= \case + Just minUnreadItemId -> do + unreadCount <- liftIO $ getLocalUnreadCount_ db user nf + let stats = ChatStats {unreadCount, minUnreadItemId, unreadChat = False} + getLocalChatAround' db user nf minUnreadItemId count "" stats + Nothing -> liftIO $ (,Just $ NavigationInfo 0 0) <$> getLocalChatLast_ db user nf count "" + +getLocalStats_ :: DB.Connection -> User -> NoteFolder -> IO ChatStats +getLocalStats_ db user nf = do + minUnreadItemId <- fromMaybe 0 <$> getLocalMinUnreadId_ db user nf + unreadCount <- getLocalUnreadCount_ db user nf + pure ChatStats {unreadCount, minUnreadItemId, unreadChat = False} + +getLocalMinUnreadId_ :: DB.Connection -> User -> NoteFolder -> IO (Maybe ChatItemId) +getLocalMinUnreadId_ db User {userId} NoteFolder {noteFolderId} = + fmap join . maybeFirstRow fromOnly $ + DB.query + db + [sql| + SELECT chat_item_id + FROM chat_items + WHERE user_id = ? AND note_folder_id = ? AND item_status = ? + ORDER BY created_at ASC, chat_item_id ASC + LIMIT 1 + |] + (userId, noteFolderId, CISRcvNew) + +getLocalUnreadCount_ :: DB.Connection -> User -> NoteFolder -> IO Int +getLocalUnreadCount_ db User {userId} NoteFolder {noteFolderId} = + fromOnly . head + <$> DB.query + db + [sql| + SELECT COUNT(1) + FROM chat_items + WHERE user_id = ? AND note_folder_id = ? AND item_status = ? + |] + (userId, noteFolderId, CISRcvNew) + +getLocalNavInfo_ :: DB.Connection -> User -> NoteFolder -> CChatItem 'CTLocal -> IO NavigationInfo +getLocalNavInfo_ db User {userId} NoteFolder {noteFolderId} afterCI = do + afterUnread <- getAfterUnreadCount + afterTotal <- getAfterTotalCount + pure NavigationInfo {afterUnread, afterTotal} + where + getAfterUnreadCount :: IO Int + getAfterUnreadCount = + fromOnly . head <$> DB.query db [sql| - SELECT chat_item_id + SELECT COUNT(1) FROM chat_items - WHERE user_id = ? AND note_folder_id = ? AND item_text LIKE '%' || ? || '%' - AND chat_item_id < ? - ORDER BY created_at DESC, chat_item_id DESC - LIMIT ? + WHERE user_id = ? AND note_folder_id = ? AND item_status = ? + AND (created_at > ? OR (created_at = ? AND chat_item_id > ?)) |] - (userId, noteFolderId, search, beforeChatItemId, count) + (userId, noteFolderId, CISRcvNew, ciCreatedAt afterCI, ciCreatedAt afterCI, cChatItemId afterCI) + getAfterTotalCount :: IO Int + getAfterTotalCount = + fromOnly . head + <$> DB.query + db + [sql| + SELECT COUNT(1) + FROM chat_items + WHERE user_id = ? AND note_folder_id = ? + AND (created_at > ? OR (created_at = ? AND chat_item_id > ?)) + |] + (userId, noteFolderId, ciCreatedAt afterCI, ciCreatedAt afterCI, cChatItemId afterCI) toChatItemRef :: (ChatItemId, Maybe Int64, Maybe Int64, Maybe Int64) -> Either StoreError (ChatRef, ChatItemId) toChatItemRef = \case @@ -1581,6 +1877,12 @@ getAllChatItems db vr user@User {userId} pagination search_ = do CPLast count -> liftIO $ getAllChatItemsLast_ count CPAfter afterId count -> liftIO . getAllChatItemsAfter_ afterId count . aChatItemTs =<< getAChatItem_ afterId CPBefore beforeId count -> liftIO . getAllChatItemsBefore_ beforeId count . aChatItemTs =<< getAChatItem_ beforeId + CPAround aroundId count -> liftIO . getAllChatItemsAround_ aroundId count . aChatItemTs =<< getAChatItem_ aroundId + CPInitial count -> do + unless (null search) $ throwError $ SEInternalError "initial chat pagination doesn't support search" + liftIO getFirstUnreadItemId_ >>= \case + Just itemId -> liftIO . getAllChatItemsAround_ itemId count . aChatItemTs =<< getAChatItem_ itemId + Nothing -> liftIO $ getAllChatItemsLast_ count mapM (uncurry (getAChatItem db vr user)) itemRefs where search = fromMaybe "" search_ @@ -1624,6 +1926,30 @@ getAllChatItems db vr user@User {userId} pagination search_ = do LIMIT ? |] (userId, search, beforeTs, beforeTs, beforeId, count) + getChatItem chatId = + DB.query + db + [sql| + SELECT chat_item_id, contact_id, group_id, note_folder_id + FROM chat_items + WHERE chat_item_id = ? + |] + (Only chatId) + getAllChatItemsAround_ aroundId count aroundTs = do + itemsBefore <- getAllChatItemsBefore_ aroundId count aroundTs + item <- getChatItem aroundId + itemsAfter <- getAllChatItemsAfter_ aroundId count aroundTs + pure $ itemsBefore <> item <> itemsAfter + getFirstUnreadItemId_ = + fmap join . maybeFirstRow fromOnly $ + DB.query + db + [sql| + SELECT MIN(chat_item_id) + FROM chat_items + WHERE user_id = ? AND item_status = ? + |] + (userId, CISRcvNew) getChatItemIdsByAgentMsgId :: DB.Connection -> Int64 -> AgentMsgId -> IO [ChatItemId] getChatItemIdsByAgentMsgId db connId msgId = @@ -2631,9 +2957,9 @@ getGroupSndStatusCounts db itemId = getGroupHistoryItems :: DB.Connection -> User -> GroupInfo -> Int -> IO [Either StoreError (CChatItem 'CTGroup)] getGroupHistoryItems db user@User {userId} GroupInfo {groupId} count = do - chatItemIds <- getLastItemIds_ + ciIds <- getLastItemIds_ -- use getGroupCIWithReactions to read reactions data - reverse <$> mapM (runExceptT . getGroupChatItem db user groupId) chatItemIds + reverse <$> mapM (runExceptT . getGroupChatItem db user groupId) ciIds where getLastItemIds_ :: IO [ChatItemId] getLastItemIds_ = diff --git a/src/Simplex/Chat/Store/Migrations.hs b/src/Simplex/Chat/Store/Migrations.hs index e2d12e78d7..2444078a33 100644 --- a/src/Simplex/Chat/Store/Migrations.hs +++ b/src/Simplex/Chat/Store/Migrations.hs @@ -114,6 +114,7 @@ import Simplex.Chat.Migrations.M20240827_calls_uuid import Simplex.Chat.Migrations.M20240920_user_order import Simplex.Chat.Migrations.M20241008_indexes import Simplex.Chat.Migrations.M20241010_contact_requests_contact_id +import Simplex.Chat.Migrations.M20241023_chat_item_autoincrement_id import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..)) schemaMigrations :: [(String, Query, Maybe Query)] @@ -227,7 +228,8 @@ schemaMigrations = ("20240827_calls_uuid", m20240827_calls_uuid, Just down_m20240827_calls_uuid), ("20240920_user_order", m20240920_user_order, Just down_m20240920_user_order), ("20241008_indexes", m20241008_indexes, Just down_m20241008_indexes), - ("20241010_contact_requests_contact_id", m20241010_contact_requests_contact_id, Just down_m20241010_contact_requests_contact_id) + ("20241010_contact_requests_contact_id", m20241010_contact_requests_contact_id, Just down_m20241010_contact_requests_contact_id), + ("20241023_chat_item_autoincrement_id", m20241023_chat_item_autoincrement_id, Just down_m20241023_chat_item_autoincrement_id) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index ade36476c7..8ae74e8961 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -93,7 +93,7 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe CRChatSuspended -> ["chat suspended"] CRApiChats u chats -> ttyUser u $ if testView then testViewChats chats else [viewJSON chats] CRChats chats -> viewChats ts tz chats - CRApiChat u chat -> ttyUser u $ if testView then testViewChat chat else [viewJSON chat] + CRApiChat u chat _ -> ttyUser u $ if testView then testViewChat chat else [viewJSON chat] CRApiParsedMarkdown ft -> [viewJSON ft] CRUserProtoServers u userServers -> ttyUser u $ viewUserServers userServers testView CRServerTestResult u srv testFailure -> ttyUser u $ viewServerTestResult srv testFailure diff --git a/tests/ChatTests/Direct.hs b/tests/ChatTests/Direct.hs index 8971e8d22d..8756657e59 100644 --- a/tests/ChatTests/Direct.hs +++ b/tests/ChatTests/Direct.hs @@ -66,6 +66,7 @@ chatDirectTests = do it "repeat AUTH errors disable contact" testRepeatAuthErrorsDisableContact it "should send multiline message" testMultilineMessage it "send large message" testLargeMessage + it "initial chat pagination" testChatPaginationInitial describe "batch send messages" $ do it "send multiple messages api" testSendMulti it "send multiple timed messages" testSendMultiTimed @@ -123,7 +124,7 @@ chatDirectTests = do it "chat items only expire for users who configured expiration" testEnableCIExpirationOnlyForOneUser it "disabling chat item expiration doesn't disable it for other users" testDisableCIExpirationOnlyForOneUser it "both users have configured timed messages with contacts, messages expire, restart" testUsersTimedMessages - it "user profile privacy: hide profiles and notificaitons" testUserPrivacy + it "user profile privacy: hide profiles and notifications" testUserPrivacy describe "settings" $ do it "set chat item expiration TTL" testSetChatItemTTL it "save/get app settings" testAppSettings @@ -210,6 +211,7 @@ testAddContact = versionTestMatrix2 runTestAddContact -- pagination alice #$> ("/_get chat @2 after=" <> itemId 1 <> " count=100", chat, [(0, "hello there"), (0, "how are you?")]) alice #$> ("/_get chat @2 before=" <> itemId 2 <> " count=100", chat, features <> [(1, "hello there 🙂")]) + alice #$> ("/_get chat @2 around=" <> itemId 2 <> " count=2", chat, [(0, "Audio/video calls: enabled"), (1, "hello there 🙂"), (0, "hello there"), (0, "how are you?")]) -- search alice #$> ("/_get chat @2 count=100 search=ello ther", chat, [(1, "hello there 🙂"), (0, "hello there")]) -- read messages @@ -360,6 +362,36 @@ testMarkReadDirect = testChat2 aliceProfile bobProfile $ \alice bob -> do let itemIds = intercalate "," $ map show [i - 3 .. i] bob #$> ("/_read chat items @2 " <> itemIds, id, "ok") +testChatPaginationInitial :: HasCallStack => FilePath -> IO () +testChatPaginationInitial = testChatOpts2 opts aliceProfile bobProfile $ \alice bob -> do + connectUsers alice bob + -- Wait, otherwise ids are going to be wrong. + threadDelay 1000000 + + -- Send messages from alice to bob + forM_ ([1 .. 10] :: [Int]) $ \n -> alice #> ("@bob " <> show n) + + -- Bob receives the messages. + forM_ ([1 .. 10] :: [Int]) $ \n -> bob <# ("alice> " <> show n) + + -- All messages are unread for bob, should return area around unread + bob #$> ("/_get chat @2 initial=2", chat, [(0, "Voice messages: enabled"), (0, "Audio/video calls: enabled"), (0, "1"), (0, "2"), (0, "3")]) + + -- Read next 2 items + let itemIds = intercalate "," $ map itemId [1 .. 2] + bob #$> ("/_read chat items @2 " <> itemIds, id, "ok") + bob #$> ("/_get chat @2 initial=2", chat, [(0, "1"), (0, "2"), (0, "3"), (0, "4"), (0, "5")]) + + -- Read all items + bob #$> ("/_read chat @2", id, "ok") + bob #$> ("/_get chat @2 initial=3", chat, [(0, "8"), (0, "9"), (0, "10")]) + bob #$> ("/_get chat @2 initial=5", chat, [(0, "6"), (0, "7"), (0, "8"), (0, "9"), (0, "10")]) + where + opts = + testOpts + { markRead = False + } + testDuplicateContactsSeparate :: HasCallStack => FilePath -> IO () testDuplicateContactsSeparate = testChat2 aliceProfile bobProfile $ @@ -791,7 +823,7 @@ testDirectMessageDelete = alice @@@ [("@bob", lastChatFeature)] alice #$> ("/_get chat @2 count=100", chat, chatFeatures) - -- alice: msg id 1 + -- alice: msg id 3 bob ##> ("/_update item @2 " <> itemId 2 <> " text hey alice") bob <# "@alice [edited] > hello 🙂" bob <## " hey alice" @@ -806,12 +838,12 @@ testDirectMessageDelete = alice @@@ [("@bob", "hey alice [marked deleted]")] alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "hey alice [marked deleted]")]) - -- alice: deletes msg id 1 that was broadcast deleted by bob - alice #$> ("/_delete item @2 " <> itemId 1 <> " internal", id, "message deleted") + -- alice: deletes msg id 3 that was broadcast deleted by bob + alice #$> ("/_delete item @2 " <> itemId 3 <> " internal", id, "message deleted") alice @@@ [("@bob", lastChatFeature)] alice #$> ("/_get chat @2 count=100", chat, chatFeatures) - -- alice: msg id 1, bob: msg id 3 (quoting message alice deleted locally) + -- alice: msg id 4, bob: msg id 3 (quoting message alice deleted locally) bob `send` "> @alice (hello 🙂) do you receive my messages?" bob <# "@alice > hello 🙂" bob <## " do you receive my messages?" @@ -819,14 +851,14 @@ testDirectMessageDelete = alice <## " do you receive my messages?" alice @@@ [("@bob", "do you receive my messages?")] alice #$> ("/_get chat @2 count=100", chat', chatFeatures' <> [((0, "do you receive my messages?"), Just (1, "hello 🙂"))]) - alice #$> ("/_delete item @2 " <> itemId 1 <> " broadcast", id, "cannot delete this item") + alice #$> ("/_delete item @2 " <> itemId 4 <> " broadcast", id, "cannot delete this item") - -- alice: msg id 2, bob: msg id 4 + -- alice: msg id 5, bob: msg id 4 bob #> "@alice how are you?" alice <# "bob> how are you?" - -- alice: deletes msg id 2 - alice #$> ("/_delete item @2 " <> itemId 2 <> " internal", id, "message deleted") + -- alice: deletes msg id 5 + alice #$> ("/_delete item @2 " <> itemId 5 <> " internal", id, "message deleted") -- bob: marks deleted msg id 4 (that alice deleted locally) bob #$> ("/_delete item @2 " <> itemId 4 <> " broadcast", id, "message marked deleted") @@ -2340,6 +2372,14 @@ testUserPrivacy = "bob> Voice messages: enabled", "bob> Audio/video calls: enabled" ] + alice ##> "/_get items around=11 count=2" + alice + <##? [ "bob> Full deletion: off", + "bob> Message reactions: enabled", + "bob> Voice messages: enabled", + "bob> Audio/video calls: enabled", + "@bob hello" + ] alice ##> "/_get items after=12 count=10" alice <##? [ "@bob hello", diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index f1a36c8722..a7de42128c 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -36,6 +36,7 @@ chatGroupTests = do describe "chat groups" $ do describe "add contacts, create group and send/receive messages" testGroupMatrix it "mark multiple messages as read" testMarkReadGroup + it "initial chat pagination" testChatPaginationInitial it "v1: add contacts, create group and send/receive messages" testGroup it "v1: add contacts, create group and send/receive messages, check messages" testGroupCheckMessages it "send large message" testGroupLargeMessage @@ -344,6 +345,7 @@ testGroupShared alice bob cath checkMessages directConnections = do -- so we take into account group event items as well as sent group invitations in direct chats alice #$> ("/_get chat #1 after=" <> msgItem1 <> " count=100", chat, [(0, "hi there"), (0, "hey team")]) alice #$> ("/_get chat #1 before=" <> msgItem2 <> " count=100", chat, [(1, e2eeInfoNoPQStr), (0, "connected"), (0, "connected"), (1, "hello"), (0, "hi there")]) + alice #$> ("/_get chat #1 around=" <> msgItem1 <> " count=2", chat, [(0, "connected"), (0, "connected"), (1, "hello"), (0, "hi there"), (0, "hey team")]) alice #$> ("/_get chat #1 count=100 search=team", chat, [(0, "hey team")]) bob @@@ [("@cath", "hey"), ("#team", "hey team"), ("@alice", "received invitation to join group team as admin")] bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "added cath (Catherine)"), (0, "connected"), (0, "hello"), (1, "hi there"), (0, "hey team")]) @@ -374,6 +376,38 @@ testMarkReadGroup = testChat2 aliceProfile bobProfile $ \alice bob -> do let itemIds = intercalate "," $ map show [i - 3 .. i] bob #$> ("/_read chat items #1 " <> itemIds, id, "ok") +testChatPaginationInitial :: HasCallStack => FilePath -> IO () +testChatPaginationInitial = testChatOpts2 opts aliceProfile bobProfile $ \alice bob -> do + createGroup2 "team" alice bob + -- Wait, otherwise ids are going to be wrong. + threadDelay 1000000 + lastEventId <- (read :: String -> Int) <$> lastItemId bob + let groupItemId n = show $ lastEventId + n + + -- Send messages from alice to bob + forM_ ([1 .. 10] :: [Int]) $ \n -> alice #> ("#team " <> show n) + + -- Bob receives the messages. + forM_ ([1 .. 10] :: [Int]) $ \n -> bob <# ("#team alice> " <> show n) + + -- All messages are unread for bob, should return area around unread + bob #$> ("/_get chat #1 initial=2", chat, [(0, "Recent history: on"), (0, "connected"), (0, "1"), (0, "2"), (0, "3")]) + + -- Read next 2 items + let itemIds = intercalate "," $ map groupItemId [1 .. 2] + bob #$> ("/_read chat items #1 " <> itemIds, id, "ok") + bob #$> ("/_get chat #1 initial=2", chat, [(0, "1"), (0, "2"), (0, "3"), (0, "4"), (0, "5")]) + + -- Read all items + bob #$> ("/_read chat #1", id, "ok") + bob #$> ("/_get chat #1 initial=3", chat, [(0, "8"), (0, "9"), (0, "10")]) + bob #$> ("/_get chat #1 initial=5", chat, [(0, "6"), (0, "7"), (0, "8"), (0, "9"), (0, "10")]) + where + opts = + testOpts + { markRead = False + } + testGroupLargeMessage :: HasCallStack => FilePath -> IO () testGroupLargeMessage = testChat2 aliceProfile bobProfile $ diff --git a/tests/ChatTests/Local.hs b/tests/ChatTests/Local.hs index da9c043648..40df02252d 100644 --- a/tests/ChatTests/Local.hs +++ b/tests/ChatTests/Local.hs @@ -51,7 +51,7 @@ testNotes tmp = withNewTestChat tmp "alice" aliceProfile $ \alice -> do alice ##> "/chats" alice /* "ahoy!" - alice ##> "/_update item *1 1 text Greetings." + alice ##> "/_update item *1 2 text Greetings." alice ##> "/tail *" alice <# "* Greetings." @@ -102,6 +102,10 @@ testChatPagination tmp = withNewTestChat tmp "alice" aliceProfile $ \alice -> do alice #$> ("/_get chat *1 count=100", chat, [(1, "hello world"), (1, "memento mori"), (1, "knock-knock"), (1, "who's there?")]) alice #$> ("/_get chat *1 count=1", chat, [(1, "who's there?")]) + alice #$> ("/_get chat *1 around=2 count=1", chat, [(1, "hello world"), (1, "memento mori"), (1, "knock-knock")]) + alice #$> ("/_get chat *1 around=2 count=3", chat, [(1, "hello world"), (1, "memento mori"), (1, "knock-knock"), (1, "who's there?")]) + alice #$> ("/_get chat *1 around=3 count=10", chat, [(1, "hello world"), (1, "memento mori"), (1, "knock-knock"), (1, "who's there?")]) + alice #$> ("/_get chat *1 around=4 count=1", chat, [(1, "knock-knock"), (1, "who's there?")]) alice #$> ("/_get chat *1 after=2 count=10", chat, [(1, "knock-knock"), (1, "who's there?")]) alice #$> ("/_get chat *1 after=2 count=2", chat, [(1, "knock-knock"), (1, "who's there?")]) alice #$> ("/_get chat *1 after=1 count=2", chat, [(1, "memento mori"), (1, "knock-knock")]) diff --git a/tests/SchemaDump.hs b/tests/SchemaDump.hs index 23f36713b4..4e63a31001 100644 --- a/tests/SchemaDump.hs +++ b/tests/SchemaDump.hs @@ -102,7 +102,9 @@ skipComparisonForDownMigrations = -- table and indexes move down to the end of the file "20231215_recreate_msg_deliveries", -- on down migration idx_msg_deliveries_agent_ack_cmd_id index moves down to the end of the file - "20240313_drop_agent_ack_cmd_id" + "20240313_drop_agent_ack_cmd_id", + -- on down migration chat_item_autoincrement_id makes sequence table creation move down on the file + "20241023_chat_item_autoincrement_id" ] getSchema :: FilePath -> FilePath -> IO String From a5061f3147165a05979d6ace33960aced2d6ac03 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Thu, 14 Nov 2024 11:59:44 +0000 Subject: [PATCH 020/167] docs: update privacy policy and conditions of use (#5129) * docs: update privacy policy and conditions of use * update * note * update date --- PRIVACY.md | 164 ++++++++++++++++++++++++++++++++++------------------- 1 file changed, 106 insertions(+), 58 deletions(-) diff --git a/PRIVACY.md b/PRIVACY.md index 669a0bf4be..7c4bfbf660 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -3,27 +3,49 @@ layout: layouts/privacy.html permalink: /privacy/index.html --- -# SimpleX Chat Privacy Policy and Conditions of Use +# SimpleX Chat Operators Privacy Policy and Conditions of Use -SimpleX Chat is the first communication network based on a new protocol stack that builds on the same ideas of complete openness and decentralization as email and web, with the focus on providing security and privacy of communications, and without compromising on usability. +## Summary -SimpleX Chat communication protocol is the first protocol that has no user profile IDs of any kind, not even random numbers, cryptographic keys or hashes that identify the users. SimpleX Chat apps allow their users to send messages and files via relay server infrastructure. Relay server owners and providers do not have any access to your messages, thanks to double-ratchet end-to-end encryption algorithm (also known as Signal algorithm - do not confuse with Signal protocols or platform) and additional encryption layers, and they also have no access to your profile and contacts - as they do not provide any user accounts. +[Introduction](#introduction) and [General principles](#general-principles) cover SimpleX Chat network design, the network operators, and the principles of privacy and security provided by SimpleX network. + +[Privacy policy](#privacy-policy) covers: +- data stored only on your device - [your profiles](#user-profiles), delivered [messages and files](#messages-and-files). You can transfer this information to another device, and you are responsible for its preservation - if you delete the app it will be lost. +- [private message delivery](#private-message-delivery) that protects your IP address and connection graph from the destination servers. +- [undelivered messages and files](#storage-of-messages-and-files-on-the-servers) stored on the servers. +- [how users connect](#connections-with-other-users) without any user profile identifiers. +- [iOS push notifications](#ios-push-notifications) privacy limitations. +- [user support](#user-support), [SimpleX directory](#simplex-directory) and [any other data](#another-information-stored-on-the-servers) that may be stored on the servers. +- [preset server operators](#preset-server-operators) and the [information they may share](#information-preset-server-operators-may-share). +- [source code license](#source-code-license) and [updates to this document](#updates). + +[Conditions of Use](#conditions-of-use-of-software-and-infrastructure) are the conditions you need to accept to use SimpleX Chat applications and the relay servers of preset operators. Their purpose is to protect the users and preset server operators. + +*Please note*: this summary and any links in this document are provided for information only - they are not a part of the Privacy Policy and Conditions of Use. + +## Introduction + +SimpleX Chat (also referred to as SimpleX) is the first communication network based on a new protocol stack that builds on the same ideas of complete openness and decentralization as email and web, with the focus on providing security and privacy of communications, and without compromising on usability. + +SimpleX messaging protocol is the first protocol that has no user profile IDs of any kind, not even random numbers, cryptographic keys or hashes that identify the users. SimpleX apps allow their users to send messages and files via relay server infrastructure. Relay server owners and operators do not have any access to your messages, thanks to double-ratchet end-to-end encryption algorithm (also known as Signal algorithm - do not confuse with Signal protocols or platform) and additional encryption layers, and they also have no access to your profile and contacts - as they do not host user accounts. Double ratchet algorithm has such important properties as [forward secrecy](/docs/GLOSSARY.md#forward-secrecy), sender [repudiation](/docs/GLOSSARY.md#) and break-in recovery (also known as [post-compromise security](/docs/GLOSSARY.md#post-compromise-security)). -If you believe that any part of this document is not aligned with our mission or values, please raise it with us via [email](mailto:chat@simplex.chat) or [chat](https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23%2F%3Fv%3D1%26dh%3DMCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion). +If you believe that any part of this document is not aligned with SimpleX network mission or values, please raise it via [email](mailto:chat@simplex.chat) or [chat](https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23%2F%3Fv%3D1%26dh%3DMCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion). ## Privacy Policy -SimpleX Chat Ltd uses the best industry practices for security and encryption to provide client and server software for secure [end-to-end encrypted](/docs/GLOSSARY.md#end-to-end-encryption) messaging via private connections. This encryption cannot be compromised by the relays servers, even if they are modified or compromised, via [man-in-the-middle attack](/docs/GLOSSARY.md#man-in-the-middle-attack), unlike most other communication platforms, services and networks. +### General principles -SimpleX Chat software is built on top of SimpleX messaging and application protocols, based on a new message routing protocol allowing to establish private connections without having any kind of addresses or other identifiers assigned to its users - it does not use emails, phone numbers, usernames, identity keys or any other user profile identifiers to pass messages between the user applications. +SimpleX network software uses the best industry practices for security and encryption to provide client and server software for secure [end-to-end encrypted](/docs/GLOSSARY.md#end-to-end-encryption) messaging via private connections. This encryption is protected from being compromised by the relays servers, even if they are modified or compromised, via [man-in-the-middle attack](/docs/GLOSSARY.md#man-in-the-middle-attack). -SimpleX Chat software is similar in its design approach to email clients and browsers - it allows you to have full control of your data and freely choose the relay server providers, in the same way you choose which website or email provider to use, or use your own relay servers, simply by changing the configuration of the client software. The only current restriction to that is Apple push notifications - at the moment they can only be delivered via the preset servers that we operate, as explained below. We are exploring the solutions to deliver push notifications to iOS devices via other providers or users' own servers. +SimpleX software is built on top of SimpleX messaging and application protocols, based on a new message routing protocol allowing to establish private connections without having identifiers assigned to its users - it does not use emails, phone numbers, usernames, identity keys or any other user profile identifiers to pass messages between the user applications. -While SimpleX Chat Ltd is not a communication service provider, and provide public preset relays "as is", as experimental, without any guarantees of availability or data retention, we are committed to maintain a high level of availability, reliability and security of these preset relays. We will be adding alternative preset infrastructure providers to the software in the future, and you will continue to be able to use any other providers or your own servers. +SimpleX software is similar in its design approach to email clients and browsers - it allows you to have full control of your data and freely choose the relay server operators, in the same way you choose which website or email provider to use, or use your own relay servers, simply by changing the configuration of the client software. The only current restriction to that is Apple push notifications - at the moment they can only be delivered via the servers operated by SimpleX Chat Ltd, as explained below. We are exploring the solutions to deliver push notifications to iOS devices via other providers or users' own servers. -We see users and data sovereignty, and device and provider portability as critically important properties for any communication system. +SimpleX network operators are not communication service provider, and provide public relays "as is", as experimental, without any guarantees of availability or data retention. The operators of the relay servers preset in the app ("Preset Server Operators"), including SimpleX Chat Ltd, are committed to maintain a high level of availability, reliability and security. SimpleX client apps can have multiple preset relay server operators that you can opt-in or opt-out of using. You are and will continue to be able to use any other operators or your own servers. + +SimpleX network design is based on the principles of users and data sovereignty, and device and operator portability. The implementation security assessment of SimpleX cryptography and networking was done in October 2022 by [Trail of Bits](https://www.trailofbits.com/about), and most fixes were released in v4.2 – see [the announcement](/blog/20221108-simplex-chat-v4.2-security-audit-new-website.md). @@ -33,35 +55,41 @@ The cryptographic review of SimpleX protocols design was done in July 2024 by Tr #### User profiles -Servers used by SimpleX Chat apps do not create, store or identify user profiles. The profiles you can create in the app are local to your device, and can be removed at any time via the app. +Servers used by SimpleX Chat apps do not create, store or identify user chat profiles. The profiles you can create in the app are local to your device, and can be removed at any time via the app. -When you create the local profile, no records are created on any of the relay servers, and infrastructure providers, whether SimpleX Chat Ltd or any other, have no access to any part of your information, and even to the fact that you created a profile - it is a local record stored only on your device. That means that if you delete the app, and have no backup, you will permanently lose all your data and the private connections you created with other software users. +When you create the local profile, no records are created on any of the relay servers, and infrastructure operators, whether preset in the app or any other, have no access to any part of your information, and even to the fact that you created a profile - it is a local record stored only on your device. That means that if you delete the app, and have no backup, you will permanently lose all your data and the private connections you created with other software users. You can transfer the profile to another device by creating a backup of the app data and restoring it on the new device, but you cannot use more than one device with the copy of the same profile at the same time - it will disrupt any active conversations on either or both devices, as a security property of end-to-end encryption. #### Messages and Files -SimpleX relay servers cannot decrypt or otherwise access the content or even the size of your messages and files you send or receive. Each message is padded to a fixed size of 16kb. Each file is sent in chunks of 64kb, 256kb, 1mb or 8mb via all or some of the configured file relay servers. Both messages and files are sent end-to-end encrypted, and the servers do not have technical means to compromise this encryption, because part of the [key exchange](/docs/GLOSSARY.md#key-exchange) happens out-of-band. +SimpleX relay servers cannot decrypt or otherwise access the content or even the size of your messages and files you send or receive. Each message is padded to a fixed size of 16kb. Each file is sent in chunks of 64kb, 256kb, 1mb or 4mb via all or some of the configured file relay servers. Both messages and files are sent end-to-end encrypted, and the servers do not have technical means to compromise this encryption, because part of the [key exchange](/docs/GLOSSARY.md#key-exchange) happens out-of-band. Your message history is stored only on your own device and the devices of your contacts. While the recipients' devices are offline, messaging relay servers temporarily store end-to-end encrypted messages – you can configure which relay servers are used to receive the messages from the new contacts, and you can manually change them for the existing contacts too. -You do not have control over which servers are used to send messages to your contacts - they are chosen by them. To send messages your client needs to connect to these servers, therefore the servers chosen by your contacts can observe your IP address. You can use VPN or some overlay network (e.g., Tor) to hide your IP address from the servers chosen by your contacts. In the near future we will add the layer in the messaging protocol that will route sent message via the relays chosen by you as well. +#### Private message delivery -The messages are permanently removed from the used relay servers as soon as they are delivered, as long as these servers used unmodified published code. Undelivered messages are deleted after the time that is configured in the messaging servers you use (21 days for preset messaging servers). +You do not have control over which servers are used to send messages to your contacts - these servers are chosen by your contacts. To send messages your client by default uses configured servers to forward messages to the destination servers, thus protecting your IP address from the servers chosen by your contacts. + +In case you use preset servers of more than one operator, the app will prefer to use a server of an operator different from the operator of the destination server to forward messages, preventing destination server to correlate messages as belonging to one client. + +You can additionally use VPN or some overlay network (e.g., Tor) to hide your IP address from the servers chosen by you. + +*Please note*: the clients allow changing configuration to connect to the destination servers directly. It is not recommended - if you make such change, your IP address will be visible to the destination servers. + +#### Storage of messages and files on the servers + +The messages are removed from the relay servers as soon as all messages of the file they were stored in are delivered and saving new messages switches to another file, as long as these servers use unmodified published code. Undelivered messages are also marked as delivered after the time that is configured in the messaging servers you use (21 days for preset messaging servers). The files are stored on file relay servers for the time configured in the relay servers you use (48 hours for preset file servers). -If a messaging servers are restarted, the encrypted message can be stored in a backup file until it is overwritten by the next restart (usually within 1 week for preset relay servers). - -As this software is fully open-source and provided under AGPLv3 license, all infrastructure providers and owners, and the developers of the client and server applications who use the SimpleX Chat source code, are required to publish any changes to this software under the same AGPLv3 license - including any modifications to the provided servers. - -In addition to the AGPLv3 license terms, SimpleX Chat Ltd is committed to the software users that the preset relays that we provide via the apps will always be compiled from the [published open-source code](https://github.com/simplex-chat/simplexmq), without any modifications. +The encrypted messages can be stored for some time after they are delivered or expired (because servers use append-only logs for message storage). This time varies, and may be longer in connections with fewer messages, but it is usually limited to 1 month, including any backup storage. #### Connections with other users -When you create a connection with another user, two messaging queues (you can think about them as mailboxes) are created on messaging relay servers (chosen by you and your contact each), that can be the preset servers or the servers that you and your contact configured in the app. SimpleX messaging protocol uses separate queues for direct and response messages, and the apps prefer to create these queues on two different relay servers for increased privacy, in case you have more than one relay server configured in the app, which is the default. +When you create a connection with another user, two messaging queues (you can think about them as mailboxes) are created on messaging relay servers (chosen by you and your contact each), that can be the preset servers or the servers that you and your contact configured in the app. SimpleX messaging protocol uses separate queues for direct and response messages, and the apps prefer to create these queues on two different relay servers, or, if available, the relays of two different operators, for increased privacy, in case you have more than one relay server configured in the app, which is the default. -SimpleX relay servers do not store information about which queues are linked to your profile on the device, and they do not collect any information that would allow infrastructure owners and providers to establish that these queues are related to your device or your profile - the access to each queue is authorized by two anonymous unique cryptographic keys, different for each queue, and separate for sender and recipient of the messages. +Preset and unmodified SimpleX relay servers do not store information about which queues are linked to your profile on the device, and they do not collect any information that would allow infrastructure owners and operators to establish that these queues are related to your device or your profile - the access to each queue is authorized by two anonymous unique cryptographic keys, different for each queue, and separate for sender and recipient of the messages. #### Connection links privacy @@ -77,6 +105,8 @@ You can always safely replace the initial part of the link `https://simplex.chat #### iOS Push Notifications +This section applies only to the notification servers operated by SimpleX Chat Ltd. + When you choose to use instant push notifications in SimpleX iOS app, because the design of push notifications requires storing the device token on notification server, the notifications server can observe how many messaging queues your device has notifications enabled for, and approximately how many messages are sent to each queue. Preset notification server cannot observe the actual addresses of these queues, as a separate address is used to subscribe to the notifications. It also cannot observe who sends messages to you. Apple push notifications servers can only observe how many notifications are sent to you, but not from how many contacts, or from which messaging relays, as notifications are delivered to your device end-to-end encrypted by one of the preset notification servers - these notifications only contain end-to-end encrypted metadata, not even encrypted message content, and they look completely random to Apple push notification servers. @@ -85,93 +115,111 @@ You can read more about the design of iOS push notifications [here](./blog/20220 #### Another information stored on the servers -Additional technical information can be stored on our servers, including randomly generated authentication tokens, keys, push tokens, and other material that is necessary to transmit messages. SimpleX Chat design limits this additional technical information to the minimum required to operate the software and servers. To prevent server overloading or attacks, the servers can temporarily store data that can link to particular users or devices, including IP addresses, geographic location, or information related to the transport sessions. This information is not stored for the absolute majority of the app users, even for those who use the servers very actively. +Additional technical information can be stored on the network servers, including randomly generated authentication tokens, keys, push tokens, and other material that is necessary to transmit messages. SimpleX network design limits this additional technical information to the minimum required to operate the software and servers. To prevent server overloading or attacks, the servers can temporarily store data that can link to particular users or devices, including IP addresses, geographic location, or information related to the transport sessions. This information is not stored for the absolute majority of the app users, even for those who use the servers very actively. #### SimpleX Directory +This section applies only to the experimental group directory operated by SimpleX Chat Ltd. + [SimpleX Directory](/docs/DIRECTORY.md) stores: your search requests, the messages and the members profiles in the registered groups. You can connect to SimpleX Directory via [this address](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion). #### User Support -If you contact SimpleX Chat Ltd, any personal data you share with us is kept only for the purposes of researching the issue and contacting you about your case. We recommend contacting support [via chat](https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23%2F%3Fv%3D1%26dh%3DMCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion) when it is possible, and avoid sharing any personal information. +The app includes support contact operated by SimpleX Chat Ltd. If you contact support, any personal data you share is kept only for the purposes of researching the issue and contacting you about your case. We recommend contacting support [via chat](https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23%2F%3Fv%3D1%26dh%3DMCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion) when it is possible, and avoid sharing any personal information. -### Information we may share +### Preset Server Operators -SimpleX Chat Ltd operates preset relay servers using third parties. While we do not have access and cannot share any user data, these third parties may access the encrypted user messages (but NOT the actual unencrypted message content or size) as it is stored or transmitted via our servers. Hosting providers can also store IP addresses and other transport information as part of their logs. +Preset server operators will not share the information on their servers with each other, other than aggregate usage statistics. -We use a third party for email services - if you ask for support via email, your and SimpleX Chat Ltd email providers may access these emails according to their privacy policies and terms. When the request is sensitive, we recommend contacting us via SimpleX Chat or using encrypted email using PGP key published at [openpgp.org](https://keys.openpgp.org/search?q=chat%40simplex.chat). +Preset server operators will not provide general access to their servers or the data on their servers to each other. -The cases when SimpleX Chat Ltd may share the data temporarily stored on the servers: +Preset server operators will provide non-administrative access to control port of preset servers to SimpleX Chat Ltd, for the purposes of removing identified illegal content. This control port access only allows deleting known links and files, and access to aggregate statistics, but does NOT allow enumerating any information on the servers. + +### Information Preset Server Operators May Share + +The preset server operators use third parties. While they do not have access and cannot share any user data, these third parties may access the encrypted user messages (but NOT the actual unencrypted message content or size) as it is stored or transmitted via the servers. Hosting and network providers can also store IP addresses and other transport information as part of their logs. + +SimpleX Chat Ltd uses a third party for email services - if you ask for support via email, your and SimpleX Chat Ltd email providers may access these emails according to their privacy policies and terms. When the request is sensitive, please contact us via SimpleX Chat apps or using encrypted email using PGP key published at [openpgp.org](https://keys.openpgp.org/search?q=chat%40simplex.chat). + +The cases when the preset server operators may share the data temporarily stored on the servers: - To meet any applicable law, or enforceable governmental request or court order. - To enforce applicable terms, including investigation of potential violations. - To detect, prevent, or otherwise address fraud, security, or technical issues. -- To protect against harm to the rights, property, or safety of software users, SimpleX Chat Ltd, or the public as required or permitted by law. +- To protect against harm to the rights, property, or safety of software users, operators of preset servers, or the public as required or permitted by law. -At the time of updating this document, we have never provided or have been requested the access to the preset relay servers or any information from the servers by any third parties. If we are ever requested to provide such access or information, we will follow the due legal process to limit any information shared with the third parties to the minimally required by law. +At the time of updating this document, the preset server operators have never provided or have been requested the access to the preset relay servers or any information from the servers by any third parties. If the preset server operators are ever requested to provide such access or information, they will follow the due legal process to limit any information shared with the third parties to the minimally required by law. -We will publish information we are legally allowed to share about such requests in the [Transparency reports](./docs/TRANSPARENCY.md). +Preset server operators will publish information they are legally allowed to share about such requests in the [Transparency reports](./docs/TRANSPARENCY.md). + +### Source code license + +As this software is fully open-source and provided under AGPLv3 license, all infrastructure owners and operators, and the developers of the client and server applications who use the SimpleX Chat source code, are required to publish any changes to this software under the same AGPLv3 license - including any modifications to the servers. + +In addition to the AGPLv3 license terms, the preset relay server operators are committed to the software users that these servers will always be compiled from the [published open-source code](https://github.com/simplex-chat/simplexmq), without any modifications. ### Updates -We will update this Privacy Policy as needed so that it is current, accurate, and as clear as possible. Your continued use of our software applications and preset relays infrastructure confirms your acceptance of our updated Privacy Policy. +This Privacy Policy applies to SimpleX Chat Ltd and all other preset server operators you use in the app. -Please also read our Conditions of Use of Software and Infrastructure below. +This Privacy Policy may be updated as needed so that it is current, accurate, and as clear as possible. When it is updated, you will have to review and accept the changed policy within 30 days of such changes to continue using preset relay servers. Even if you fail to accept the changed policy, your continued use of SimpleX Chat software applications and preset relay servers confirms your acceptance of the updated Privacy Policy. -If you have questions about our Privacy Policy please contact us via [email](mailto:chat@simplex.chat) or [chat](https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23%2F%3Fv%3D1%26dh%3DMCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion). +Please also read The Conditions of Use of Software and Infrastructure below. + +If you have questions about this Privacy Policy please contact SimpleX Chat Ltd via [email](mailto:chat@simplex.chat) or [chat](https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23%2F%3Fv%3D1%26dh%3DMCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion). ## Conditions of Use of Software and Infrastructure -You accept the Conditions of Use of Software and Infrastructure ("Conditions") by installing or using any of our software or using any of our server infrastructure (collectively referred to as "Applications"), whether preset in the software or not. +You accept the Conditions of Use of Software and Infrastructure ("Conditions") by installing or using any of SimpleX Chat software or using any of server infrastructure (collectively referred to as "Applications") operated by the Preset Server Operators, including SimpleX Chat Ltd, whether these servers are preset in the software or not. -**Minimal age**. You must be at least 13 years old to use our Applications. The minimum age to use our Applications without parental approval may be higher in your country. +**Minimal age**. You must be at least 13 years old to use SimpleX Chat Applications. The minimum age to use SimpleX Applications without parental approval may be higher in your country. -**Infrastructure**. Our Infrastructure includes preset messaging and file relay servers, and iOS push notification servers provided by SimpleX Chat Ltd for public use. Our infrastructure does not have any modifications from the [published open-source code](https://github.com/simplex-chat/simplexmq) available under AGPLv3 license. Any infrastructure provider, whether commercial or not, is required by the Affero clause (named after Affero Inc. company that pioneered the community-based Q&A sites in early 2000s) to publish any modifications under the same license. The statements in relation to Infrastructure and relay servers anywhere in this document assume no modifications to the published code, even in the cases when it is not explicitly stated. +**Infrastructure**. Infrastructure of the preset server operators includes messaging and file relay servers. SimpleX Chat Ltd also provides iOS push notification servers for public use. This infrastructure does not have any modifications from the [published open-source code](https://github.com/simplex-chat/simplexmq) available under AGPLv3 license. Any infrastructure provider, whether commercial or not, is required by the Affero clause (named after Affero Inc. company that pioneered the community-based Q&A sites in early 2000s) to publish any modifications under the same license. The statements in relation to Infrastructure and relay servers anywhere in this document assume no modifications to the published code, even in the cases when it is not explicitly stated. -**Client applications**. Our client application Software (referred to as "app" or "apps") also has no modifications compared with published open-source code, and any developers of the alternative client apps based on our code are required to publish any modifications under the same AGPLv3 license. Client applications should not include any tracking or analytics code, and do not share any information with SimpleX Chat Ltd or any other third parties. If you ever discover any tracking or analytics code, please report it to us, so we can remove it. +**Client applications**. SimpleX Chat client application Software (referred to as "app" or "apps") also has no modifications compared with published open-source code, and any developers of the alternative client apps based on SimpleX Chat code are required to publish any modifications under the same AGPLv3 license. Client applications should not include any tracking or analytics code, and do not share any tracking information with SimpleX Chat Ltd, preset server operators or any other third parties. If you ever discover any tracking or analytics code, please report it to SimpleX Chat Ltd, so it can be removed. -**Accessing the infrastructure**. For the efficiency of the network access, the client Software by default accesses all queues your app creates on any relay server within one user profile via the same network (TCP/IP) connection. At the cost of additional traffic this configuration can be changed to use different transport session for each connection. Relay servers do not collect information about which queues were created or accessed via the same connection, so the relay servers cannot establish which queues belong to the same user profile. Whoever might observe your network traffic would know which relay servers you use, and how much data you send, but not to whom it is sent - the data that leaves the servers is always different from the data they receive - there are no identifiers or ciphertext in common, even inside TLS encryption layer. Please refer to our [technical design document](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md) for more information about our privacy model and known security and privacy risks. +**Accessing the infrastructure**. For the efficiency of the network access, the client Software by default accesses all queues your app creates on any relay server within one user profile via the same network (TCP/IP) connection. At the cost of additional traffic this configuration can be changed to use different transport session for each connection. Relay servers do not collect information about which queues were created or accessed via the same connection, so the relay servers cannot establish which queues belong to the same user profile. Whoever might observe your network traffic would know which relay servers you use, and how much data you send, but not to whom it is sent - the data that leaves the servers is always different from the data they receive - there are no identifiers or ciphertext in common, even inside TLS encryption layer. Please refer to the [technical design document](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md) for more information about the privacy model and known security and privacy risks. -**Privacy of user data**. Servers do not retain any data we transmit for any longer than necessary to deliver the messages between apps. SimpleX Chat Ltd collects aggregate statistics across all its servers, as supported by published code and can be enabled by any infrastructure provider, but not any statistics per-user, or per geographic location, or per IP address, or per transport session. We do not have information about how many people use SimpleX Chat applications, we only know an approximate number of app installations and the aggregate traffic through the preset servers. In any case, we do not and will not sell or in any way monetize user data. Our future business model assumes charging for some optional Software features instead, in a transparent and fair way. +**Privacy of user data**. Servers do not retain any data you transmit for any longer than necessary to deliver the messages between apps. Preset server operators collect aggregate statistics across all their servers, as supported by published code and can be enabled by any infrastructure operator, but not any statistics per-user, or per geographic location, or per IP address, or per transport session. SimpleX Chat Ltd does not have information about how many people use SimpleX Chat applications, it only knows an approximate number of app installations and the aggregate traffic through the preset servers. In any case, preset server operators do not and will not sell or in any way monetize user data. The future business model assumes charging for some optional Software features instead, in a transparent and fair way. -**Operating our Infrastructure**. For the purpose of using our Software, if you continue using preset servers, you agree that your end-to-end encrypted messages are transferred via the preset servers in any countries where we have or use facilities and service providers or partners. The information about geographic location of the servers will be made available in the apps in the near future. +**Operating Infrastructure**. For the purpose of using SimpleX Chat Software, if you continue using preset servers, you agree that your end-to-end encrypted messages are transferred via the preset servers in any countries where preset server operators have or use facilities and service providers or partners. The information about geographic location and hosting providers of the preset messaging servers is available on server pages. -**Software**. You agree to downloading and installing updates to our Applications when they are available; they would only be automatic if you configure your devices in this way. +**Software**. You agree to downloading and installing updates to SimpleX Chat Applications when they are available; they would only be automatic if you configure your devices in this way. -**Traffic and device costs**. You are solely responsible for the traffic and device costs that you incur while using our Applications, and any associated taxes. +**Traffic and device costs**. You are solely responsible for the traffic and device costs that you incur while using SimpleX Chat Applications, and any associated taxes. -**Legal usage**. You agree to use our Applications only for legal purposes. You will not use (or assist others in using) our Applications in ways that: 1) violate or infringe the rights of Software users, SimpleX Chat Ltd, or others, including privacy, publicity, intellectual property, or other proprietary rights; 2) involve sending illegal communications, e.g. spam. While we cannot access content or identify messages or groups, in some cases the links to the illegal communications available via our Applications can be shared publicly on social media or websites. We reserve the right to remove such links from the preset servers and disrupt the conversations that send illegal content via our servers, whether they were reported by the users or discovered by our team. +**Legal usage**. You agree to use SimpleX Chat Applications only for legal purposes. You will not use (or assist others in using) the Applications in ways that: 1) violate or infringe the rights of Software users, SimpleX Chat Ltd, other preset server operators, or others, including privacy, publicity, intellectual property, or other proprietary rights; 2) involve sending illegal communications, e.g. spam. While server operators cannot access content or identify messages or groups, in some cases the links to the illegal communications can be shared publicly on social media or websites. Preset server operators reserve the right to remove such links from the preset servers and disrupt the conversations that send illegal content via their servers, whether they were reported by the users or discovered by the operators themselves. -**Damage to SimpleX Chat Ltd**. You must not (or assist others to) access, use, modify, distribute, transfer, or exploit our Applications in unauthorized manners, or in ways that harm Software users, SimpleX Chat Ltd, our Infrastructure, or any other systems. For example, you must not 1) access our Infrastructure or systems without authorization, in any way other than by using the Software; 2) disrupt the integrity or performance of our Infrastructure; 3) collect information about our users in any manner; or 4) sell, rent, or charge for our Infrastructure. This does not prohibit you from providing your own Infrastructure to others, whether free or for a fee, as long as you do not violate these Conditions and AGPLv3 license, including the requirement to publish any modifications of the relay server software. +**Damage to SimpleX Chat Ltd and Preset Server Operators**. You must not (or assist others to) access, use, modify, distribute, transfer, or exploit SimpleX Chat Applications in unauthorized manners, or in ways that harm Software users, SimpleX Chat Ltd, other preset server operators, their Infrastructure, or any other systems. For example, you must not 1) access preset operators' Infrastructure or systems without authorization, in any way other than by using the Software; 2) disrupt the integrity or performance of preset operators' Infrastructure; 3) collect information about the users in any manner; or 4) sell, rent, or charge for preset operators' Infrastructure. This does not prohibit you from providing your own Infrastructure to others, whether free or for a fee, as long as you do not violate these Conditions and AGPLv3 license, including the requirement to publish any modifications of the relay server software. -**Keeping your data secure**. SimpleX Chat is the first communication software that aims to be 100% private by design - server software neither has the ability to access your messages, nor it has information about who you communicate with. That means that you are solely responsible for keeping your device, your user profile and any data safe and secure. If you lose your phone or remove the Software from the device, you will not be able to recover the lost data, unless you made a back up. To protect the data you need to make regular backups, as using old backups may disrupt your communication with some of the contacts. +**Keeping your data secure**. SimpleX Chat is the first communication software that aims to be 100% private by design - server software neither has the ability to access your messages, nor it has information about who you communicate with. That means that you are solely responsible for keeping your device, your user profile and any data safe and secure. If you lose your phone or remove the Software from the device, you will not be able to recover the lost data, unless you made a back up. To protect the data you need to make regular backups, as using old backups may disrupt your communication with some of the contacts. SimpleX Chat Ltd and other preset server operators are not responsible for any data loss. **Storing the messages on the device**. The messages are stored in the encrypted database on your device. Whether and how database passphrase is stored is determined by the configuration of the Software you use. The databases created prior to 2023 or in CLI (terminal) app may remain unencrypted, and it will be indicated in the app interface. In this case, if you make a backup of the data and store it unencrypted, the backup provider may be able to access the messages. Please note, that the desktop apps can be configured to store the database passphrase in the configuration file in plaintext, and unless you set the passphrase when first running the app, a random passphrase will be used and stored on the device. You can remove it from the device via the app settings. **Storing the files on the device**. The files currently sent and received in the apps by default (except CLI app) are stored on your device encrypted using unique keys, different for each file, that are stored in the database. Once the message that the file was attached to is removed, even if the copy of the encrypted file is retained, it should be impossible to recover the key allowing to decrypt the file. This local file encryption may affect app performance, and it can be disabled via the app settings. This change will only affect the new files. If you later re-enable the encryption, it will also affect only the new files. If you make a backup of the app data and store it unencrypted, the backup provider will be able to access any unencrypted files. In any case, irrespective of the storage setting, the files are always sent by all apps end-to-end encrypted. -**No Access to Emergency Services**. Our Applications do not provide access to emergency service providers like the police, fire department, hospitals, or other public safety organizations. Make sure you can contact emergency service providers through a mobile, fixed-line telephone, or other service. +**No Access to Emergency Services**. SimpleX Chat Applications do not provide access to emergency service providers like the police, fire department, hospitals, or other public safety organizations. Make sure you can contact emergency service providers through a mobile, fixed-line telephone, or other service. -**Third-party services**. Our Applications may allow you to access, use, or interact with our or third-party websites, apps, content, and other products and services. When you use third-party services, their terms and privacy policies govern your use of those services. +**Third-party services**. SimpleX Chat Applications may allow you to access, use, or interact with the websites of SimpleX Chat Ltd, preset server operators or other third-party websites, apps, content, and other products and services. When you use third-party services, their terms and privacy policies govern your use of those services. -**Your Rights**. You own the messages and the information you transmit through our Applications. Your recipients are able to retain the messages they receive from you; there is no technical ability to delete data from their devices. While there are various app features that allow deleting messages from the recipients' devices, such as _disappearing messages_ and _full message deletion_, their functioning on your recipients' devices cannot be guaranteed or enforced, as the device may be offline or have a modified version of the Software. At the same time, repudiation property of the end-to-end encryption algorithm allows you to plausibly deny having sent the message, like you can deny what you said in a private face-to-face conversation, as the recipient cannot provide any proof to the third parties, by design. +**Your Rights**. You own the messages and the information you transmit through SimpleX Applications. Your recipients are able to retain the messages they receive from you; there is no technical ability to delete data from their devices. While there are various app features that allow deleting messages from the recipients' devices, such as _disappearing messages_ and _full message deletion_, their functioning on your recipients' devices cannot be guaranteed or enforced, as the device may be offline or have a modified version of the Software. At the same time, repudiation property of the end-to-end encryption algorithm allows you to plausibly deny having sent the message, like you can deny what you said in a private face-to-face conversation, as the recipient cannot provide any proof to the third parties, by design. -**License**. SimpleX Chat Ltd grants you a limited, revocable, non-exclusive, and non-transferable license to use our Applications in accordance with these Conditions. The source-code of Applications is available and can be used under [AGPL v3 license](https://github.com/simplex-chat/simplex-chat/blob/stable/LICENSE). +**License**. SimpleX Chat Ltd grants you a limited, revocable, non-exclusive, and non-transferable license to use SimpleX Chat Applications in accordance with these Conditions. The source-code of Applications is available and can be used under [AGPL v3 license](https://github.com/simplex-chat/simplex-chat/blob/stable/LICENSE). -**SimpleX Chat Ltd Rights**. We own all copyrights, trademarks, domains, logos, trade secrets, and other intellectual property rights associated with our Applications. You may not use our copyrights, trademarks, domains, logos, and other intellectual property rights unless you have our written permission, and unless under an open-source license distributed together with the source code. To report copyright, trademark, or other intellectual property infringement, please contact chat@simplex.chat. +**SimpleX Chat Ltd Rights**. SimpleX Chat Ltd (and, where applicable, preset server operators) owns all copyrights, trademarks, domains, logos, trade secrets, and other intellectual property rights associated with the Applications. You may not use SimpleX Chat Ltd copyrights, trademarks, domains, logos, and other intellectual property rights unless you have SimpleX Chat Ltd written permission, and unless under an open-source license distributed together with the source code. To report copyright, trademark, or other intellectual property infringement, please contact chat@simplex.chat. -**Disclaimers**. YOU USE OUR APPLICATIONS AT YOUR OWN RISK AND SUBJECT TO THE FOLLOWING DISCLAIMERS. WE PROVIDE OUR APPLICATIONS ON AN “AS IS” BASIS WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE, NON-INFRINGEMENT, AND FREEDOM FROM COMPUTER VIRUS OR OTHER HARMFUL CODE. SIMPLEX CHAT LTD DOES NOT WARRANT THAT ANY INFORMATION PROVIDED BY US IS ACCURATE, COMPLETE, OR USEFUL, THAT OUR APPLICATIONS WILL BE OPERATIONAL, ERROR-FREE, SECURE, OR SAFE, OR THAT OUR APPLICATIONS WILL FUNCTION WITHOUT DISRUPTIONS, DELAYS, OR IMPERFECTIONS. WE DO NOT CONTROL, AND ARE NOT RESPONSIBLE FOR, CONTROLLING HOW OR WHEN OUR USERS USE OUR APPLICATIONS. WE ARE NOT RESPONSIBLE FOR THE ACTIONS OR INFORMATION (INCLUDING CONTENT) OF OUR USERS OR OTHER THIRD PARTIES. YOU RELEASE US, AFFILIATES, DIRECTORS, OFFICERS, EMPLOYEES, PARTNERS, AND AGENTS ("SIMPLEX PARTIES") FROM ANY CLAIM, COMPLAINT, CAUSE OF ACTION, CONTROVERSY, OR DISPUTE (TOGETHER, "CLAIM") AND DAMAGES, KNOWN AND UNKNOWN, RELATING TO, ARISING OUT OF, OR IN ANY WAY CONNECTED WITH ANY SUCH CLAIM YOU HAVE AGAINST ANY THIRD PARTIES. +**Disclaimers**. YOU USE SIMPLEX APPLICATIONS AT YOUR OWN RISK AND SUBJECT TO THE FOLLOWING DISCLAIMERS. SIMPLEX CHAT LTD PROVIDES APPLICATIONS ON AN “AS IS” BASIS WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE, NON-INFRINGEMENT, AND FREEDOM FROM COMPUTER VIRUS OR OTHER HARMFUL CODE. SIMPLEX CHAT LTD DOES NOT WARRANT THAT ANY INFORMATION PROVIDED BY THEM IS ACCURATE, COMPLETE, OR USEFUL, THAT THEIR APPLICATIONS WILL BE OPERATIONAL, ERROR-FREE, SECURE, OR SAFE, OR THAT THEIR APPLICATIONS WILL FUNCTION WITHOUT DISRUPTIONS, DELAYS, OR IMPERFECTIONS. SIMPLEX CHAT LTD AND OTHER PRESET OPERATORS DO NOT CONTROL, AND ARE NOT RESPONSIBLE FOR, CONTROLLING HOW OR WHEN THE USERS USE APPLICATIONS. SIMPLEX CHAT LTD AND OTHER PRESET OPERATORS ARE NOT RESPONSIBLE FOR THE ACTIONS OR INFORMATION (INCLUDING CONTENT) OF THEIR USERS OR OTHER THIRD PARTIES. YOU RELEASE SIMPLEX CHAT LTD, OTHER PRESET OPERATORS, AFFILIATES, DIRECTORS, OFFICERS, EMPLOYEES, PARTNERS, AND AGENTS ("SIMPLEX PARTIES") FROM ANY CLAIM, COMPLAINT, CAUSE OF ACTION, CONTROVERSY, OR DISPUTE (TOGETHER, "CLAIM") AND DAMAGES, KNOWN AND UNKNOWN, RELATING TO, ARISING OUT OF, OR IN ANY WAY CONNECTED WITH ANY SUCH CLAIM YOU HAVE AGAINST ANY THIRD PARTIES. -**Limitation of liability**. THE SIMPLEX PARTIES WILL NOT BE LIABLE TO YOU FOR ANY LOST PROFITS OR CONSEQUENTIAL, SPECIAL, PUNITIVE, INDIRECT, OR INCIDENTAL DAMAGES RELATING TO, ARISING OUT OF, OR IN ANY WAY IN CONNECTION WITH OUR CONDITIONS, US, OR OUR APPLICATIONS, EVEN IF THE SIMPLEX PARTIES HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. OUR AGGREGATE LIABILITY RELATING TO, ARISING OUT OF, OR IN ANY WAY IN CONNECTION WITH OUR CONDITIONS, US, OR OUR APPLICATIONS WILL NOT EXCEED ONE DOLLAR ($1). THE FOREGOING DISCLAIMER OF CERTAIN DAMAGES AND LIMITATION OF LIABILITY WILL APPLY TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW. THE LAWS OF SOME JURISDICTIONS MAY NOT ALLOW THE EXCLUSION OR LIMITATION OF CERTAIN DAMAGES, SO SOME OR ALL OF THE EXCLUSIONS AND LIMITATIONS SET FORTH ABOVE MAY NOT APPLY TO YOU. NOTWITHSTANDING ANYTHING TO THE CONTRARY IN OUR CONDITIONS, IN SUCH CASES, THE LIABILITY OF THE SIMPLEX PARTIES WILL BE LIMITED TO THE EXTENT PERMITTED BY APPLICABLE LAW. +**Limitation of liability**. THE SIMPLEX PARTIES WILL NOT BE LIABLE TO YOU FOR ANY LOST PROFITS OR CONSEQUENTIAL, SPECIAL, PUNITIVE, INDIRECT, OR INCIDENTAL DAMAGES RELATING TO, ARISING OUT OF, OR IN ANY WAY IN CONNECTION WITH OUR CONDITIONS, US, OR SIMPLEX APPLICATIONS, EVEN IF THE SIMPLEX PARTIES HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. THE AGGREGATE LIABILITY OF THE SIMPLEX PARTIES RELATING TO, ARISING OUT OF, OR IN ANY WAY IN CONNECTION WITH THESE CONDITIONS, THE SIMPLEX PARTIES, OR THE APPLICATIONS WILL NOT EXCEED ONE DOLLAR ($1). THE FOREGOING DISCLAIMER OF CERTAIN DAMAGES AND LIMITATION OF LIABILITY WILL APPLY TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW. THE LAWS OF SOME JURISDICTIONS MAY NOT ALLOW THE EXCLUSION OR LIMITATION OF CERTAIN DAMAGES, SO SOME OR ALL OF THE EXCLUSIONS AND LIMITATIONS SET FORTH ABOVE MAY NOT APPLY TO YOU. NOTWITHSTANDING ANYTHING TO THE CONTRARY IN THE CONDITIONS, IN SUCH CASES, THE LIABILITY OF THE SIMPLEX PARTIES WILL BE LIMITED TO THE EXTENT PERMITTED BY APPLICABLE LAW. -**Availability**. Our Applications may be disrupted, including for maintenance, upgrades, or network or equipment failures. We may discontinue some or all of our Applications, including certain features and the support for certain devices and platforms, at any time. +**Availability**. The Applications may be disrupted, including for maintenance, upgrades, or network or equipment failures. SimpleX Chat Ltd may discontinue some or all of their Applications, including certain features and the support for certain devices and platforms, at any time. Preset server operators may discontinue providing the servers, at any time. -**Resolving disputes**. You agree to resolve any Claim you have with us relating to or arising from our Conditions, us, or our Applications in the courts of England and Wales. You also agree to submit to the personal jurisdiction of such courts for the purpose of resolving all such disputes. The laws of England govern our Conditions, as well as any disputes, whether in court or arbitration, which might arise between SimpleX Chat Ltd and you, without regard to conflict of law provisions. +**Resolving disputes**. You agree to resolve any Claim you have with SimpleX Chat Ltd and/or preset server operators relating to or arising from these Conditions, them, or the Applications in the courts of England and Wales. You also agree to submit to the personal jurisdiction of such courts for the purpose of resolving all such disputes. The laws of England govern these Conditions, as well as any disputes, whether in court or arbitration, which might arise between SimpleX Chat Ltd (or preset server operators) and you, without regard to conflict of law provisions. -**Changes to the conditions**. SimpleX Chat Ltd may update the Conditions from time to time. Your continued use of our Applications confirms your acceptance of our updated Conditions and supersedes any prior Conditions. You will comply with all applicable export control and trade sanctions laws. Our Conditions cover the entire agreement between you and SimpleX Chat Ltd regarding our Applications. If you do not agree with our Conditions, you should stop using our Applications. +**Changes to the conditions**. SimpleX Chat Ltd may update the Conditions from time to time. The updated conditions have to be accepted within 30 days. Even if you fail to accept updated conditions, your continued use of SimpleX Chat Applications confirms your acceptance of the updated Conditions and supersedes any prior Conditions. You will comply with all applicable export control and trade sanctions laws. These Conditions cover the entire agreement between you and SimpleX Chat Ltd, and any preset server operators where applicable, regarding SimpleX Chat Applications. If you do not agree with these Conditions, you should stop using the Applications. -**Enforcing the conditions**. If we fail to enforce any of our Conditions, that does not mean we waive the right to enforce them. If any provision of the Conditions is deemed unlawful, void, or unenforceable, that provision shall be deemed severable from our Conditions and shall not affect the enforceability of the remaining provisions. Our Applications are not intended for distribution to or use in any country where such distribution or use would violate local law or would subject us to any regulations in another country. We reserve the right to limit our Applications in any country. If you have specific questions about these Conditions, please contact us at chat@simplex.chat. +**Enforcing the conditions**. If SimpleX Chat Ltd or preset server operators fail to enforce any of these Conditions, that does not mean they waive the right to enforce them. If any provision of the Conditions is deemed unlawful, void, or unenforceable, that provision shall be deemed severable from the Conditions and shall not affect the enforceability of the remaining provisions. The Applications are not intended for distribution to or use in any country where such distribution or use would violate local law or would subject SimpleX Chat Ltd to any regulations in another country. SimpleX Chat Ltd reserve the right to limit the access to the Applications in any country. Preset operators reserve the right to limit access to their servers in any country. If you have specific questions about these Conditions, please contact SimpleX Chat Ltd at chat@simplex.chat. -**Ending these conditions**. You may end these Conditions with SimpleX Chat Ltd at any time by deleting our Applications from your devices and discontinuing use of our Infrastructure. The provisions related to Licenses, Disclaimers, Limitation of Liability, Resolving dispute, Availability, Changes to the conditions, Enforcing the conditions, and Ending these conditions will survive termination of your relationship with SimpleX Chat Ltd. +**Ending these conditions**. You may end these Conditions with SimpleX Chat Ltd and preset server operators at any time by deleting the Applications from your devices and discontinuing use of the Infrastructure of SimpleX Chat Ltd and preset server operators. The provisions related to Licenses, Disclaimers, Limitation of Liability, Resolving dispute, Availability, Changes to the conditions, Enforcing the conditions, and Ending these conditions will survive termination of your relationship with SimpleX Chat Ltd and/or preset server operators. -Updated October 14, 2024 +Updated November 14, 2024 From e45a96935c452946266aa41d62868d70945d948b Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Thu, 14 Nov 2024 12:16:51 +0000 Subject: [PATCH 021/167] ci: update website build --- .github/workflows/web.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/web.yml b/.github/workflows/web.yml index 7fc66308f8..6839d48aeb 100644 --- a/.github/workflows/web.yml +++ b/.github/workflows/web.yml @@ -10,6 +10,7 @@ on: - blog/** - docs/** - .github/workflows/web.yml + - PRIVACY.md jobs: build: From d42cab8e227db171ac4292e2523454fb925529f2 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Thu, 14 Nov 2024 17:43:34 +0000 Subject: [PATCH 022/167] core: preset operators and servers (#5142) * core: preset servers and operators (WIP) * usageConditionsToAdd * simplify * WIP * database entity IDs * preset operators and servers (compiles) * update (most tests pass) * remove imports * fix * update * make preset servers lists potentially empty in some operators, as long as the combined list is not empty * CLI API in progress, validateUserServers * make servers of disabled operators "unknown", consider only enabled servers when switching profile links * exclude disabled operators when receiving files * fix TH in ghc 8.10.7 * add type for ghc 8.10.7 * pattern match for ghc 8.10.7 * ghc 8.10.7 fix attempt * remove additional pattern, update servers * do not strip title from conditions * remove space --------- Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> --- cabal.project | 2 +- package.yaml | 3 +- scripts/nix/sha256map.nix | 2 +- simplex-chat.cabal | 21 +- src/Simplex/Chat.hs | 449 +++++++++------- src/Simplex/Chat/Controller.hs | 96 ++-- .../Migrations/M20241027_server_operators.hs | 18 +- src/Simplex/Chat/Migrations/chat_schema.sql | 9 +- src/Simplex/Chat/Operators.hs | 396 ++++++++++++--- src/Simplex/Chat/Operators/Conditions.hs | 2 +- src/Simplex/Chat/Stats.hs | 3 +- src/Simplex/Chat/Store/Profiles.hs | 478 ++++++++++-------- src/Simplex/Chat/Store/Shared.hs | 1 + src/Simplex/Chat/Terminal.hs | 37 +- src/Simplex/Chat/Terminal/Main.hs | 4 +- src/Simplex/Chat/View.hs | 115 +++-- tests/ChatClient.hs | 19 +- tests/ChatTests/Direct.hs | 77 ++- tests/ChatTests/Groups.hs | 2 + tests/ChatTests/Profiles.hs | 12 +- tests/RandomServers.hs | 51 +- 21 files changed, 1148 insertions(+), 649 deletions(-) diff --git a/cabal.project b/cabal.project index 61ce04a569..74f944c37d 100644 --- a/cabal.project +++ b/cabal.project @@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: ff05a465ee15ac7ae2c14a9fb703a18564950631 + tag: 93f30c8edf9243ad2291dd6427d87328e282560a source-repository-package type: git diff --git a/package.yaml b/package.yaml index 2fc50a3532..4a95d52044 100644 --- a/package.yaml +++ b/package.yaml @@ -39,6 +39,7 @@ dependencies: - optparse-applicative >= 0.15 && < 0.17 - random >= 1.1 && < 1.3 - record-hasfield == 1.0.* + - scientific ==0.3.7.* - simple-logger == 0.1.* - simplexmq >= 5.0 - socks == 0.6.* @@ -73,7 +74,7 @@ when: - bytestring == 0.10.* - process >= 1.6 && < 1.6.18 - template-haskell == 2.16.* - - text >= 1.2.3.0 && < 1.3 + - text >= 1.2.4.0 && < 1.3 library: source-dirs: src diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 3e0f103641..bd2602c1b6 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."ff05a465ee15ac7ae2c14a9fb703a18564950631" = "1gv4nwqzbqkj7y3ffkiwkr4qwv52vdzppsds5vsfqaayl14rzmgp"; + "https://github.com/simplex-chat/simplexmq.git"."93f30c8edf9243ad2291dd6427d87328e282560a" = "1zf0sp9dy6kz4zvyz6mdgmhydps7khcq84n30irp983w1xh7gzs7"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 47987cd697..d3ea814011 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -228,6 +228,7 @@ library , optparse-applicative >=0.15 && <0.17 , random >=1.1 && <1.3 , record-hasfield ==1.0.* + , scientific ==0.3.7.* , simple-logger ==0.1.* , simplexmq >=5.0 , socks ==0.6.* @@ -254,7 +255,7 @@ library bytestring ==0.10.* , process >=1.6 && <1.6.18 , template-haskell ==2.16.* - , text >=1.2.3.0 && <1.3 + , text >=1.2.4.0 && <1.3 executable simplex-bot main-is: Main.hs @@ -292,6 +293,7 @@ executable simplex-bot , optparse-applicative >=0.15 && <0.17 , random >=1.1 && <1.3 , record-hasfield ==1.0.* + , scientific ==0.3.7.* , simple-logger ==0.1.* , simplex-chat , simplexmq >=5.0 @@ -319,7 +321,7 @@ executable simplex-bot bytestring ==0.10.* , process >=1.6 && <1.6.18 , template-haskell ==2.16.* - , text >=1.2.3.0 && <1.3 + , text >=1.2.4.0 && <1.3 executable simplex-bot-advanced main-is: Main.hs @@ -357,6 +359,7 @@ executable simplex-bot-advanced , optparse-applicative >=0.15 && <0.17 , random >=1.1 && <1.3 , record-hasfield ==1.0.* + , scientific ==0.3.7.* , simple-logger ==0.1.* , simplex-chat , simplexmq >=5.0 @@ -384,7 +387,7 @@ executable simplex-bot-advanced bytestring ==0.10.* , process >=1.6 && <1.6.18 , template-haskell ==2.16.* - , text >=1.2.3.0 && <1.3 + , text >=1.2.4.0 && <1.3 executable simplex-broadcast-bot main-is: Main.hs @@ -425,6 +428,7 @@ executable simplex-broadcast-bot , optparse-applicative >=0.15 && <0.17 , random >=1.1 && <1.3 , record-hasfield ==1.0.* + , scientific ==0.3.7.* , simple-logger ==0.1.* , simplex-chat , simplexmq >=5.0 @@ -452,7 +456,7 @@ executable simplex-broadcast-bot bytestring ==0.10.* , process >=1.6 && <1.6.18 , template-haskell ==2.16.* - , text >=1.2.3.0 && <1.3 + , text >=1.2.4.0 && <1.3 executable simplex-chat main-is: Main.hs @@ -491,6 +495,7 @@ executable simplex-chat , optparse-applicative >=0.15 && <0.17 , random >=1.1 && <1.3 , record-hasfield ==1.0.* + , scientific ==0.3.7.* , simple-logger ==0.1.* , simplex-chat , simplexmq >=5.0 @@ -519,7 +524,7 @@ executable simplex-chat bytestring ==0.10.* , process >=1.6 && <1.6.18 , template-haskell ==2.16.* - , text >=1.2.3.0 && <1.3 + , text >=1.2.4.0 && <1.3 executable simplex-directory-service main-is: Main.hs @@ -563,6 +568,7 @@ executable simplex-directory-service , optparse-applicative >=0.15 && <0.17 , random >=1.1 && <1.3 , record-hasfield ==1.0.* + , scientific ==0.3.7.* , simple-logger ==0.1.* , simplex-chat , simplexmq >=5.0 @@ -590,7 +596,7 @@ executable simplex-directory-service bytestring ==0.10.* , process >=1.6 && <1.6.18 , template-haskell ==2.16.* - , text >=1.2.3.0 && <1.3 + , text >=1.2.4.0 && <1.3 test-suite simplex-chat-test type: exitcode-stdio-1.0 @@ -664,6 +670,7 @@ test-suite simplex-chat-test , optparse-applicative >=0.15 && <0.17 , random >=1.1 && <1.3 , record-hasfield ==1.0.* + , scientific ==0.3.7.* , silently ==1.2.* , simple-logger ==0.1.* , simplex-chat @@ -692,7 +699,7 @@ test-suite simplex-chat-test bytestring ==0.10.* , process >=1.6 && <1.6.18 , template-haskell ==2.16.* - , text >=1.2.3.0 && <1.3 + , text >=1.2.4.0 && <1.3 if impl(ghc >= 9.6.2) build-depends: hspec ==2.11.* diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index c517aa52d5..86b6a5e51b 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -6,6 +6,7 @@ {-# LANGUAGE LambdaCase #-} {-# LANGUAGE MultiWayIf #-} {-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedLists #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE RankNTypes #-} @@ -43,7 +44,7 @@ import Data.Functor (($>)) import Data.Functor.Identity import Data.Int (Int64) import Data.List (find, foldl', isSuffixOf, mapAccumL, partition, sortOn, zipWith4) -import Data.List.NonEmpty (NonEmpty (..), nonEmpty, toList, (<|)) +import Data.List.NonEmpty (NonEmpty (..), (<|)) import qualified Data.List.NonEmpty as L import Data.Map.Strict (Map) import qualified Data.Map.Strict as M @@ -54,6 +55,7 @@ import qualified Data.Text as T import Data.Text.Encoding (decodeLatin1, encodeUtf8) import Data.Time (NominalDiffTime, addUTCTime, defaultTimeLocale, formatTime) import Data.Time.Clock (UTCTime, diffUTCTime, getCurrentTime, nominalDay, nominalDiffTimeToSeconds) +import Data.Type.Equality import qualified Data.UUID as UUID import qualified Data.UUID.V4 as V4 import Data.Word (Word32) @@ -98,7 +100,7 @@ import qualified Simplex.FileTransfer.Transport as XFTP import Simplex.FileTransfer.Types (FileErrorType (..), RcvFileId, SndFileId) import Simplex.Messaging.Agent as Agent import Simplex.Messaging.Agent.Client (SubInfo (..), agentClientStore, getAgentQueuesInfo, getAgentWorkersDetails, getAgentWorkersSummary, getFastNetworkConfig, ipAddressProtected, withLockMap) -import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), OperatorId, ServerCfg (..), allRoles, createAgentStore, defaultAgentConfig, enabledServerCfg, presetServerCfg) +import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), ServerCfg (..), ServerRoles (..), allRoles, createAgentStore, defaultAgentConfig) import Simplex.Messaging.Agent.Lock (withLock) import Simplex.Messaging.Agent.Protocol import qualified Simplex.Messaging.Agent.Protocol as AP (AgentErrorType (..)) @@ -138,6 +140,32 @@ import qualified UnliftIO.Exception as E import UnliftIO.IO (hClose, hSeek, hTell, openFile) import UnliftIO.STM +operatorSimpleXChat :: NewServerOperator +operatorSimpleXChat = + ServerOperator + { operatorId = DBNewEntity, + operatorTag = Just OTSimplex, + tradeName = "SimpleX Chat", + legalName = Just "SimpleX Chat Ltd", + serverDomains = ["simplex.im"], + conditionsAcceptance = CARequired Nothing, + enabled = True, + roles = allRoles + } + +operatorFlux :: NewServerOperator +operatorFlux = + ServerOperator + { operatorId = DBNewEntity, + operatorTag = Just OTFlux, + tradeName = "Flux", + legalName = Just "InFlux Technologies Limited", + serverDomains = ["simplexonflux.com"], + conditionsAcceptance = CARequired Nothing, + enabled = False, + roles = ServerRoles {storage = False, proxy = True} + } + defaultChatConfig :: ChatConfig defaultChatConfig = ChatConfig @@ -148,13 +176,25 @@ defaultChatConfig = }, chatVRange = supportedChatVRange, confirmMigrations = MCConsole, - defaultServers = - DefaultAgentServers - { smp = _defaultSMPServers, - useSMP = 4, + presetServers = + PresetServers + { operators = + [ PresetOperator + { operator = Just operatorSimpleXChat, + smp = simplexChatSMPServers, + useSMP = 4, + xftp = map (presetServer True) $ L.toList defaultXFTPServers, + useXFTP = 3 + }, + PresetOperator + { operator = Just operatorFlux, + smp = fluxSMPServers, + useSMP = 3, + xftp = fluxXFTPServers, + useXFTP = 3 + } + ], ntf = _defaultNtfServers, - xftp = L.map (presetServerCfg True allRoles operatorSimpleXChat) defaultXFTPServers, - useXFTP = L.length defaultXFTPServers, netCfg = defaultNetworkConfig }, tbqSize = 1024, @@ -178,32 +218,52 @@ defaultChatConfig = chatHooks = defaultChatHooks } -_defaultSMPServers :: NonEmpty (ServerCfg 'PSMP) -_defaultSMPServers = - L.fromList $ - map - (presetServerCfg True allRoles operatorSimpleXChat) - [ "smp://0YuTwO05YJWS8rkjn9eLJDjQhFKvIYd8d4xG8X1blIU=@smp8.simplex.im,beccx4yfxxbvyhqypaavemqurytl6hozr47wfc7uuecacjqdvwpw2xid.onion", - "smp://SkIkI6EPd2D63F4xFKfHk7I1UGZVNn6k1QWZ5rcyr6w=@smp9.simplex.im,jssqzccmrcws6bhmn77vgmhfjmhwlyr3u7puw4erkyoosywgl67slqqd.onion", - "smp://6iIcWT_dF2zN_w5xzZEY7HI2Prbh3ldP07YTyDexPjE=@smp10.simplex.im,rb2pbttocvnbrngnwziclp2f4ckjq65kebafws6g4hy22cdaiv5dwjqd.onion", - "smp://1OwYGt-yqOfe2IyVHhxz3ohqo3aCCMjtB-8wn4X_aoY=@smp11.simplex.im,6ioorbm6i3yxmuoezrhjk6f6qgkc4syabh7m3so74xunb5nzr4pwgfqd.onion", - "smp://UkMFNAXLXeAAe0beCa4w6X_zp18PwxSaSjY17BKUGXQ=@smp12.simplex.im,ie42b5weq7zdkghocs3mgxdjeuycheeqqmksntj57rmejagmg4eor5yd.onion", - "smp://enEkec4hlR3UtKx2NMpOUK_K4ZuDxjWBO1d9Y4YXVaA=@smp14.simplex.im,aspkyu2sopsnizbyfabtsicikr2s4r3ti35jogbcekhm3fsoeyjvgrid.onion", - "smp://h--vW7ZSkXPeOUpfxlFGgauQmXNFOzGoizak7Ult7cw=@smp15.simplex.im,oauu4bgijybyhczbnxtlggo6hiubahmeutaqineuyy23aojpih3dajad.onion", - "smp://hejn2gVIqNU6xjtGM3OwQeuk8ZEbDXVJXAlnSBJBWUA=@smp16.simplex.im,p3ktngodzi6qrf7w64mmde3syuzrv57y55hxabqcq3l5p6oi7yzze6qd.onion", - "smp://ZKe4uxF4Z_aLJJOEsC-Y6hSkXgQS5-oc442JQGkyP8M=@smp17.simplex.im,ogtwfxyi3h2h5weftjjpjmxclhb5ugufa5rcyrmg7j4xlch7qsr5nuqd.onion", - "smp://PtsqghzQKU83kYTlQ1VKg996dW4Cw4x_bvpKmiv8uns=@smp18.simplex.im,lyqpnwbs2zqfr45jqkncwpywpbtq7jrhxnib5qddtr6npjyezuwd3nqd.onion", - "smp://N_McQS3F9TGoh4ER0QstUf55kGnNSd-wXfNPZ7HukcM=@smp19.simplex.im,i53bbtoqhlc365k6kxzwdp5w3cdt433s7bwh3y32rcbml2vztiyyz5id.onion" +simplexChatSMPServers :: [NewUserServer 'PSMP] +simplexChatSMPServers = + map + (presetServer True) + [ "smp://0YuTwO05YJWS8rkjn9eLJDjQhFKvIYd8d4xG8X1blIU=@smp8.simplex.im,beccx4yfxxbvyhqypaavemqurytl6hozr47wfc7uuecacjqdvwpw2xid.onion", + "smp://SkIkI6EPd2D63F4xFKfHk7I1UGZVNn6k1QWZ5rcyr6w=@smp9.simplex.im,jssqzccmrcws6bhmn77vgmhfjmhwlyr3u7puw4erkyoosywgl67slqqd.onion", + "smp://6iIcWT_dF2zN_w5xzZEY7HI2Prbh3ldP07YTyDexPjE=@smp10.simplex.im,rb2pbttocvnbrngnwziclp2f4ckjq65kebafws6g4hy22cdaiv5dwjqd.onion", + "smp://1OwYGt-yqOfe2IyVHhxz3ohqo3aCCMjtB-8wn4X_aoY=@smp11.simplex.im,6ioorbm6i3yxmuoezrhjk6f6qgkc4syabh7m3so74xunb5nzr4pwgfqd.onion", + "smp://UkMFNAXLXeAAe0beCa4w6X_zp18PwxSaSjY17BKUGXQ=@smp12.simplex.im,ie42b5weq7zdkghocs3mgxdjeuycheeqqmksntj57rmejagmg4eor5yd.onion", + "smp://enEkec4hlR3UtKx2NMpOUK_K4ZuDxjWBO1d9Y4YXVaA=@smp14.simplex.im,aspkyu2sopsnizbyfabtsicikr2s4r3ti35jogbcekhm3fsoeyjvgrid.onion", + "smp://h--vW7ZSkXPeOUpfxlFGgauQmXNFOzGoizak7Ult7cw=@smp15.simplex.im,oauu4bgijybyhczbnxtlggo6hiubahmeutaqineuyy23aojpih3dajad.onion", + "smp://hejn2gVIqNU6xjtGM3OwQeuk8ZEbDXVJXAlnSBJBWUA=@smp16.simplex.im,p3ktngodzi6qrf7w64mmde3syuzrv57y55hxabqcq3l5p6oi7yzze6qd.onion", + "smp://ZKe4uxF4Z_aLJJOEsC-Y6hSkXgQS5-oc442JQGkyP8M=@smp17.simplex.im,ogtwfxyi3h2h5weftjjpjmxclhb5ugufa5rcyrmg7j4xlch7qsr5nuqd.onion", + "smp://PtsqghzQKU83kYTlQ1VKg996dW4Cw4x_bvpKmiv8uns=@smp18.simplex.im,lyqpnwbs2zqfr45jqkncwpywpbtq7jrhxnib5qddtr6npjyezuwd3nqd.onion", + "smp://N_McQS3F9TGoh4ER0QstUf55kGnNSd-wXfNPZ7HukcM=@smp19.simplex.im,i53bbtoqhlc365k6kxzwdp5w3cdt433s7bwh3y32rcbml2vztiyyz5id.onion" + ] + <> map + (presetServer False) + [ "smp://u2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU=@smp4.simplex.im,o5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion", + "smp://hpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg=@smp5.simplex.im,jjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion", + "smp://PQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo=@smp6.simplex.im,bylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion" ] - <> map - (presetServerCfg False allRoles operatorSimpleXChat) - [ "smp://u2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU=@smp4.simplex.im,o5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion", - "smp://hpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg=@smp5.simplex.im,jjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion", - "smp://PQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo=@smp6.simplex.im,bylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion" - ] -operatorSimpleXChat :: Maybe OperatorId -operatorSimpleXChat = Just 1 +fluxSMPServers :: [NewUserServer 'PSMP] +fluxSMPServers = + map + (presetServer True) + [ "smp://xQW_ufMkGE20UrTlBl8QqceG1tbuylXhr9VOLPyRJmw=@smp1.simplexonflux.com,qb4yoanyl4p7o33yrknv4rs6qo7ugeb2tu2zo66sbebezs4cpyosarid.onion", + "smp://LDnWZVlAUInmjmdpQQoIo6FUinRXGe0q3zi5okXDE4s=@smp2.simplexonflux.com,yiqtuh3q4x7hgovkomafsod52wvfjucdljqbbipg5sdssnklgongxbqd.onion", + "smp://1jne379u7IDJSxAvXbWb_JgoE7iabcslX0LBF22Rej0=@smp3.simplexonflux.com,a5lm4k7ufei66cdck6fy63r4lmkqy3dekmmb7jkfdm5ivi6kfaojshad.onion", + "smp://xmAmqj75I9mWrUihLUlI0ZuNLXlIwFIlHRq5Pb6cHAU=@smp4.simplexonflux.com,qpcz2axyy66u26hfdd2e23uohcf3y6c36mn7dcuilcgnwjasnrvnxjqd.onion", + "smp://rWvBYyTamuRCBYb_KAn-nsejg879ndhiTg5Sq3k0xWA=@smp5.simplexonflux.com,4ao347qwiuluyd45xunmii4skjigzuuox53hpdsgbwxqafd4yrticead.onion", + "smp://PN7-uqLBToqlf1NxHEaiL35lV2vBpXq8Nj8BW11bU48=@smp6.simplexonflux.com,hury6ot3ymebbr2535mlp7gcxzrjpc6oujhtfxcfh2m4fal4xw5fq6qd.onion" + ] + +fluxXFTPServers :: [NewUserServer 'PXFTP] +fluxXFTPServers = + map + (presetServer True) + [ "xftp://92Sctlc09vHl_nAqF2min88zKyjdYJ9mgxRCJns5K2U=@xftp1.simplexonflux.com,apl3pumq3emwqtrztykyyoomdx4dg6ysql5zek2bi3rgznz7ai3odkid.onion", + "xftp://YBXy4f5zU1CEhnbbCzVWTNVNsaETcAGmYqGNxHntiE8=@xftp2.simplexonflux.com,c5jjecisncnngysah3cz2mppediutfelco4asx65mi75d44njvua3xid.onion", + "xftp://ARQO74ZSvv2OrulRF3CdgwPz_AMy27r0phtLSq5b664=@xftp3.simplexonflux.com,dc4mohiubvbnsdfqqn7xhlhpqs5u4tjzp7xpz6v6corwvzvqjtaqqiqd.onion", + "xftp://ub2jmAa9U0uQCy90O-fSUNaYCj6sdhl49Jh3VpNXP58=@xftp4.simplexonflux.com,4qq5pzier3i4yhpuhcrhfbl6j25udc4czoyascrj4yswhodhfwev3nyd.onion", + "xftp://Rh19D5e4Eez37DEE9hAlXDB3gZa1BdFYJTPgJWPO9OI=@xftp5.simplexonflux.com,q7itltdn32hjmgcqwhow4tay5ijetng3ur32bolssw32fvc5jrwvozad.onion", + "xftp://0AznwoyfX8Od9T_acp1QeeKtxUi676IBIiQjXVwbdyU=@xftp6.simplexonflux.com,upvzf23ou6nrmaf3qgnhd6cn3d74tvivlmz3p7wdfwq6fhthjrjiiqid.onion" + ] _defaultNtfServers :: [NtfServer] _defaultNtfServers = @@ -240,16 +300,19 @@ newChatController :: ChatDatabase -> Maybe User -> ChatConfig -> ChatOpts -> Boo newChatController ChatDatabase {chatStore, agentStore} user - cfg@ChatConfig {agentConfig = aCfg, defaultServers, inlineFiles, deviceNameForRemote, confirmMigrations} + cfg@ChatConfig {agentConfig = aCfg, presetServers, inlineFiles, deviceNameForRemote, confirmMigrations} ChatOpts {coreOptions = CoreChatOpts {smpServers, xftpServers, simpleNetCfg, logLevel, logConnections, logServerHosts, logFile, tbqSize, highlyAvailable, yesToUpMigrations}, deviceName, optFilesFolder, optTempDirectory, showReactions, allowInstantFiles, autoAcceptFileSize} backgroundMode = do let inlineFiles' = if allowInstantFiles || autoAcceptFileSize > 0 then inlineFiles else inlineFiles {sendChunks = 0, receiveInstant = False} confirmMigrations' = if confirmMigrations == MCConsole && yesToUpMigrations then MCYesUp else confirmMigrations - config = cfg {logLevel, showReactions, tbqSize, subscriptionEvents = logConnections, hostEvents = logServerHosts, defaultServers = configServers, inlineFiles = inlineFiles', autoAcceptFileSize, highlyAvailable, confirmMigrations = confirmMigrations'} + config = cfg {logLevel, showReactions, tbqSize, subscriptionEvents = logConnections, hostEvents = logServerHosts, presetServers = presetServers', inlineFiles = inlineFiles', autoAcceptFileSize, highlyAvailable, confirmMigrations = confirmMigrations'} firstTime = dbNew chatStore currentUser <- newTVarIO user + randomSMP <- randomPresetServers SPSMP presetServers' + randomXFTP <- randomPresetServers SPXFTP presetServers' + let randomServers = RandomServers {smpServers = randomSMP, xftpServers = randomXFTP} currentRemoteHost <- newTVarIO Nothing - servers <- agentServers config + servers <- withTransaction chatStore $ \db -> agentServers db config randomServers smpAgent <- getSMPAgentClient aCfg {tbqSize} servers agentStore backgroundMode agentAsync <- newTVarIO Nothing random <- liftIO C.newRandom @@ -285,6 +348,7 @@ newChatController ChatController { firstTime, currentUser, + randomServers, currentRemoteHost, smpAgent, agentAsync, @@ -322,28 +386,41 @@ newChatController contactMergeEnabled } where - configServers :: DefaultAgentServers - configServers = - let DefaultAgentServers {smp = defSmp, xftp = defXftp, netCfg} = defaultServers - smp' = maybe defSmp (L.map enabledServerCfg) (nonEmpty smpServers) - xftp' = maybe defXftp (L.map enabledServerCfg) (nonEmpty xftpServers) - in defaultServers {smp = smp', xftp = xftp', netCfg = updateNetworkConfig netCfg simpleNetCfg} - agentServers :: ChatConfig -> IO InitialAgentServers - agentServers config@ChatConfig {defaultServers = defServers@DefaultAgentServers {ntf, netCfg}} = do - users <- withTransaction chatStore getUsers - smp' <- getUserServers users SPSMP - xftp' <- getUserServers users SPXFTP + presetServers' :: PresetServers + presetServers' = presetServers {operators = operators', netCfg = netCfg'} + where + PresetServers {operators, netCfg} = presetServers + netCfg' = updateNetworkConfig netCfg simpleNetCfg + operators' = case (smpServers, xftpServers) of + ([], []) -> operators + (smpSrvs, []) -> L.map disableSMP operators <> [custom smpSrvs []] + ([], xftpSrvs) -> L.map disableXFTP operators <> [custom [] xftpSrvs] + (smpSrvs, xftpSrvs) -> [custom smpSrvs xftpSrvs] + disableSMP op@PresetOperator {smp} = (op :: PresetOperator) {smp = map disableSrv smp} + disableXFTP op@PresetOperator {xftp} = (op :: PresetOperator) {xftp = map disableSrv xftp} + disableSrv :: forall p. NewUserServer p -> NewUserServer p + disableSrv srv = (srv :: NewUserServer p) {enabled = False} + custom smpSrvs xftpSrvs = + PresetOperator + { operator = Nothing, + smp = map newUserServer smpSrvs, + useSMP = 0, + xftp = map newUserServer xftpSrvs, + useXFTP = 0 + } + agentServers :: DB.Connection -> ChatConfig -> RandomServers -> IO InitialAgentServers + agentServers db ChatConfig {presetServers = PresetServers {operators = presetOps, ntf, netCfg}} rs = do + users <- getUsers db + opDomains <- operatorDomains <$> getUpdateServerOperators db presetOps (null users) + smp' <- getServers SPSMP users opDomains + xftp' <- getServers SPXFTP users opDomains pure InitialAgentServers {smp = smp', xftp = xftp', ntf, netCfg} where - getUserServers :: forall p. (ProtocolTypeI p, UserProtocol p) => [User] -> SProtocolType p -> IO (Map UserId (NonEmpty (ServerCfg p))) - getUserServers users protocol = case users of - [] -> pure $ M.fromList [(1, cfgServers protocol defServers)] - _ -> M.fromList <$> initialServers - where - initialServers :: IO [(UserId, NonEmpty (ServerCfg p))] - initialServers = mapM (\u -> (aUserId u,) <$> userServers u) users - userServers :: User -> IO (NonEmpty (ServerCfg p)) - userServers user' = useServers config protocol <$> withTransaction chatStore (`getProtocolServers` user') + getServers :: forall p. (ProtocolTypeI p, UserProtocol p) => SProtocolType p -> [User] -> [(Text, ServerOperator)] -> IO (Map UserId (NonEmpty (ServerCfg p))) + getServers p users opDomains = do + let rs' = rndServers p rs + fmap M.fromList $ forM users $ \u -> + (aUserId u,) . agentServerCfgs opDomains rs' <$> getUpdateUserServers db p presetOps rs' u updateNetworkConfig :: NetworkConfig -> SimpleNetCfg -> NetworkConfig updateNetworkConfig cfg SimpleNetCfg {socksProxy, socksMode, hostMode, requiredHostMode, smpProxyMode_, smpProxyFallback_, smpWebPort, tcpTimeout_, logTLSErrors} = @@ -386,33 +463,37 @@ withFileLock :: String -> Int64 -> CM a -> CM a withFileLock name = withEntityLock name . CLFile {-# INLINE withFileLock #-} -useServers :: UserProtocol p => ChatConfig -> SProtocolType p -> [ServerCfg p] -> NonEmpty (ServerCfg p) -useServers ChatConfig {defaultServers} p = fromMaybe (cfgServers p defaultServers) . nonEmpty +serverCfg :: ProtoServerWithAuth p -> ServerCfg p +serverCfg server = ServerCfg {server, operator = Nothing, enabled = True, roles = allRoles} -randomServers :: forall p. UserProtocol p => SProtocolType p -> ChatConfig -> IO (NonEmpty (ServerCfg p), [ServerCfg p]) -randomServers p ChatConfig {defaultServers} = do - let srvs = cfgServers p defaultServers - (enbldSrvs, dsbldSrvs) = L.partition (\ServerCfg {enabled} -> enabled) srvs - toUse = cfgServersToUse p defaultServers - if length enbldSrvs <= toUse - then pure (srvs, []) - else do - (enbldSrvs', srvsToDisable) <- splitAt toUse <$> shuffle enbldSrvs - let dsbldSrvs' = map (\srv -> (srv :: ServerCfg p) {enabled = False}) srvsToDisable - srvs' = sortOn server' $ enbldSrvs' <> dsbldSrvs' <> dsbldSrvs - pure (fromMaybe srvs $ L.nonEmpty srvs', srvs') +useServers :: forall p. UserProtocol p => SProtocolType p -> RandomServers -> [UserServer p] -> NonEmpty (NewUserServer p) +useServers p rs servers = case L.nonEmpty servers of + Nothing -> rndServers p rs + Just srvs -> L.map (\srv -> (srv :: UserServer p) {serverId = DBNewEntity}) srvs + +rndServers :: UserProtocol p => SProtocolType p -> RandomServers -> NonEmpty (NewUserServer p) +rndServers p RandomServers {smpServers, xftpServers} = case p of + SPSMP -> smpServers + SPXFTP -> xftpServers + +randomPresetServers :: forall p. UserProtocol p => SProtocolType p -> PresetServers -> IO (NonEmpty (NewUserServer p)) +randomPresetServers p PresetServers {operators} = toJust . L.nonEmpty . concat =<< mapM opSrvs operators where - server' ServerCfg {server = ProtoServerWithAuth srv _} = srv - -cfgServers :: UserProtocol p => SProtocolType p -> DefaultAgentServers -> NonEmpty (ServerCfg p) -cfgServers p DefaultAgentServers {smp, xftp} = case p of - SPSMP -> smp - SPXFTP -> xftp - -cfgServersToUse :: UserProtocol p => SProtocolType p -> DefaultAgentServers -> Int -cfgServersToUse p DefaultAgentServers {useSMP, useXFTP} = case p of - SPSMP -> useSMP - SPXFTP -> useXFTP + toJust = \case + Just a -> pure a + Nothing -> E.throwIO $ userError "no preset servers" + opSrvs :: PresetOperator -> IO [NewUserServer p] + opSrvs op = do + let srvs = operatorServers p op + toUse = operatorServersToUse p op + (enbldSrvs, dsbldSrvs) = partition (\UserServer {enabled} -> enabled) srvs + if toUse <= 0 || toUse >= length enbldSrvs + then pure srvs + else do + (enbldSrvs', srvsToDisable) <- splitAt toUse <$> shuffle enbldSrvs + let dsbldSrvs' = map (\srv -> (srv :: NewUserServer p) {enabled = False}) srvsToDisable + pure $ sortOn server' $ enbldSrvs' <> dsbldSrvs' <> dsbldSrvs + server' UserServer {server = ProtoServerWithAuth srv _} = srv -- enableSndFiles has no effect when mainApp is True startChatController :: Bool -> Bool -> CM' (Async ()) @@ -556,19 +637,24 @@ processChatCommand' vr = \case forM_ profile $ \Profile {displayName} -> checkValidName displayName p@Profile {displayName} <- liftIO $ maybe generateRandomProfile pure profile u <- asks currentUser - (smp, smpServers) <- chooseServers SPSMP - (xftp, xftpServers) <- chooseServers SPXFTP + smpServers <- chooseServers SPSMP + xftpServers <- chooseServers SPXFTP users <- withFastStore' getUsers forM_ users $ \User {localDisplayName = n, activeUser, viewPwdHash} -> when (n == displayName) . throwChatError $ if activeUser || isNothing viewPwdHash then CEUserExists displayName else CEInvalidDisplayName {displayName, validName = ""} + opDomains <- operatorDomains . fst <$> withFastStore getServerOperators + rs <- asks randomServers + let smp = agentServerCfgs opDomains (rndServers SPSMP rs) smpServers + xftp = agentServerCfgs opDomains (rndServers SPXFTP rs) xftpServers auId <- withAgent (\a -> createUser a smp xftp) ts <- liftIO $ getCurrentTime >>= if pastTimestamp then coupleDaysAgo else pure user <- withFastStore $ \db -> createUserRecordAt db (AgentUserId auId) p True ts createPresetContactCards user `catchChatError` \_ -> pure () - withFastStore $ \db -> createNoteFolder db user - storeServers user smpServers - storeServers user xftpServers + withFastStore $ \db -> do + createNoteFolder db user + liftIO $ mapM_ (insertProtocolServer db SPSMP user ts) $ useServers SPSMP rs smpServers + liftIO $ mapM_ (insertProtocolServer db SPXFTP user ts) $ useServers SPXFTP rs xftpServers atomically . writeTVar u $ Just user pure $ CRActiveUser user where @@ -577,18 +663,10 @@ processChatCommand' vr = \case withFastStore $ \db -> do createContact db user simplexStatusContactProfile createContact db user simplexTeamContactProfile - chooseServers :: (ProtocolTypeI p, UserProtocol p) => SProtocolType p -> CM (NonEmpty (ServerCfg p), [ServerCfg p]) - chooseServers protocol = - asks currentUser >>= readTVarIO >>= \case - Nothing -> asks config >>= liftIO . randomServers protocol - Just user -> chosenServers =<< withFastStore' (`getProtocolServers` user) - where - chosenServers servers = do - cfg <- asks config - pure (useServers cfg protocol servers, servers) - storeServers user servers = - unless (null servers) . withFastStore $ - \db -> overwriteProtocolServers db user servers + chooseServers :: forall p. ProtocolTypeI p => SProtocolType p -> CM [UserServer p] + chooseServers p = do + srvs <- chatReadVar currentUser >>= mapM (\user -> withFastStore' $ \db -> getProtocolServers db p user) + pure $ fromMaybe [] srvs coupleDaysAgo t = (`addUTCTime` t) . fromInteger . negate . (+ (2 * day)) <$> randomRIO (0, day) day = 86400 ListUsers -> CRUsersList <$> withFastStore' getUsersInfo @@ -1486,57 +1564,67 @@ processChatCommand' vr = \case msgs <- lift $ withAgent' $ \a -> getConnectionMessages a acIds let ntfMsgs = L.map (\msg -> receivedMsgInfo <$> msg) msgs pure $ CRConnNtfMessages ntfMsgs - APIGetUserProtoServers userId (AProtocolType p) -> withUserId userId $ \user -> withServerProtocol p $ do - cfg@ChatConfig {defaultServers} <- asks config - srvs <- withFastStore' (`getProtocolServers` user) - (operators, _) <- withFastStore $ \db -> getServerOperators db - let servers = AUPS $ UserProtoServers p (useServers cfg p srvs) (cfgServers p defaultServers) - pure $ CRUserProtoServers {user, servers, operators} - GetUserProtoServers aProtocol -> withUser $ \User {userId} -> - processChatCommand $ APIGetUserProtoServers userId aProtocol - APISetUserProtoServers userId (APSC p (ProtoServersConfig servers)) - | null servers || any (\ServerCfg {enabled} -> enabled) servers -> withUserId userId $ \user -> withServerProtocol p $ do - withFastStore $ \db -> overwriteProtocolServers db user servers - cfg <- asks config - lift $ withAgent' $ \a -> setProtocolServers a (aUserId user) $ useServers cfg p servers - ok user - | otherwise -> withUserId userId $ \user -> pure $ chatCmdError (Just user) "all servers are disabled" - SetUserProtoServers serversConfig -> withUser $ \User {userId} -> - processChatCommand $ APISetUserProtoServers userId serversConfig + GetUserProtoServers (AProtocolType p) -> withUser $ \user -> withServerProtocol p $ do + srvs <- withFastStore (`getUserServers` user) + CRUserServers user <$> liftIO (groupedServers srvs p) + where + groupedServers :: UserProtocol p => ([ServerOperator], [UserServer 'PSMP], [UserServer 'PXFTP]) -> SProtocolType p -> IO [UserOperatorServers] + groupedServers (operators, smpServers, xftpServers) = \case + SPSMP -> groupByOperator (operators, smpServers, []) + SPXFTP -> groupByOperator (operators, [], xftpServers) + SetUserProtoServers (AProtocolType (p :: SProtocolType p)) srvs -> withUser $ \user@User {userId} -> withServerProtocol p $ do + srvs' <- mapM aUserServer srvs + userServers_ <- liftIO . groupByOperator =<< withFastStore (`getUserServers` user) + case L.nonEmpty userServers_ of + Nothing -> throwChatError $ CECommandError "no servers" + Just userServers -> case srvs of + [] -> throwChatError $ CECommandError "no servers" + _ -> processChatCommand $ APISetUserServers userId $ L.map (updatedSrvs p) userServers + where + -- disable preset and replace custom servers (groupByOperator always adds custom) + updatedSrvs :: UserProtocol p => SProtocolType p -> UserOperatorServers -> UpdatedUserOperatorServers + updatedSrvs p' UserOperatorServers {operator, smpServers, xftpServers} = case p' of + SPSMP -> u (updateSrvs smpServers, map (AUS SDBStored) xftpServers) + SPXFTP -> u (map (AUS SDBStored) smpServers, updateSrvs xftpServers) + where + u = uncurry $ UpdatedUserOperatorServers operator + updateSrvs :: [UserServer p] -> [AUserServer p] + updateSrvs pSrvs = map disableSrv pSrvs <> maybe srvs' (const []) operator + disableSrv srv@UserServer {preset} = + AUS SDBStored $ if preset then srv {enabled = False} else srv {deleted = True} + where + aUserServer :: AProtoServerWithAuth -> CM (AUserServer p) + aUserServer (AProtoServerWithAuth p' srv) = case testEquality p p' of + Just Refl -> pure $ AUS SDBNew $ newUserServer srv + Nothing -> throwChatError $ CECommandError $ "incorrect server protocol: " <> B.unpack (strEncode srv) APITestProtoServer userId srv@(AProtoServerWithAuth _ server) -> withUserId userId $ \user -> lift $ CRServerTestResult user srv <$> withAgent' (\a -> testProtocolServer a (aUserId user) server) TestProtoServer srv -> withUser $ \User {userId} -> processChatCommand $ APITestProtoServer userId srv - APIGetServerOperators -> do - (operators, conditionsAction) <- withFastStore $ \db -> getServerOperators db - pure $ CRServerOperators operators conditionsAction - APISetServerOperators operatorsEnabled -> do - (operators, conditionsAction) <- withFastStore $ \db -> setServerOperators db operatorsEnabled - pure $ CRServerOperators operators conditionsAction - APIGetUserServers userId -> withUserId userId $ \user -> do - (operators, smpServers, xftpServers) <- withFastStore $ \db -> do - (operators, _) <- getServerOperators db - smpServers <- liftIO $ getServers db user SPSMP - xftpServers <- liftIO $ getServers db user SPXFTP - pure (operators, smpServers, xftpServers) - let userServers = groupByOperator operators smpServers xftpServers - pure $ CRUserServers user userServers - where - getServers :: ProtocolTypeI p => DB.Connection -> User -> SProtocolType p -> IO [ServerCfg p] - getServers db user _p = getProtocolServers db user + APIGetServerOperators -> uncurry CRServerOperators <$> withFastStore getServerOperators + APISetServerOperators operatorsEnabled -> withFastStore $ \db -> do + liftIO $ setServerOperators db operatorsEnabled + uncurry CRServerOperators <$> getServerOperators db + APIGetUserServers userId -> withUserId userId $ \user -> withFastStore $ \db -> + CRUserServers user <$> (liftIO . groupByOperator =<< getUserServers db user) APISetUserServers userId userServers -> withUserId userId $ \user -> do let errors = validateUserServers userServers unless (null errors) $ throwChatError (CECommandError $ "user servers validation error(s): " <> show errors) - withFastStore $ \db -> setUserServers db user userServers - -- TODO set protocol servers for agent + (operators, smpServers, xftpServers) <- withFastStore $ \db -> do + setUserServers db user userServers + getUserServers db user + let opDomains = operatorDomains operators + rs <- asks randomServers + lift $ withAgent' $ \a -> do + let auId = aUserId user + setProtocolServers a auId $ agentServerCfgs opDomains (rndServers SPSMP rs) smpServers + setProtocolServers a auId $ agentServerCfgs opDomains (rndServers SPXFTP rs) xftpServers ok_ - APIValidateServers userServers -> do - let errors = validateUserServers userServers - pure $ CRUserServersValidation errors + APIValidateServers userServers -> pure $ CRUserServersValidation $ validateUserServers userServers APIGetUsageConditions -> do (usageConditions, acceptedConditions) <- withFastStore $ \db -> do usageConditions <- getCurrentUsageConditions db - acceptedConditions <- getLatestAcceptedConditions db + acceptedConditions <- liftIO $ getLatestAcceptedConditions db pure (usageConditions, acceptedConditions) -- TODO if db commit is different from source commit, conditionsText should be nothing in response pure @@ -1545,14 +1633,14 @@ processChatCommand' vr = \case conditionsText = usageConditionsText, acceptedConditions } - APISetConditionsNotified conditionsId -> do + APISetConditionsNotified condId -> do currentTs <- liftIO getCurrentTime - withFastStore' $ \db -> setConditionsNotified db conditionsId currentTs + withFastStore' $ \db -> setConditionsNotified db condId currentTs ok_ - APIAcceptConditions conditionsId operators -> do + APIAcceptConditions condId opIds -> withFastStore $ \db -> do currentTs <- liftIO getCurrentTime - (operators', conditionsAction) <- withFastStore $ \db -> acceptConditions db conditionsId operators currentTs - pure $ CRServerOperators operators' conditionsAction + acceptConditions db condId opIds currentTs + uncurry CRServerOperators <$> getServerOperators db APISetChatItemTTL userId newTTL_ -> withUserId userId $ \user -> checkStoreNotChanged $ withChatLock "setChatItemTTL" $ do @@ -1805,8 +1893,9 @@ processChatCommand' vr = \case canKeepLink (CRInvitationUri crData _) newUser = do let ConnReqUriData {crSmpQueues = q :| _} = crData SMPQueueUri {queueAddress = SMPQueueAddress {smpServer}} = q - cfg <- asks config - newUserServers <- L.map (\ServerCfg {server} -> protoServer server) . useServers cfg SPSMP <$> withFastStore' (`getProtocolServers` newUser) + newUserServers <- + map protoServer' . filter (\ServerCfg {enabled} -> enabled) + <$> getKnownAgentServers SPSMP newUser pure $ smpServer `elem` newUserServers updateConnRecord user@User {userId} conn@PendingContactConnection {customUserProfileId} newUser = do withAgent $ \a -> changeConnectionUser a (aUserId user) (aConnId' conn) (aUserId newUser) @@ -2140,7 +2229,7 @@ processChatCommand' vr = \case where changeMemberRole user gInfo members m gEvent = do let GroupMember {memberId = mId, memberRole = mRole, memberStatus = mStatus, memberContactId, localDisplayName = cName} = m - assertUserGroupRole gInfo $ maximum [GRAdmin, mRole, memRole] + assertUserGroupRole gInfo $ maximum ([GRAdmin, mRole, memRole] :: [GroupMemberRole]) withGroupLock "memberRole" groupId . procCmd $ do unless (mRole == memRole) $ do withFastStore' $ \db -> updateGroupMemberRole db user m memRole @@ -2538,14 +2627,15 @@ processChatCommand' vr = \case pure $ CRAgentSubsTotal user subsTotal hasSession GetAgentServersSummary userId -> withUserId userId $ \user -> do agentServersSummary <- lift $ withAgent' getAgentServersSummary - cfg <- asks config - (users, smpServers, xftpServers) <- - withStore' $ \db -> (,,) <$> getUsers db <*> getServers db cfg user SPSMP <*> getServers db cfg user SPXFTP - let presentedServersSummary = toPresentedServersSummary agentServersSummary users user smpServers xftpServers _defaultNtfServers - pure $ CRAgentServersSummary user presentedServersSummary + withStore' $ \db -> do + users <- getUsers db + smpServers <- getServers db user SPSMP + xftpServers <- getServers db user SPXFTP + let presentedServersSummary = toPresentedServersSummary agentServersSummary users user smpServers xftpServers _defaultNtfServers + pure $ CRAgentServersSummary user presentedServersSummary where - getServers :: (ProtocolTypeI p, UserProtocol p) => DB.Connection -> ChatConfig -> User -> SProtocolType p -> IO (NonEmpty (ProtocolServer p)) - getServers db cfg user p = L.map (\ServerCfg {server} -> protoServer server) . useServers cfg p <$> getProtocolServers db user + getServers :: ProtocolTypeI p => DB.Connection -> User -> SProtocolType p -> IO [ProtocolServer p] + getServers db user p = map (\UserServer {server} -> protoServer server) <$> getProtocolServers db p user ResetAgentServersStats -> withAgent resetAgentServersStats >> ok_ GetAgentWorkers -> lift $ CRAgentWorkersSummary <$> withAgent' getAgentWorkersSummary GetAgentWorkersDetails -> lift $ CRAgentWorkersDetails <$> withAgent' getAgentWorkersDetails @@ -3663,8 +3753,7 @@ receiveViaCompleteFD user fileId RcvFileDescr {fileDescrText, fileDescrComplete} S.toList $ S.fromList $ concatMap (\FD.FileChunk {replicas} -> map (\FD.FileChunkReplica {server} -> server) replicas) chunks getUnknownSrvs :: [XFTPServer] -> CM [XFTPServer] getUnknownSrvs srvs = do - cfg <- asks config - knownSrvs <- L.map (\ServerCfg {server} -> protoServer server) . useServers cfg SPXFTP <$> withStore' (`getProtocolServers` user) + knownSrvs <- map protoServer' <$> getKnownAgentServers SPXFTP user pure $ filter (`notElem` knownSrvs) srvs ipProtectedForSrvs :: [XFTPServer] -> CM Bool ipProtectedForSrvs srvs = do @@ -3678,6 +3767,17 @@ receiveViaCompleteFD user fileId RcvFileDescr {fileDescrText, fileDescrComplete} toView $ CRChatItemUpdated user aci throwChatError $ CEFileNotApproved fileId unknownSrvs +getKnownAgentServers :: (ProtocolTypeI p, UserProtocol p) => SProtocolType p -> User -> CM [ServerCfg p] +getKnownAgentServers p user = do + rs <- asks randomServers + withStore $ \db -> do + opDomains <- operatorDomains . fst <$> getServerOperators db + srvs <- liftIO $ getProtocolServers db p user + pure $ L.toList $ agentServerCfgs opDomains (rndServers p rs) srvs + +protoServer' :: ServerCfg p -> ProtocolServer p +protoServer' ServerCfg {server} = protoServer server + getNetworkConfig :: CM' NetworkConfig getNetworkConfig = withAgent' $ liftIO . getFastNetworkConfig @@ -3876,7 +3976,7 @@ subscribeUserConnections vr onlyNeeded agentBatchSubscribe user = do (sftConns, sfts) <- getSndFileTransferConns (rftConns, rfts) <- getRcvFileTransferConns (pcConns, pcs) <- getPendingContactConns - let conns = concat [ctConns, ucConns, mConns, sftConns, rftConns, pcConns] + let conns = concat ([ctConns, ucConns, mConns, sftConns, rftConns, pcConns] :: [[ConnId]]) pure (conns, cts, ucs, gs, ms, sfts, rfts, pcs) -- subscribe using batched commands rs <- withAgent $ \a -> agentBatchSubscribe a conns @@ -4684,7 +4784,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = ctItem = AChatItem SCTDirect SMDSnd (DirectChat ct) SWITCH qd phase cStats -> do toView $ CRContactSwitch user ct (SwitchProgress qd phase cStats) - when (phase `elem` [SPStarted, SPCompleted]) $ case qd of + when (phase == SPStarted || phase == SPCompleted) $ case qd of QDRcv -> createInternalChatItem user (CDDirectSnd ct) (CISndConnEvent $ SCESwitchQueue phase Nothing) Nothing QDSnd -> createInternalChatItem user (CDDirectRcv ct) (CIRcvConnEvent $ RCESwitchQueue phase) Nothing RSYNC rss cryptoErr_ cStats -> @@ -4969,7 +5069,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = (Just fileDescrText, Just msgId) -> do partSize <- asks $ xftpDescrPartSize . config let parts = splitFileDescr partSize fileDescrText - pure . toList $ L.map (XMsgFileDescr msgId) parts + pure . L.toList $ L.map (XMsgFileDescr msgId) parts _ -> pure [] let fileDescrChatMsgs = map (ChatMessage senderVRange Nothing) fileDescrEvents GroupMember {memberId} = sender @@ -5095,7 +5195,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = when continued $ sendPendingGroupMessages user m conn SWITCH qd phase cStats -> do toView $ CRGroupMemberSwitch user gInfo m (SwitchProgress qd phase cStats) - when (phase `elem` [SPStarted, SPCompleted]) $ case qd of + when (phase == SPStarted || phase == SPCompleted) $ case qd of QDRcv -> createInternalChatItem user (CDGroupSnd gInfo) (CISndConnEvent . SCESwitchQueue phase . Just $ groupMemberRef m) Nothing QDSnd -> createInternalChatItem user (CDGroupRcv gInfo m) (CIRcvConnEvent $ RCESwitchQueue phase) Nothing RSYNC rss cryptoErr_ cStats -> @@ -6659,15 +6759,17 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = messageWarning "x.grp.mem.con: neither member is invitee" where inviteeXGrpMemCon :: GroupMemberIntro -> CM () - inviteeXGrpMemCon GroupMemberIntro {introId, introStatus} - | introStatus == GMIntroReConnected = updateStatus introId GMIntroConnected - | introStatus `elem` [GMIntroToConnected, GMIntroConnected] = pure () - | otherwise = updateStatus introId GMIntroToConnected + inviteeXGrpMemCon GroupMemberIntro {introId, introStatus} = case introStatus of + GMIntroReConnected -> updateStatus introId GMIntroConnected + GMIntroToConnected -> pure () + GMIntroConnected -> pure () + _ -> updateStatus introId GMIntroToConnected forwardMemberXGrpMemCon :: GroupMemberIntro -> CM () - forwardMemberXGrpMemCon GroupMemberIntro {introId, introStatus} - | introStatus == GMIntroToConnected = updateStatus introId GMIntroConnected - | introStatus `elem` [GMIntroReConnected, GMIntroConnected] = pure () - | otherwise = updateStatus introId GMIntroReConnected + forwardMemberXGrpMemCon GroupMemberIntro {introId, introStatus} = case introStatus of + GMIntroToConnected -> updateStatus introId GMIntroConnected + GMIntroReConnected -> pure () + GMIntroConnected -> pure () + _ -> updateStatus introId GMIntroReConnected updateStatus introId status = withStore' $ \db -> updateIntroStatus db introId status xGrpMemDel :: GroupInfo -> GroupMember -> MemberId -> RcvMessage -> UTCTime -> CM () @@ -8132,22 +8234,18 @@ chatCommandP = "/smp test " *> (TestProtoServer . AProtoServerWithAuth SPSMP <$> strP), "/xftp test " *> (TestProtoServer . AProtoServerWithAuth SPXFTP <$> strP), "/ntf test " *> (TestProtoServer . AProtoServerWithAuth SPNTF <$> strP), - "/_servers " *> (APISetUserProtoServers <$> A.decimal <* A.space <*> srvCfgP), - "/smp " *> (SetUserProtoServers . APSC SPSMP . ProtoServersConfig . map enabledServerCfg <$> protocolServersP), - "/smp default" $> SetUserProtoServers (APSC SPSMP $ ProtoServersConfig []), - "/xftp " *> (SetUserProtoServers . APSC SPXFTP . ProtoServersConfig . map enabledServerCfg <$> protocolServersP), - "/xftp default" $> SetUserProtoServers (APSC SPXFTP $ ProtoServersConfig []), - "/_servers " *> (APIGetUserProtoServers <$> A.decimal <* A.space <*> strP), + "/smp " *> (SetUserProtoServers (AProtocolType SPSMP) . map (AProtoServerWithAuth SPSMP) <$> protocolServersP), + "/xftp " *> (SetUserProtoServers (AProtocolType SPXFTP) . map (AProtoServerWithAuth SPXFTP) <$> protocolServersP), "/smp" $> GetUserProtoServers (AProtocolType SPSMP), "/xftp" $> GetUserProtoServers (AProtocolType SPXFTP), "/_operators" $> APIGetServerOperators, "/_operators " *> (APISetServerOperators <$> jsonP), - "/_user_servers " *> (APIGetUserServers <$> A.decimal), - "/_user_servers " *> (APISetUserServers <$> A.decimal <* A.space <*> jsonP), + "/_servers " *> (APIGetUserServers <$> A.decimal), + "/_servers " *> (APISetUserServers <$> A.decimal <* A.space <*> jsonP), "/_validate_servers " *> (APIValidateServers <$> jsonP), "/_conditions" $> APIGetUsageConditions, "/_conditions_notified " *> (APISetConditionsNotified <$> A.decimal), - "/_accept_conditions " *> (APIAcceptConditions <$> A.decimal <* A.space <*> jsonP), + "/_accept_conditions " *> (APIAcceptConditions <$> A.decimal <*> _strP), "/_ttl " *> (APISetChatItemTTL <$> A.decimal <* A.space <*> ciTTLDecimal), "/ttl " *> (SetChatItemTTL <$> ciTTL), "/_ttl " *> (APIGetChatItemTTL <$> A.decimal), @@ -8491,7 +8589,6 @@ chatCommandP = onOffP (Just <$> (AutoAccept <$> (" incognito=" *> onOffP <|> pure False) <*> optional (A.space *> msgContentP))) (pure Nothing) - srvCfgP = strP >>= \case AProtocolType p -> APSC p <$> (A.space *> jsonP) rcCtrlAddressP = RCCtrlAddress <$> ("addr=" *> strP) <*> (" iface=" *> (jsonP <|> text1P)) text1P = safeDecodeUtf8 <$> A.takeTill (== ' ') char_ = optional . A.char diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 8406e214c9..3c2b8045d7 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -35,7 +35,6 @@ import qualified Data.ByteArray as BA import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import Data.Char (ord) -import Data.Constraint (Dict (..)) import Data.Int (Int64) import Data.List.NonEmpty (NonEmpty) import Data.Map.Strict (Map) @@ -71,7 +70,7 @@ import Simplex.Chat.Util (liftIOEither) import Simplex.FileTransfer.Description (FileDescriptionURI) import Simplex.Messaging.Agent (AgentClient, SubscriptionsInfo) import Simplex.Messaging.Agent.Client (AgentLocks, AgentQueuesInfo (..), AgentWorkersDetails (..), AgentWorkersSummary (..), ProtocolTestFailure, SMPServerSubs, ServerQueueInfo, UserNetworkInfo) -import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, NetworkConfig, ServerCfg) +import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, NetworkConfig) import Simplex.Messaging.Agent.Lock import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation, SQLiteStore, UpMigration, withTransaction, withTransactionPriority) @@ -85,7 +84,7 @@ import Simplex.Messaging.Crypto.Ratchet (PQEncryption) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Notifications.Protocol (DeviceToken (..), NtfTknStatus) import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, parseAll, parseString, sumTypeJSON) -import Simplex.Messaging.Protocol (AProtoServerWithAuth, AProtocolType (..), CorrId, MsgId, NMsgMeta (..), NtfServer, ProtocolType (..), ProtocolTypeI, QueueId, SMPMsgMeta (..), SProtocolType, SubscriptionMode (..), UserProtocol, XFTPServer, userProtocol) +import Simplex.Messaging.Protocol (AProtoServerWithAuth, AProtocolType (..), CorrId, MsgId, NMsgMeta (..), NtfServer, ProtocolType (..), QueueId, SMPMsgMeta (..), SubscriptionMode (..), XFTPServer) import Simplex.Messaging.TMap (TMap) import Simplex.Messaging.Transport (TLS, simplexMQVersion) import Simplex.Messaging.Transport.Client (SocksProxyWithAuth, TransportHost) @@ -133,7 +132,7 @@ data ChatConfig = ChatConfig { agentConfig :: AgentConfig, chatVRange :: VersionRangeChat, confirmMigrations :: MigrationConfirmation, - defaultServers :: DefaultAgentServers, + presetServers :: PresetServers, tbqSize :: Natural, fileChunkSize :: Integer, xftpDescrPartSize :: Int, @@ -155,6 +154,12 @@ data ChatConfig = ChatConfig chatHooks :: ChatHooks } +data RandomServers = RandomServers + { smpServers :: NonEmpty (NewUserServer 'PSMP), + xftpServers :: NonEmpty (NewUserServer 'PXFTP) + } + deriving (Show) + -- The hooks can be used to extend or customize chat core in mobile or CLI clients. data ChatHooks = ChatHooks { -- preCmdHook can be used to process or modify the commands before they are processed. @@ -173,12 +178,9 @@ defaultChatHooks = eventHook = \_ -> pure } -data DefaultAgentServers = DefaultAgentServers - { smp :: NonEmpty (ServerCfg 'PSMP), - useSMP :: Int, +data PresetServers = PresetServers + { operators :: NonEmpty PresetOperator, ntf :: [NtfServer], - xftp :: NonEmpty (ServerCfg 'PXFTP), - useXFTP :: Int, netCfg :: NetworkConfig } @@ -204,6 +206,7 @@ data ChatDatabase = ChatDatabase {chatStore :: SQLiteStore, agentStore :: SQLite data ChatController = ChatController { currentUser :: TVar (Maybe User), + randomServers :: RandomServers, currentRemoteHost :: TVar (Maybe RemoteHostId), firstTime :: Bool, smpAgent :: AgentClient, @@ -347,20 +350,18 @@ data ChatCommand | APIGetGroupLink GroupId | APICreateMemberContact GroupId GroupMemberId | APISendMemberContactInvitation {contactId :: ContactId, msgContent_ :: Maybe MsgContent} - | APIGetUserProtoServers UserId AProtocolType | GetUserProtoServers AProtocolType - | APISetUserProtoServers UserId AProtoServersConfig - | SetUserProtoServers AProtoServersConfig + | SetUserProtoServers AProtocolType [AProtoServerWithAuth] | APITestProtoServer UserId AProtoServerWithAuth | TestProtoServer AProtoServerWithAuth | APIGetServerOperators - | APISetServerOperators (NonEmpty OperatorEnabled) + | APISetServerOperators (NonEmpty ServerOperator) | APIGetUserServers UserId - | APISetUserServers UserId (NonEmpty UserServers) - | APIValidateServers (NonEmpty UserServers) -- response is CRUserServersValidation + | APISetUserServers UserId (NonEmpty UpdatedUserOperatorServers) + | APIValidateServers (NonEmpty UpdatedUserOperatorServers) -- response is CRUserServersValidation | APIGetUsageConditions | APISetConditionsNotified Int64 - | APIAcceptConditions Int64 (NonEmpty ServerOperator) + | APIAcceptConditions Int64 (NonEmpty Int64) | APISetChatItemTTL UserId (Maybe Int64) | SetChatItemTTL (Maybe Int64) | APIGetChatItemTTL UserId @@ -586,10 +587,9 @@ data ChatResponse | CRChatItemInfo {user :: User, chatItem :: AChatItem, chatItemInfo :: ChatItemInfo} | CRChatItemId User (Maybe ChatItemId) | CRApiParsedMarkdown {formattedText :: Maybe MarkdownList} - | CRUserProtoServers {user :: User, servers :: AUserProtoServers, operators :: [ServerOperator]} | CRServerTestResult {user :: User, testServer :: AProtoServerWithAuth, testFailure :: Maybe ProtocolTestFailure} | CRServerOperators {operators :: [ServerOperator], conditionsAction :: Maybe UsageConditionsAction} - | CRUserServers {user :: User, userServers :: [UserServers]} + | CRUserServers {user :: User, userServers :: [UserOperatorServers]} | CRUserServersValidation {serverErrors :: [UserServersError]} | CRUsageConditions {usageConditions :: UsageConditions, conditionsText :: Text, acceptedConditions :: Maybe UsageConditions} | CRChatItemTTL {user :: User, chatItemTTL :: Maybe Int64} @@ -956,23 +956,23 @@ instance ToJSON AgentQueueId where toJSON = strToJSON toEncoding = strToJEncoding -data ProtoServersConfig p = ProtoServersConfig {servers :: [ServerCfg p]} - deriving (Show) +-- data ProtoServersConfig p = ProtoServersConfig {servers :: [ServerCfg p]} +-- deriving (Show) -data AProtoServersConfig = forall p. ProtocolTypeI p => APSC (SProtocolType p) (ProtoServersConfig p) +-- data AProtoServersConfig = forall p. ProtocolTypeI p => APSC (SProtocolType p) (ProtoServersConfig p) -deriving instance Show AProtoServersConfig +-- deriving instance Show AProtoServersConfig -data UserProtoServers p = UserProtoServers - { serverProtocol :: SProtocolType p, - protoServers :: NonEmpty (ServerCfg p), - presetServers :: NonEmpty (ServerCfg p) - } - deriving (Show) +-- data UserProtoServers p = UserProtoServers +-- { serverProtocol :: SProtocolType p, +-- protoServers :: NonEmpty (ServerCfg p), +-- presetServers :: NonEmpty (ServerCfg p) +-- } +-- deriving (Show) -data AUserProtoServers = forall p. (ProtocolTypeI p, UserProtocol p) => AUPS (UserProtoServers p) +-- data AUserProtoServers = forall p. (ProtocolTypeI p, UserProtocol p) => AUPS (UserProtoServers p) -deriving instance Show AUserProtoServers +-- deriving instance Show AUserProtoServers data ArchiveConfig = ArchiveConfig {archivePath :: FilePath, disableCompression :: Maybe Bool, parentTempDirectory :: Maybe FilePath} deriving (Show) @@ -1575,28 +1575,28 @@ $(JQ.deriveJSON defaultJSON ''CoreVersionInfo) $(JQ.deriveJSON defaultJSON ''SlowSQLQuery) -instance ProtocolTypeI p => FromJSON (ProtoServersConfig p) where - parseJSON = $(JQ.mkParseJSON defaultJSON ''ProtoServersConfig) +-- instance ProtocolTypeI p => FromJSON (ProtoServersConfig p) where +-- parseJSON = $(JQ.mkParseJSON defaultJSON ''ProtoServersConfig) -instance ProtocolTypeI p => FromJSON (UserProtoServers p) where - parseJSON = $(JQ.mkParseJSON defaultJSON ''UserProtoServers) +-- instance ProtocolTypeI p => FromJSON (UserProtoServers p) where +-- parseJSON = $(JQ.mkParseJSON defaultJSON ''UserProtoServers) -instance ProtocolTypeI p => ToJSON (UserProtoServers p) where - toJSON = $(JQ.mkToJSON defaultJSON ''UserProtoServers) - toEncoding = $(JQ.mkToEncoding defaultJSON ''UserProtoServers) +-- instance ProtocolTypeI p => ToJSON (UserProtoServers p) where +-- toJSON = $(JQ.mkToJSON defaultJSON ''UserProtoServers) +-- toEncoding = $(JQ.mkToEncoding defaultJSON ''UserProtoServers) -instance FromJSON AUserProtoServers where - parseJSON v = J.withObject "AUserProtoServers" parse v - where - parse o = do - AProtocolType (p :: SProtocolType p) <- o .: "serverProtocol" - case userProtocol p of - Just Dict -> AUPS <$> J.parseJSON @(UserProtoServers p) v - Nothing -> fail $ "AUserProtoServers: unsupported protocol " <> show p +-- instance FromJSON AUserProtoServers where +-- parseJSON v = J.withObject "AUserProtoServers" parse v +-- where +-- parse o = do +-- AProtocolType (p :: SProtocolType p) <- o .: "serverProtocol" +-- case userProtocol p of +-- Just Dict -> AUPS <$> J.parseJSON @(UserProtoServers p) v +-- Nothing -> fail $ "AUserProtoServers: unsupported protocol " <> show p -instance ToJSON AUserProtoServers where - toJSON (AUPS s) = $(JQ.mkToJSON defaultJSON ''UserProtoServers) s - toEncoding (AUPS s) = $(JQ.mkToEncoding defaultJSON ''UserProtoServers) s +-- instance ToJSON AUserProtoServers where +-- toJSON (AUPS s) = $(JQ.mkToJSON defaultJSON ''UserProtoServers) s +-- toEncoding (AUPS s) = $(JQ.mkToEncoding defaultJSON ''UserProtoServers) s $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "RCS") ''RemoteCtrlSessionState) diff --git a/src/Simplex/Chat/Migrations/M20241027_server_operators.hs b/src/Simplex/Chat/Migrations/M20241027_server_operators.hs index fc0ca21e54..d84cc5aa73 100644 --- a/src/Simplex/Chat/Migrations/M20241027_server_operators.hs +++ b/src/Simplex/Chat/Migrations/M20241027_server_operators.hs @@ -11,7 +11,6 @@ m20241027_server_operators = CREATE TABLE server_operators ( server_operator_id INTEGER PRIMARY KEY AUTOINCREMENT, server_operator_tag TEXT, - app_vendor INTEGER NOT NULL, trade_name TEXT NOT NULL, legal_name TEXT, server_domains TEXT, @@ -22,8 +21,6 @@ CREATE TABLE server_operators ( updated_at TEXT NOT NULL DEFAULT (datetime('now')) ); -ALTER TABLE protocol_servers ADD COLUMN server_operator_id INTEGER REFERENCES server_operators ON DELETE SET NULL; - CREATE TABLE usage_conditions ( usage_conditions_id INTEGER PRIMARY KEY AUTOINCREMENT, conditions_commit TEXT NOT NULL UNIQUE, @@ -41,18 +38,8 @@ CREATE TABLE operator_usage_conditions ( created_at TEXT NOT NULL DEFAULT (datetime('now')) ); -CREATE INDEX idx_protocol_servers_server_operator_id ON protocol_servers(server_operator_id); CREATE INDEX idx_operator_usage_conditions_server_operator_id ON operator_usage_conditions(server_operator_id); -CREATE UNIQUE INDEX idx_operator_usage_conditions_conditions_commit ON operator_usage_conditions(server_operator_id, conditions_commit); - -INSERT INTO server_operators - (server_operator_id, server_operator_tag, app_vendor, trade_name, legal_name, server_domains, enabled) - VALUES (1, 'simplex', 1, 'SimpleX Chat', 'SimpleX Chat Ltd', 'simplex.im', 1); -INSERT INTO server_operators - (server_operator_id, server_operator_tag, app_vendor, trade_name, legal_name, server_domains, enabled) - VALUES (2, 'xyz', 0, 'XYZ', 'XYZ Ltd', 'xyz.com', 0); - --- UPDATE protocol_servers SET server_operator_id = 1 WHERE host LIKE "%.simplex.im" OR host LIKE "%.simplex.im,%"; +CREATE UNIQUE INDEX idx_operator_usage_conditions_conditions_commit ON operator_usage_conditions(conditions_commit, server_operator_id); |] down_m20241027_server_operators :: Query @@ -60,9 +47,6 @@ down_m20241027_server_operators = [sql| DROP INDEX idx_operator_usage_conditions_conditions_commit; DROP INDEX idx_operator_usage_conditions_server_operator_id; -DROP INDEX idx_protocol_servers_server_operator_id; - -ALTER TABLE protocol_servers DROP COLUMN server_operator_id; DROP TABLE operator_usage_conditions; DROP TABLE usage_conditions; diff --git a/src/Simplex/Chat/Migrations/chat_schema.sql b/src/Simplex/Chat/Migrations/chat_schema.sql index 0eb7f66913..c037a60770 100644 --- a/src/Simplex/Chat/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Migrations/chat_schema.sql @@ -450,7 +450,6 @@ CREATE TABLE IF NOT EXISTS "protocol_servers"( created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')), protocol TEXT NOT NULL DEFAULT 'smp', - server_operator_id INTEGER REFERENCES server_operators ON DELETE SET NULL, UNIQUE(user_id, host, port) ); CREATE TABLE xftp_file_descriptions( @@ -593,7 +592,6 @@ CREATE TABLE app_settings(app_settings TEXT NOT NULL); CREATE TABLE server_operators( server_operator_id INTEGER PRIMARY KEY AUTOINCREMENT, server_operator_tag TEXT, - app_vendor INTEGER NOT NULL, trade_name TEXT NOT NULL, legal_name TEXT, server_domains TEXT, @@ -919,13 +917,10 @@ CREATE INDEX idx_received_probes_group_member_id on received_probes( group_member_id ); CREATE INDEX idx_contact_requests_contact_id ON contact_requests(contact_id); -CREATE INDEX idx_protocol_servers_server_operator_id ON protocol_servers( - server_operator_id -); CREATE INDEX idx_operator_usage_conditions_server_operator_id ON operator_usage_conditions( server_operator_id ); CREATE UNIQUE INDEX idx_operator_usage_conditions_conditions_commit ON operator_usage_conditions( - server_operator_id, - conditions_commit + conditions_commit, + server_operator_id ); diff --git a/src/Simplex/Chat/Operators.hs b/src/Simplex/Chat/Operators.hs index b3f92caaf9..55de357090 100644 --- a/src/Simplex/Chat/Operators.hs +++ b/src/Simplex/Chat/Operators.hs @@ -1,24 +1,42 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE KindSignatures #-} {-# LANGUAGE LambdaCase #-} +{-# LANGUAGE MultiWayIf #-} {-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedLists #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TupleSections #-} +{-# LANGUAGE TypeApplications #-} +{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} module Simplex.Chat.Operators where +import Control.Applicative ((<|>)) import Data.Aeson (FromJSON (..), ToJSON (..)) import qualified Data.Aeson as J import qualified Data.Aeson.Encoding as JE import qualified Data.Aeson.TH as JQ import Data.FileEmbed +import Data.Foldable (foldMap') +import Data.IORef import Data.Int (Int64) +import Data.List (find, foldl') import Data.List.NonEmpty (NonEmpty) import qualified Data.List.NonEmpty as L import Data.Map.Strict (Map) import qualified Data.Map.Strict as M -import Data.Maybe (fromMaybe, isNothing) +import Data.Maybe (fromMaybe, isNothing, mapMaybe) +import Data.Scientific (floatingOrInteger) +import Data.Set (Set) +import qualified Data.Set as S import Data.Text (Text) +import qualified Data.Text as T import Data.Time (addUTCTime) import Data.Time.Clock (UTCTime, nominalDay) import Database.SQLite.Simple.FromField (FromField (..)) @@ -26,23 +44,51 @@ import Database.SQLite.Simple.ToField (ToField (..)) import Language.Haskell.TH.Syntax (lift) import Simplex.Chat.Operators.Conditions import Simplex.Chat.Types.Util (textParseJSON) -import Simplex.Messaging.Agent.Env.SQLite (OperatorId, ServerCfg (..), ServerRoles (..)) +import Simplex.Messaging.Agent.Env.SQLite (ServerCfg (..), ServerRoles (..), allRoles) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, fromTextField_, sumTypeJSON) -import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), ProtoServerWithAuth (..), ProtocolServer (..), ProtocolType (..), SProtocolType (..)) -import Simplex.Messaging.Util (safeDecodeUtf8) +import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), ProtoServerWithAuth (..), ProtocolServer (..), ProtocolType (..), ProtocolTypeI, SProtocolType (..), UserProtocol) +import Simplex.Messaging.Transport.Client (TransportHost (..)) +import Simplex.Messaging.Util (atomicModifyIORef'_, safeDecodeUtf8) usageConditionsCommit :: Text -usageConditionsCommit = "165143a1112308c035ac00ed669b96b60599aa1c" +usageConditionsCommit = "a5061f3147165a05979d6ace33960aced2d6ac03" + +previousConditionsCommit :: Text +previousConditionsCommit = "11a44dc1fd461a93079f897048b46998db55da5c" usageConditionsText :: Text usageConditionsText = $( let s = $(embedFile =<< makeRelativeToProject "PRIVACY.md") - in [|stripFrontMatter (safeDecodeUtf8 $(lift s))|] + in [|stripFrontMatter $(lift (safeDecodeUtf8 s))|] ) -data OperatorTag = OTSimplex | OTXyz - deriving (Show) +data DBStored = DBStored | DBNew + +data SDBStored (s :: DBStored) where + SDBStored :: SDBStored 'DBStored + SDBNew :: SDBStored 'DBNew + +deriving instance Show (SDBStored s) + +class DBStoredI s where sdbStored :: SDBStored s + +instance DBStoredI 'DBStored where sdbStored = SDBStored + +instance DBStoredI 'DBNew where sdbStored = SDBNew + +data DBEntityId' (s :: DBStored) where + DBEntityId :: Int64 -> DBEntityId' 'DBStored + DBNewEntity :: DBEntityId' 'DBNew + +deriving instance Show (DBEntityId' s) + +type DBEntityId = DBEntityId' 'DBStored + +type DBNewEntity = DBEntityId' 'DBNew + +data OperatorTag = OTSimplex | OTFlux + deriving (Eq, Ord, Show) instance FromField OperatorTag where fromField = fromTextField_ textDecode @@ -58,11 +104,17 @@ instance ToJSON OperatorTag where instance TextEncoding OperatorTag where textDecode = \case "simplex" -> Just OTSimplex - "xyz" -> Just OTXyz + "flux" -> Just OTFlux _ -> Nothing textEncode = \case OTSimplex -> "simplex" - OTXyz -> "xyz" + OTFlux -> "flux" + +-- this and other types only define instances of serialization for known DB IDs only, +-- entities without IDs cannot be serialized to JSON +instance FromField DBEntityId where fromField f = DBEntityId <$> fromField f + +instance ToField DBEntityId where toField (DBEntityId i) = toField i data UsageConditions = UsageConditions { conditionsId :: Int64, @@ -80,18 +132,16 @@ data UsageConditionsAction usageConditionsAction :: [ServerOperator] -> UsageConditions -> UTCTime -> Maybe UsageConditionsAction usageConditionsAction operators UsageConditions {createdAt, notifiedAt} now = do let enabledOperators = filter (\ServerOperator {enabled} -> enabled) operators - if null enabledOperators - then Nothing - else - if all conditionsAccepted enabledOperators - then - let acceptedForOperators = filter conditionsAccepted operators - in Just $ UCAAccepted acceptedForOperators - else - let acceptForOperators = filter (not . conditionsAccepted) enabledOperators - deadline = conditionsRequiredOrDeadline createdAt (fromMaybe now notifiedAt) - showNotice = isNothing notifiedAt - in Just $ UCAReview acceptForOperators deadline showNotice + if + | null enabledOperators -> Nothing + | all conditionsAccepted enabledOperators -> + let acceptedForOperators = filter conditionsAccepted operators + in Just $ UCAAccepted acceptedForOperators + | otherwise -> + let acceptForOperators = filter (not . conditionsAccepted) enabledOperators + deadline = conditionsRequiredOrDeadline createdAt (fromMaybe now notifiedAt) + showNotice = isNothing notifiedAt + in Just $ UCAReview acceptForOperators deadline showNotice conditionsRequiredOrDeadline :: UTCTime -> UTCTime -> Maybe UTCTime conditionsRequiredOrDeadline createdAt notifiedAtOrNow = @@ -107,8 +157,16 @@ data ConditionsAcceptance | CARequired {deadline :: Maybe UTCTime} deriving (Show) -data ServerOperator = ServerOperator - { operatorId :: OperatorId, +type ServerOperator = ServerOperator' 'DBStored + +type NewServerOperator = ServerOperator' 'DBNew + +data AServerOperator = forall s. ASO (SDBStored s) (ServerOperator' s) + +deriving instance Show AServerOperator + +data ServerOperator' s = ServerOperator + { operatorId :: DBEntityId' s, operatorTag :: Maybe OperatorTag, tradeName :: Text, legalName :: Maybe Text, @@ -124,81 +182,257 @@ conditionsAccepted ServerOperator {conditionsAcceptance} = case conditionsAccept CAAccepted {} -> True _ -> False -data OperatorEnabled = OperatorEnabled - { operatorId :: OperatorId, - enabled :: Bool, - roles :: ServerRoles - } - deriving (Show) - -data UserServers = UserServers +data UserOperatorServers = UserOperatorServers { operator :: Maybe ServerOperator, - smpServers :: [ServerCfg 'PSMP], - xftpServers :: [ServerCfg 'PXFTP] + smpServers :: [UserServer 'PSMP], + xftpServers :: [UserServer 'PXFTP] } deriving (Show) -groupByOperator :: [ServerOperator] -> [ServerCfg 'PSMP] -> [ServerCfg 'PXFTP] -> [UserServers] -groupByOperator srvOperators smpSrvs xftpSrvs = - map createOperatorServers (M.toList combinedMap) +data UpdatedUserOperatorServers = UpdatedUserOperatorServers + { operator :: Maybe ServerOperator, + smpServers :: [AUserServer 'PSMP], + xftpServers :: [AUserServer 'PXFTP] + } + deriving (Show) + +updatedServers :: UserProtocol p => UpdatedUserOperatorServers -> SProtocolType p -> [AUserServer p] +updatedServers UpdatedUserOperatorServers {smpServers, xftpServers} = \case + SPSMP -> smpServers + SPXFTP -> xftpServers + +type UserServer p = UserServer' 'DBStored p + +type NewUserServer p = UserServer' 'DBNew p + +data AUserServer p = forall s. AUS (SDBStored s) (UserServer' s p) + +deriving instance Show (AUserServer p) + +data UserServer' s p = UserServer + { serverId :: DBEntityId' s, + server :: ProtoServerWithAuth p, + preset :: Bool, + tested :: Maybe Bool, + enabled :: Bool, + deleted :: Bool + } + deriving (Show) + +data PresetOperator = PresetOperator + { operator :: Maybe NewServerOperator, + smp :: [NewUserServer 'PSMP], + useSMP :: Int, + xftp :: [NewUserServer 'PXFTP], + useXFTP :: Int + } + +operatorServers :: UserProtocol p => SProtocolType p -> PresetOperator -> [NewUserServer p] +operatorServers p PresetOperator {smp, xftp} = case p of + SPSMP -> smp + SPXFTP -> xftp + +operatorServersToUse :: UserProtocol p => SProtocolType p -> PresetOperator -> Int +operatorServersToUse p PresetOperator {useSMP, useXFTP} = case p of + SPSMP -> useSMP + SPXFTP -> useXFTP + +presetServer :: Bool -> ProtoServerWithAuth p -> NewUserServer p +presetServer = newUserServer_ True + +newUserServer :: ProtoServerWithAuth p -> NewUserServer p +newUserServer = newUserServer_ False True + +newUserServer_ :: Bool -> Bool -> ProtoServerWithAuth p -> NewUserServer p +newUserServer_ preset enabled server = + UserServer {serverId = DBNewEntity, server, preset, tested = Nothing, enabled, deleted = False} + +-- This function should be used inside DB transaction to update conditions in the database +-- it evaluates to (conditions to mark as accepted to SimpleX operator, current conditions, and conditions to add) +usageConditionsToAdd :: Bool -> UTCTime -> [UsageConditions] -> (Maybe UsageConditions, UsageConditions, [UsageConditions]) +usageConditionsToAdd = usageConditionsToAdd' previousConditionsCommit usageConditionsCommit + +-- This function is used in unit tests +usageConditionsToAdd' :: Text -> Text -> Bool -> UTCTime -> [UsageConditions] -> (Maybe UsageConditions, UsageConditions, [UsageConditions]) +usageConditionsToAdd' prevCommit sourceCommit newUser createdAt = \case + [] + | newUser -> (Just sourceCond, sourceCond, [sourceCond]) + | otherwise -> (Just prevCond, sourceCond, [prevCond, sourceCond]) + where + prevCond = conditions 1 prevCommit + sourceCond = conditions 2 sourceCommit + conds + | hasSourceCond -> (Nothing, last conds, []) + | otherwise -> (Nothing, sourceCond, [sourceCond]) + where + hasSourceCond = any ((sourceCommit ==) . conditionsCommit) conds + sourceCond = conditions cId sourceCommit + cId = maximum (map conditionsId conds) + 1 where - srvOperatorId ServerCfg {operator} = operator - opId ServerOperator {operatorId} = operatorId - operatorMap :: Map (Maybe Int64) (Maybe ServerOperator) - operatorMap = M.fromList [(Just (opId op), Just op) | op <- srvOperators] `M.union` M.singleton Nothing Nothing - initialMap :: Map (Maybe Int64) ([ServerCfg 'PSMP], [ServerCfg 'PXFTP]) - initialMap = M.fromList [(key, ([], [])) | key <- M.keys operatorMap] - smpsMap = foldr (\server acc -> M.adjust (\(smps, xftps) -> (server : smps, xftps)) (srvOperatorId server) acc) initialMap smpSrvs - combinedMap = foldr (\server acc -> M.adjust (\(smps, xftps) -> (smps, server : xftps)) (srvOperatorId server) acc) smpsMap xftpSrvs - createOperatorServers (key, (groupedSmps, groupedXftps)) = - UserServers - { operator = fromMaybe Nothing (M.lookup key operatorMap), - smpServers = groupedSmps, - xftpServers = groupedXftps - } + conditions cId commit = UsageConditions {conditionsId = cId, conditionsCommit = commit, notifiedAt = Nothing, createdAt} + +-- This function should be used inside DB transaction to update operators. +-- It allows to add/remove/update preset operators in the database preserving enabled and roles settings, +-- and preserves custom operators without tags for forward compatibility. +updatedServerOperators :: NonEmpty PresetOperator -> [ServerOperator] -> [AServerOperator] +updatedServerOperators presetOps storedOps = + foldr addPreset [] presetOps + <> map (ASO SDBStored) (filter (isNothing . operatorTag) storedOps) + where + -- TODO remove domains of preset operators from custom + addPreset PresetOperator {operator} = case operator of + Nothing -> id + Just presetOp -> (storedOp' :) + where + storedOp' = case find ((operatorTag presetOp ==) . operatorTag) storedOps of + Just ServerOperator {operatorId, conditionsAcceptance, enabled, roles} -> + ASO SDBStored presetOp {operatorId, conditionsAcceptance, enabled, roles} + Nothing -> ASO SDBNew presetOp + +-- This function should be used inside DB transaction to update servers. +updatedUserServers :: forall p. UserProtocol p => SProtocolType p -> NonEmpty PresetOperator -> NonEmpty (NewUserServer p) -> [UserServer p] -> NonEmpty (AUserServer p) +updatedUserServers _ _ randomSrvs [] = L.map (AUS SDBNew) randomSrvs +updatedUserServers p presetOps randomSrvs srvs = + fromMaybe (L.map (AUS SDBNew) randomSrvs) (L.nonEmpty updatedSrvs) + where + updatedSrvs = map userServer presetSrvs <> map (AUS SDBStored) (filter customServer srvs) + storedSrvs :: Map (ProtoServerWithAuth p) (UserServer p) + storedSrvs = foldl' (\ss srv@UserServer {server} -> M.insert server srv ss) M.empty srvs + customServer :: UserServer p -> Bool + customServer srv = not (preset srv) && all (`S.notMember` presetHosts) (srvHost srv) + presetSrvs :: [NewUserServer p] + presetSrvs = concatMap (operatorServers p) presetOps + presetHosts :: Set TransportHost + presetHosts = foldMap' (S.fromList . L.toList . srvHost) presetSrvs + userServer :: NewUserServer p -> AUserServer p + userServer srv@UserServer {server} = maybe (AUS SDBNew srv) (AUS SDBStored) (M.lookup server storedSrvs) + +srvHost :: UserServer' s p -> NonEmpty TransportHost +srvHost UserServer {server = ProtoServerWithAuth srv _} = host srv + +agentServerCfgs :: [(Text, ServerOperator)] -> NonEmpty (NewUserServer p) -> [UserServer' s p] -> NonEmpty (ServerCfg p) +agentServerCfgs opDomains randomSrvs = + fromMaybe fallbackSrvs . L.nonEmpty . mapMaybe enabledOpAgentServer + where + fallbackSrvs = L.map (snd . agentServer) randomSrvs + enabledOpAgentServer srv = + let (opEnabled, srvCfg) = agentServer srv + in if opEnabled then Just srvCfg else Nothing + agentServer :: UserServer' s p -> (Bool, ServerCfg p) + agentServer srv@UserServer {server, enabled} = + case find (\(d, _) -> any (matchingHost d) (srvHost srv)) opDomains of + Just (_, ServerOperator {operatorId = DBEntityId opId, enabled = opEnabled, roles}) -> + (opEnabled, ServerCfg {server, enabled, operator = Just opId, roles}) + Nothing -> + (True, ServerCfg {server, enabled, operator = Nothing, roles = allRoles}) + +matchingHost :: Text -> TransportHost -> Bool +matchingHost d = \case + THDomainName h -> d `T.isSuffixOf` T.pack h + _ -> False + +operatorDomains :: [ServerOperator] -> [(Text, ServerOperator)] +operatorDomains = foldr (\op ds -> foldr (\d -> ((d, op) :)) ds (serverDomains op)) [] + +groupByOperator :: ([ServerOperator], [UserServer 'PSMP], [UserServer 'PXFTP]) -> IO [UserOperatorServers] +groupByOperator (ops, smpSrvs, xftpSrvs) = do + ss <- mapM (\op -> (serverDomains op,) <$> newIORef (UserOperatorServers (Just op) [] [])) ops + custom <- newIORef $ UserOperatorServers Nothing [] [] + mapM_ (addServer ss custom addSMP) (reverse smpSrvs) + mapM_ (addServer ss custom addXFTP) (reverse xftpSrvs) + opSrvs <- mapM (readIORef . snd) ss + customSrvs <- readIORef custom + pure $ opSrvs <> [customSrvs] + where + addServer :: [([Text], IORef UserOperatorServers)] -> IORef UserOperatorServers -> (UserServer p -> UserOperatorServers -> UserOperatorServers) -> UserServer p -> IO () + addServer ss custom add srv = + let v = maybe custom snd $ find (\(ds, _) -> any (\d -> any (matchingHost d) (srvHost srv)) ds) ss + in atomicModifyIORef'_ v $ add srv + addSMP srv s@UserOperatorServers {smpServers} = (s :: UserOperatorServers) {smpServers = srv : smpServers} + addXFTP srv s@UserOperatorServers {xftpServers} = (s :: UserOperatorServers) {xftpServers = srv : xftpServers} data UserServersError - = USEStorageMissing - | USEProxyMissing - | USEDuplicateSMP {server :: AProtoServerWithAuth} - | USEDuplicateXFTP {server :: AProtoServerWithAuth} + = USEStorageMissing {protocol :: AProtocolType} + | USEProxyMissing {protocol :: AProtocolType} + | USEDuplicateServer {protocol :: AProtocolType, duplicateServer :: AProtoServerWithAuth, duplicateHost :: TransportHost} deriving (Show) -validateUserServers :: NonEmpty UserServers -> [UserServersError] -validateUserServers userServers = - let storageMissing_ = if any (canUseForRole storage) userServers then [] else [USEStorageMissing] - proxyMissing_ = if any (canUseForRole proxy) userServers then [] else [USEProxyMissing] - - allSMPServers = map (\ServerCfg {server} -> server) $ concatMap (\UserServers {smpServers} -> smpServers) userServers - duplicateSMPServers = findDuplicatesByHost allSMPServers - duplicateSMPErrors = map (USEDuplicateSMP . AProtoServerWithAuth SPSMP) duplicateSMPServers - - allXFTPServers = map (\ServerCfg {server} -> server) $ concatMap (\UserServers {xftpServers} -> xftpServers) userServers - duplicateXFTPServers = findDuplicatesByHost allXFTPServers - duplicateXFTPErrors = map (USEDuplicateXFTP . AProtoServerWithAuth SPXFTP) duplicateXFTPServers - in storageMissing_ <> proxyMissing_ <> duplicateSMPErrors <> duplicateXFTPErrors +validateUserServers :: NonEmpty UpdatedUserOperatorServers -> [UserServersError] +validateUserServers uss = + missingRolesErr SPSMP storage USEStorageMissing + <> missingRolesErr SPSMP proxy USEProxyMissing + <> missingRolesErr SPXFTP storage USEStorageMissing + <> duplicatServerErrs SPSMP + <> duplicatServerErrs SPXFTP where - canUseForRole :: (ServerRoles -> Bool) -> UserServers -> Bool - canUseForRole roleSel UserServers {operator, smpServers, xftpServers} = case operator of - Just ServerOperator {roles} -> roleSel roles - Nothing -> not (null smpServers) && not (null xftpServers) - findDuplicatesByHost :: [ProtoServerWithAuth p] -> [ProtoServerWithAuth p] - findDuplicatesByHost servers = - let allHosts = concatMap (L.toList . host . protoServer) servers - hostCounts = M.fromListWith (+) [(host, 1 :: Int) | host <- allHosts] - duplicateHosts = M.keys $ M.filter (> 1) hostCounts - in filter (\srv -> any (`elem` duplicateHosts) (L.toList $ host . protoServer $ srv)) servers + missingRolesErr :: (ProtocolTypeI p, UserProtocol p) => SProtocolType p -> (ServerRoles -> Bool) -> (AProtocolType -> UserServersError) -> [UserServersError] + missingRolesErr p roleSel err = [err (AProtocolType p) | not hasRole] + where + hasRole = + any (\(AUS _ UserServer {deleted, enabled}) -> enabled && not deleted) $ + concatMap (`updatedServers` p) $ filter roleEnabled (L.toList uss) + roleEnabled UpdatedUserOperatorServers {operator} = + maybe True (\ServerOperator {enabled, roles} -> enabled && roleSel roles) operator + duplicatServerErrs :: (ProtocolTypeI p, UserProtocol p) => SProtocolType p -> [UserServersError] + duplicatServerErrs p = mapMaybe duplicateErr_ srvs + where + srvs = + filter (\(AUS _ UserServer {deleted}) -> not deleted) $ + concatMap (`updatedServers` p) (L.toList uss) + duplicateErr_ (AUS _ srv@UserServer {server}) = + USEDuplicateServer (AProtocolType p) (AProtoServerWithAuth p server) + <$> find (`S.member` duplicateHosts) (srvHost srv) + duplicateHosts = snd $ foldl' addHost (S.empty, S.empty) allHosts + allHosts = concatMap (\(AUS _ srv) -> L.toList $ srvHost srv) srvs + addHost (hs, dups) h + | h `S.member` hs = (hs, S.insert h dups) + | otherwise = (S.insert h hs, dups) + +instance ToJSON (DBEntityId' s) where + toEncoding = \case + DBEntityId i -> toEncoding i + DBNewEntity -> JE.null_ + toJSON = \case + DBEntityId i -> toJSON i + DBNewEntity -> J.Null + +instance DBStoredI s => FromJSON (DBEntityId' s) where + parseJSON v = case (v, sdbStored @s) of + (J.Null, SDBNew) -> pure DBNewEntity + (J.Number n, SDBStored) -> case floatingOrInteger n of + Left (_ :: Double) -> fail "bad DBEntityId" + Right i -> pure $ DBEntityId (fromInteger i) + _ -> fail "bad DBEntityId" + omittedField = case sdbStored @s of + SDBStored -> Nothing + SDBNew -> Just DBNewEntity $(JQ.deriveJSON defaultJSON ''UsageConditions) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CA") ''ConditionsAcceptance) -$(JQ.deriveJSON defaultJSON ''ServerOperator) +instance ToJSON (ServerOperator' s) where + toEncoding = $(JQ.mkToEncoding defaultJSON ''ServerOperator') + toJSON = $(JQ.mkToJSON defaultJSON ''ServerOperator') -$(JQ.deriveJSON defaultJSON ''OperatorEnabled) +instance DBStoredI s => FromJSON (ServerOperator' s) where + parseJSON = $(JQ.mkParseJSON defaultJSON ''ServerOperator') $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "UCA") ''UsageConditionsAction) -$(JQ.deriveJSON defaultJSON ''UserServers) +instance ProtocolTypeI p => ToJSON (UserServer' s p) where + toEncoding = $(JQ.mkToEncoding defaultJSON ''UserServer') + toJSON = $(JQ.mkToJSON defaultJSON ''UserServer') + +instance (DBStoredI s, ProtocolTypeI p) => FromJSON (UserServer' s p) where + parseJSON = $(JQ.mkParseJSON defaultJSON ''UserServer') + +instance ProtocolTypeI p => FromJSON (AUserServer p) where + parseJSON v = (AUS SDBStored <$> parseJSON v) <|> (AUS SDBNew <$> parseJSON v) + +$(JQ.deriveJSON defaultJSON ''UserOperatorServers) + +instance FromJSON UpdatedUserOperatorServers where + parseJSON = $(JQ.mkParseJSON defaultJSON ''UpdatedUserOperatorServers) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "USE") ''UserServersError) diff --git a/src/Simplex/Chat/Operators/Conditions.hs b/src/Simplex/Chat/Operators/Conditions.hs index 55cf8b658d..a314c1901a 100644 --- a/src/Simplex/Chat/Operators/Conditions.hs +++ b/src/Simplex/Chat/Operators/Conditions.hs @@ -9,7 +9,7 @@ import qualified Data.Text as T stripFrontMatter :: Text -> Text stripFrontMatter = T.unlines - . dropWhile ("# " `T.isPrefixOf`) -- strip title + -- . dropWhile ("# " `T.isPrefixOf`) -- strip title . dropWhile (T.all isSpace) . dropWhile fm . (\ls -> let ls' = dropWhile (not . fm) ls in if null ls' then ls else ls') diff --git a/src/Simplex/Chat/Stats.hs b/src/Simplex/Chat/Stats.hs index 6dd5c79ab1..21ad25b311 100644 --- a/src/Simplex/Chat/Stats.hs +++ b/src/Simplex/Chat/Stats.hs @@ -7,7 +7,6 @@ module Simplex.Chat.Stats where import qualified Data.Aeson.TH as J import Data.List (partition) -import Data.List.NonEmpty (NonEmpty) import Data.Map.Strict (Map) import qualified Data.Map.Strict as M import Data.Maybe (fromMaybe, isJust) @@ -131,7 +130,7 @@ data NtfServerSummary = NtfServerSummary -- - users are passed to exclude hidden users from totalServersSummary; -- - if currentUser is hidden, it should be accounted in totalServersSummary; -- - known is set only in user level summaries based on passed userSMPSrvs and userXFTPSrvs -toPresentedServersSummary :: AgentServersSummary -> [User] -> User -> NonEmpty SMPServer -> NonEmpty XFTPServer -> [NtfServer] -> PresentedServersSummary +toPresentedServersSummary :: AgentServersSummary -> [User] -> User -> [SMPServer] -> [XFTPServer] -> [NtfServer] -> PresentedServersSummary toPresentedServersSummary agentSummary users currentUser userSMPSrvs userXFTPSrvs userNtfSrvs = do let (userSMPSrvsSumms, allSMPSrvsSumms) = accSMPSrvsSummaries (userSMPCurr, userSMPPrev, userSMPProx) = smpSummsIntoCategories userSMPSrvsSumms diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs index f4f574c3d7..39bd4bb985 100644 --- a/src/Simplex/Chat/Store/Profiles.hs +++ b/src/Simplex/Chat/Store/Profiles.hs @@ -1,5 +1,8 @@ +{-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} @@ -47,9 +50,13 @@ module Simplex.Chat.Store.Profiles getContactWithoutConnViaAddress, updateUserAddressAutoAccept, getProtocolServers, + getUpdateUserServers, -- overwriteOperatorsAndServers, overwriteProtocolServers, + insertProtocolServer, + getUpdateServerOperators, getServerOperators, + getUserServers, setServerOperators, getCurrentUsageConditions, getLatestAcceptedConditions, @@ -77,10 +84,11 @@ import Data.Int (Int64) import Data.List.NonEmpty (NonEmpty) import qualified Data.List.NonEmpty as L import Data.Maybe (fromMaybe) -import Data.Text (Text, splitOn) +import Data.Text (Text) +import qualified Data.Text as T import Data.Text.Encoding (decodeLatin1, encodeUtf8) import Data.Time.Clock (UTCTime (..), getCurrentTime) -import Database.SQLite.Simple (NamedParam (..), Only (..), (:.) (..)) +import Database.SQLite.Simple (NamedParam (..), Only (..), Query, (:.) (..)) import Database.SQLite.Simple.QQ (sql) import Simplex.Chat.Call import Simplex.Chat.Messages @@ -92,7 +100,7 @@ import Simplex.Chat.Types import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Shared import Simplex.Chat.Types.UITheme -import Simplex.Messaging.Agent.Env.SQLite (OperatorId, ServerCfg (..), ServerRoles (..)) +import Simplex.Messaging.Agent.Env.SQLite (ServerRoles (..)) import Simplex.Messaging.Agent.Protocol (ACorrId, ConnId, UserId) import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow) import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB @@ -100,7 +108,7 @@ import qualified Simplex.Messaging.Crypto as C import qualified Simplex.Messaging.Crypto.Ratchet as CR import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON) -import Simplex.Messaging.Protocol (BasicAuth (..), ProtoServerWithAuth (..), ProtocolServer (..), ProtocolTypeI (..), SubscriptionMode) +import Simplex.Messaging.Protocol (BasicAuth (..), ProtoServerWithAuth (..), ProtocolServer (..), ProtocolType (..), ProtocolTypeI (..), SProtocolType (..), SubscriptionMode, UserProtocol) import Simplex.Messaging.Transport.Client (TransportHost) import Simplex.Messaging.Util (eitherToMaybe, safeDecodeUtf8) @@ -524,177 +532,282 @@ updateUserAddressAutoAccept db user@User {userId} autoAccept = do Just AutoAccept {acceptIncognito, autoReply} -> (True, acceptIncognito, autoReply) _ -> (False, False, Nothing) -getProtocolServers :: forall p. ProtocolTypeI p => DB.Connection -> User -> IO [ServerCfg p] -getProtocolServers db User {userId} = - map toServerCfg +getUpdateUserServers :: forall p. (ProtocolTypeI p, UserProtocol p) => DB.Connection -> SProtocolType p -> NonEmpty PresetOperator -> NonEmpty (NewUserServer p) -> User -> IO [UserServer p] +getUpdateUserServers db p presetOps randomSrvs user = do + ts <- getCurrentTime + srvs <- getProtocolServers db p user + let srvs' = L.toList $ updatedUserServers p presetOps randomSrvs srvs + mapM (upsertServer ts) srvs' + where + upsertServer :: UTCTime -> AUserServer p -> IO (UserServer p) + upsertServer ts (AUS _ s@UserServer {serverId}) = case serverId of + DBNewEntity -> insertProtocolServer db p user ts s + DBEntityId _ -> updateProtocolServer db p ts s $> s + +getProtocolServers :: forall p. ProtocolTypeI p => DB.Connection -> SProtocolType p -> User -> IO [UserServer p] +getProtocolServers db p User {userId} = + map toUserServer <$> DB.query db [sql| - SELECT s.host, s.port, s.key_hash, s.basic_auth, s.server_operator_id, s.preset, s.tested, s.enabled, o.role_storage, o.role_proxy - FROM protocol_servers s - LEFT JOIN server_operators o USING (server_operator_id) - WHERE s.user_id = ? AND s.protocol = ? + SELECT smp_server_id, host, port, key_hash, basic_auth, preset, tested, enabled + FROM protocol_servers + WHERE user_id = ? AND protocol = ? |] - (userId, decodeLatin1 $ strEncode protocol) + (userId, decodeLatin1 $ strEncode p) where - protocol = protocolTypeI @p - toServerCfg :: (NonEmpty TransportHost, String, C.KeyHash, Maybe Text, Maybe OperatorId, Bool, Maybe Bool, Bool, Maybe Bool, Maybe Bool) -> ServerCfg p - toServerCfg (host, port, keyHash, auth_, operator, preset, tested, enabled, storage_, proxy_) = - let server = ProtoServerWithAuth (ProtocolServer protocol host port keyHash) (BasicAuth . encodeUtf8 <$> auth_) - roles = ServerRoles {storage = fromMaybe True storage_, proxy = fromMaybe True proxy_} - in ServerCfg {server, operator, preset, tested, enabled, roles} + toUserServer :: (DBEntityId, NonEmpty TransportHost, String, C.KeyHash, Maybe Text, Bool, Maybe Bool, Bool) -> UserServer p + toUserServer (serverId, host, port, keyHash, auth_, preset, tested, enabled) = + let server = ProtoServerWithAuth (ProtocolServer p host port keyHash) (BasicAuth . encodeUtf8 <$> auth_) + in UserServer {serverId, server, preset, tested, enabled, deleted = False} -- TODO remove -- overwriteOperatorsAndServers :: forall p. ProtocolTypeI p => DB.Connection -> User -> Maybe [ServerOperator] -> [ServerCfg p] -> ExceptT StoreError IO [ServerCfg p] -- overwriteOperatorsAndServers db user@User {userId} operators_ servers = do -overwriteProtocolServers :: forall p. ProtocolTypeI p => DB.Connection -> User -> [ServerCfg p] -> ExceptT StoreError IO () -overwriteProtocolServers db User {userId} servers = +overwriteProtocolServers :: ProtocolTypeI p => DB.Connection -> SProtocolType p -> User -> [UserServer p] -> ExceptT StoreError IO () +overwriteProtocolServers db p User {userId} servers = -- liftIO $ mapM_ (updateServerOperators_ db) operators_ checkConstraint SEUniqueID . ExceptT $ do currentTs <- getCurrentTime - DB.execute db "DELETE FROM protocol_servers WHERE user_id = ? AND protocol = ? " (userId, protocol) - forM_ servers $ \ServerCfg {server, preset, tested, enabled} -> do - let ProtoServerWithAuth ProtocolServer {host, port, keyHash} auth_ = server + DB.execute db "DELETE FROM protocol_servers WHERE user_id = ? AND protocol = ? " (userId, decodeLatin1 $ strEncode p) + forM_ servers $ \UserServer {serverId, server, preset, tested, enabled} -> do DB.execute db [sql| INSERT INTO protocol_servers - (protocol, host, port, key_hash, basic_auth, preset, tested, enabled, user_id, created_at, updated_at) - VALUES (?,?,?,?,?,?,?,?,?,?,?) + (server_id, protocol, host, port, key_hash, basic_auth, preset, tested, enabled, user_id, created_at, updated_at) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?) |] - ((protocol, host, port, keyHash, safeDecodeUtf8 . unBasicAuth <$> auth_) :. (preset, tested, enabled, userId, currentTs, currentTs)) - -- Right <$> getProtocolServers db user + (Only serverId :. serverColumns p server :. (preset, tested, enabled, userId, currentTs, currentTs)) pure $ Right () - where - protocol = decodeLatin1 $ strEncode $ protocolTypeI @p + +insertProtocolServer :: forall p. ProtocolTypeI p => DB.Connection -> SProtocolType p -> User -> UTCTime -> NewUserServer p -> IO (UserServer p) +insertProtocolServer db p User {userId} ts srv@UserServer {server, preset, tested, enabled} = do + DB.execute + db + [sql| + INSERT INTO protocol_servers + (protocol, host, port, key_hash, basic_auth, preset, tested, enabled, user_id, created_at, updated_at) + VALUES (?,?,?,?,?,?,?,?,?,?,?) + |] + (serverColumns p server :. (preset, tested, enabled, userId, ts, ts)) + sId <- insertedRowId db + pure (srv :: NewUserServer p) {serverId = DBEntityId sId} + +updateProtocolServer :: ProtocolTypeI p => DB.Connection -> SProtocolType p -> UTCTime -> UserServer p -> IO () +updateProtocolServer db p ts UserServer {serverId, server, preset, tested, enabled} = + DB.execute + db + [sql| + UPDATE protocol_servers + SET protocol = ?, host = ?, port = ?, key_hash = ?, basic_auth = ?, + preset = ?, tested = ?, enabled = ?, updated_at = ? + WHERE smp_server_id = ? + |] + (serverColumns p server :. (preset, tested, enabled, ts, serverId)) + +serverColumns :: ProtocolTypeI p => SProtocolType p -> ProtoServerWithAuth p -> (Text, NonEmpty TransportHost, String, C.KeyHash, Maybe Text) +serverColumns p (ProtoServerWithAuth ProtocolServer {host, port, keyHash} auth_) = + let protocol = decodeLatin1 $ strEncode p + auth = safeDecodeUtf8 . unBasicAuth <$> auth_ + in (protocol, host, port, keyHash, auth) getServerOperators :: DB.Connection -> ExceptT StoreError IO ([ServerOperator], Maybe UsageConditionsAction) getServerOperators db = do - now <- liftIO getCurrentTime - currentConditions <- getCurrentUsageConditions db - latestAcceptedConditions <- getLatestAcceptedConditions db - operators <- - liftIO $ - map (toOperator now currentConditions latestAcceptedConditions) - <$> DB.query_ - db - [sql| - SELECT - so.server_operator_id, so.server_operator_tag, so.trade_name, so.legal_name, - so.server_domains, so.enabled, so.role_storage, so.role_proxy, - AcceptedConditions.conditions_commit, AcceptedConditions.accepted_at - FROM server_operators so - LEFT JOIN ( - SELECT server_operator_id, conditions_commit, accepted_at, MAX(operator_usage_conditions_id) - FROM operator_usage_conditions - GROUP BY server_operator_id - ) AcceptedConditions ON AcceptedConditions.server_operator_id = so.server_operator_id - |] - pure (operators, usageConditionsAction operators currentConditions now) - where - toOperator :: - UTCTime -> - UsageConditions -> - Maybe UsageConditions -> - ( (OperatorId, Maybe OperatorTag, Text, Maybe Text, Text, Bool, Bool, Bool) - :. (Maybe Text, Maybe UTCTime) - ) -> - ServerOperator - toOperator - now - UsageConditions {conditionsCommit = currentCommit, createdAt, notifiedAt} - latestAcceptedConditions_ - ( (operatorId, operatorTag, tradeName, legalName, domains, enabled, storage, proxy) - :. (operatorCommit_, acceptedAt_) - ) = - let roles = ServerRoles {storage, proxy} - serverDomains = splitOn "," domains - conditionsAcceptance = case (latestAcceptedConditions_, operatorCommit_) of - -- no conditions were ever accepted for any operator(s) - -- (shouldn't happen as there should always be record for SimpleX Chat) - (Nothing, _) -> CARequired Nothing - -- no conditions were ever accepted for this operator - (_, Nothing) -> CARequired Nothing - (Just UsageConditions {conditionsCommit = latestAcceptedCommit}, Just operatorCommit) - | latestAcceptedCommit == currentCommit -> - if operatorCommit == latestAcceptedCommit - then -- current conditions were accepted for operator - CAAccepted acceptedAt_ - else -- current conditions were NOT accepted for operator, but were accepted for other operator(s) - CARequired Nothing - | otherwise -> - if operatorCommit == latestAcceptedCommit - then -- new conditions available, latest accepted conditions were accepted for operator - CARequired $ conditionsRequiredOrDeadline createdAt (fromMaybe now notifiedAt) - else -- new conditions available, latest accepted conditions were NOT accepted for operator (were accepted for other operator(s)) - CARequired Nothing - in ServerOperator {operatorId, operatorTag, tradeName, legalName, serverDomains, conditionsAcceptance, enabled, roles} + currentConds <- getCurrentUsageConditions db + liftIO $ do + now <- getCurrentTime + latestAcceptedConds_ <- getLatestAcceptedConditions db + let getConds op = (\ca -> op {conditionsAcceptance = ca}) <$> getOperatorConditions_ db op currentConds latestAcceptedConds_ now + operators <- mapM getConds =<< getServerOperators_ db + pure (operators, usageConditionsAction operators currentConds now) -setServerOperators :: DB.Connection -> NonEmpty OperatorEnabled -> ExceptT StoreError IO ([ServerOperator], Maybe UsageConditionsAction) -setServerOperators db operatorsEnabled = do - liftIO $ forM_ operatorsEnabled $ \OperatorEnabled {operatorId, enabled, roles = ServerRoles {storage, proxy}} -> - DB.execute - db - "UPDATE server_operators SET enabled = ?, role_storage = ?, role_proxy = ? WHERE server_operator_id = ?" - (enabled, storage, proxy, operatorId) - getServerOperators db +getUserServers :: DB.Connection -> User -> ExceptT StoreError IO ([ServerOperator], [UserServer 'PSMP], [UserServer 'PXFTP]) +getUserServers db user = + (,,) + <$> (fst <$> getServerOperators db) + <*> liftIO (getProtocolServers db SPSMP user) + <*> liftIO (getProtocolServers db SPXFTP user) + +setServerOperators :: DB.Connection -> NonEmpty ServerOperator -> IO () +setServerOperators db ops = do + currentTs <- getCurrentTime + mapM_ (updateServerOperator db currentTs) ops + +updateServerOperator :: DB.Connection -> UTCTime -> ServerOperator -> IO () +updateServerOperator db currentTs ServerOperator {operatorId, enabled, roles = ServerRoles {storage, proxy}} = + DB.execute + db + [sql| + UPDATE server_operators + SET enabled = ?, role_storage = ?, role_proxy = ?, updated_at = ? + WHERE server_operator_id = ? + |] + (enabled, storage, proxy, operatorId, currentTs) + +getUpdateServerOperators :: DB.Connection -> NonEmpty PresetOperator -> Bool -> IO [ServerOperator] +getUpdateServerOperators db presetOps newUser = do + conds <- map toUsageConditions <$> DB.query_ db usageCondsQuery + now <- getCurrentTime + let (acceptForSimplex_, currentConds, condsToAdd) = usageConditionsToAdd newUser now conds + mapM_ insertConditions condsToAdd + latestAcceptedConds_ <- getLatestAcceptedConditions db + ops <- updatedServerOperators presetOps <$> getServerOperators_ db + forM ops $ \(ASO _ op) -> + case operatorId op of + DBNewEntity -> do + op' <- insertOperator op + case (operatorTag op', acceptForSimplex_) of + (Just OTSimplex, Just cond) -> autoAcceptConditions op' cond + _ -> pure op' + DBEntityId _ -> do + updateOperator op + getOperatorConditions_ db op currentConds latestAcceptedConds_ now >>= \case + CARequired Nothing | operatorTag op == Just OTSimplex -> autoAcceptConditions op currentConds + CARequired (Just ts) | ts < now -> autoAcceptConditions op currentConds + ca -> pure op {conditionsAcceptance = ca} + where + insertConditions UsageConditions {conditionsId, conditionsCommit, notifiedAt, createdAt} = + DB.execute + db + [sql| + INSERT INTO usage_conditions + (usage_conditions_id, conditions_commit, notified_at, created_at) + VALUES (?,?,?,?) + |] + (conditionsId, conditionsCommit, notifiedAt, createdAt) + updateOperator :: ServerOperator -> IO () + updateOperator ServerOperator {operatorId, tradeName, legalName, serverDomains, enabled, roles = ServerRoles {storage, proxy}} = + DB.execute + db + [sql| + UPDATE server_operators + SET trade_name = ?, legal_name = ?, server_domains = ?, enabled = ?, role_storage = ?, role_proxy = ? + WHERE server_operator_id = ? + |] + (tradeName, legalName, T.intercalate "," serverDomains, enabled, storage, proxy, operatorId) + insertOperator :: NewServerOperator -> IO ServerOperator + insertOperator op@ServerOperator {operatorTag, tradeName, legalName, serverDomains, enabled, roles = ServerRoles {storage, proxy}} = do + DB.execute + db + [sql| + INSERT INTO server_operators + (server_operator_tag, trade_name, legal_name, server_domains, enabled, role_storage, role_proxy) + VALUES (?,?,?,?,?,?,?) + |] + (operatorTag, tradeName, legalName, T.intercalate "," serverDomains, enabled, storage, proxy) + opId <- insertedRowId db + pure op {operatorId = DBEntityId opId} + autoAcceptConditions op UsageConditions {conditionsCommit} = + acceptConditions_ db op conditionsCommit Nothing + $> op {conditionsAcceptance = CAAccepted Nothing} + +serverOperatorQuery :: Query +serverOperatorQuery = + [sql| + SELECT server_operator_id, server_operator_tag, trade_name, legal_name, + server_domains, enabled, role_storage, role_proxy + FROM server_operators + |] + +getServerOperators_ :: DB.Connection -> IO [ServerOperator] +getServerOperators_ db = map toServerOperator <$> DB.query_ db serverOperatorQuery + +toServerOperator :: (DBEntityId, Maybe OperatorTag, Text, Maybe Text, Text, Bool, Bool, Bool) -> ServerOperator +toServerOperator (operatorId, operatorTag, tradeName, legalName, domains, enabled, storage, proxy) = + ServerOperator + { operatorId, + operatorTag, + tradeName, + legalName, + serverDomains = T.splitOn "," domains, + conditionsAcceptance = CARequired Nothing, + enabled, + roles = ServerRoles {storage, proxy} + } + +getOperatorConditions_ :: DB.Connection -> ServerOperator -> UsageConditions -> Maybe UsageConditions -> UTCTime -> IO ConditionsAcceptance +getOperatorConditions_ db ServerOperator {operatorId} UsageConditions {conditionsCommit = currentCommit, createdAt, notifiedAt} latestAcceptedConds_ now = do + case latestAcceptedConds_ of + Nothing -> pure $ CARequired Nothing -- no conditions accepted by any operator + Just UsageConditions {conditionsCommit = latestAcceptedCommit} -> do + operatorAcceptedConds_ <- + maybeFirstRow id $ + DB.query + db + [sql| + SELECT conditions_commit, accepted_at + FROM operator_usage_conditions + WHERE server_operator_id = ? + ORDER BY operator_usage_conditions_id DESC + LIMIT 1 + |] + (Only operatorId) + pure $ case operatorAcceptedConds_ of + Just (operatorCommit, acceptedAt_) + | operatorCommit /= latestAcceptedCommit -> CARequired Nothing -- TODO should we consider this operator disabled? + | currentCommit /= latestAcceptedCommit -> CARequired $ conditionsRequiredOrDeadline createdAt (fromMaybe now notifiedAt) + | otherwise -> CAAccepted acceptedAt_ + _ -> CARequired Nothing -- no conditions were accepted for this operator getCurrentUsageConditions :: DB.Connection -> ExceptT StoreError IO UsageConditions getCurrentUsageConditions db = ExceptT . firstRow toUsageConditions SEUsageConditionsNotFound $ - DB.query_ - db - [sql| - SELECT usage_conditions_id, conditions_commit, notified_at, created_at - FROM usage_conditions - ORDER BY usage_conditions_id DESC LIMIT 1 - |] + DB.query_ db (usageCondsQuery <> " DESC LIMIT 1") + +usageCondsQuery :: Query +usageCondsQuery = + [sql| + SELECT usage_conditions_id, conditions_commit, notified_at, created_at + FROM usage_conditions + ORDER BY usage_conditions_id + |] toUsageConditions :: (Int64, Text, Maybe UTCTime, UTCTime) -> UsageConditions toUsageConditions (conditionsId, conditionsCommit, notifiedAt, createdAt) = UsageConditions {conditionsId, conditionsCommit, notifiedAt, createdAt} -getLatestAcceptedConditions :: DB.Connection -> ExceptT StoreError IO (Maybe UsageConditions) -getLatestAcceptedConditions db = do - (latestAcceptedCommit_ :: Maybe Text) <- - liftIO $ - maybeFirstRow fromOnly $ - DB.query_ - db - [sql| +getLatestAcceptedConditions :: DB.Connection -> IO (Maybe UsageConditions) +getLatestAcceptedConditions db = + maybeFirstRow toUsageConditions $ + DB.query_ + db + [sql| + SELECT usage_conditions_id, conditions_commit, notified_at, created_at + FROM usage_conditions + WHERE conditions_commit = ( SELECT conditions_commit FROM operator_usage_conditions ORDER BY accepted_at DESC LIMIT 1 - |] - forM latestAcceptedCommit_ $ \latestAcceptedCommit -> - ExceptT . firstRow toUsageConditions SEUsageConditionsNotFound $ - DB.query - db - [sql| - SELECT usage_conditions_id, conditions_commit, notified_at, created_at - FROM usage_conditions - WHERE conditions_commit = ? - |] - (Only latestAcceptedCommit) + ) + |] setConditionsNotified :: DB.Connection -> Int64 -> UTCTime -> IO () -setConditionsNotified db conditionsId notifiedAt = - DB.execute db "UPDATE usage_conditions SET notified_at = ? WHERE usage_conditions_id = ?" (notifiedAt, conditionsId) +setConditionsNotified db condId notifiedAt = + DB.execute db "UPDATE usage_conditions SET notified_at = ? WHERE usage_conditions_id = ?" (notifiedAt, condId) -acceptConditions :: DB.Connection -> Int64 -> NonEmpty ServerOperator -> UTCTime -> ExceptT StoreError IO ([ServerOperator], Maybe UsageConditionsAction) -acceptConditions db conditionsId operators acceptedAt = do - UsageConditions {conditionsCommit} <- getUsageConditionsById_ db conditionsId - liftIO $ forM_ operators $ \ServerOperator {operatorId, operatorTag} -> - DB.execute - db - [sql| - INSERT INTO operator_usage_conditions - (server_operator_id, server_operator_tag, conditions_commit, accepted_at) - VALUES (?,?,?,?) - |] - (operatorId, operatorTag, conditionsCommit, acceptedAt) - getServerOperators db +acceptConditions :: DB.Connection -> Int64 -> NonEmpty Int64 -> UTCTime -> ExceptT StoreError IO () +acceptConditions db condId opIds acceptedAt = do + UsageConditions {conditionsCommit} <- getUsageConditionsById_ db condId + operators <- mapM getServerOperator_ opIds + let ts = Just acceptedAt + liftIO $ forM_ operators $ \op -> acceptConditions_ db op conditionsCommit ts + where + getServerOperator_ opId = + ExceptT $ firstRow toServerOperator (SEOperatorNotFound opId) $ + DB.query db (serverOperatorQuery <> " WHERE operator_id = ?") (Only opId) + +acceptConditions_ :: DB.Connection -> ServerOperator -> Text -> Maybe UTCTime -> IO () +acceptConditions_ db ServerOperator {operatorId, operatorTag} conditionsCommit acceptedAt = + DB.execute + db + [sql| + INSERT INTO operator_usage_conditions + (server_operator_id, server_operator_tag, conditions_commit, accepted_at) + VALUES (?,?,?,?) + |] + (operatorId, operatorTag, conditionsCommit, acceptedAt) getUsageConditionsById_ :: DB.Connection -> Int64 -> ExceptT StoreError IO UsageConditions getUsageConditionsById_ db conditionsId = @@ -708,83 +821,22 @@ getUsageConditionsById_ db conditionsId = |] (Only conditionsId) -setUserServers :: DB.Connection -> User -> NonEmpty UserServers -> ExceptT StoreError IO () -setUserServers db User {userId} userServers = do - currentTs <- liftIO getCurrentTime - forM_ userServers $ do - \UserServers {operator, smpServers, xftpServers} -> do - forM_ operator $ \op -> liftIO $ updateOperator currentTs op - overwriteServers currentTs operator smpServers - overwriteServers currentTs operator xftpServers +setUserServers :: DB.Connection -> User -> NonEmpty UpdatedUserOperatorServers -> ExceptT StoreError IO () +setUserServers db user@User {userId} userServers = checkConstraint SEUniqueID $ liftIO $ do + ts <- getCurrentTime + forM_ userServers $ \UpdatedUserOperatorServers {operator, smpServers, xftpServers} -> do + mapM_ (updateServerOperator db ts) operator + mapM_ (upsertOrDelete SPSMP ts) smpServers + mapM_ (upsertOrDelete SPXFTP ts) xftpServers where - updateOperator :: UTCTime -> ServerOperator -> IO () - updateOperator currentTs ServerOperator {operatorId, enabled, roles = ServerRoles {storage, proxy}} = - DB.execute - db - [sql| - UPDATE server_operators - SET enabled = ?, role_storage = ?, role_proxy = ?, updated_at = ? - WHERE server_operator_id = ? - |] - (enabled, storage, proxy, operatorId, currentTs) - overwriteServers :: forall p. ProtocolTypeI p => UTCTime -> Maybe ServerOperator -> [ServerCfg p] -> ExceptT StoreError IO () - overwriteServers currentTs serverOperator servers = - checkConstraint SEUniqueID . ExceptT $ do - case serverOperator of - Nothing -> - DB.execute db "DELETE FROM protocol_servers WHERE user_id = ? AND server_operator_id IS NULL AND protocol = ?" (userId, protocol) - Just ServerOperator {operatorId} -> - DB.execute db "DELETE FROM protocol_servers WHERE user_id = ? AND server_operator_id = ? AND protocol = ?" (userId, operatorId, protocol) - forM_ servers $ \ServerCfg {server, operator, preset, tested, enabled} -> do - let ProtoServerWithAuth ProtocolServer {host, port, keyHash} auth_ = server - DB.execute - db - [sql| - INSERT INTO protocol_servers - (protocol, host, port, key_hash, basic_auth, operator, preset, tested, enabled, user_id, created_at, updated_at) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?) - |] - ((protocol, host, port, keyHash, safeDecodeUtf8 . unBasicAuth <$> auth_, operator) :. (preset, tested, enabled, userId, currentTs, currentTs)) - pure $ Right () - where - protocol = decodeLatin1 $ strEncode $ protocolTypeI @p - --- updateServerOperators_ :: DB.Connection -> [ServerOperator] -> IO [ServerOperator] --- updateServerOperators_ db operators = do --- DB.execute_ db "DELETE FROM server_operators WHERE preset = 0" --- let (existing, new) = partition (isJust . operatorId) operators --- existing' <- mapM (\op -> upsertExisting op $> op) existing --- new' <- mapM insertNew new --- pure $ existing' <> new' --- where --- upsertExisting ServerOperator {operatorId, name, preset, enabled, roles = ServerRoles {storage, proxy}} --- | preset = --- DB.execute --- db --- [sql| --- UPDATE server_operators --- SET enabled = ?, role_storage = ?, role_proxy = ? --- WHERE server_operator_id = ? --- |] --- (enabled, storage, proxy, operatorId) --- | otherwise = --- DB.execute --- db --- [sql| --- INSERT INTO server_operators (server_operator_id, name, preset, enabled, role_storage, role_proxy) --- VALUES (?,?,?,?,?,?) --- |] --- (operatorId, name, preset, enabled, storage, proxy) --- insertNew op@ServerOperator {name, preset, enabled, roles = ServerRoles {storage, proxy}} = do --- DB.execute --- db --- [sql| --- INSERT INTO server_operators (name, preset, enabled, role_storage, role_proxy) --- VALUES (?,?,?,?,?) --- |] --- (name, preset, enabled, storage, proxy) --- opId <- insertedRowId db --- pure op {operatorId = Just opId} + upsertOrDelete :: ProtocolTypeI p => SProtocolType p -> UTCTime -> AUserServer p -> IO () + upsertOrDelete p ts (AUS _ s@UserServer {serverId, deleted}) = case serverId of + DBNewEntity + | deleted -> pure () + | otherwise -> void $ insertProtocolServer db p user ts s + DBEntityId srvId + | deleted -> DB.execute db "DELETE FROM protocol_servers WHERE user_id = ? AND smp_server_id = ? AND preset = ?" (userId, srvId, False) + | otherwise -> updateProtocolServer db p ts s createCall :: DB.Connection -> User -> Call -> UTCTime -> IO () createCall db user@User {userId} Call {contactId, callId, callUUID, chatItemId, callState} callTs = do diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs index 083079e2ea..fcd9896917 100644 --- a/src/Simplex/Chat/Store/Shared.hs +++ b/src/Simplex/Chat/Store/Shared.hs @@ -127,6 +127,7 @@ data StoreError | SERemoteCtrlNotFound {remoteCtrlId :: RemoteCtrlId} | SERemoteCtrlDuplicateCA | SEProhibitedDeleteUser {userId :: UserId, contactId :: ContactId} + | SEOperatorNotFound {serverOperatorId :: Int64} | SEUsageConditionsNotFound deriving (Show, Exception) diff --git a/src/Simplex/Chat/Terminal.hs b/src/Simplex/Chat/Terminal.hs index e38a34d45f..aa6babfcbd 100644 --- a/src/Simplex/Chat/Terminal.hs +++ b/src/Simplex/Chat/Terminal.hs @@ -1,6 +1,7 @@ {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedLists #-} {-# LANGUAGE OverloadedStrings #-} module Simplex.Chat.Terminal where @@ -13,15 +14,15 @@ import qualified Data.Text as T import Data.Text.Encoding (encodeUtf8) import Database.SQLite.Simple (SQLError (..)) import qualified Database.SQLite.Simple as DB -import Simplex.Chat (defaultChatConfig, operatorSimpleXChat) +import Simplex.Chat (_defaultNtfServers, defaultChatConfig, operatorSimpleXChat) import Simplex.Chat.Controller import Simplex.Chat.Core import Simplex.Chat.Help (chatWelcome) +import Simplex.Chat.Operators import Simplex.Chat.Options import Simplex.Chat.Terminal.Input import Simplex.Chat.Terminal.Output import Simplex.FileTransfer.Client.Presets (defaultXFTPServers) -import Simplex.Messaging.Agent.Env.SQLite (allRoles, presetServerCfg) import Simplex.Messaging.Client (NetworkConfig (..), SMPProxyFallback (..), SMPProxyMode (..), defaultNetworkConfig) import Simplex.Messaging.Util (raceAny_) import System.IO (hFlush, hSetEcho, stdin, stdout) @@ -29,20 +30,24 @@ import System.IO (hFlush, hSetEcho, stdin, stdout) terminalChatConfig :: ChatConfig terminalChatConfig = defaultChatConfig - { defaultServers = - DefaultAgentServers - { smp = - L.fromList $ - map - (presetServerCfg True allRoles operatorSimpleXChat) - [ "smp://u2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU=@smp4.simplex.im,o5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion", - "smp://hpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg=@smp5.simplex.im,jjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion", - "smp://PQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo=@smp6.simplex.im,bylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion" - ], - useSMP = 3, - ntf = ["ntf://FB-Uop7RTaZZEG0ZLD2CIaTjsPh-Fw0zFAnb7QyA8Ks=@ntf2.simplex.im,ntg7jdjy2i3qbib3sykiho3enekwiaqg3icctliqhtqcg6jmoh6cxiad.onion"], - xftp = L.map (presetServerCfg True allRoles operatorSimpleXChat) defaultXFTPServers, - useXFTP = L.length defaultXFTPServers, + { presetServers = + PresetServers + { operators = + [ PresetOperator + { operator = Just operatorSimpleXChat, + smp = + map + (presetServer True) + [ "smp://u2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU=@smp4.simplex.im,o5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion", + "smp://hpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg=@smp5.simplex.im,jjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion", + "smp://PQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo=@smp6.simplex.im,bylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion" + ], + useSMP = 3, + xftp = map (presetServer True) $ L.toList defaultXFTPServers, + useXFTP = 3 + } + ], + ntf = _defaultNtfServers, netCfg = defaultNetworkConfig { smpProxyMode = SPMUnknown, diff --git a/src/Simplex/Chat/Terminal/Main.hs b/src/Simplex/Chat/Terminal/Main.hs index 64703a3a92..b0eb4dac88 100644 --- a/src/Simplex/Chat/Terminal/Main.hs +++ b/src/Simplex/Chat/Terminal/Main.hs @@ -10,7 +10,7 @@ import Data.Maybe (fromMaybe) import Data.Time.Clock (getCurrentTime) import Data.Time.LocalTime (getCurrentTimeZone) import Network.Socket -import Simplex.Chat.Controller (ChatConfig (..), ChatController (..), ChatResponse (..), DefaultAgentServers (DefaultAgentServers, netCfg), SimpleNetCfg (..), currentRemoteHost, versionNumber, versionString) +import Simplex.Chat.Controller (ChatConfig (..), ChatController (..), ChatResponse (..), PresetServers (..), SimpleNetCfg (..), currentRemoteHost, versionNumber, versionString) import Simplex.Chat.Core import Simplex.Chat.Options import Simplex.Chat.Terminal @@ -56,7 +56,7 @@ simplexChatCLI' cfg opts@ChatOpts {chatCmd, chatCmdLog, chatCmdDelay, chatServer putStrLn $ serializeChatResponse (rh, Just user) ts tz rh r welcome :: ChatConfig -> ChatOpts -> IO () -welcome ChatConfig {defaultServers = DefaultAgentServers {netCfg}} ChatOpts {coreOptions = CoreChatOpts {dbFilePrefix, simpleNetCfg = SimpleNetCfg {socksProxy, socksMode, smpProxyMode_, smpProxyFallback_}}} = +welcome ChatConfig {presetServers = PresetServers {netCfg}} ChatOpts {coreOptions = CoreChatOpts {dbFilePrefix, simpleNetCfg = SimpleNetCfg {socksProxy, socksMode, smpProxyMode_, smpProxyFallback_}}} = mapM_ putStrLn [ versionString versionNumber, diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 1e6986ee03..2f289afe4b 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -19,12 +19,13 @@ import qualified Data.ByteString.Lazy.Char8 as LB import Data.Char (isSpace, toUpper) import Data.Function (on) import Data.Int (Int64) -import Data.List (foldl', groupBy, intercalate, intersperse, partition, sortOn) +import Data.List (groupBy, intercalate, intersperse, partition, sortOn) import Data.List.NonEmpty (NonEmpty (..)) import qualified Data.List.NonEmpty as L import Data.Map.Strict (Map) import qualified Data.Map.Strict as M import Data.Maybe (fromMaybe, isJust, isNothing, mapMaybe) +import Data.String import Data.Text (Text) import qualified Data.Text as T import Data.Text.Encoding (decodeLatin1) @@ -54,7 +55,7 @@ import Simplex.Chat.Types.Shared import Simplex.Chat.Types.UITheme import qualified Simplex.FileTransfer.Transport as XFTP import Simplex.Messaging.Agent.Client (ProtocolTestFailure (..), ProtocolTestStep (..), SubscriptionsInfo (..)) -import Simplex.Messaging.Agent.Env.SQLite (NetworkConfig (..), ServerCfg (..)) +import Simplex.Messaging.Agent.Env.SQLite (NetworkConfig (..), ServerRoles (..)) import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..)) import Simplex.Messaging.Client (SMPProxyFallback, SMPProxyMode (..), SocksMode (..)) @@ -96,10 +97,9 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe CRChats chats -> viewChats ts tz chats CRApiChat u chat _ -> ttyUser u $ if testView then testViewChat chat else [viewJSON chat] CRApiParsedMarkdown ft -> [viewJSON ft] - CRUserProtoServers u userServers operators -> ttyUser u $ viewUserServers userServers operators testView CRServerTestResult u srv testFailure -> ttyUser u $ viewServerTestResult srv testFailure - CRServerOperators {} -> [] - CRUserServers {} -> [] + CRServerOperators ops ca -> viewServerOperators ops ca + CRUserServers u uss -> ttyUser u $ concatMap viewUserServers uss <> (if testView then [] else serversUserHelp) CRUserServersValidation _ -> [] CRUsageConditions {} -> [] CRChatItemTTL u ttl -> ttyUser u $ viewChatItemTTL ttl @@ -1214,27 +1214,31 @@ viewUserPrivacy User {userId} User {userId = userId', localDisplayName = n', sho "profile is " <> if isJust viewPwdHash then "hidden" else "visible" ] -viewUserServers :: AUserProtoServers -> [ServerOperator] -> Bool -> [StyledString] -viewUserServers (AUPS UserProtoServers {serverProtocol = p, protoServers, presetServers}) operators testView = - customServers - <> if testView - then [] - else - [ "", - "use " <> highlight (srvCmd <> " test ") <> " to test " <> pName <> " server connection", - "use " <> highlight (srvCmd <> " ") <> " to configure " <> pName <> " servers", - "use " <> highlight (srvCmd <> " default") <> " to remove configured " <> pName <> " servers and use presets" - ] - <> case p of - SPSMP -> ["(chat option " <> highlight' "-s" <> " (" <> highlight' "--server" <> ") has precedence over saved SMP servers for chat session)"] - SPXFTP -> ["(chat option " <> highlight' "-xftp-servers" <> " has precedence over saved XFTP servers for chat session)"] +viewUserServers :: UserOperatorServers -> [StyledString] +viewUserServers (UserOperatorServers _ [] []) = [] +viewUserServers UserOperatorServers {operator, smpServers, xftpServers} = + [plain $ maybe "Your servers" shortViewOperator operator] + <> viewServers SPSMP smpServers + <> viewServers SPXFTP xftpServers where - srvCmd = "/" <> strEncode p - pName = protocolName p - customServers = - if null protoServers - then ("no " <> pName <> " servers saved, using presets: ") : viewServers operators presetServers - else viewServers operators protoServers + viewServers :: ProtocolTypeI p => SProtocolType p -> [UserServer p] -> [StyledString] + viewServers _ [] = [] + viewServers p srvs = [" " <> protocolName p <> " servers"] <> map (plain . (" " <> ) . viewServer) srvs + where + viewServer UserServer {server, preset, tested, enabled} = safeDecodeUtf8 (strEncode server) <> serverInfo + where + serverInfo = if null serverInfo_ then "" else parens $ T.intercalate ", " serverInfo_ + serverInfo_ = ["preset" | preset] <> testedInfo <> ["disabled" | not enabled] + testedInfo = maybe [] (\t -> ["test: " <> if t then "passed" else "failed"]) tested + +serversUserHelp :: [StyledString] +serversUserHelp = + [ "", + "use " <> highlight' "/smp test " <> " to test SMP server connection", + "use " <> highlight' "/smp " <> " to configure SMP servers", + "or the same commands starting from /xftp for XFTP servers", + "chat options " <> highlight' "-s" <> " (" <> highlight' "--server" <> ") and " <> highlight' "--xftp-servers" <> " have precedence over preset servers for new user profiles" + ] protocolName :: ProtocolTypeI p => SProtocolType p -> StyledString protocolName = plain . map toUpper . T.unpack . decodeLatin1 . strEncode @@ -1255,6 +1259,53 @@ viewServerTestResult (AProtoServerWithAuth p _) = \case where pName = protocolName p +viewServerOperators :: [ServerOperator] -> Maybe UsageConditionsAction -> [StyledString] +viewServerOperators ops ca = map (plain . viewOperator) ops <> maybe [] viewConditionsAction ca + +viewOperator :: ServerOperator' s -> Text +viewOperator op@ServerOperator {tradeName, legalName, serverDomains, conditionsAcceptance} = + viewOpIdTag op + <> tradeName + <> maybe "" parens legalName + <> (", domains: " <> T.intercalate ", " serverDomains) + <> (", conditions: " <> viewOpConditions conditionsAcceptance) + <> (", " <> viewOpEnabled op) + +shortViewOperator :: ServerOperator -> Text +shortViewOperator op@ServerOperator {operatorId = DBEntityId opId, tradeName} = + tshow opId <> ". " <> tradeName <> parens (viewOpEnabled op) + +viewOpIdTag :: ServerOperator' s -> Text +viewOpIdTag ServerOperator {operatorId, operatorTag} = case operatorId of + DBEntityId i -> tshow i <> " - " <> tag + DBNewEntity -> tag + where + tag = maybe "" textEncode operatorTag <> ". " + +viewOpConditions :: ConditionsAcceptance -> Text +viewOpConditions = \case + CAAccepted ts -> viewCond "accepted" ts + CARequired ts -> viewCond "required" ts + where + viewCond w ts = w <> maybe "" (parens . tshow) ts + +viewOpEnabled :: ServerOperator' s -> Text +viewOpEnabled ServerOperator {enabled, roles = ServerRoles {storage, proxy}} + | enabled && storage && proxy = "enabled" + | enabled && storage = "enabled storage" + | enabled && proxy = "enabled proxy" + | otherwise = "disabled" + +viewConditionsAction :: UsageConditionsAction -> [StyledString] +viewConditionsAction = \case + UCAReview {operators, deadline, showNotice} | showNotice -> case deadline of + Just ts -> [plain $ "New conditions will be accepted at " <> tshow ts <> " for " <> ops] + Nothing -> [plain $ "New conditions have to be accepted for " <> ops] + where + ops = T.intercalate ", " $ map legalName_ operators + legalName_ ServerOperator {tradeName, legalName} = fromMaybe tradeName legalName + _ -> [] + viewChatItemTTL :: Maybe Int64 -> [StyledString] viewChatItemTTL = \case Nothing -> ["old messages are not being deleted"] @@ -1331,11 +1382,11 @@ viewConnectionStats ConnectionStats {rcvQueuesInfo, sndQueuesInfo} = ["receiving messages via: " <> viewRcvQueuesInfo rcvQueuesInfo | not $ null rcvQueuesInfo] <> ["sending messages via: " <> viewSndQueuesInfo sndQueuesInfo | not $ null sndQueuesInfo] -viewServers :: ProtocolTypeI p => [ServerOperator] -> NonEmpty (ServerCfg p) -> [StyledString] -viewServers operators = map (plain . (\ServerCfg {server, operator} -> B.unpack (strEncode server) <> viewOperator operator)) . L.toList - where - ops :: Map (Maybe Int64) Text = foldl' (\m ServerOperator {operatorId, tradeName} -> M.insert (Just operatorId) tradeName m) M.empty operators - viewOperator = maybe "" $ \op -> " (operator " <> maybe (show op) T.unpack (M.lookup (Just op) ops) <> ")" +-- viewServers :: ProtocolTypeI p => [ServerOperator] -> NonEmpty (ServerCfg p) -> [StyledString] +-- viewServers operators = map (plain . (\ServerCfg {server, operator} -> B.unpack (strEncode server) <> viewOperator operator)) . L.toList +-- where +-- ops :: Map (Maybe DBEntityId) Text = foldl' (\m ServerOperator {operatorId, tradeName} -> M.insert (Just operatorId) tradeName m) M.empty operators +-- viewOperator = maybe "" $ \op -> " (operator " <> maybe (show op) T.unpack (M.lookup (Just op) ops) <> ")" viewRcvQueuesInfo :: [RcvQueueInfo] -> StyledString viewRcvQueuesInfo = plain . intercalate ", " . map showQueueInfo @@ -1934,7 +1985,9 @@ viewVersionInfo logLevel CoreVersionInfo {version, simplexmqVersion, simplexmqCo then [versionString version, updateStr, "simplexmq: " <> simplexmqVersion <> parens simplexmqCommit] else [versionString version, updateStr] where - parens s = " (" <> s <> ")" + +parens :: (IsString a, Semigroup a) => a -> a +parens s = " (" <> s <> ")" viewRemoteHosts :: [RemoteHostInfo] -> [StyledString] viewRemoteHosts = \case diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index d435af186e..b3d8166f9f 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -25,9 +25,10 @@ import Data.Maybe (isNothing) import qualified Data.Text as T import Network.Socket import Simplex.Chat -import Simplex.Chat.Controller (ChatCommand (..), ChatConfig (..), ChatController (..), ChatDatabase (..), ChatLogLevel (..), defaultSimpleNetCfg) +import Simplex.Chat.Controller (ChatCommand (..), ChatConfig (..), ChatController (..), ChatDatabase (..), ChatLogLevel (..), PresetServers (..), defaultSimpleNetCfg) import Simplex.Chat.Core import Simplex.Chat.Options +import Simplex.Chat.Operators (PresetOperator (..), presetServer) import Simplex.Chat.Protocol (currentChatVersion, pqEncryptionCompressionVersion) import Simplex.Chat.Store import Simplex.Chat.Store.Profiles @@ -94,8 +95,8 @@ testCoreOpts = { dbFilePrefix = "./simplex_v1", dbKey = "", -- dbKey = "this is a pass-phrase to encrypt the database", - smpServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001"], - xftpServers = ["xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002"], + smpServers = [], + xftpServers = [], simpleNetCfg = defaultSimpleNetCfg, logLevel = CLLImportant, logConnections = False, @@ -149,6 +150,18 @@ testCfg :: ChatConfig testCfg = defaultChatConfig { agentConfig = testAgentCfg, + presetServers = + (presetServers defaultChatConfig) + { operators = + [ PresetOperator + { operator = Nothing, + smp = map (presetServer True) ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001"], + useSMP = 1, + xftp = map (presetServer True) ["xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002"], + useXFTP = 1 + } + ] + }, showReceipts = False, testView = True, tbqSize = 16 diff --git a/tests/ChatTests/Direct.hs b/tests/ChatTests/Direct.hs index 8756657e59..bd2a267c3a 100644 --- a/tests/ChatTests/Direct.hs +++ b/tests/ChatTests/Direct.hs @@ -25,7 +25,7 @@ import Database.SQLite.Simple (Only (..)) import Simplex.Chat.AppSettings (defaultAppSettings) import qualified Simplex.Chat.AppSettings as AS import Simplex.Chat.Call -import Simplex.Chat.Controller (ChatConfig (..), DefaultAgentServers (..)) +import Simplex.Chat.Controller (ChatConfig (..), PresetServers (..)) import Simplex.Chat.Messages (ChatItemId) import Simplex.Chat.Options import Simplex.Chat.Protocol (supportedChatVRange) @@ -334,8 +334,8 @@ testRetryConnectingClientTimeout tmp = do { quotaExceededTimeout = 1, messageRetryInterval = RetryInterval2 {riFast = fastRetryInterval, riSlow = fastRetryInterval} }, - defaultServers = - let def@DefaultAgentServers {netCfg} = defaultServers testCfg + presetServers = + let def@PresetServers {netCfg} = presetServers testCfg in def {netCfg = (netCfg :: NetworkConfig) {tcpTimeout = 10}} } opts' = @@ -1141,17 +1141,32 @@ testGetSetSMPServers :: HasCallStack => FilePath -> IO () testGetSetSMPServers = testChat2 aliceProfile bobProfile $ \alice _ -> do - alice #$> ("/_servers 1 smp", id, "smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001") + alice ##> "/_servers 1" + alice <## "Your servers" + alice <## " SMP servers" + alice <## " smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001 (preset)" + alice <## " XFTP servers" + alice <## " xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002 (preset)" alice #$> ("/smp smp://1234-w==@smp1.example.im", id, "ok") - alice #$> ("/smp", id, "smp://1234-w==@smp1.example.im") + alice ##> "/smp" + alice <## "Your servers" + alice <## " SMP servers" + alice <## " smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001 (preset, disabled)" + alice <## " smp://1234-w==@smp1.example.im" alice #$> ("/smp smp://1234-w==:password@smp1.example.im", id, "ok") - alice #$> ("/smp", id, "smp://1234-w==:password@smp1.example.im") + -- alice #$> ("/smp", id, "smp://1234-w==:password@smp1.example.im") + alice ##> "/smp" + alice <## "Your servers" + alice <## " SMP servers" + alice <## " smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001 (preset, disabled)" + alice <## " smp://1234-w==:password@smp1.example.im" alice #$> ("/smp smp://2345-w==@smp2.example.im smp://3456-w==@smp3.example.im:5224", id, "ok") alice ##> "/smp" - alice <## "smp://2345-w==@smp2.example.im" - alice <## "smp://3456-w==@smp3.example.im:5224" - alice #$> ("/smp default", id, "ok") - alice #$> ("/smp", id, "smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001") + alice <## "Your servers" + alice <## " SMP servers" + alice <## " smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001 (preset, disabled)" + alice <## " smp://2345-w==@smp2.example.im" + alice <## " smp://3456-w==@smp3.example.im:5224" testTestSMPServerConnection :: HasCallStack => FilePath -> IO () testTestSMPServerConnection = @@ -1172,17 +1187,31 @@ testGetSetXFTPServers :: HasCallStack => FilePath -> IO () testGetSetXFTPServers = testChat2 aliceProfile bobProfile $ \alice _ -> withXFTPServer $ do - alice #$> ("/_servers 1 xftp", id, "xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002") + alice ##> "/_servers 1" + alice <## "Your servers" + alice <## " SMP servers" + alice <## " smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001 (preset)" + alice <## " XFTP servers" + alice <## " xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002 (preset)" alice #$> ("/xftp xftp://1234-w==@xftp1.example.im", id, "ok") - alice #$> ("/xftp", id, "xftp://1234-w==@xftp1.example.im") + alice ##> "/xftp" + alice <## "Your servers" + alice <## " XFTP servers" + alice <## " xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002 (preset, disabled)" + alice <## " xftp://1234-w==@xftp1.example.im" alice #$> ("/xftp xftp://1234-w==:password@xftp1.example.im", id, "ok") - alice #$> ("/xftp", id, "xftp://1234-w==:password@xftp1.example.im") + alice ##> "/xftp" + alice <## "Your servers" + alice <## " XFTP servers" + alice <## " xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002 (preset, disabled)" + alice <## " xftp://1234-w==:password@xftp1.example.im" alice #$> ("/xftp xftp://2345-w==@xftp2.example.im xftp://3456-w==@xftp3.example.im:5224", id, "ok") alice ##> "/xftp" - alice <## "xftp://2345-w==@xftp2.example.im" - alice <## "xftp://3456-w==@xftp3.example.im:5224" - alice #$> ("/xftp default", id, "ok") - alice #$> ("/xftp", id, "xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002") + alice <## "Your servers" + alice <## " XFTP servers" + alice <## " xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002 (preset, disabled)" + alice <## " xftp://2345-w==@xftp2.example.im" + alice <## " xftp://3456-w==@xftp3.example.im:5224" testTestXFTPServer :: HasCallStack => FilePath -> IO () testTestXFTPServer = @@ -1800,11 +1829,17 @@ testCreateUserSameServers = where checkCustomServers alice = do alice ##> "/smp" - alice <## "smp://2345-w==@smp2.example.im" - alice <## "smp://3456-w==@smp3.example.im:5224" + alice <## "Your servers" + alice <## " SMP servers" + alice <## " smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001 (preset, disabled)" + alice <## " smp://2345-w==@smp2.example.im" + alice <## " smp://3456-w==@smp3.example.im:5224" alice ##> "/xftp" - alice <## "xftp://2345-w==@xftp2.example.im" - alice <## "xftp://3456-w==@xftp3.example.im:5224" + alice <## "Your servers" + alice <## " XFTP servers" + alice <## " xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002 (preset, disabled)" + alice <## " xftp://2345-w==@xftp2.example.im" + alice <## " xftp://3456-w==@xftp3.example.im:5224" testDeleteUser :: HasCallStack => FilePath -> IO () testDeleteUser = diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index a7de42128c..a51f42114b 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -1,8 +1,10 @@ +{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE NumericUnderscores #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PostfixOperators #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} +{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} module ChatTests.Groups where diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index 06ed9aa5bc..1d390e1236 100644 --- a/tests/ChatTests/Profiles.hs +++ b/tests/ChatTests/Profiles.hs @@ -2,6 +2,7 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PostfixOperators #-} {-# LANGUAGE TypeApplications #-} +{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} module ChatTests.Profiles where @@ -1733,7 +1734,16 @@ testChangePCCUserDiffSrv tmp = do -- Create new user with different servers alice ##> "/create user alisa" showActiveUser alice "alisa" - alice #$> ("/smp smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7003", id, "ok") + alice ##> "/smp" + alice <## "Your servers" + alice <## " SMP servers" + alice <## " smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001 (preset)" + alice #$> ("/smp smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@127.0.0.1:7003", id, "ok") + alice ##> "/smp" + alice <## "Your servers" + alice <## " SMP servers" + alice <## " smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001 (preset, disabled)" + alice <## " smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@127.0.0.1:7003" alice ##> "/user alice" showActiveUser alice "alice (Alice)" -- Change connection to newly created user and use the newly created connection diff --git a/tests/RandomServers.hs b/tests/RandomServers.hs index e0b1939c9e..8b0b94dbd5 100644 --- a/tests/RandomServers.hs +++ b/tests/RandomServers.hs @@ -1,53 +1,64 @@ +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE GADTs #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE StandaloneDeriving #-} {-# OPTIONS_GHC -Wno-orphans #-} +{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} module RandomServers where import Control.Monad (replicateM) +import Data.Foldable (foldMap') +import Data.List (sortOn) +import Data.List.NonEmpty (NonEmpty) import qualified Data.List.NonEmpty as L -import Simplex.Chat (cfgServers, cfgServersToUse, defaultChatConfig, randomServers) -import Simplex.Chat.Controller (ChatConfig (..)) -import Simplex.Messaging.Agent.Env.SQLite (ServerCfg (..), ServerRoles (..)) +import Data.Monoid (Sum (..)) +import Simplex.Chat (defaultChatConfig, randomPresetServers) +import Simplex.Chat.Controller (ChatConfig (..), PresetServers (..)) +import Simplex.Chat.Operators (DBEntityId' (..), NewUserServer, UserServer' (..), operatorServers, operatorServersToUse) +import Simplex.Messaging.Agent.Env.SQLite (ServerRoles (..)) import Simplex.Messaging.Protocol (ProtoServerWithAuth (..), SProtocolType (..), UserProtocol) import Test.Hspec randomServersTests :: Spec randomServersTests = describe "choosig random servers" $ do - it "should choose 4 random SMP servers and keep the rest disabled" testRandomSMPServers - it "should keep all 6 XFTP servers" testRandomXFTPServers + it "should choose 4 + 3 random SMP servers and keep the rest disabled" testRandomSMPServers + it "should choose 3 + 3 random XFTP servers and keep the rest disabled" testRandomXFTPServers deriving instance Eq ServerRoles -deriving instance Eq (ServerCfg p) +deriving instance Eq (DBEntityId' s) + +deriving instance Eq (UserServer' s p) testRandomSMPServers :: IO () testRandomSMPServers = do [srvs1, srvs2, srvs3] <- replicateM 3 $ - checkEnabled SPSMP 4 False =<< randomServers SPSMP defaultChatConfig + checkEnabled SPSMP 7 False =<< randomPresetServers SPSMP (presetServers defaultChatConfig) (srvs1 == srvs2 && srvs2 == srvs3) `shouldBe` False -- && to avoid rare failures testRandomXFTPServers :: IO () testRandomXFTPServers = do [srvs1, srvs2, srvs3] <- replicateM 3 $ - checkEnabled SPXFTP 6 True =<< randomServers SPXFTP defaultChatConfig - (srvs1 == srvs2 && srvs2 == srvs3) `shouldBe` True + checkEnabled SPXFTP 6 False =<< randomPresetServers SPXFTP (presetServers defaultChatConfig) + (srvs1 == srvs2 && srvs2 == srvs3) `shouldBe` False -- && to avoid rare failures -checkEnabled :: UserProtocol p => SProtocolType p -> Int -> Bool -> (L.NonEmpty (ServerCfg p), [ServerCfg p]) -> IO [ServerCfg p] -checkEnabled p n allUsed (srvs, _) = do - let def = defaultServers defaultChatConfig - cfgSrvs = L.sortWith server' $ cfgServers p def - toUse = cfgServersToUse p def - srvs == cfgSrvs `shouldBe` allUsed - L.map enable srvs `shouldBe` L.map enable cfgSrvs - let enbldSrvs = L.filter (\ServerCfg {enabled} -> enabled) srvs +checkEnabled :: UserProtocol p => SProtocolType p -> Int -> Bool -> NonEmpty (NewUserServer p) -> IO [NewUserServer p] +checkEnabled p n allUsed srvs = do + let srvs' = sortOn server' $ L.toList srvs + PresetServers {operators = presetOps} = presetServers defaultChatConfig + presetSrvs = sortOn server' $ concatMap (operatorServers p) presetOps + Sum toUse = foldMap' (Sum . operatorServersToUse p) presetOps + srvs' == presetSrvs `shouldBe` allUsed + map enable srvs' `shouldBe` map enable presetSrvs + let enbldSrvs = filter (\UserServer {enabled} -> enabled) srvs' toUse `shouldBe` n length enbldSrvs `shouldBe` n pure enbldSrvs where - server' ServerCfg {server = ProtoServerWithAuth srv _} = srv - enable :: forall p. ServerCfg p -> ServerCfg p - enable srv = (srv :: ServerCfg p) {enabled = False} + server' UserServer {server = ProtoServerWithAuth srv _} = srv + enable :: forall p. NewUserServer p -> NewUserServer p + enable srv = (srv :: NewUserServer p) {enabled = False} From 1fbf21d3953bea03ff05d827fe46dca05845bc90 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Fri, 15 Nov 2024 07:15:04 +0000 Subject: [PATCH 023/167] core: validate servers of all user profiles (#5180) * core: validate servers of all user profiles * validate all servers * fix parsing, test --- simplex-chat.cabal | 1 + src/Simplex/Chat.hs | 12 ++- src/Simplex/Chat/Controller.hs | 4 +- src/Simplex/Chat/Operators.hs | 130 ++++++++++++++++++++++++--------- src/Simplex/Chat/View.hs | 2 +- tests/OperatorTests.hs | 92 +++++++++++++++++++++++ tests/RandomServers.hs | 4 +- tests/Test.hs | 2 + 8 files changed, 207 insertions(+), 40 deletions(-) create mode 100644 tests/OperatorTests.hs diff --git a/simplex-chat.cabal b/simplex-chat.cabal index d3ea814011..8d1a298af4 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -618,6 +618,7 @@ test-suite simplex-chat-test MarkdownTests MessageBatching MobileTests + OperatorTests ProtocolTests RandomServers RemoteTests diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 86b6a5e51b..05f99656bb 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -1608,7 +1608,7 @@ processChatCommand' vr = \case APIGetUserServers userId -> withUserId userId $ \user -> withFastStore $ \db -> CRUserServers user <$> (liftIO . groupByOperator =<< getUserServers db user) APISetUserServers userId userServers -> withUserId userId $ \user -> do - let errors = validateUserServers userServers + errors <- validateAllUsersServers userId $ L.toList userServers unless (null errors) $ throwChatError (CECommandError $ "user servers validation error(s): " <> show errors) (operators, smpServers, xftpServers) <- withFastStore $ \db -> do setUserServers db user userServers @@ -1620,7 +1620,8 @@ processChatCommand' vr = \case setProtocolServers a auId $ agentServerCfgs opDomains (rndServers SPSMP rs) smpServers setProtocolServers a auId $ agentServerCfgs opDomains (rndServers SPXFTP rs) xftpServers ok_ - APIValidateServers userServers -> pure $ CRUserServersValidation $ validateUserServers userServers + APIValidateServers userId userServers -> withUserId userId $ \user -> + CRUserServersValidation user <$> validateAllUsersServers userId userServers APIGetUsageConditions -> do (usageConditions, acceptedConditions) <- withFastStore $ \db -> do usageConditions <- getCurrentUsageConditions db @@ -2926,6 +2927,11 @@ processChatCommand' vr = \case withServerProtocol p action = case userProtocol p of Just Dict -> action _ -> throwChatError $ CEServerProtocol $ AProtocolType p + validateAllUsersServers :: UserServersClass u => Int64 -> [u] -> CM [UserServersError] + validateAllUsersServers currUserId userServers = withFastStore $ \db -> do + users' <- filter (\User {userId} -> userId /= currUserId) <$> liftIO (getUsers db) + others <- mapM (\user -> liftIO . fmap (user,) . groupByOperator =<< getUserServers db user) users' + pure $ validateUserServers userServers others forwardFile :: ChatName -> FileTransferId -> (ChatName -> CryptoFile -> ChatCommand) -> CM ChatResponse forwardFile chatName fileId sendCommand = withUser $ \user -> do withStore (\db -> getFileTransfer db user fileId) >>= \case @@ -8242,7 +8248,7 @@ chatCommandP = "/_operators " *> (APISetServerOperators <$> jsonP), "/_servers " *> (APIGetUserServers <$> A.decimal), "/_servers " *> (APISetUserServers <$> A.decimal <* A.space <*> jsonP), - "/_validate_servers " *> (APIValidateServers <$> jsonP), + "/_validate_servers " *> (APIValidateServers <$> A.decimal <* A.space <*> jsonP), "/_conditions" $> APIGetUsageConditions, "/_conditions_notified " *> (APISetConditionsNotified <$> A.decimal), "/_accept_conditions " *> (APIAcceptConditions <$> A.decimal <*> _strP), diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 3c2b8045d7..27acf8990b 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -358,7 +358,7 @@ data ChatCommand | APISetServerOperators (NonEmpty ServerOperator) | APIGetUserServers UserId | APISetUserServers UserId (NonEmpty UpdatedUserOperatorServers) - | APIValidateServers (NonEmpty UpdatedUserOperatorServers) -- response is CRUserServersValidation + | APIValidateServers UserId [ValidatedUserOperatorServers] -- response is CRUserServersValidation | APIGetUsageConditions | APISetConditionsNotified Int64 | APIAcceptConditions Int64 (NonEmpty Int64) @@ -590,7 +590,7 @@ data ChatResponse | CRServerTestResult {user :: User, testServer :: AProtoServerWithAuth, testFailure :: Maybe ProtocolTestFailure} | CRServerOperators {operators :: [ServerOperator], conditionsAction :: Maybe UsageConditionsAction} | CRUserServers {user :: User, userServers :: [UserOperatorServers]} - | CRUserServersValidation {serverErrors :: [UserServersError]} + | CRUserServersValidation {user :: User, serverErrors :: [UserServersError]} | CRUsageConditions {usageConditions :: UsageConditions, conditionsText :: Text, acceptedConditions :: Maybe UsageConditions} | CRChatItemTTL {user :: User, chatItemTTL :: Maybe Int64} | CRNetworkConfig {networkConfig :: NetworkConfig} diff --git a/src/Simplex/Chat/Operators.hs b/src/Simplex/Chat/Operators.hs index 55de357090..6bf1a75da4 100644 --- a/src/Simplex/Chat/Operators.hs +++ b/src/Simplex/Chat/Operators.hs @@ -1,5 +1,6 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE KindSignatures #-} @@ -13,6 +14,7 @@ {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TupleSections #-} {-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeFamilyDependencies #-} {-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} module Simplex.Chat.Operators where @@ -22,10 +24,12 @@ import Data.Aeson (FromJSON (..), ToJSON (..)) import qualified Data.Aeson as J import qualified Data.Aeson.Encoding as JE import qualified Data.Aeson.TH as JQ +import Data.Either (partitionEithers) import Data.FileEmbed import Data.Foldable (foldMap') import Data.IORef import Data.Int (Int64) +import Data.Kind import Data.List (find, foldl') import Data.List.NonEmpty (NonEmpty) import qualified Data.List.NonEmpty as L @@ -43,11 +47,12 @@ import Database.SQLite.Simple.FromField (FromField (..)) import Database.SQLite.Simple.ToField (ToField (..)) import Language.Haskell.TH.Syntax (lift) import Simplex.Chat.Operators.Conditions +import Simplex.Chat.Types (User) import Simplex.Chat.Types.Util (textParseJSON) import Simplex.Messaging.Agent.Env.SQLite (ServerCfg (..), ServerRoles (..), allRoles) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, fromTextField_, sumTypeJSON) -import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), ProtoServerWithAuth (..), ProtocolServer (..), ProtocolType (..), ProtocolTypeI, SProtocolType (..), UserProtocol) +import Simplex.Messaging.Protocol (AProtocolType (..), ProtoServerWithAuth (..), ProtocolServer (..), ProtocolType (..), ProtocolTypeI, SProtocolType (..), UserProtocol) import Simplex.Messaging.Transport.Client (TransportHost (..)) import Simplex.Messaging.Util (atomicModifyIORef'_, safeDecodeUtf8) @@ -196,10 +201,56 @@ data UpdatedUserOperatorServers = UpdatedUserOperatorServers } deriving (Show) -updatedServers :: UserProtocol p => UpdatedUserOperatorServers -> SProtocolType p -> [AUserServer p] -updatedServers UpdatedUserOperatorServers {smpServers, xftpServers} = \case - SPSMP -> smpServers - SPXFTP -> xftpServers +data ValidatedUserOperatorServers = ValidatedUserOperatorServers + { operator :: Maybe ServerOperator, + smpServers :: [AValidatedServer 'PSMP], + xftpServers :: [AValidatedServer 'PXFTP] + } + deriving (Show) + +data AValidatedServer p = forall s. AVS (SDBStored s) (ValidatedServer s p) + +deriving instance Show (AValidatedServer p) + +type ValidatedServer s p = UserServer_ s ValidatedProtoServer p + +data ValidatedProtoServer p = ValidatedProtoServer {unVPS :: Either Text (ProtoServerWithAuth p)} + deriving (Show) + +class UserServersClass u where + type AServer u = (s :: ProtocolType -> Type) | s -> u + operator' :: u -> Maybe ServerOperator + partitionValid :: [AServer u p] -> ([Text], [AUserServer p]) + servers' :: UserProtocol p => u -> SProtocolType p -> [AServer u p] + +instance UserServersClass UserOperatorServers where + type AServer UserOperatorServers = UserServer_ 'DBStored ProtoServerWithAuth + operator' UserOperatorServers {operator} = operator + partitionValid ss = ([], map (AUS SDBStored) ss) + servers' UserOperatorServers {smpServers, xftpServers} = \case + SPSMP -> smpServers + SPXFTP -> xftpServers + +instance UserServersClass UpdatedUserOperatorServers where + type AServer UpdatedUserOperatorServers = AUserServer + operator' UpdatedUserOperatorServers {operator} = operator + partitionValid = ([],) + servers' UpdatedUserOperatorServers {smpServers, xftpServers} = \case + SPSMP -> smpServers + SPXFTP -> xftpServers + +instance UserServersClass ValidatedUserOperatorServers where + type AServer ValidatedUserOperatorServers = AValidatedServer + operator' ValidatedUserOperatorServers {operator} = operator + partitionValid = partitionEithers . map serverOrErr + where + serverOrErr :: AValidatedServer p -> Either Text (AUserServer p) + serverOrErr (AVS s srv@UserServer {server = server'}) = (\server -> AUS s srv {server}) <$> unVPS server' + servers' ValidatedUserOperatorServers {smpServers, xftpServers} = \case + SPSMP -> smpServers + SPXFTP -> xftpServers + +type UserServer' s p = UserServer_ s ProtoServerWithAuth p type UserServer p = UserServer' 'DBStored p @@ -209,9 +260,9 @@ data AUserServer p = forall s. AUS (SDBStored s) (UserServer' s p) deriving instance Show (AUserServer p) -data UserServer' s p = UserServer +data UserServer_ s (srv :: ProtocolType -> Type) (p :: ProtocolType) = UserServer { serverId :: DBEntityId' s, - server :: ProtoServerWithAuth p, + server :: srv p, preset :: Bool, tested :: Maybe Bool, enabled :: Bool, @@ -352,35 +403,36 @@ groupByOperator (ops, smpSrvs, xftpSrvs) = do addXFTP srv s@UserOperatorServers {xftpServers} = (s :: UserOperatorServers) {xftpServers = srv : xftpServers} data UserServersError - = USEStorageMissing {protocol :: AProtocolType} - | USEProxyMissing {protocol :: AProtocolType} - | USEDuplicateServer {protocol :: AProtocolType, duplicateServer :: AProtoServerWithAuth, duplicateHost :: TransportHost} + = USENoServers {protocol :: AProtocolType, user :: Maybe User} + | USEStorageMissing {protocol :: AProtocolType, user :: Maybe User} + | USEProxyMissing {protocol :: AProtocolType, user :: Maybe User} + | USEInvalidServer {protocol :: AProtocolType, invalidServer :: Text} + | USEDuplicateServer {protocol :: AProtocolType, duplicateServer :: Text, duplicateHost :: TransportHost} deriving (Show) -validateUserServers :: NonEmpty UpdatedUserOperatorServers -> [UserServersError] -validateUserServers uss = - missingRolesErr SPSMP storage USEStorageMissing - <> missingRolesErr SPSMP proxy USEProxyMissing - <> missingRolesErr SPXFTP storage USEStorageMissing - <> duplicatServerErrs SPSMP - <> duplicatServerErrs SPXFTP +validateUserServers :: UserServersClass u' => [u'] -> [(User, [UserOperatorServers])] -> [UserServersError] +validateUserServers curr others = currUserErrs <> concatMap otherUserErrs others where - missingRolesErr :: (ProtocolTypeI p, UserProtocol p) => SProtocolType p -> (ServerRoles -> Bool) -> (AProtocolType -> UserServersError) -> [UserServersError] - missingRolesErr p roleSel err = [err (AProtocolType p) | not hasRole] + currUserErrs = noServersErrs SPSMP Nothing curr <> noServersErrs SPXFTP Nothing curr <> serverErrs SPSMP curr <> serverErrs SPXFTP curr + otherUserErrs (user, uss) = noServersErrs SPSMP (Just user) uss <> noServersErrs SPXFTP (Just user) uss + noServersErrs :: (UserServersClass u, ProtocolTypeI p, UserProtocol p) => SProtocolType p -> Maybe User -> [u] -> [UserServersError] + noServersErrs p user uss + | noServers opEnabled = [USENoServers p' user] + | otherwise = [USEStorageMissing p' user | noServers (hasRole storage)] <> [USEProxyMissing p' user | noServers (hasRole proxy)] where - hasRole = - any (\(AUS _ UserServer {deleted, enabled}) -> enabled && not deleted) $ - concatMap (`updatedServers` p) $ filter roleEnabled (L.toList uss) - roleEnabled UpdatedUserOperatorServers {operator} = - maybe True (\ServerOperator {enabled, roles} -> enabled && roleSel roles) operator - duplicatServerErrs :: (ProtocolTypeI p, UserProtocol p) => SProtocolType p -> [UserServersError] - duplicatServerErrs p = mapMaybe duplicateErr_ srvs + p' = AProtocolType p + noServers cond = not $ any srvEnabled $ snd $ partitionValid $ concatMap (`servers'` p) $ filter cond uss + opEnabled = maybe True (\ServerOperator {enabled} -> enabled) . operator' + hasRole roleSel = maybe True (\ServerOperator {enabled, roles} -> enabled && roleSel roles) . operator' + srvEnabled (AUS _ UserServer {deleted, enabled}) = enabled && not deleted + serverErrs :: (UserServersClass u, ProtocolTypeI p, UserProtocol p) => SProtocolType p -> [u] -> [UserServersError] + serverErrs p uss = map (USEInvalidServer p') invalidSrvs <> mapMaybe duplicateErr_ srvs where - srvs = - filter (\(AUS _ UserServer {deleted}) -> not deleted) $ - concatMap (`updatedServers` p) (L.toList uss) + p' = AProtocolType p + (invalidSrvs, userSrvs) = partitionValid $ concatMap (`servers'` p) uss + srvs = filter (\(AUS _ UserServer {deleted}) -> not deleted) userSrvs duplicateErr_ (AUS _ srv@UserServer {server}) = - USEDuplicateServer (AProtocolType p) (AProtoServerWithAuth p server) + USEDuplicateServer p' (safeDecodeUtf8 $ strEncode server) <$> find (`S.member` duplicateHosts) (srvHost srv) duplicateHosts = snd $ foldl' addHost (S.empty, S.empty) allHosts allHosts = concatMap (\(AUS _ srv) -> L.toList $ srvHost srv) srvs @@ -421,18 +473,30 @@ instance DBStoredI s => FromJSON (ServerOperator' s) where $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "UCA") ''UsageConditionsAction) instance ProtocolTypeI p => ToJSON (UserServer' s p) where - toEncoding = $(JQ.mkToEncoding defaultJSON ''UserServer') - toJSON = $(JQ.mkToJSON defaultJSON ''UserServer') + toEncoding = $(JQ.mkToEncoding defaultJSON ''UserServer_) + toJSON = $(JQ.mkToJSON defaultJSON ''UserServer_) instance (DBStoredI s, ProtocolTypeI p) => FromJSON (UserServer' s p) where - parseJSON = $(JQ.mkParseJSON defaultJSON ''UserServer') + parseJSON = $(JQ.mkParseJSON defaultJSON ''UserServer_) instance ProtocolTypeI p => FromJSON (AUserServer p) where parseJSON v = (AUS SDBStored <$> parseJSON v) <|> (AUS SDBNew <$> parseJSON v) +instance ProtocolTypeI p => FromJSON (ValidatedProtoServer p) where + parseJSON v = ValidatedProtoServer <$> ((Right <$> parseJSON v) <|> (Left <$> parseJSON v)) + +instance (DBStoredI s, ProtocolTypeI p) => FromJSON (ValidatedServer s p) where + parseJSON = $(JQ.mkParseJSON defaultJSON ''UserServer_) + +instance ProtocolTypeI p => FromJSON (AValidatedServer p) where + parseJSON v = (AVS SDBStored <$> parseJSON v) <|> (AVS SDBNew <$> parseJSON v) + $(JQ.deriveJSON defaultJSON ''UserOperatorServers) instance FromJSON UpdatedUserOperatorServers where parseJSON = $(JQ.mkParseJSON defaultJSON ''UpdatedUserOperatorServers) +instance FromJSON ValidatedUserOperatorServers where + parseJSON = $(JQ.mkParseJSON defaultJSON ''ValidatedUserOperatorServers) + $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "USE") ''UserServersError) diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 2f289afe4b..317fd58a8e 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -100,7 +100,7 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe CRServerTestResult u srv testFailure -> ttyUser u $ viewServerTestResult srv testFailure CRServerOperators ops ca -> viewServerOperators ops ca CRUserServers u uss -> ttyUser u $ concatMap viewUserServers uss <> (if testView then [] else serversUserHelp) - CRUserServersValidation _ -> [] + CRUserServersValidation {} -> [] CRUsageConditions {} -> [] CRChatItemTTL u ttl -> ttyUser u $ viewChatItemTTL ttl CRNetworkConfig cfg -> viewNetworkConfig cfg diff --git a/tests/OperatorTests.hs b/tests/OperatorTests.hs new file mode 100644 index 0000000000..1b867a3e1d --- /dev/null +++ b/tests/OperatorTests.hs @@ -0,0 +1,92 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE StandaloneDeriving #-} +{-# LANGUAGE TypeApplications #-} +{-# OPTIONS_GHC -Wno-orphans #-} +{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} + +module OperatorTests (operatorTests) where + +import qualified Data.List.NonEmpty as L +import Simplex.Chat +import Simplex.Chat.Operators +import Simplex.Chat.Types +import Simplex.FileTransfer.Client.Presets (defaultXFTPServers) +import Simplex.Messaging.Agent.Env.SQLite (ServerRoles (..), allRoles) +import Simplex.Messaging.Protocol +import Test.Hspec + +operatorTests :: Spec +operatorTests = describe "managing server operators" $ do + validateServers + +validateServers :: Spec +validateServers = describe "validate user servers" $ do + it "should pass valid user servers" $ validateUserServers [valid] [] `shouldBe` [] + it "should fail without servers" $ do + validateUserServers [invalidNoServers] [] `shouldBe` [USENoServers aSMP Nothing] + validateUserServers [invalidDisabled] [] `shouldBe` [USENoServers aSMP Nothing] + validateUserServers [invalidDisabledOp] [] `shouldBe` [USENoServers aSMP Nothing, USENoServers aXFTP Nothing] + it "should fail without servers with storage role" $ do + validateUserServers [invalidNoStorage] [] `shouldBe` [USEStorageMissing aSMP Nothing, USEStorageMissing aXFTP Nothing] + it "should fail with duplicate host" $ do + validateUserServers [invalidDuplicate] [] `shouldBe` + [ USEDuplicateServer aSMP "smp://0YuTwO05YJWS8rkjn9eLJDjQhFKvIYd8d4xG8X1blIU=@smp8.simplex.im,beccx4yfxxbvyhqypaavemqurytl6hozr47wfc7uuecacjqdvwpw2xid.onion" "smp8.simplex.im", + USEDuplicateServer aSMP "smp://abcd@smp8.simplex.im" "smp8.simplex.im" + ] + it "should fail with invalid host" $ do + validateUserServers [invalidHost] [] `shouldBe` [USENoServers aXFTP Nothing, USEInvalidServer aSMP "smp:abcd@smp8.simplex.im"] + where + aSMP = AProtocolType SPSMP + aXFTP = AProtocolType SPXFTP + +deriving instance Eq User + +deriving instance Eq UserServersError + +valid :: UpdatedUserOperatorServers +valid = + UpdatedUserOperatorServers + { operator = Just operatorSimpleXChat {operatorId = DBEntityId 1}, + smpServers = map (AUS SDBNew) simplexChatSMPServers, + xftpServers = map (AUS SDBNew . presetServer True) $ L.toList defaultXFTPServers + } + +invalidNoServers :: UpdatedUserOperatorServers +invalidNoServers = (valid :: UpdatedUserOperatorServers) {smpServers = []} + +invalidDisabled :: UpdatedUserOperatorServers +invalidDisabled = + (valid :: UpdatedUserOperatorServers) + { smpServers = map (AUS SDBNew . (\srv -> (srv :: NewUserServer 'PSMP) {enabled = False})) simplexChatSMPServers + } + +invalidDisabledOp :: UpdatedUserOperatorServers +invalidDisabledOp = + (valid :: UpdatedUserOperatorServers) + { operator = Just operatorSimpleXChat {operatorId = DBEntityId 1, enabled = False} + } + +invalidNoStorage :: UpdatedUserOperatorServers +invalidNoStorage = + (valid :: UpdatedUserOperatorServers) + { operator = Just operatorSimpleXChat {operatorId = DBEntityId 1, roles = allRoles {storage = False}} + } + +invalidDuplicate :: UpdatedUserOperatorServers +invalidDuplicate = + (valid :: UpdatedUserOperatorServers) + { smpServers = map (AUS SDBNew) $ simplexChatSMPServers <> [presetServer True "smp://abcd@smp8.simplex.im"] + } + +invalidHost :: ValidatedUserOperatorServers +invalidHost = + ValidatedUserOperatorServers + { operator = Just operatorSimpleXChat {operatorId = DBEntityId 1}, + smpServers = [validatedServer (Left "smp:abcd@smp8.simplex.im"), validatedServer (Right "smp://abcd@smp8.simplex.im")], + xftpServers = [] + } + where + validatedServer srv = + AVS SDBNew (presetServer @'PSMP True "smp://abcd@smp8.simplex.im") {server = ValidatedProtoServer srv} diff --git a/tests/RandomServers.hs b/tests/RandomServers.hs index 8b0b94dbd5..048a2b5e5a 100644 --- a/tests/RandomServers.hs +++ b/tests/RandomServers.hs @@ -1,8 +1,10 @@ {-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE StandaloneDeriving #-} +{-# LANGUAGE TypeSynonymInstances #-} {-# OPTIONS_GHC -Wno-orphans #-} {-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} @@ -16,7 +18,7 @@ import qualified Data.List.NonEmpty as L import Data.Monoid (Sum (..)) import Simplex.Chat (defaultChatConfig, randomPresetServers) import Simplex.Chat.Controller (ChatConfig (..), PresetServers (..)) -import Simplex.Chat.Operators (DBEntityId' (..), NewUserServer, UserServer' (..), operatorServers, operatorServersToUse) +import Simplex.Chat.Operators import Simplex.Messaging.Agent.Env.SQLite (ServerRoles (..)) import Simplex.Messaging.Protocol (ProtoServerWithAuth (..), SProtocolType (..), UserProtocol) import Test.Hspec diff --git a/tests/Test.hs b/tests/Test.hs index 3d59b840dd..079c583a6e 100644 --- a/tests/Test.hs +++ b/tests/Test.hs @@ -10,6 +10,7 @@ import MarkdownTests import MessageBatching import MobileTests import ProtocolTests +import OperatorTests import RandomServers import RemoteTests import SchemaDump @@ -31,6 +32,7 @@ main = do around tmpBracket $ describe "WebRTC encryption" webRTCTests describe "Valid names" validNameTests describe "Message batching" batchingTests + describe "Operators" operatorTests describe "Random servers" randomServersTests around testBracket $ do describe "Mobile API Tests" mobileTests From ff8e29c0eb202792058d5ed391c152d3558ec07c Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Fri, 15 Nov 2024 11:20:32 +0400 Subject: [PATCH 024/167] core: fix accept conditions query (#5187) --- src/Simplex/Chat/Store/Profiles.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs index 39bd4bb985..87b5d2fd64 100644 --- a/src/Simplex/Chat/Store/Profiles.hs +++ b/src/Simplex/Chat/Store/Profiles.hs @@ -796,7 +796,7 @@ acceptConditions db condId opIds acceptedAt = do where getServerOperator_ opId = ExceptT $ firstRow toServerOperator (SEOperatorNotFound opId) $ - DB.query db (serverOperatorQuery <> " WHERE operator_id = ?") (Only opId) + DB.query db (serverOperatorQuery <> " WHERE server_operator_id = ?") (Only opId) acceptConditions_ :: DB.Connection -> ServerOperator -> Text -> Maybe UTCTime -> IO () acceptConditions_ db ServerOperator {operatorId, operatorTag} conditionsCommit acceptedAt = From feb687d3b8bae376691f01807305d76504cfbe73 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Fri, 15 Nov 2024 12:08:15 +0000 Subject: [PATCH 025/167] core: different roles for different protocols (#5185) * core: different roles for different protocols * include current conditions in responses * fix * fix test * fix --------- Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> --- src/Simplex/Chat.hs | 28 ++++++----- src/Simplex/Chat/Controller.hs | 2 +- .../Migrations/M20241027_server_operators.hs | 6 ++- src/Simplex/Chat/Migrations/chat_schema.sql | 6 ++- src/Simplex/Chat/Operators.hs | 31 ++++++++---- src/Simplex/Chat/Store/Profiles.hs | 44 +++++++++-------- src/Simplex/Chat/View.hs | 48 ++++++++++++------- tests/OperatorTests.hs | 4 +- 8 files changed, 104 insertions(+), 65 deletions(-) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 05f99656bb..95bb405bae 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -150,7 +150,8 @@ operatorSimpleXChat = serverDomains = ["simplex.im"], conditionsAcceptance = CARequired Nothing, enabled = True, - roles = allRoles + smpRoles = allRoles, + xftpRoles = allRoles } operatorFlux :: NewServerOperator @@ -163,7 +164,8 @@ operatorFlux = serverDomains = ["simplexonflux.com"], conditionsAcceptance = CARequired Nothing, enabled = False, - roles = ServerRoles {storage = False, proxy = True} + smpRoles = ServerRoles {storage = False, proxy = True}, + xftpRoles = allRoles } defaultChatConfig :: ChatConfig @@ -420,7 +422,7 @@ newChatController getServers p users opDomains = do let rs' = rndServers p rs fmap M.fromList $ forM users $ \u -> - (aUserId u,) . agentServerCfgs opDomains rs' <$> getUpdateUserServers db p presetOps rs' u + (aUserId u,) . agentServerCfgs p opDomains rs' <$> getUpdateUserServers db p presetOps rs' u updateNetworkConfig :: NetworkConfig -> SimpleNetCfg -> NetworkConfig updateNetworkConfig cfg SimpleNetCfg {socksProxy, socksMode, hostMode, requiredHostMode, smpProxyMode_, smpProxyFallback_, smpWebPort, tcpTimeout_, logTLSErrors} = @@ -643,10 +645,10 @@ processChatCommand' vr = \case forM_ users $ \User {localDisplayName = n, activeUser, viewPwdHash} -> when (n == displayName) . throwChatError $ if activeUser || isNothing viewPwdHash then CEUserExists displayName else CEInvalidDisplayName {displayName, validName = ""} - opDomains <- operatorDomains . fst <$> withFastStore getServerOperators + opDomains <- operatorDomains . serverOperators <$> withFastStore getServerOperators rs <- asks randomServers - let smp = agentServerCfgs opDomains (rndServers SPSMP rs) smpServers - xftp = agentServerCfgs opDomains (rndServers SPXFTP rs) xftpServers + let smp = agentServerCfgs SPSMP opDomains (rndServers SPSMP rs) smpServers + xftp = agentServerCfgs SPXFTP opDomains (rndServers SPXFTP rs) xftpServers auId <- withAgent (\a -> createUser a smp xftp) ts <- liftIO $ getCurrentTime >>= if pastTimestamp then coupleDaysAgo else pure user <- withFastStore $ \db -> createUserRecordAt db (AgentUserId auId) p True ts @@ -1601,10 +1603,10 @@ processChatCommand' vr = \case lift $ CRServerTestResult user srv <$> withAgent' (\a -> testProtocolServer a (aUserId user) server) TestProtoServer srv -> withUser $ \User {userId} -> processChatCommand $ APITestProtoServer userId srv - APIGetServerOperators -> uncurry CRServerOperators <$> withFastStore getServerOperators + APIGetServerOperators -> CRServerOperatorConditions <$> withFastStore getServerOperators APISetServerOperators operatorsEnabled -> withFastStore $ \db -> do liftIO $ setServerOperators db operatorsEnabled - uncurry CRServerOperators <$> getServerOperators db + CRServerOperatorConditions <$> getServerOperators db APIGetUserServers userId -> withUserId userId $ \user -> withFastStore $ \db -> CRUserServers user <$> (liftIO . groupByOperator =<< getUserServers db user) APISetUserServers userId userServers -> withUserId userId $ \user -> do @@ -1617,8 +1619,8 @@ processChatCommand' vr = \case rs <- asks randomServers lift $ withAgent' $ \a -> do let auId = aUserId user - setProtocolServers a auId $ agentServerCfgs opDomains (rndServers SPSMP rs) smpServers - setProtocolServers a auId $ agentServerCfgs opDomains (rndServers SPXFTP rs) xftpServers + setProtocolServers a auId $ agentServerCfgs SPSMP opDomains (rndServers SPSMP rs) smpServers + setProtocolServers a auId $ agentServerCfgs SPXFTP opDomains (rndServers SPXFTP rs) xftpServers ok_ APIValidateServers userId userServers -> withUserId userId $ \user -> CRUserServersValidation user <$> validateAllUsersServers userId userServers @@ -1641,7 +1643,7 @@ processChatCommand' vr = \case APIAcceptConditions condId opIds -> withFastStore $ \db -> do currentTs <- liftIO getCurrentTime acceptConditions db condId opIds currentTs - uncurry CRServerOperators <$> getServerOperators db + CRServerOperatorConditions <$> getServerOperators db APISetChatItemTTL userId newTTL_ -> withUserId userId $ \user -> checkStoreNotChanged $ withChatLock "setChatItemTTL" $ do @@ -3777,9 +3779,9 @@ getKnownAgentServers :: (ProtocolTypeI p, UserProtocol p) => SProtocolType p -> getKnownAgentServers p user = do rs <- asks randomServers withStore $ \db -> do - opDomains <- operatorDomains . fst <$> getServerOperators db + opDomains <- operatorDomains . serverOperators <$> getServerOperators db srvs <- liftIO $ getProtocolServers db p user - pure $ L.toList $ agentServerCfgs opDomains (rndServers p rs) srvs + pure $ L.toList $ agentServerCfgs p opDomains (rndServers p rs) srvs protoServer' :: ServerCfg p -> ProtocolServer p protoServer' ServerCfg {server} = protoServer server diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 27acf8990b..7fb811255f 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -588,7 +588,7 @@ data ChatResponse | CRChatItemId User (Maybe ChatItemId) | CRApiParsedMarkdown {formattedText :: Maybe MarkdownList} | CRServerTestResult {user :: User, testServer :: AProtoServerWithAuth, testFailure :: Maybe ProtocolTestFailure} - | CRServerOperators {operators :: [ServerOperator], conditionsAction :: Maybe UsageConditionsAction} + | CRServerOperatorConditions {conditions :: ServerOperatorConditions} | CRUserServers {user :: User, userServers :: [UserOperatorServers]} | CRUserServersValidation {user :: User, serverErrors :: [UserServersError]} | CRUsageConditions {usageConditions :: UsageConditions, conditionsText :: Text, acceptedConditions :: Maybe UsageConditions} diff --git a/src/Simplex/Chat/Migrations/M20241027_server_operators.hs b/src/Simplex/Chat/Migrations/M20241027_server_operators.hs index d84cc5aa73..c4b40c4706 100644 --- a/src/Simplex/Chat/Migrations/M20241027_server_operators.hs +++ b/src/Simplex/Chat/Migrations/M20241027_server_operators.hs @@ -15,8 +15,10 @@ CREATE TABLE server_operators ( legal_name TEXT, server_domains TEXT, enabled INTEGER NOT NULL DEFAULT 1, - role_storage INTEGER NOT NULL DEFAULT 1, - role_proxy INTEGER NOT NULL DEFAULT 1, + smp_role_storage INTEGER NOT NULL DEFAULT 1, + smp_role_proxy INTEGER NOT NULL DEFAULT 1, + xftp_role_storage INTEGER NOT NULL DEFAULT 1, + xftp_role_proxy INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')) ); diff --git a/src/Simplex/Chat/Migrations/chat_schema.sql b/src/Simplex/Chat/Migrations/chat_schema.sql index c037a60770..0dc68034e7 100644 --- a/src/Simplex/Chat/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Migrations/chat_schema.sql @@ -596,8 +596,10 @@ CREATE TABLE server_operators( legal_name TEXT, server_domains TEXT, enabled INTEGER NOT NULL DEFAULT 1, - role_storage INTEGER NOT NULL DEFAULT 1, - role_proxy INTEGER NOT NULL DEFAULT 1, + smp_role_storage INTEGER NOT NULL DEFAULT 1, + smp_role_proxy INTEGER NOT NULL DEFAULT 1, + xftp_role_storage INTEGER NOT NULL DEFAULT 1, + xftp_role_proxy INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')) ); diff --git a/src/Simplex/Chat/Operators.hs b/src/Simplex/Chat/Operators.hs index 6bf1a75da4..c3d9a8823b 100644 --- a/src/Simplex/Chat/Operators.hs +++ b/src/Simplex/Chat/Operators.hs @@ -134,6 +134,13 @@ data UsageConditionsAction | UCAAccepted {operators :: [ServerOperator]} deriving (Show) +data ServerOperatorConditions = ServerOperatorConditions + { serverOperators :: [ServerOperator], + currentConditions :: UsageConditions, + conditionsAction :: Maybe UsageConditionsAction + } + deriving (Show) + usageConditionsAction :: [ServerOperator] -> UsageConditions -> UTCTime -> Maybe UsageConditionsAction usageConditionsAction operators UsageConditions {createdAt, notifiedAt} now = do let enabledOperators = filter (\ServerOperator {enabled} -> enabled) operators @@ -178,10 +185,16 @@ data ServerOperator' s = ServerOperator serverDomains :: [Text], conditionsAcceptance :: ConditionsAcceptance, enabled :: Bool, - roles :: ServerRoles + smpRoles :: ServerRoles, + xftpRoles :: ServerRoles } deriving (Show) +operatorRoles :: UserProtocol p => SProtocolType p -> ServerOperator -> ServerRoles +operatorRoles p op = case p of + SPSMP -> smpRoles op + SPXFTP -> xftpRoles op + conditionsAccepted :: ServerOperator -> Bool conditionsAccepted ServerOperator {conditionsAcceptance} = case conditionsAcceptance of CAAccepted {} -> True @@ -336,8 +349,8 @@ updatedServerOperators presetOps storedOps = Just presetOp -> (storedOp' :) where storedOp' = case find ((operatorTag presetOp ==) . operatorTag) storedOps of - Just ServerOperator {operatorId, conditionsAcceptance, enabled, roles} -> - ASO SDBStored presetOp {operatorId, conditionsAcceptance, enabled, roles} + Just ServerOperator {operatorId, conditionsAcceptance, enabled, smpRoles, xftpRoles} -> + ASO SDBStored presetOp {operatorId, conditionsAcceptance, enabled, smpRoles, xftpRoles} Nothing -> ASO SDBNew presetOp -- This function should be used inside DB transaction to update servers. @@ -361,8 +374,8 @@ updatedUserServers p presetOps randomSrvs srvs = srvHost :: UserServer' s p -> NonEmpty TransportHost srvHost UserServer {server = ProtoServerWithAuth srv _} = host srv -agentServerCfgs :: [(Text, ServerOperator)] -> NonEmpty (NewUserServer p) -> [UserServer' s p] -> NonEmpty (ServerCfg p) -agentServerCfgs opDomains randomSrvs = +agentServerCfgs :: UserProtocol p => SProtocolType p -> [(Text, ServerOperator)] -> NonEmpty (NewUserServer p) -> [UserServer' s p] -> NonEmpty (ServerCfg p) +agentServerCfgs p opDomains randomSrvs = fromMaybe fallbackSrvs . L.nonEmpty . mapMaybe enabledOpAgentServer where fallbackSrvs = L.map (snd . agentServer) randomSrvs @@ -372,8 +385,8 @@ agentServerCfgs opDomains randomSrvs = agentServer :: UserServer' s p -> (Bool, ServerCfg p) agentServer srv@UserServer {server, enabled} = case find (\(d, _) -> any (matchingHost d) (srvHost srv)) opDomains of - Just (_, ServerOperator {operatorId = DBEntityId opId, enabled = opEnabled, roles}) -> - (opEnabled, ServerCfg {server, enabled, operator = Just opId, roles}) + Just (_, op@ServerOperator {operatorId = DBEntityId opId, enabled = opEnabled}) -> + (opEnabled, ServerCfg {server, enabled, operator = Just opId, roles = operatorRoles p op}) Nothing -> (True, ServerCfg {server, enabled, operator = Nothing, roles = allRoles}) @@ -423,7 +436,7 @@ validateUserServers curr others = currUserErrs <> concatMap otherUserErrs others p' = AProtocolType p noServers cond = not $ any srvEnabled $ snd $ partitionValid $ concatMap (`servers'` p) $ filter cond uss opEnabled = maybe True (\ServerOperator {enabled} -> enabled) . operator' - hasRole roleSel = maybe True (\ServerOperator {enabled, roles} -> enabled && roleSel roles) . operator' + hasRole roleSel = maybe True (\op@ServerOperator {enabled} -> enabled && roleSel (operatorRoles p op)) . operator' srvEnabled (AUS _ UserServer {deleted, enabled}) = enabled && not deleted serverErrs :: (UserServersClass u, ProtocolTypeI p, UserProtocol p) => SProtocolType p -> [u] -> [UserServersError] serverErrs p uss = map (USEInvalidServer p') invalidSrvs <> mapMaybe duplicateErr_ srvs @@ -472,6 +485,8 @@ instance DBStoredI s => FromJSON (ServerOperator' s) where $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "UCA") ''UsageConditionsAction) +$(JQ.deriveJSON defaultJSON ''ServerOperatorConditions) + instance ProtocolTypeI p => ToJSON (UserServer' s p) where toEncoding = $(JQ.mkToEncoding defaultJSON ''UserServer_) toJSON = $(JQ.mkToJSON defaultJSON ''UserServer_) diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs index 87b5d2fd64..daf9a78fca 100644 --- a/src/Simplex/Chat/Store/Profiles.hs +++ b/src/Simplex/Chat/Store/Profiles.hs @@ -612,20 +612,21 @@ serverColumns p (ProtoServerWithAuth ProtocolServer {host, port, keyHash} auth_) auth = safeDecodeUtf8 . unBasicAuth <$> auth_ in (protocol, host, port, keyHash, auth) -getServerOperators :: DB.Connection -> ExceptT StoreError IO ([ServerOperator], Maybe UsageConditionsAction) +getServerOperators :: DB.Connection -> ExceptT StoreError IO ServerOperatorConditions getServerOperators db = do - currentConds <- getCurrentUsageConditions db + currentConditions <- getCurrentUsageConditions db liftIO $ do now <- getCurrentTime latestAcceptedConds_ <- getLatestAcceptedConditions db - let getConds op = (\ca -> op {conditionsAcceptance = ca}) <$> getOperatorConditions_ db op currentConds latestAcceptedConds_ now - operators <- mapM getConds =<< getServerOperators_ db - pure (operators, usageConditionsAction operators currentConds now) + let getConds op = (\ca -> op {conditionsAcceptance = ca}) <$> getOperatorConditions_ db op currentConditions latestAcceptedConds_ now + ops <- mapM getConds =<< getServerOperators_ db + let conditionsAction = usageConditionsAction ops currentConditions now + pure ServerOperatorConditions {serverOperators = ops, currentConditions, conditionsAction} getUserServers :: DB.Connection -> User -> ExceptT StoreError IO ([ServerOperator], [UserServer 'PSMP], [UserServer 'PXFTP]) getUserServers db user = (,,) - <$> (fst <$> getServerOperators db) + <$> (serverOperators <$> getServerOperators db) <*> liftIO (getProtocolServers db SPSMP user) <*> liftIO (getProtocolServers db SPXFTP user) @@ -635,15 +636,15 @@ setServerOperators db ops = do mapM_ (updateServerOperator db currentTs) ops updateServerOperator :: DB.Connection -> UTCTime -> ServerOperator -> IO () -updateServerOperator db currentTs ServerOperator {operatorId, enabled, roles = ServerRoles {storage, proxy}} = +updateServerOperator db currentTs ServerOperator {operatorId, enabled, smpRoles, xftpRoles} = DB.execute db [sql| UPDATE server_operators - SET enabled = ?, role_storage = ?, role_proxy = ?, updated_at = ? + SET enabled = ?, smp_role_storage = ?, smp_role_proxy = ?, xftp_role_storage = ?, xftp_role_proxy = ?, updated_at = ? WHERE server_operator_id = ? |] - (enabled, storage, proxy, operatorId, currentTs) + (enabled, storage smpRoles, proxy smpRoles, storage xftpRoles, proxy xftpRoles, currentTs, operatorId) getUpdateServerOperators :: DB.Connection -> NonEmpty PresetOperator -> Bool -> IO [ServerOperator] getUpdateServerOperators db presetOps newUser = do @@ -677,25 +678,25 @@ getUpdateServerOperators db presetOps newUser = do |] (conditionsId, conditionsCommit, notifiedAt, createdAt) updateOperator :: ServerOperator -> IO () - updateOperator ServerOperator {operatorId, tradeName, legalName, serverDomains, enabled, roles = ServerRoles {storage, proxy}} = + updateOperator ServerOperator {operatorId, tradeName, legalName, serverDomains, enabled, smpRoles, xftpRoles} = DB.execute db [sql| UPDATE server_operators - SET trade_name = ?, legal_name = ?, server_domains = ?, enabled = ?, role_storage = ?, role_proxy = ? + SET trade_name = ?, legal_name = ?, server_domains = ?, enabled = ?, smp_role_storage = ?, smp_role_proxy = ?, xftp_role_storage = ?, xftp_role_proxy = ? WHERE server_operator_id = ? |] - (tradeName, legalName, T.intercalate "," serverDomains, enabled, storage, proxy, operatorId) + (tradeName, legalName, T.intercalate "," serverDomains, enabled, storage smpRoles, proxy smpRoles, storage xftpRoles, proxy xftpRoles, operatorId) insertOperator :: NewServerOperator -> IO ServerOperator - insertOperator op@ServerOperator {operatorTag, tradeName, legalName, serverDomains, enabled, roles = ServerRoles {storage, proxy}} = do + insertOperator op@ServerOperator {operatorTag, tradeName, legalName, serverDomains, enabled, smpRoles, xftpRoles} = do DB.execute db [sql| INSERT INTO server_operators - (server_operator_tag, trade_name, legal_name, server_domains, enabled, role_storage, role_proxy) - VALUES (?,?,?,?,?,?,?) + (server_operator_tag, trade_name, legal_name, server_domains, enabled, smp_role_storage, smp_role_proxy, xftp_role_storage, xftp_role_proxy) + VALUES (?,?,?,?,?,?,?,?,?) |] - (operatorTag, tradeName, legalName, T.intercalate "," serverDomains, enabled, storage, proxy) + (operatorTag, tradeName, legalName, T.intercalate "," serverDomains, enabled, storage smpRoles, proxy smpRoles, storage xftpRoles, proxy xftpRoles) opId <- insertedRowId db pure op {operatorId = DBEntityId opId} autoAcceptConditions op UsageConditions {conditionsCommit} = @@ -706,15 +707,15 @@ serverOperatorQuery :: Query serverOperatorQuery = [sql| SELECT server_operator_id, server_operator_tag, trade_name, legal_name, - server_domains, enabled, role_storage, role_proxy + server_domains, enabled, smp_role_storage, smp_role_proxy, xftp_role_storage, xftp_role_proxy FROM server_operators |] getServerOperators_ :: DB.Connection -> IO [ServerOperator] getServerOperators_ db = map toServerOperator <$> DB.query_ db serverOperatorQuery -toServerOperator :: (DBEntityId, Maybe OperatorTag, Text, Maybe Text, Text, Bool, Bool, Bool) -> ServerOperator -toServerOperator (operatorId, operatorTag, tradeName, legalName, domains, enabled, storage, proxy) = +toServerOperator :: (DBEntityId, Maybe OperatorTag, Text, Maybe Text, Text, Bool) :. (Bool, Bool) :. (Bool, Bool) -> ServerOperator +toServerOperator ((operatorId, operatorTag, tradeName, legalName, domains, enabled) :. smpRoles' :. xftpRoles') = ServerOperator { operatorId, operatorTag, @@ -723,8 +724,11 @@ toServerOperator (operatorId, operatorTag, tradeName, legalName, domains, enable serverDomains = T.splitOn "," domains, conditionsAcceptance = CARequired Nothing, enabled, - roles = ServerRoles {storage, proxy} + smpRoles = serverRoles smpRoles', + xftpRoles = serverRoles xftpRoles' } + where + serverRoles (storage, proxy) = ServerRoles {storage, proxy} getOperatorConditions_ :: DB.Connection -> ServerOperator -> UsageConditions -> Maybe UsageConditions -> UTCTime -> IO ConditionsAcceptance getOperatorConditions_ db ServerOperator {operatorId} UsageConditions {conditionsCommit = currentCommit, createdAt, notifiedAt} latestAcceptedConds_ now = do diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 317fd58a8e..e4c0fd5606 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -65,7 +65,7 @@ import qualified Simplex.Messaging.Crypto.Ratchet as CR import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (dropPrefix, taggedObjectJSON) -import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType, ProtocolServer (..), ProtocolTypeI, SProtocolType (..)) +import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType, ProtocolServer (..), ProtocolTypeI, SProtocolType (..), UserProtocol) import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.Transport.Client (TransportHost (..)) import Simplex.Messaging.Util (safeDecodeUtf8, tshow) @@ -98,7 +98,7 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe CRApiChat u chat _ -> ttyUser u $ if testView then testViewChat chat else [viewJSON chat] CRApiParsedMarkdown ft -> [viewJSON ft] CRServerTestResult u srv testFailure -> ttyUser u $ viewServerTestResult srv testFailure - CRServerOperators ops ca -> viewServerOperators ops ca + CRServerOperatorConditions (ServerOperatorConditions ops _ ca) -> viewServerOperators ops ca CRUserServers u uss -> ttyUser u $ concatMap viewUserServers uss <> (if testView then [] else serversUserHelp) CRUserServersValidation {} -> [] CRUsageConditions {} -> [] @@ -1221,15 +1221,27 @@ viewUserServers UserOperatorServers {operator, smpServers, xftpServers} = <> viewServers SPSMP smpServers <> viewServers SPXFTP xftpServers where - viewServers :: ProtocolTypeI p => SProtocolType p -> [UserServer p] -> [StyledString] + viewServers :: (ProtocolTypeI p, UserProtocol p) => SProtocolType p -> [UserServer p] -> [StyledString] viewServers _ [] = [] - viewServers p srvs = [" " <> protocolName p <> " servers"] <> map (plain . (" " <> ) . viewServer) srvs + viewServers p srvs + | maybe True (\ServerOperator {enabled} -> enabled) operator = + [" " <> protocolName p <> " servers" <> maybe "" ((" " <>) . viewRoles) operator] + <> map (plain . (" " <> ) . viewServer) srvs + | otherwise = [] where viewServer UserServer {server, preset, tested, enabled} = safeDecodeUtf8 (strEncode server) <> serverInfo where serverInfo = if null serverInfo_ then "" else parens $ T.intercalate ", " serverInfo_ serverInfo_ = ["preset" | preset] <> testedInfo <> ["disabled" | not enabled] testedInfo = maybe [] (\t -> ["test: " <> if t then "passed" else "failed"]) tested + viewRoles op@ServerOperator {enabled} + | not enabled = "disabled" + | storage rs && proxy rs = "enabled" + | storage rs = "enabled storage" + | proxy rs = "enabled proxy" + | otherwise = "disabled (servers known)" + where + rs = operatorRoles p op serversUserHelp :: [StyledString] serversUserHelp = @@ -1272,8 +1284,8 @@ viewOperator op@ServerOperator {tradeName, legalName, serverDomains, conditionsA <> (", " <> viewOpEnabled op) shortViewOperator :: ServerOperator -> Text -shortViewOperator op@ServerOperator {operatorId = DBEntityId opId, tradeName} = - tshow opId <> ". " <> tradeName <> parens (viewOpEnabled op) +shortViewOperator ServerOperator {operatorId = DBEntityId opId, tradeName, enabled} = + tshow opId <> ". " <> tradeName <> parens (if enabled then "enabled" else "disabled") viewOpIdTag :: ServerOperator' s -> Text viewOpIdTag ServerOperator {operatorId, operatorTag} = case operatorId of @@ -1290,11 +1302,19 @@ viewOpConditions = \case viewCond w ts = w <> maybe "" (parens . tshow) ts viewOpEnabled :: ServerOperator' s -> Text -viewOpEnabled ServerOperator {enabled, roles = ServerRoles {storage, proxy}} - | enabled && storage && proxy = "enabled" - | enabled && storage = "enabled storage" - | enabled && proxy = "enabled proxy" - | otherwise = "disabled" +viewOpEnabled ServerOperator {enabled, smpRoles, xftpRoles} + | not enabled = "disabled" + | no smpRoles && no xftpRoles = "disabled (servers known)" + | both smpRoles && both xftpRoles = "enabled" + | otherwise = "SMP " <> viewRoles smpRoles <> ", XFTP" <> viewRoles xftpRoles + where + no rs = not $ storage rs || proxy rs + both rs = storage rs && proxy rs + viewRoles rs + | both rs = "enabled" + | storage rs = "enabled storage" + | proxy rs = "enabled proxy" + | otherwise = "disabled (servers known)" viewConditionsAction :: UsageConditionsAction -> [StyledString] viewConditionsAction = \case @@ -1382,12 +1402,6 @@ viewConnectionStats ConnectionStats {rcvQueuesInfo, sndQueuesInfo} = ["receiving messages via: " <> viewRcvQueuesInfo rcvQueuesInfo | not $ null rcvQueuesInfo] <> ["sending messages via: " <> viewSndQueuesInfo sndQueuesInfo | not $ null sndQueuesInfo] --- viewServers :: ProtocolTypeI p => [ServerOperator] -> NonEmpty (ServerCfg p) -> [StyledString] --- viewServers operators = map (plain . (\ServerCfg {server, operator} -> B.unpack (strEncode server) <> viewOperator operator)) . L.toList --- where --- ops :: Map (Maybe DBEntityId) Text = foldl' (\m ServerOperator {operatorId, tradeName} -> M.insert (Just operatorId) tradeName m) M.empty operators --- viewOperator = maybe "" $ \op -> " (operator " <> maybe (show op) T.unpack (M.lookup (Just op) ops) <> ")" - viewRcvQueuesInfo :: [RcvQueueInfo] -> StyledString viewRcvQueuesInfo = plain . intercalate ", " . map showQueueInfo where diff --git a/tests/OperatorTests.hs b/tests/OperatorTests.hs index 1b867a3e1d..4966bfbb97 100644 --- a/tests/OperatorTests.hs +++ b/tests/OperatorTests.hs @@ -29,7 +29,7 @@ validateServers = describe "validate user servers" $ do validateUserServers [invalidDisabled] [] `shouldBe` [USENoServers aSMP Nothing] validateUserServers [invalidDisabledOp] [] `shouldBe` [USENoServers aSMP Nothing, USENoServers aXFTP Nothing] it "should fail without servers with storage role" $ do - validateUserServers [invalidNoStorage] [] `shouldBe` [USEStorageMissing aSMP Nothing, USEStorageMissing aXFTP Nothing] + validateUserServers [invalidNoStorage] [] `shouldBe` [USEStorageMissing aSMP Nothing] it "should fail with duplicate host" $ do validateUserServers [invalidDuplicate] [] `shouldBe` [ USEDuplicateServer aSMP "smp://0YuTwO05YJWS8rkjn9eLJDjQhFKvIYd8d4xG8X1blIU=@smp8.simplex.im,beccx4yfxxbvyhqypaavemqurytl6hozr47wfc7uuecacjqdvwpw2xid.onion" "smp8.simplex.im", @@ -71,7 +71,7 @@ invalidDisabledOp = invalidNoStorage :: UpdatedUserOperatorServers invalidNoStorage = (valid :: UpdatedUserOperatorServers) - { operator = Just operatorSimpleXChat {operatorId = DBEntityId 1, roles = allRoles {storage = False}} + { operator = Just operatorSimpleXChat {operatorId = DBEntityId 1, smpRoles = allRoles {storage = False}} } invalidDuplicate :: UpdatedUserOperatorServers From b605ebfd2adf3b426d0abc1a4fe00bd54740b48b Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Fri, 15 Nov 2024 12:14:53 +0000 Subject: [PATCH 026/167] core: remove comments --- src/Simplex/Chat/Controller.hs | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 7fb811255f..c085dcf470 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -956,24 +956,6 @@ instance ToJSON AgentQueueId where toJSON = strToJSON toEncoding = strToJEncoding --- data ProtoServersConfig p = ProtoServersConfig {servers :: [ServerCfg p]} --- deriving (Show) - --- data AProtoServersConfig = forall p. ProtocolTypeI p => APSC (SProtocolType p) (ProtoServersConfig p) - --- deriving instance Show AProtoServersConfig - --- data UserProtoServers p = UserProtoServers --- { serverProtocol :: SProtocolType p, --- protoServers :: NonEmpty (ServerCfg p), --- presetServers :: NonEmpty (ServerCfg p) --- } --- deriving (Show) - --- data AUserProtoServers = forall p. (ProtocolTypeI p, UserProtocol p) => AUPS (UserProtoServers p) - --- deriving instance Show AUserProtoServers - data ArchiveConfig = ArchiveConfig {archivePath :: FilePath, disableCompression :: Maybe Bool, parentTempDirectory :: Maybe FilePath} deriving (Show) From 6843269cff36b3b167f4c74e8bb7c4ef4d4468ef Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sun, 17 Nov 2024 10:25:03 +0000 Subject: [PATCH 027/167] core: 6.2.0.0 (simplexmq: 6.2.0.3) --- cabal.project | 2 +- package.yaml | 2 +- scripts/nix/sha256map.nix | 2 +- simplex-chat.cabal | 2 +- src/Simplex/Chat/Remote.hs | 4 ++-- tests/ChatClient.hs | 4 +++- 6 files changed, 9 insertions(+), 7 deletions(-) diff --git a/cabal.project b/cabal.project index c9b8b11722..7cc11fc583 100644 --- a/cabal.project +++ b/cabal.project @@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: ffecf200d4874dfa34f6d15b269964c0115a54ca + tag: a64c1aa2c41938c5e18cc49d08075f14e5d25f0d source-repository-package type: git diff --git a/package.yaml b/package.yaml index 94dc13ad2e..7bfa560a26 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: simplex-chat -version: 6.1.1.0 +version: 6.2.0.0 #synopsis: #description: homepage: https://github.com/simplex-chat/simplex-chat#readme diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 8de91675e3..b63bafcd96 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."ffecf200d4874dfa34f6d15b269964c0115a54ca" = "0kb8hq37fc5g198wq7dswnlwjzk67q8rrzil2dii5lc6xfr47jbs"; + "https://github.com/simplex-chat/simplexmq.git"."a64c1aa2c41938c5e18cc49d08075f14e5d25f0d" = "1kf86vrh5zfrqyczfjcj3d2nagmqb0rwhhdc10fw5n8jcgmdw6rp"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; diff --git a/simplex-chat.cabal b/simplex-chat.cabal index fb7f32faa5..4a5c3b6970 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplex-chat -version: 6.1.1.0 +version: 6.2.0.0 category: Web, System, Services, Cryptography homepage: https://github.com/simplex-chat/simplex-chat#readme author: simplex.chat diff --git a/src/Simplex/Chat/Remote.hs b/src/Simplex/Chat/Remote.hs index 88539b55e3..320a35815d 100644 --- a/src/Simplex/Chat/Remote.hs +++ b/src/Simplex/Chat/Remote.hs @@ -73,11 +73,11 @@ import UnliftIO.Directory (copyFile, createDirectoryIfMissing, doesDirectoryExis -- when acting as host minRemoteCtrlVersion :: AppVersion -minRemoteCtrlVersion = AppVersion [6, 1, 0, 8] +minRemoteCtrlVersion = AppVersion [6, 2, 0, 0] -- when acting as controller minRemoteHostVersion :: AppVersion -minRemoteHostVersion = AppVersion [6, 1, 0, 8] +minRemoteHostVersion = AppVersion [6, 2, 0, 0] currentAppVersion :: AppVersion currentAppVersion = AppVersion SC.version diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index 75b85d7a5f..73f3e350df 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -439,6 +439,8 @@ smpServerCfg = controlPortUserAuth = Nothing, controlPortAdminAuth = Nothing, messageExpiration = Just defaultMessageExpiration, + expireMessagesOnStart = False, + idleQueueInterval = defaultIdleQueueInterval, notificationExpiration = defaultNtfExpiration, inactiveClientExpiration = Just defaultInactiveClientExpiration, smpCredentials = @@ -524,7 +526,7 @@ serverBracket server f = do started <- newEmptyTMVarIO bracket (forkIOWithUnmask ($ server started)) - (\t -> killThread t >> waitFor started "stop") + (\t -> killThread t >> waitFor started "stop" >> threadDelay 100000) (\_ -> waitFor started "start" >> f) where waitFor started s = From e645dd99e7cd41ef2b3efdc2c13106cb09cd4554 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sun, 17 Nov 2024 22:37:18 +0000 Subject: [PATCH 028/167] 6.2-beta.0: ios 246, android 251, desktop 75 --- apps/ios/SimpleX.xcodeproj/project.pbxproj | 56 +++++++++++----------- apps/multiplatform/gradle.properties | 8 ++-- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index cd146d4292..4061ef0f9c 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -149,9 +149,9 @@ 6419EC582AB97507004A607A /* CIMemberCreatedContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6419EC572AB97507004A607A /* CIMemberCreatedContactView.swift */; }; 6432857C2925443C00FBE5C8 /* GroupPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */; }; 643B3B452CCBEB080083A2CF /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643B3B402CCBEB080083A2CF /* libgmpxx.a */; }; - 643B3B462CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643B3B412CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5-ghc9.6.3.a */; }; + 643B3B462CCBEB080083A2CF /* libHSsimplex-chat-6.2.0.0-ICCEDdz3q5b2XylcUTCFFs-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643B3B412CCBEB080083A2CF /* libHSsimplex-chat-6.2.0.0-ICCEDdz3q5b2XylcUTCFFs-ghc9.6.3.a */; }; 643B3B472CCBEB080083A2CF /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643B3B422CCBEB080083A2CF /* libffi.a */; }; - 643B3B482CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643B3B432CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5.a */; }; + 643B3B482CCBEB080083A2CF /* libHSsimplex-chat-6.2.0.0-ICCEDdz3q5b2XylcUTCFFs.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643B3B432CCBEB080083A2CF /* libHSsimplex-chat-6.2.0.0-ICCEDdz3q5b2XylcUTCFFs.a */; }; 643B3B492CCBEB080083A2CF /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643B3B442CCBEB080083A2CF /* libgmp.a */; }; 6440CA00288857A10062C672 /* CIEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440C9FF288857A10062C672 /* CIEventView.swift */; }; 6440CA03288AECA70062C672 /* AddGroupMembersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440CA02288AECA70062C672 /* AddGroupMembersView.swift */; }; @@ -492,9 +492,9 @@ 6419EC572AB97507004A607A /* CIMemberCreatedContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIMemberCreatedContactView.swift; sourceTree = ""; }; 6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupPreferencesView.swift; sourceTree = ""; }; 643B3B402CCBEB080083A2CF /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libgmpxx.a; path = Libraries/libgmpxx.a; sourceTree = ""; }; - 643B3B412CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5-ghc9.6.3.a"; path = "Libraries/libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5-ghc9.6.3.a"; sourceTree = ""; }; + 643B3B412CCBEB080083A2CF /* libHSsimplex-chat-6.2.0.0-ICCEDdz3q5b2XylcUTCFFs-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libHSsimplex-chat-6.2.0.0-ICCEDdz3q5b2XylcUTCFFs-ghc9.6.3.a"; path = "Libraries/libHSsimplex-chat-6.2.0.0-ICCEDdz3q5b2XylcUTCFFs-ghc9.6.3.a"; sourceTree = ""; }; 643B3B422CCBEB080083A2CF /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libffi.a; path = Libraries/libffi.a; sourceTree = ""; }; - 643B3B432CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5.a"; path = "Libraries/libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5.a"; sourceTree = ""; }; + 643B3B432CCBEB080083A2CF /* libHSsimplex-chat-6.2.0.0-ICCEDdz3q5b2XylcUTCFFs.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libHSsimplex-chat-6.2.0.0-ICCEDdz3q5b2XylcUTCFFs.a"; path = "Libraries/libHSsimplex-chat-6.2.0.0-ICCEDdz3q5b2XylcUTCFFs.a"; sourceTree = ""; }; 643B3B442CCBEB080083A2CF /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libgmp.a; path = Libraries/libgmp.a; sourceTree = ""; }; 6440C9FF288857A10062C672 /* CIEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIEventView.swift; sourceTree = ""; }; 6440CA02288AECA70062C672 /* AddGroupMembersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGroupMembersView.swift; sourceTree = ""; }; @@ -663,8 +663,8 @@ 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */, - 643B3B482CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5.a in Frameworks */, - 643B3B462CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5-ghc9.6.3.a in Frameworks */, + 643B3B482CCBEB080083A2CF /* libHSsimplex-chat-6.2.0.0-ICCEDdz3q5b2XylcUTCFFs.a in Frameworks */, + 643B3B462CCBEB080083A2CF /* libHSsimplex-chat-6.2.0.0-ICCEDdz3q5b2XylcUTCFFs-ghc9.6.3.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -815,8 +815,8 @@ 643B3B422CCBEB080083A2CF /* libffi.a */, 643B3B442CCBEB080083A2CF /* libgmp.a */, 643B3B402CCBEB080083A2CF /* libgmpxx.a */, - 643B3B412CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5-ghc9.6.3.a */, - 643B3B432CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5.a */, + 643B3B412CCBEB080083A2CF /* libHSsimplex-chat-6.2.0.0-ICCEDdz3q5b2XylcUTCFFs-ghc9.6.3.a */, + 643B3B432CCBEB080083A2CF /* libHSsimplex-chat-6.2.0.0-ICCEDdz3q5b2XylcUTCFFs.a */, 5CA059C2279559F40002BEB4 /* Shared */, 5CDCAD462818589900503DA2 /* SimpleX NSE */, CEE723A82C3BD3D70009AE93 /* SimpleX SE */, @@ -1903,7 +1903,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 245; + CURRENT_PROJECT_VERSION = 246; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -1928,7 +1928,7 @@ "@executable_path/Frameworks", ); LLVM_LTO = YES_THIN; - MARKETING_VERSION = 6.1.1; + MARKETING_VERSION = 6.2; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_NAME = SimpleX; SDKROOT = iphoneos; @@ -1952,7 +1952,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 245; + CURRENT_PROJECT_VERSION = 246; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -1977,7 +1977,7 @@ "@executable_path/Frameworks", ); LLVM_LTO = YES; - MARKETING_VERSION = 6.1.1; + MARKETING_VERSION = 6.2; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_NAME = SimpleX; SDKROOT = iphoneos; @@ -1993,11 +1993,11 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 245; + CURRENT_PROJECT_VERSION = 246; DEVELOPMENT_TEAM = 5NN7GUYB6T; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 6.1.1; + MARKETING_VERSION = 6.2; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -2013,11 +2013,11 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 245; + CURRENT_PROJECT_VERSION = 246; DEVELOPMENT_TEAM = 5NN7GUYB6T; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 6.1.1; + MARKETING_VERSION = 6.2; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -2038,7 +2038,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 245; + CURRENT_PROJECT_VERSION = 246; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GCC_OPTIMIZATION_LEVEL = s; @@ -2053,7 +2053,7 @@ "@executable_path/../../Frameworks", ); LLVM_LTO = YES; - MARKETING_VERSION = 6.1.1; + MARKETING_VERSION = 6.2; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -2075,7 +2075,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 245; + CURRENT_PROJECT_VERSION = 246; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_CODE_COVERAGE = NO; @@ -2090,7 +2090,7 @@ "@executable_path/../../Frameworks", ); LLVM_LTO = YES; - MARKETING_VERSION = 6.1.1; + MARKETING_VERSION = 6.2; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -2112,7 +2112,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 245; + CURRENT_PROJECT_VERSION = 246; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -2138,7 +2138,7 @@ "$(PROJECT_DIR)/Libraries/sim", ); LLVM_LTO = YES; - MARKETING_VERSION = 6.1.1; + MARKETING_VERSION = 6.2; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; @@ -2163,7 +2163,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 245; + CURRENT_PROJECT_VERSION = 246; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -2189,7 +2189,7 @@ "$(PROJECT_DIR)/Libraries/sim", ); LLVM_LTO = YES; - MARKETING_VERSION = 6.1.1; + MARKETING_VERSION = 6.2; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; @@ -2214,7 +2214,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 245; + CURRENT_PROJECT_VERSION = 246; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -2229,7 +2229,7 @@ "@executable_path/../../Frameworks", ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 6.1.1; + MARKETING_VERSION = 6.2; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -2248,7 +2248,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 245; + CURRENT_PROJECT_VERSION = 246; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -2263,7 +2263,7 @@ "@executable_path/../../Frameworks", ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 6.1.1; + MARKETING_VERSION = 6.2; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; diff --git a/apps/multiplatform/gradle.properties b/apps/multiplatform/gradle.properties index d795257a76..e105e61dde 100644 --- a/apps/multiplatform/gradle.properties +++ b/apps/multiplatform/gradle.properties @@ -24,11 +24,11 @@ android.nonTransitiveRClass=true kotlin.mpp.androidSourceSetLayoutVersion=2 kotlin.jvm.target=11 -android.version_name=6.1.1 -android.version_code=249 +android.version_name=6.2-beta.0 +android.version_code=251 -desktop.version_name=6.1.1 -desktop.version_code=74 +desktop.version_name=6.2-beta.0 +desktop.version_code=75 kotlin.version=1.9.23 gradle.plugin.version=8.2.0 From d1ae3ba2d39fdd2aa9c67a9fa6ab5de7ddf955aa Mon Sep 17 00:00:00 2001 From: Diogo Date: Mon, 18 Nov 2024 15:36:54 +0000 Subject: [PATCH 029/167] desktop, android: add address card to chat list and remove address from onboarding (#5177) * desktop, android: add address card to chat list * add create address button to address learn more view * envelope size to match avatars * refactor * no color for info icon * envelope padding * remove address from onboarding * show create in address card info * backwards compatibility for address onboarding step * paddings between cards * paddings * toolbar -> chats -> cards * dont hide address card * update string --------- Co-authored-by: Evgeny Poberezkin --- .../kotlin/chat/simplex/common/App.kt | 3 +- .../chat/simplex/common/model/SimpleXAPI.kt | 3 + .../chat/simplex/common/platform/Core.kt | 7 +- .../chat/simplex/common/views/WelcomeView.kt | 6 +- .../common/views/chatlist/ChatListView.kt | 200 +++++++++++++----- .../views/onboarding/CreateSimpleXAddress.kt | 197 ----------------- .../views/onboarding/SetNotificationsMode.kt | 14 ++ .../onboarding/SetupDatabasePassphrase.kt | 2 +- .../usersettings/UserAddressLearnMore.kt | 27 ++- .../views/usersettings/UserAddressView.kt | 49 +++-- .../commonMain/resources/MR/base/strings.xml | 1 + 11 files changed, 233 insertions(+), 276 deletions(-) delete mode 100644 apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/CreateSimpleXAddress.kt diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt index ee38cb80fe..7af1d574ad 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt @@ -194,7 +194,8 @@ fun MainScreen() { OnboardingStage.Step2_CreateProfile -> CreateFirstProfile(chatModel) {} OnboardingStage.LinkAMobile -> LinkAMobile() OnboardingStage.Step2_5_SetupDatabasePassphrase -> SetupDatabasePassphrase(chatModel) - OnboardingStage.Step3_CreateSimpleXAddress -> CreateSimpleXAddress(chatModel, null) + // Ensure backwards compatibility with old onboarding stage for address creation, otherwise notification setup would be skipped + OnboardingStage.Step3_CreateSimpleXAddress -> SetNotificationsMode(chatModel) OnboardingStage.Step4_SetNotificationsMode -> SetNotificationsMode(chatModel) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index fab85fa679..5674920914 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -179,6 +179,7 @@ class AppPreferences { val liveMessageAlertShown = mkBoolPreference(SHARED_PREFS_LIVE_MESSAGE_ALERT_SHOWN, false) val showHiddenProfilesNotice = mkBoolPreference(SHARED_PREFS_SHOW_HIDDEN_PROFILES_NOTICE, true) val oneHandUICardShown = mkBoolPreference(SHARED_PREFS_ONE_HAND_UI_CARD_SHOWN, false) + val addressCreationCardShown = mkBoolPreference(SHARED_PREFS_ADDRESS_CREATION_CARD_SHOWN, false) val showMuteProfileAlert = mkBoolPreference(SHARED_PREFS_SHOW_MUTE_PROFILE_ALERT, true) val appLanguage = mkStrPreference(SHARED_PREFS_APP_LANGUAGE, null) val appUpdateChannel = mkEnumPreference(SHARED_PREFS_APP_UPDATE_CHANNEL, AppUpdatesChannel.DISABLED) { AppUpdatesChannel.entries.firstOrNull { it.name == this } } @@ -254,6 +255,7 @@ class AppPreferences { val hintPreferences: List, Boolean>> = listOf( laNoticeShown to false, oneHandUICardShown to false, + addressCreationCardShown to false, liveMessageAlertShown to false, showHiddenProfilesNotice to true, showMuteProfileAlert to true, @@ -408,6 +410,7 @@ class AppPreferences { private const val SHARED_PREFS_LIVE_MESSAGE_ALERT_SHOWN = "LiveMessageAlertShown" private const val SHARED_PREFS_SHOW_HIDDEN_PROFILES_NOTICE = "ShowHiddenProfilesNotice" private const val SHARED_PREFS_ONE_HAND_UI_CARD_SHOWN = "OneHandUICardShown" + private const val SHARED_PREFS_ADDRESS_CREATION_CARD_SHOWN = "AddressCreationCardShown" private const val SHARED_PREFS_SHOW_MUTE_PROFILE_ALERT = "ShowMuteProfileAlert" private const val SHARED_PREFS_STORE_DB_PASSPHRASE = "StoreDBPassphrase" private const val SHARED_PREFS_INITIAL_RANDOM_DB_PASSPHRASE = "InitialRandomDBPassphrase" diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt index 57b93d4d6e..79132b5eb1 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt @@ -137,8 +137,13 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat } } else if (startChat().await()) { val savedOnboardingStage = appPreferences.onboardingStage.get() + val next = if (appPlatform.isAndroid) { + OnboardingStage.Step4_SetNotificationsMode + } else { + OnboardingStage.OnboardingComplete + } val newStage = if (listOf(OnboardingStage.Step1_SimpleXInfo, OnboardingStage.Step2_CreateProfile).contains(savedOnboardingStage) && chatModel.users.size == 1) { - OnboardingStage.Step3_CreateSimpleXAddress + next } else { savedOnboardingStage } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt index e1e3dcb56b..17658d23e8 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt @@ -165,7 +165,7 @@ fun createProfileInNoProfileSetup(displayName: String, close: () -> Unit) { if (!chatModel.connectedToRemote()) { chatModel.localUserCreated.value = true } - controller.appPrefs.onboardingStage.set(OnboardingStage.Step3_CreateSimpleXAddress) + controller.appPrefs.onboardingStage.set(OnboardingStage.Step4_SetNotificationsMode) controller.startChat(user) controller.switchUIRemoteHost(null) close() @@ -181,7 +181,7 @@ fun createProfileInProfiles(chatModel: ChatModel, displayName: String, close: () chatModel.currentUser.value = user if (chatModel.users.isEmpty()) { chatModel.controller.startChat(user) - chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step3_CreateSimpleXAddress) + chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step4_SetNotificationsMode) } else { val users = chatModel.controller.listUsers(rhId) chatModel.users.clear() @@ -204,7 +204,7 @@ fun createProfileOnboarding(chatModel: ChatModel, displayName: String, close: () onboardingStage.set(if (appPlatform.isDesktop && chatModel.controller.appPrefs.initialRandomDBPassphrase.get() && !chatModel.desktopOnboardingRandomPassword.value) { OnboardingStage.Step2_5_SetupDatabasePassphrase } else { - OnboardingStage.Step3_CreateSimpleXAddress + OnboardingStage.Step4_SetNotificationsMode }) } else { // the next two lines are only needed for failure case when because of the database error the app gets stuck on on-boarding screen, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt index 586bca87d0..9661a305cc 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt @@ -34,7 +34,9 @@ import chat.simplex.common.platform.* import chat.simplex.common.views.call.Call import chat.simplex.common.views.chat.item.CIFileViewScope import chat.simplex.common.views.chat.topPaddingToContent +import chat.simplex.common.views.mkValidName import chat.simplex.common.views.newchat.* +import chat.simplex.common.views.showInvalidNameAlert import chat.simplex.common.views.usersettings.* import chat.simplex.res.MR import kotlinx.coroutines.* @@ -72,58 +74,39 @@ private fun showNewChatSheet(oneHandUI: State) { @Composable fun ToggleChatListCard() { - Column( - modifier = Modifier - .padding(16.dp) - .clip(RoundedCornerShape(18.dp)) + ChatListCard( + close = { + appPrefs.oneHandUICardShown.set(true) + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.one_hand_ui), + text = generalGetString(MR.strings.one_hand_ui_change_instruction), + ) + } ) { - Box( + Column( modifier = Modifier - .background(MaterialTheme.appColors.sentMessage) + .padding(horizontal = DEFAULT_PADDING) + .padding(top = DEFAULT_PADDING) ) { - Box( - modifier = Modifier.fillMaxWidth().matchParentSize().padding(5.dp), - contentAlignment = Alignment.TopEnd + Row( + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() ) { - IconButton( - onClick = { - appPrefs.oneHandUICardShown.set(true) - AlertManager.shared.showAlertMsg( - title = generalGetString(MR.strings.one_hand_ui), - text = generalGetString(MR.strings.one_hand_ui_change_instruction), - ) - } - ) { - Icon( - painterResource(MR.images.ic_close), stringResource(MR.strings.back), tint = MaterialTheme.colors.secondary - ) - } + Text(stringResource(MR.strings.one_hand_ui_card_title), style = MaterialTheme.typography.h3) } - Column( - modifier = Modifier - .padding(horizontal = DEFAULT_PADDING) - .padding(top = DEFAULT_PADDING) + Row( + Modifier.fillMaxWidth().padding(top = 6.dp, bottom = 12.dp), + verticalAlignment = Alignment.CenterVertically ) { - Row( - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth() - ) { - Text(stringResource(MR.strings.one_hand_ui_card_title), style = MaterialTheme.typography.h3) - } - Row( - Modifier.fillMaxWidth().padding(top = 6.dp, bottom = 12.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text(stringResource(MR.strings.one_hand_ui), Modifier.weight(10f), style = MaterialTheme.typography.body1) + Text(stringResource(MR.strings.one_hand_ui), Modifier.weight(10f), style = MaterialTheme.typography.body1) - Spacer(Modifier.fillMaxWidth().weight(1f)) + Spacer(Modifier.fillMaxWidth().weight(1f)) - SharedPreferenceToggle( - appPrefs.oneHandUI, - enabled = true - ) - } + SharedPreferenceToggle( + appPrefs.oneHandUI, + enabled = true + ) } } } @@ -196,6 +179,94 @@ fun ChatListView(chatModel: ChatModel, userPickerState: MutableStateFlow Unit, + onCardClick: (() -> Unit)? = null, + content: @Composable BoxScope.() -> Unit +) { + Column( + modifier = Modifier.clip(RoundedCornerShape(18.dp)) + ) { + Box( + modifier = Modifier + .background(MaterialTheme.appColors.sentMessage) + .clickable { + onCardClick?.invoke() + } + ) { + Box( + modifier = Modifier.fillMaxWidth().matchParentSize().padding(5.dp), + contentAlignment = Alignment.TopEnd + ) { + IconButton( + onClick = { + close() + } + ) { + Icon( + painterResource(MR.images.ic_close), stringResource(MR.strings.back), tint = MaterialTheme.colors.secondary + ) + } + } + content() + } + } +} + +@Composable +private fun AddressCreationCard() { + ChatListCard( + close = { + appPrefs.addressCreationCardShown.set(true) + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.simplex_address), + text = generalGetString(MR.strings.address_creation_instruction), + ) + }, + onCardClick = { + ModalManager.start.showModal { + UserAddressLearnMore(showCreateAddressButton = true) + } + } + ) { + Box(modifier = Modifier.matchParentSize().padding(end = (DEFAULT_PADDING_HALF + 2.dp) * fontSizeSqrtMultiplier, bottom = 2.dp), contentAlignment = Alignment.BottomEnd) { + TextButton( + onClick = { + ModalManager.start.showModalCloseable { close -> + UserAddressView(chatModel = chatModel, shareViaProfile = false, autoCreateAddress = true, close = close) + } + }, + ) { + Text(stringResource(MR.strings.create_address_button), style = MaterialTheme.typography.body1) + } + } + Row( + Modifier + .fillMaxWidth() + .padding(DEFAULT_PADDING), + verticalAlignment = Alignment.CenterVertically + ) { + Box(Modifier.padding(vertical = 4.dp)) { + Box(Modifier.background(MaterialTheme.colors.primary, CircleShape).padding(12.dp)) { + ProfileImage(size = 37.dp, null, icon = MR.images.ic_mail_filled, color = Color.White, backgroundColor = Color.Red) + } + } + Column(modifier = Modifier.padding(start = DEFAULT_PADDING)) { + Text(stringResource(MR.strings.your_simplex_contact_address), style = MaterialTheme.typography.h3) + Spacer(Modifier.fillMaxWidth().padding(DEFAULT_PADDING_HALF)) + Row(verticalAlignment = Alignment.CenterVertically) { + Text(stringResource(MR.strings.how_to_use_simplex_chat), Modifier.padding(end = DEFAULT_SPACE_AFTER_ICON), style = MaterialTheme.typography.body1) + Icon( + painterResource(MR.images.ic_info), + null, + ) + } + } + } + } +} + @Composable private fun BoxScope.ChatListWithLoadingScreen(searchText: MutableState, listState: LazyListState) { if (!chatModel.desktopNoUserNoRemote) { @@ -641,6 +712,7 @@ private fun BoxScope.ChatList(searchText: MutableState, listStat val keyboardState by getKeyboardState() val oneHandUI = remember { appPrefs.oneHandUI.state } val oneHandUICardShown = remember { appPrefs.oneHandUICardShown.state } + val addressCreationCardShown = remember { appPrefs.addressCreationCardShown.state } LaunchedEffect(listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset) { val currentIndex = listState.firstVisibleItemIndex @@ -709,20 +781,15 @@ private fun BoxScope.ChatList(searchText: MutableState, listStat } } } - if (!oneHandUICardShown.value && chats.size > 1) { - item { - ToggleChatListCard() - } - } itemsIndexed(chats, key = { _, chat -> chat.remoteHostId to chat.id }) { index, chat -> val nextChatSelected = remember(chat.id, chats) { derivedStateOf { chatModel.chatId.value != null && chats.getOrNull(index + 1)?.id == chatModel.chatId.value } } ChatListNavLinkView(chat, nextChatSelected) } - if (!oneHandUICardShown.value && chats.size <= 1) { + if (!oneHandUICardShown.value || !addressCreationCardShown.value) { item { - ToggleChatListCard() + ChatListFeatureCards() } } if (appPlatform.isAndroid) { @@ -741,7 +808,36 @@ private fun BoxScope.ChatList(searchText: MutableState, listStat } if (!oneHandUICardShown.value) { LaunchedEffect(chats.size) { - if (chats.size >= 3) appPrefs.oneHandUICardShown.set(true) + if (chats.size >= 3) { + appPrefs.oneHandUICardShown.set(true) + } + } + } + + if (!addressCreationCardShown.value) { + LaunchedEffect(chatModel.userAddress.value) { + if (chatModel.userAddress.value != null) { + appPrefs.addressCreationCardShown.set(true) + } + } + } +} + +@Composable +private fun ChatListFeatureCards() { + val oneHandUI = remember { appPrefs.oneHandUI.state } + val oneHandUICardShown = remember { appPrefs.oneHandUICardShown.state } + val addressCreationCardShown = remember { appPrefs.addressCreationCardShown.state } + + Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) { + if (!oneHandUICardShown.value && !oneHandUI.value) { + ToggleChatListCard() + } + if (!addressCreationCardShown.value) { + AddressCreationCard() + } + if (!oneHandUICardShown.value && oneHandUI.value) { + ToggleChatListCard() } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/CreateSimpleXAddress.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/CreateSimpleXAddress.kt deleted file mode 100644 index 28ad0fdb7b..0000000000 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/CreateSimpleXAddress.kt +++ /dev/null @@ -1,197 +0,0 @@ -package chat.simplex.common.views.onboarding - -import androidx.compose.foundation.* -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalClipboardManager -import androidx.compose.ui.platform.LocalUriHandler -import androidx.compose.ui.text.font.FontWeight -import dev.icerock.moko.resources.compose.painterResource -import dev.icerock.moko.resources.compose.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import chat.simplex.common.model.* -import chat.simplex.common.platform.* -import chat.simplex.common.ui.theme.* -import chat.simplex.common.views.helpers.* -import chat.simplex.common.views.newchat.SimpleXLinkQRCode -import chat.simplex.common.views.newchat.simplexChatLink -import chat.simplex.res.MR - -@Composable -fun CreateSimpleXAddress(m: ChatModel, rhId: Long?) { - var progressIndicator by remember { mutableStateOf(false) } - val userAddress = remember { m.userAddress } - val clipboard = LocalClipboardManager.current - val uriHandler = LocalUriHandler.current - - LaunchedEffect(Unit) { - prepareChatBeforeAddressCreation(rhId) - } - - CreateSimpleXAddressLayout( - userAddress.value, - share = { address: String -> clipboard.shareText(address) }, - sendEmail = { address -> - uriHandler.sendEmail( - generalGetString(MR.strings.email_invite_subject), - generalGetString(MR.strings.email_invite_body).format(simplexChatLink(address.connReqContact)) - ) - }, - createAddress = { - withBGApi { - progressIndicator = true - val connReqContact = m.controller.apiCreateUserAddress(rhId) - if (connReqContact != null) { - m.userAddress.value = UserContactLinkRec(connReqContact) - progressIndicator = false - } - } - }, - nextStep = { - val next = if (appPlatform.isAndroid) { - OnboardingStage.Step4_SetNotificationsMode - } else { - OnboardingStage.OnboardingComplete - } - m.controller.appPrefs.onboardingStage.set(next) - }, - ) - - if (progressIndicator) { - ProgressIndicator() - } -} - -@Composable -private fun CreateSimpleXAddressLayout( - userAddress: UserContactLinkRec?, - share: (String) -> Unit, - sendEmail: (UserContactLinkRec) -> Unit, - createAddress: () -> Unit, - nextStep: () -> Unit, -) { - CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) { - ModalView({}, showClose = false) { - ColumnWithScrollBar( - horizontalAlignment = Alignment.CenterHorizontally, - ) { - AppBarTitle(stringResource(MR.strings.simplex_address)) - - Spacer(Modifier.weight(1f)) - - if (userAddress != null) { - SimpleXLinkQRCode(userAddress.connReqContact) - Spacer(Modifier.height(DEFAULT_PADDING_HALF)) - Row { - ShareAddressButton { share(simplexChatLink(userAddress.connReqContact)) } - Spacer(Modifier.width(DEFAULT_PADDING * 2)) - ShareViaEmailButton { sendEmail(userAddress) } - } - Spacer(Modifier.height(DEFAULT_PADDING)) - Spacer(Modifier.weight(1f)) - Column(Modifier.widthIn(max = if (appPlatform.isAndroid) 450.dp else 1000.dp).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) { - OnboardingActionButton( - modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_PADDING * 2).fillMaxWidth() else Modifier.widthIn(min = 300.dp), - labelId = MR.strings.continue_to_next_step, - onboarding = null, - onclick = nextStep - ) - // Reserve space - TextButtonBelowOnboardingButton("", null) - } - } else { - Button(createAddress, Modifier, shape = CircleShape, contentPadding = PaddingValues()) { - Icon(painterResource(MR.images.ic_mail_filled), null, Modifier.size(100.dp).background(MaterialTheme.colors.primary, CircleShape).padding(25.dp), tint = Color.White) - } - Spacer(Modifier.height(DEFAULT_PADDING)) - Spacer(Modifier.weight(1f)) - Text(stringResource(MR.strings.create_simplex_address), style = MaterialTheme.typography.h3, fontWeight = FontWeight.Bold) - TextBelowButton(stringResource(MR.strings.you_can_make_address_visible_via_settings)) - Spacer(Modifier.height(DEFAULT_PADDING)) - Spacer(Modifier.weight(1f)) - - Column(Modifier.widthIn(max = if (appPlatform.isAndroid) 450.dp else 1000.dp).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) { - OnboardingActionButton( - modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_PADDING * 2).fillMaxWidth() else Modifier.widthIn(min = 300.dp), - labelId = MR.strings.create_address_button, - onboarding = null, - onclick = createAddress - ) - TextButtonBelowOnboardingButton(stringResource(MR.strings.dont_create_address), nextStep) - } - } - } - } - } -} - -@Composable -fun ShareAddressButton(onClick: () -> Unit) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - IconButton(onClick, Modifier.padding(bottom = DEFAULT_PADDING_HALF).border(1.dp, MaterialTheme.colors.secondary.copy(0.1f), CircleShape)) { - Icon( - painterResource(MR.images.ic_share_filled), generalGetString(MR.strings.share_verb), tint = MaterialTheme.colors.primary, - modifier = Modifier.size(50.dp).padding(DEFAULT_PADDING_HALF) - ) - } - Text(stringResource(MR.strings.share_verb)) - } -} - -@Composable -fun ShareViaEmailButton(onClick: () -> Unit) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - IconButton(onClick, Modifier.padding(bottom = DEFAULT_PADDING_HALF).border(1.dp, MaterialTheme.colors.secondary.copy(0.1f), CircleShape)) { - Icon( - painterResource(MR.images.ic_mail), generalGetString(MR.strings.share_verb), tint = MaterialTheme.colors.primary, - modifier = Modifier.size(50.dp).padding(DEFAULT_PADDING_HALF) - ) - } - Text(stringResource(MR.strings.invite_friends_short)) - } -} - -@Composable -private fun TextBelowButton(text: String) { - Text( - text, - Modifier - .fillMaxWidth() - .padding(horizontal = DEFAULT_PADDING * 3, vertical = DEFAULT_PADDING_HALF), - style = MaterialTheme.typography.subtitle1, - color = MaterialTheme.colors.secondary, - textAlign = TextAlign.Center, - ) -} - -@Composable -private fun ProgressIndicator() { - Box( - Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator( - Modifier - .padding(horizontal = 2.dp) - .size(30.dp), - color = MaterialTheme.colors.secondary, - strokeWidth = 3.dp - ) - } -} - -private fun prepareChatBeforeAddressCreation(rhId: Long?) { - // No visible users but may have hidden. In this case chat should be started anyway because it's stopped on this stage with hidden users - if (chatModel.users.any { u -> !u.user.hidden }) return - withBGApi { - val user = chatModel.controller.apiGetActiveUser(rhId) ?: return@withBGApi - chatModel.currentUser.value = user - chatModel.controller.startChat(user) - } -} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetNotificationsMode.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetNotificationsMode.kt index e480d4330b..d6d5753b6c 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetNotificationsMode.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetNotificationsMode.kt @@ -25,6 +25,10 @@ import chat.simplex.res.MR @Composable fun SetNotificationsMode(m: ChatModel) { + LaunchedEffect(Unit) { + prepareChatBeforeNotificationsSetup(m) + } + CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) { ModalView({}, showClose = false) { ColumnWithScrollBar(Modifier.themedBackground(bgLayerSize = LocalAppBarHandler.current?.backgroundGraphicsLayerSize, bgLayer = LocalAppBarHandler.current?.backgroundGraphicsLayer)) { @@ -94,3 +98,13 @@ fun SelectableCard(currentValue: State, newValue: T, title: String, descr } Spacer(Modifier.height(14.dp)) } + +private fun prepareChatBeforeNotificationsSetup(chatModel: ChatModel) { + // No visible users but may have hidden. In this case chat should be started anyway because it's stopped on this stage with hidden users + if (chatModel.users.any { u -> !u.user.hidden }) return + withBGApi { + val user = chatModel.controller.apiGetActiveUser(null) ?: return@withBGApi + chatModel.currentUser.value = user + chatModel.controller.startChat(user) + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetupDatabasePassphrase.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetupDatabasePassphrase.kt index d0a3e601d2..4ad2675e83 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetupDatabasePassphrase.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetupDatabasePassphrase.kt @@ -36,7 +36,7 @@ fun SetupDatabasePassphrase(m: ChatModel) { val confirmNewKey = rememberSaveable { mutableStateOf("") } fun nextStep() { if (appPlatform.isAndroid || chatModel.currentUser.value != null) { - m.controller.appPrefs.onboardingStage.set(OnboardingStage.Step3_CreateSimpleXAddress) + m.controller.appPrefs.onboardingStage.set(OnboardingStage.Step4_SetNotificationsMode) } else { m.controller.appPrefs.onboardingStage.set(OnboardingStage.LinkAMobile) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressLearnMore.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressLearnMore.kt index 6d6b72d2d1..efc161ac65 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressLearnMore.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressLearnMore.kt @@ -1,9 +1,16 @@ package chat.simplex.common.views.usersettings import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.* import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp import chat.simplex.common.platform.ColumnWithScrollBar +import chat.simplex.common.platform.chatModel import chat.simplex.common.ui.theme.DEFAULT_PADDING import dev.icerock.moko.resources.compose.stringResource import chat.simplex.common.views.helpers.* @@ -12,12 +19,30 @@ import chat.simplex.common.views.onboarding.ReadableTextWithLink import chat.simplex.res.MR @Composable -fun UserAddressLearnMore() { +fun UserAddressLearnMore(showCreateAddressButton: Boolean = false) { ColumnWithScrollBar(Modifier .padding(horizontal = DEFAULT_PADDING)) { AppBarTitle(stringResource(MR.strings.simplex_address), withPadding = false) ReadableText(MR.strings.you_can_share_your_address) ReadableText(MR.strings.you_wont_lose_your_contacts_if_delete_address) ReadableText(MR.strings.you_can_accept_or_reject_connection) ReadableTextWithLink(MR.strings.read_more_in_user_guide_with_link, "https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address") + + if (showCreateAddressButton) { + Spacer(Modifier.weight(1f)) + Column(Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING * 2), horizontalAlignment = Alignment.CenterHorizontally) { + Button( + onClick = { + ModalManager.start.showModalCloseable { close -> + UserAddressView(chatModel = chatModel, shareViaProfile = false, autoCreateAddress = true, close = close) + } + }, + shape = CircleShape, + contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 2, vertical = DEFAULT_PADDING), + colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary, disabledBackgroundColor = MaterialTheme.colors.secondary) + ) { + Text(stringResource(MR.strings.create_simplex_address), style = MaterialTheme.typography.h2, color = Color.White, fontSize = 18.sp, fontWeight = FontWeight.Medium) + } + } + } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt index b357272e16..aa9ba70b02 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt @@ -33,6 +33,7 @@ fun UserAddressView( chatModel: ChatModel, viaCreateLinkView: Boolean = false, shareViaProfile: Boolean = false, + autoCreateAddress: Boolean = false, close: () -> Unit ) { // TODO close when remote host changes @@ -58,6 +59,33 @@ fun UserAddressView( } } } + + fun createAddress() { + withBGApi { + progressIndicator = true + val connReqContact = chatModel.controller.apiCreateUserAddress(user?.value?.remoteHostId) + if (connReqContact != null) { + chatModel.userAddress.value = UserContactLinkRec(connReqContact) + + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.share_address_with_contacts_question), + text = generalGetString(MR.strings.add_address_to_your_profile), + confirmText = generalGetString(MR.strings.share_verb), + onConfirm = { + setProfileAddress(true) + shareViaProfile.value = true + } + ) + } + progressIndicator = false + } + } + + LaunchedEffect(autoCreateAddress) { + if (autoCreateAddress) { + createAddress() + } + } val userAddress = remember { chatModel.userAddress } val clipboard = LocalClipboardManager.current val uriHandler = LocalUriHandler.current @@ -67,26 +95,7 @@ fun UserAddressView( userAddress = userAddress.value, shareViaProfile, onCloseHandler, - createAddress = { - withBGApi { - progressIndicator = true - val connReqContact = chatModel.controller.apiCreateUserAddress(user?.value?.remoteHostId) - if (connReqContact != null) { - chatModel.userAddress.value = UserContactLinkRec(connReqContact) - - AlertManager.shared.showAlertDialog( - title = generalGetString(MR.strings.share_address_with_contacts_question), - text = generalGetString(MR.strings.add_address_to_your_profile), - confirmText = generalGetString(MR.strings.share_verb), - onConfirm = { - setProfileAddress(true) - shareViaProfile.value = true - } - ) - } - progressIndicator = false - } - }, + createAddress = { createAddress() }, learnMore = { ModalManager.start.showModal { UserAddressLearnMore() diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 0ada4d3095..673100bd8d 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -378,6 +378,7 @@ Tap to Connect Connect with %1$s? Search or paste SimpleX link + Tap Create SimpleX address in the menu to create it later. No selected chat From 619985730ec5027ee4109eb44a80d03bc48a28d0 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Mon, 18 Nov 2024 18:44:28 +0000 Subject: [PATCH 030/167] core: use random servers for each operator (#5192) * core: use random servers for each operator (WIP, compiles with undefined stub) * compiles * fix some, break some * tests pass * cleanup * delays in tests * enable random servers test * remove new preset servers in down migration * fix migration * test --- src/Simplex/Chat.hs | 216 ++++++++++-------- src/Simplex/Chat/Controller.hs | 12 +- .../Migrations/M20241027_server_operators.hs | 2 + src/Simplex/Chat/Operators.hs | 137 ++++++----- src/Simplex/Chat/Store/Profiles.hs | 76 ++---- tests/ChatClient.hs | 19 +- tests/ChatTests/Direct.hs | 17 +- tests/ChatTests/Groups.hs | 5 - tests/ChatTests/Profiles.hs | 4 +- tests/OperatorTests.hs | 59 ++++- tests/RandomServers.hs | 20 +- 11 files changed, 319 insertions(+), 248 deletions(-) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 95bb405bae..819832a1ed 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -3,6 +3,7 @@ {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GADTs #-} +{-# LANGUAGE KindSignatures #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE MultiWayIf #-} {-# LANGUAGE NamedFieldPuns #-} @@ -178,6 +179,8 @@ defaultChatConfig = }, chatVRange = supportedChatVRange, confirmMigrations = MCConsole, + -- this property should NOT use operator = Nothing + -- non-operator servers can be passed via options presetServers = PresetServers { operators = @@ -310,11 +313,15 @@ newChatController config = cfg {logLevel, showReactions, tbqSize, subscriptionEvents = logConnections, hostEvents = logServerHosts, presetServers = presetServers', inlineFiles = inlineFiles', autoAcceptFileSize, highlyAvailable, confirmMigrations = confirmMigrations'} firstTime = dbNew chatStore currentUser <- newTVarIO user - randomSMP <- randomPresetServers SPSMP presetServers' - randomXFTP <- randomPresetServers SPXFTP presetServers' - let randomServers = RandomServers {smpServers = randomSMP, xftpServers = randomXFTP} + randomPresetServers <- chooseRandomServers presetServers' + let rndSrvs = L.toList randomPresetServers + operatorWithId (i, op) = (\o -> o {operatorId = DBEntityId i}) <$> pOperator op + opDomains = operatorDomains $ mapMaybe operatorWithId $ zip [1..] rndSrvs + agentSMP <- randomServerCfgs "agent SMP servers" SPSMP opDomains rndSrvs + agentXFTP <- randomServerCfgs "agent XFTP servers" SPXFTP opDomains rndSrvs + let randomAgentServers = RandomAgentServers {smpServers = agentSMP, xftpServers = agentXFTP} currentRemoteHost <- newTVarIO Nothing - servers <- withTransaction chatStore $ \db -> agentServers db config randomServers + servers <- withTransaction chatStore $ \db -> agentServers db config randomPresetServers randomAgentServers smpAgent <- getSMPAgentClient aCfg {tbqSize} servers agentStore backgroundMode agentAsync <- newTVarIO Nothing random <- liftIO C.newRandom @@ -350,7 +357,8 @@ newChatController ChatController { firstTime, currentUser, - randomServers, + randomPresetServers, + randomAgentServers, currentRemoteHost, smpAgent, agentAsync, @@ -410,19 +418,26 @@ newChatController xftp = map newUserServer xftpSrvs, useXFTP = 0 } - agentServers :: DB.Connection -> ChatConfig -> RandomServers -> IO InitialAgentServers - agentServers db ChatConfig {presetServers = PresetServers {operators = presetOps, ntf, netCfg}} rs = do + randomServerCfgs :: UserProtocol p => String -> SProtocolType p -> [(Text, ServerOperator)] -> [PresetOperator] -> IO (NonEmpty (ServerCfg p)) + randomServerCfgs name p opDomains rndSrvs = + toJustOrError name $ L.nonEmpty $ agentServerCfgs p opDomains $ concatMap (pServers p) rndSrvs + agentServers :: DB.Connection -> ChatConfig -> NonEmpty PresetOperator -> RandomAgentServers -> IO InitialAgentServers + agentServers db ChatConfig {presetServers = PresetServers {ntf, netCfg}} presetOps as = do users <- getUsers db - opDomains <- operatorDomains <$> getUpdateServerOperators db presetOps (null users) - smp' <- getServers SPSMP users opDomains - xftp' <- getServers SPXFTP users opDomains - pure InitialAgentServers {smp = smp', xftp = xftp', ntf, netCfg} + ops <- getUpdateServerOperators db presetOps (null users) + let opDomains = operatorDomains $ mapMaybe snd ops + (smp', xftp') <- unzip <$> mapM (getServers ops opDomains) users + pure InitialAgentServers {smp = M.fromList smp', xftp = M.fromList xftp', ntf, netCfg} where - getServers :: forall p. (ProtocolTypeI p, UserProtocol p) => SProtocolType p -> [User] -> [(Text, ServerOperator)] -> IO (Map UserId (NonEmpty (ServerCfg p))) - getServers p users opDomains = do - let rs' = rndServers p rs - fmap M.fromList $ forM users $ \u -> - (aUserId u,) . agentServerCfgs p opDomains rs' <$> getUpdateUserServers db p presetOps rs' u + getServers :: [(Maybe PresetOperator, Maybe ServerOperator)] -> [(Text, ServerOperator)] -> User -> IO ((UserId, NonEmpty (ServerCfg 'PSMP)), (UserId, NonEmpty (ServerCfg 'PXFTP))) + getServers ops opDomains user' = do + smpSrvs <- getProtocolServers db SPSMP user' + xftpSrvs <- getProtocolServers db SPXFTP user' + uss <- groupByOperator' (ops, smpSrvs, xftpSrvs) + ts <- getCurrentTime + uss' <- mapM (setUserServers' db user' ts . updatedUserServers) uss + let auId = aUserId user' + pure $ bimap (auId,) (auId,) $ useServers as opDomains uss' updateNetworkConfig :: NetworkConfig -> SimpleNetCfg -> NetworkConfig updateNetworkConfig cfg SimpleNetCfg {socksProxy, socksMode, hostMode, requiredHostMode, smpProxyMode_, smpProxyFallback_, smpWebPort, tcpTimeout_, logTLSErrors} = @@ -465,28 +480,31 @@ withFileLock :: String -> Int64 -> CM a -> CM a withFileLock name = withEntityLock name . CLFile {-# INLINE withFileLock #-} -serverCfg :: ProtoServerWithAuth p -> ServerCfg p -serverCfg server = ServerCfg {server, operator = Nothing, enabled = True, roles = allRoles} +useServers :: Foldable f => RandomAgentServers -> [(Text, ServerOperator)] -> f UserOperatorServers -> (NonEmpty (ServerCfg 'PSMP), NonEmpty (ServerCfg 'PXFTP)) +useServers as opDomains uss = + let smp' = useServerCfgs SPSMP as opDomains $ concatMap (servers' SPSMP) uss + xftp' = useServerCfgs SPXFTP as opDomains $ concatMap (servers' SPXFTP) uss + in (smp', xftp') -useServers :: forall p. UserProtocol p => SProtocolType p -> RandomServers -> [UserServer p] -> NonEmpty (NewUserServer p) -useServers p rs servers = case L.nonEmpty servers of - Nothing -> rndServers p rs - Just srvs -> L.map (\srv -> (srv :: UserServer p) {serverId = DBNewEntity}) srvs - -rndServers :: UserProtocol p => SProtocolType p -> RandomServers -> NonEmpty (NewUserServer p) -rndServers p RandomServers {smpServers, xftpServers} = case p of - SPSMP -> smpServers - SPXFTP -> xftpServers - -randomPresetServers :: forall p. UserProtocol p => SProtocolType p -> PresetServers -> IO (NonEmpty (NewUserServer p)) -randomPresetServers p PresetServers {operators} = toJust . L.nonEmpty . concat =<< mapM opSrvs operators +useServerCfgs :: forall p. UserProtocol p => SProtocolType p -> RandomAgentServers -> [(Text, ServerOperator)] -> [UserServer p] -> NonEmpty (ServerCfg p) +useServerCfgs p RandomAgentServers {smpServers, xftpServers} opDomains = + fromMaybe (rndAgentServers p) . L.nonEmpty . agentServerCfgs p opDomains where - toJust = \case - Just a -> pure a - Nothing -> E.throwIO $ userError "no preset servers" - opSrvs :: PresetOperator -> IO [NewUserServer p] - opSrvs op = do - let srvs = operatorServers p op + rndAgentServers :: SProtocolType p -> NonEmpty (ServerCfg p) + rndAgentServers = \case + SPSMP -> smpServers + SPXFTP -> xftpServers + +chooseRandomServers :: PresetServers -> IO (NonEmpty PresetOperator) +chooseRandomServers PresetServers {operators} = + forM operators $ \op -> do + smp' <- opSrvs SPSMP op + xftp' <- opSrvs SPXFTP op + pure (op :: PresetOperator) {smp = smp', xftp = xftp'} + where + opSrvs :: forall p. UserProtocol p => SProtocolType p -> PresetOperator -> IO [NewUserServer p] + opSrvs p op = do + let srvs = pServers p op toUse = operatorServersToUse p op (enbldSrvs, dsbldSrvs) = partition (\UserServer {enabled} -> enabled) srvs if toUse <= 0 || toUse >= length enbldSrvs @@ -497,6 +515,13 @@ randomPresetServers p PresetServers {operators} = toJust . L.nonEmpty . concat = pure $ sortOn server' $ enbldSrvs' <> dsbldSrvs' <> dsbldSrvs server' UserServer {server = ProtoServerWithAuth srv _} = srv +toJustOrError :: String -> Maybe a -> IO a +toJustOrError name = \case + Just a -> pure a + Nothing -> do + putStrLn $ name <> ": expected Just, exiting" + E.throwIO $ userError name + -- enableSndFiles has no effect when mainApp is True startChatController :: Bool -> Bool -> CM' (Async ()) startChatController mainApp enableSndFiles = do @@ -525,7 +550,7 @@ startChatController mainApp enableSndFiles = do startXFTP startWorkers = do tmp <- readTVarIO =<< asks tempDirectory runExceptT (withAgent $ \a -> startWorkers a tmp) >>= \case - Left e -> liftIO $ print $ "Error starting XFTP workers: " <> show e + Left e -> liftIO $ putStrLn $ "Error starting XFTP workers: " <> show e Right _ -> pure () startCleanupManager = do cleanupAsync <- asks cleanupManagerAsync @@ -639,36 +664,43 @@ processChatCommand' vr = \case forM_ profile $ \Profile {displayName} -> checkValidName displayName p@Profile {displayName} <- liftIO $ maybe generateRandomProfile pure profile u <- asks currentUser - smpServers <- chooseServers SPSMP - xftpServers <- chooseServers SPXFTP users <- withFastStore' getUsers forM_ users $ \User {localDisplayName = n, activeUser, viewPwdHash} -> when (n == displayName) . throwChatError $ if activeUser || isNothing viewPwdHash then CEUserExists displayName else CEInvalidDisplayName {displayName, validName = ""} - opDomains <- operatorDomains . serverOperators <$> withFastStore getServerOperators - rs <- asks randomServers - let smp = agentServerCfgs SPSMP opDomains (rndServers SPSMP rs) smpServers - xftp = agentServerCfgs SPXFTP opDomains (rndServers SPXFTP rs) xftpServers - auId <- withAgent (\a -> createUser a smp xftp) + (uss, (smp', xftp')) <- chooseServers =<< readTVarIO u + auId <- withAgent $ \a -> createUser a smp' xftp' ts <- liftIO $ getCurrentTime >>= if pastTimestamp then coupleDaysAgo else pure - user <- withFastStore $ \db -> createUserRecordAt db (AgentUserId auId) p True ts - createPresetContactCards user `catchChatError` \_ -> pure () - withFastStore $ \db -> do + user <- withFastStore $ \db -> do + user <- createUserRecordAt db (AgentUserId auId) p True ts + mapM_ (setUserServers db user ts) uss + createPresetContactCards db user `catchStoreError` \_ -> pure () createNoteFolder db user - liftIO $ mapM_ (insertProtocolServer db SPSMP user ts) $ useServers SPSMP rs smpServers - liftIO $ mapM_ (insertProtocolServer db SPXFTP user ts) $ useServers SPXFTP rs xftpServers + pure user atomically . writeTVar u $ Just user pure $ CRActiveUser user where - createPresetContactCards :: User -> CM () - createPresetContactCards user = - withFastStore $ \db -> do - createContact db user simplexStatusContactProfile - createContact db user simplexTeamContactProfile - chooseServers :: forall p. ProtocolTypeI p => SProtocolType p -> CM [UserServer p] - chooseServers p = do - srvs <- chatReadVar currentUser >>= mapM (\user -> withFastStore' $ \db -> getProtocolServers db p user) - pure $ fromMaybe [] srvs + createPresetContactCards :: DB.Connection -> User -> ExceptT StoreError IO () + createPresetContactCards db user = do + createContact db user simplexStatusContactProfile + createContact db user simplexTeamContactProfile + chooseServers :: Maybe User -> CM ([UpdatedUserOperatorServers], (NonEmpty (ServerCfg 'PSMP), NonEmpty (ServerCfg 'PXFTP))) + chooseServers user_ = do + as <- asks randomAgentServers + mapM (withFastStore . flip getUserServers >=> liftIO . groupByOperator) user_ >>= \case + Just uss -> do + let opDomains = operatorDomains $ mapMaybe operator' uss + uss' = map copyServers uss + pure $ (uss',) $ useServers as opDomains uss + Nothing -> do + ps <- asks randomPresetServers + uss <- presetUserServers <$> withFastStore' (\db -> getUpdateServerOperators db ps True) + let RandomAgentServers {smpServers = smp', xftpServers = xftp'} = as + pure (uss, (smp', xftp')) + copyServers :: UserOperatorServers -> UpdatedUserOperatorServers + copyServers UserOperatorServers {operator, smpServers, xftpServers} = + let new srv = AUS SDBNew srv {serverId = DBNewEntity} + in UpdatedUserOperatorServers {operator, smpServers = map new smpServers, xftpServers = map new xftpServers} coupleDaysAgo t = (`addUTCTime` t) . fromInteger . negate . (+ (2 * day)) <$> randomRIO (0, day) day = 86400 ListUsers -> CRUsersList <$> withFastStore' getUsersInfo @@ -1568,32 +1600,16 @@ processChatCommand' vr = \case pure $ CRConnNtfMessages ntfMsgs GetUserProtoServers (AProtocolType p) -> withUser $ \user -> withServerProtocol p $ do srvs <- withFastStore (`getUserServers` user) - CRUserServers user <$> liftIO (groupedServers srvs p) - where - groupedServers :: UserProtocol p => ([ServerOperator], [UserServer 'PSMP], [UserServer 'PXFTP]) -> SProtocolType p -> IO [UserOperatorServers] - groupedServers (operators, smpServers, xftpServers) = \case - SPSMP -> groupByOperator (operators, smpServers, []) - SPXFTP -> groupByOperator (operators, [], xftpServers) + liftIO $ CRUserServers user <$> groupByOperator (protocolServers p srvs) SetUserProtoServers (AProtocolType (p :: SProtocolType p)) srvs -> withUser $ \user@User {userId} -> withServerProtocol p $ do - srvs' <- mapM aUserServer srvs userServers_ <- liftIO . groupByOperator =<< withFastStore (`getUserServers` user) case L.nonEmpty userServers_ of Nothing -> throwChatError $ CECommandError "no servers" Just userServers -> case srvs of [] -> throwChatError $ CECommandError "no servers" - _ -> processChatCommand $ APISetUserServers userId $ L.map (updatedSrvs p) userServers - where - -- disable preset and replace custom servers (groupByOperator always adds custom) - updatedSrvs :: UserProtocol p => SProtocolType p -> UserOperatorServers -> UpdatedUserOperatorServers - updatedSrvs p' UserOperatorServers {operator, smpServers, xftpServers} = case p' of - SPSMP -> u (updateSrvs smpServers, map (AUS SDBStored) xftpServers) - SPXFTP -> u (map (AUS SDBStored) smpServers, updateSrvs xftpServers) - where - u = uncurry $ UpdatedUserOperatorServers operator - updateSrvs :: [UserServer p] -> [AUserServer p] - updateSrvs pSrvs = map disableSrv pSrvs <> maybe srvs' (const []) operator - disableSrv srv@UserServer {preset} = - AUS SDBStored $ if preset then srv {enabled = False} else srv {deleted = True} + _ -> do + srvs' <- mapM aUserServer srvs + processChatCommand $ APISetUserServers userId $ L.map (updatedServers p srvs') userServers where aUserServer :: AProtoServerWithAuth -> CM (AUserServer p) aUserServer (AProtoServerWithAuth p' srv) = case testEquality p p' of @@ -1607,20 +1623,21 @@ processChatCommand' vr = \case APISetServerOperators operatorsEnabled -> withFastStore $ \db -> do liftIO $ setServerOperators db operatorsEnabled CRServerOperatorConditions <$> getServerOperators db - APIGetUserServers userId -> withUserId userId $ \user -> withFastStore $ \db -> + APIGetUserServers userId -> withUserId userId $ \user -> withFastStore $ \db -> do CRUserServers user <$> (liftIO . groupByOperator =<< getUserServers db user) APISetUserServers userId userServers -> withUserId userId $ \user -> do errors <- validateAllUsersServers userId $ L.toList userServers unless (null errors) $ throwChatError (CECommandError $ "user servers validation error(s): " <> show errors) - (operators, smpServers, xftpServers) <- withFastStore $ \db -> do - setUserServers db user userServers - getUserServers db user - let opDomains = operatorDomains operators - rs <- asks randomServers + uss <- withFastStore $ \db -> do + ts <- liftIO getCurrentTime + mapM (setUserServers db user ts) userServers + as <- asks randomAgentServers lift $ withAgent' $ \a -> do let auId = aUserId user - setProtocolServers a auId $ agentServerCfgs SPSMP opDomains (rndServers SPSMP rs) smpServers - setProtocolServers a auId $ agentServerCfgs SPXFTP opDomains (rndServers SPXFTP rs) xftpServers + opDomains = operatorDomains $ mapMaybe operator' $ L.toList uss + (smp', xftp') = useServers as opDomains uss + setProtocolServers a auId smp' + setProtocolServers a auId xftp' ok_ APIValidateServers userId userServers -> withUserId userId $ \user -> CRUserServersValidation user <$> validateAllUsersServers userId userServers @@ -1897,7 +1914,7 @@ processChatCommand' vr = \case let ConnReqUriData {crSmpQueues = q :| _} = crData SMPQueueUri {queueAddress = SMPQueueAddress {smpServer}} = q newUserServers <- - map protoServer' . filter (\ServerCfg {enabled} -> enabled) + map protoServer' . L.filter (\ServerCfg {enabled} -> enabled) <$> getKnownAgentServers SPSMP newUser pure $ smpServer `elem` newUserServers updateConnRecord user@User {userId} conn@PendingContactConnection {customUserProfileId} newUser = do @@ -3375,6 +3392,23 @@ processChatCommand' vr = \case msgInfo <- withFastStore' (`getLastRcvMsgInfo` connId) CRQueueInfo user msgInfo <$> withAgent (`getConnectionQueueInfo` acId) +protocolServers :: UserProtocol p => SProtocolType p -> ([Maybe ServerOperator], [UserServer 'PSMP], [UserServer 'PXFTP]) -> ([Maybe ServerOperator], [UserServer 'PSMP], [UserServer 'PXFTP]) +protocolServers p (operators, smpServers, xftpServers) = case p of + SPSMP -> (operators, smpServers, []) + SPXFTP -> (operators, [], xftpServers) + +-- disable preset and replace custom servers (groupByOperator always adds custom) +updatedServers :: forall p. UserProtocol p => SProtocolType p -> [AUserServer p] -> UserOperatorServers -> UpdatedUserOperatorServers +updatedServers p' srvs UserOperatorServers {operator, smpServers, xftpServers} = case p' of + SPSMP -> u (updateSrvs smpServers, map (AUS SDBStored) xftpServers) + SPXFTP -> u (map (AUS SDBStored) smpServers, updateSrvs xftpServers) + where + u = uncurry $ UpdatedUserOperatorServers operator + updateSrvs :: [UserServer p] -> [AUserServer p] + updateSrvs pSrvs = map disableSrv pSrvs <> maybe srvs (const []) operator + disableSrv srv@UserServer {preset} = + AUS SDBStored $ if preset then srv {enabled = False} else srv {deleted = True} + type ComposeMessageReq = (ComposedMessage, Maybe CIForwardedFrom) contactCITimed :: Contact -> CM (Maybe CITimed) @@ -3761,7 +3795,7 @@ receiveViaCompleteFD user fileId RcvFileDescr {fileDescrText, fileDescrComplete} S.toList $ S.fromList $ concatMap (\FD.FileChunk {replicas} -> map (\FD.FileChunkReplica {server} -> server) replicas) chunks getUnknownSrvs :: [XFTPServer] -> CM [XFTPServer] getUnknownSrvs srvs = do - knownSrvs <- map protoServer' <$> getKnownAgentServers SPXFTP user + knownSrvs <- L.map protoServer' <$> getKnownAgentServers SPXFTP user pure $ filter (`notElem` knownSrvs) srvs ipProtectedForSrvs :: [XFTPServer] -> CM Bool ipProtectedForSrvs srvs = do @@ -3775,13 +3809,13 @@ receiveViaCompleteFD user fileId RcvFileDescr {fileDescrText, fileDescrComplete} toView $ CRChatItemUpdated user aci throwChatError $ CEFileNotApproved fileId unknownSrvs -getKnownAgentServers :: (ProtocolTypeI p, UserProtocol p) => SProtocolType p -> User -> CM [ServerCfg p] +getKnownAgentServers :: (ProtocolTypeI p, UserProtocol p) => SProtocolType p -> User -> CM (NonEmpty (ServerCfg p)) getKnownAgentServers p user = do - rs <- asks randomServers + as <- asks randomAgentServers withStore $ \db -> do opDomains <- operatorDomains . serverOperators <$> getServerOperators db srvs <- liftIO $ getProtocolServers db p user - pure $ L.toList $ agentServerCfgs p opDomains (rndServers p rs) srvs + pure $ useServerCfgs p as opDomains srvs protoServer' :: ServerCfg p -> ProtocolServer p protoServer' ServerCfg {server} = protoServer server diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index c085dcf470..b6229e07ba 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -70,7 +70,7 @@ import Simplex.Chat.Util (liftIOEither) import Simplex.FileTransfer.Description (FileDescriptionURI) import Simplex.Messaging.Agent (AgentClient, SubscriptionsInfo) import Simplex.Messaging.Agent.Client (AgentLocks, AgentQueuesInfo (..), AgentWorkersDetails (..), AgentWorkersSummary (..), ProtocolTestFailure, SMPServerSubs, ServerQueueInfo, UserNetworkInfo) -import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, NetworkConfig) +import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, NetworkConfig, ServerCfg) import Simplex.Messaging.Agent.Lock import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation, SQLiteStore, UpMigration, withTransaction, withTransactionPriority) @@ -154,9 +154,9 @@ data ChatConfig = ChatConfig chatHooks :: ChatHooks } -data RandomServers = RandomServers - { smpServers :: NonEmpty (NewUserServer 'PSMP), - xftpServers :: NonEmpty (NewUserServer 'PXFTP) +data RandomAgentServers = RandomAgentServers + { smpServers :: NonEmpty (ServerCfg 'PSMP), + xftpServers :: NonEmpty (ServerCfg 'PXFTP) } deriving (Show) @@ -183,6 +183,7 @@ data PresetServers = PresetServers ntf :: [NtfServer], netCfg :: NetworkConfig } + deriving (Show) data InlineFilesConfig = InlineFilesConfig { offerChunks :: Integer, @@ -206,7 +207,8 @@ data ChatDatabase = ChatDatabase {chatStore :: SQLiteStore, agentStore :: SQLite data ChatController = ChatController { currentUser :: TVar (Maybe User), - randomServers :: RandomServers, + randomPresetServers :: NonEmpty PresetOperator, + randomAgentServers :: RandomAgentServers, currentRemoteHost :: TVar (Maybe RemoteHostId), firstTime :: Bool, smpAgent :: AgentClient, diff --git a/src/Simplex/Chat/Migrations/M20241027_server_operators.hs b/src/Simplex/Chat/Migrations/M20241027_server_operators.hs index c4b40c4706..1316e3c006 100644 --- a/src/Simplex/Chat/Migrations/M20241027_server_operators.hs +++ b/src/Simplex/Chat/Migrations/M20241027_server_operators.hs @@ -53,4 +53,6 @@ DROP INDEX idx_operator_usage_conditions_server_operator_id; DROP TABLE operator_usage_conditions; DROP TABLE usage_conditions; DROP TABLE server_operators; + +DELETE FROM protocol_servers WHERE host LIKE "%.simplexonflux.com,%"; |] diff --git a/src/Simplex/Chat/Operators.hs b/src/Simplex/Chat/Operators.hs index c3d9a8823b..f7a07682f9 100644 --- a/src/Simplex/Chat/Operators.hs +++ b/src/Simplex/Chat/Operators.hs @@ -27,6 +27,7 @@ import qualified Data.Aeson.TH as JQ import Data.Either (partitionEithers) import Data.FileEmbed import Data.Foldable (foldMap') +import Data.Functor.Identity import Data.IORef import Data.Int (Int64) import Data.Kind @@ -234,13 +235,13 @@ class UserServersClass u where type AServer u = (s :: ProtocolType -> Type) | s -> u operator' :: u -> Maybe ServerOperator partitionValid :: [AServer u p] -> ([Text], [AUserServer p]) - servers' :: UserProtocol p => u -> SProtocolType p -> [AServer u p] + servers' :: UserProtocol p => SProtocolType p -> u -> [AServer u p] instance UserServersClass UserOperatorServers where type AServer UserOperatorServers = UserServer_ 'DBStored ProtoServerWithAuth operator' UserOperatorServers {operator} = operator partitionValid ss = ([], map (AUS SDBStored) ss) - servers' UserOperatorServers {smpServers, xftpServers} = \case + servers' p UserOperatorServers {smpServers, xftpServers} = case p of SPSMP -> smpServers SPXFTP -> xftpServers @@ -248,7 +249,7 @@ instance UserServersClass UpdatedUserOperatorServers where type AServer UpdatedUserOperatorServers = AUserServer operator' UpdatedUserOperatorServers {operator} = operator partitionValid = ([],) - servers' UpdatedUserOperatorServers {smpServers, xftpServers} = \case + servers' p UpdatedUserOperatorServers {smpServers, xftpServers} = case p of SPSMP -> smpServers SPXFTP -> xftpServers @@ -259,7 +260,7 @@ instance UserServersClass ValidatedUserOperatorServers where where serverOrErr :: AValidatedServer p -> Either Text (AUserServer p) serverOrErr (AVS s srv@UserServer {server = server'}) = (\server -> AUS s srv {server}) <$> unVPS server' - servers' ValidatedUserOperatorServers {smpServers, xftpServers} = \case + servers' p ValidatedUserOperatorServers {smpServers, xftpServers} = case p of SPSMP -> smpServers SPXFTP -> xftpServers @@ -290,9 +291,13 @@ data PresetOperator = PresetOperator xftp :: [NewUserServer 'PXFTP], useXFTP :: Int } + deriving (Show) -operatorServers :: UserProtocol p => SProtocolType p -> PresetOperator -> [NewUserServer p] -operatorServers p PresetOperator {smp, xftp} = case p of +pOperator :: PresetOperator -> Maybe NewServerOperator +pOperator PresetOperator {operator} = operator + +pServers :: UserProtocol p => SProtocolType p -> PresetOperator -> [NewUserServer p] +pServers p PresetOperator {smp, xftp} = case p of SPSMP -> smp SPXFTP -> xftp @@ -335,83 +340,113 @@ usageConditionsToAdd' prevCommit sourceCommit newUser createdAt = \case where conditions cId commit = UsageConditions {conditionsId = cId, conditionsCommit = commit, notifiedAt = Nothing, createdAt} +presetUserServers :: [(Maybe PresetOperator, Maybe ServerOperator)] -> [UpdatedUserOperatorServers] +presetUserServers = mapMaybe $ \(presetOp_, op) -> mkUS op <$> presetOp_ + where + mkUS op PresetOperator {smp, xftp} = + UpdatedUserOperatorServers op (map (AUS SDBNew) smp) (map (AUS SDBNew) xftp) + -- This function should be used inside DB transaction to update operators. -- It allows to add/remove/update preset operators in the database preserving enabled and roles settings, -- and preserves custom operators without tags for forward compatibility. -updatedServerOperators :: NonEmpty PresetOperator -> [ServerOperator] -> [AServerOperator] +updatedServerOperators :: NonEmpty PresetOperator -> [ServerOperator] -> [(Maybe PresetOperator, Maybe AServerOperator)] updatedServerOperators presetOps storedOps = foldr addPreset [] presetOps - <> map (ASO SDBStored) (filter (isNothing . operatorTag) storedOps) + <> map (\op -> (Nothing, Just $ ASO SDBStored op)) (filter (isNothing . operatorTag) storedOps) where -- TODO remove domains of preset operators from custom - addPreset PresetOperator {operator} = case operator of - Nothing -> id - Just presetOp -> (storedOp' :) - where - storedOp' = case find ((operatorTag presetOp ==) . operatorTag) storedOps of - Just ServerOperator {operatorId, conditionsAcceptance, enabled, smpRoles, xftpRoles} -> - ASO SDBStored presetOp {operatorId, conditionsAcceptance, enabled, smpRoles, xftpRoles} - Nothing -> ASO SDBNew presetOp + addPreset op = ((Just op, storedOp' <$> pOperator op) :) + where + storedOp' presetOp = case find ((operatorTag presetOp ==) . operatorTag) storedOps of + Just ServerOperator {operatorId, conditionsAcceptance, enabled, smpRoles, xftpRoles} -> + ASO SDBStored presetOp {operatorId, conditionsAcceptance, enabled, smpRoles, xftpRoles} + Nothing -> ASO SDBNew presetOp -- This function should be used inside DB transaction to update servers. -updatedUserServers :: forall p. UserProtocol p => SProtocolType p -> NonEmpty PresetOperator -> NonEmpty (NewUserServer p) -> [UserServer p] -> NonEmpty (AUserServer p) -updatedUserServers _ _ randomSrvs [] = L.map (AUS SDBNew) randomSrvs -updatedUserServers p presetOps randomSrvs srvs = - fromMaybe (L.map (AUS SDBNew) randomSrvs) (L.nonEmpty updatedSrvs) +updatedUserServers :: (Maybe PresetOperator, UserOperatorServers) -> UpdatedUserOperatorServers +updatedUserServers (presetOp_, UserOperatorServers {operator, smpServers, xftpServers}) = + UpdatedUserOperatorServers {operator, smpServers = smp', xftpServers = xftp'} where - updatedSrvs = map userServer presetSrvs <> map (AUS SDBStored) (filter customServer srvs) - storedSrvs :: Map (ProtoServerWithAuth p) (UserServer p) - storedSrvs = foldl' (\ss srv@UserServer {server} -> M.insert server srv ss) M.empty srvs - customServer :: UserServer p -> Bool - customServer srv = not (preset srv) && all (`S.notMember` presetHosts) (srvHost srv) - presetSrvs :: [NewUserServer p] - presetSrvs = concatMap (operatorServers p) presetOps - presetHosts :: Set TransportHost - presetHosts = foldMap' (S.fromList . L.toList . srvHost) presetSrvs - userServer :: NewUserServer p -> AUserServer p - userServer srv@UserServer {server} = maybe (AUS SDBNew srv) (AUS SDBStored) (M.lookup server storedSrvs) + stored = map (AUS SDBStored) + (smp', xftp') = case presetOp_ of + Nothing -> (stored smpServers, stored xftpServers) + Just presetOp -> (updated SPSMP smpServers, updated SPXFTP xftpServers) + where + updated :: forall p. UserProtocol p => SProtocolType p -> [UserServer p] -> [AUserServer p] + updated p srvs = map userServer presetSrvs <> stored (filter customServer srvs) + where + storedSrvs :: Map (ProtoServerWithAuth p) (UserServer p) + storedSrvs = foldl' (\ss srv@UserServer {server} -> M.insert server srv ss) M.empty srvs + customServer :: UserServer p -> Bool + customServer srv = not (preset srv) && all (`S.notMember` presetHosts) (srvHost srv) + presetSrvs :: [NewUserServer p] + presetSrvs = pServers p presetOp + presetHosts :: Set TransportHost + presetHosts = foldMap' (S.fromList . L.toList . srvHost) presetSrvs + userServer :: NewUserServer p -> AUserServer p + userServer srv@UserServer {server} = maybe (AUS SDBNew srv) (AUS SDBStored) (M.lookup server storedSrvs) srvHost :: UserServer' s p -> NonEmpty TransportHost srvHost UserServer {server = ProtoServerWithAuth srv _} = host srv -agentServerCfgs :: UserProtocol p => SProtocolType p -> [(Text, ServerOperator)] -> NonEmpty (NewUserServer p) -> [UserServer' s p] -> NonEmpty (ServerCfg p) -agentServerCfgs p opDomains randomSrvs = - fromMaybe fallbackSrvs . L.nonEmpty . mapMaybe enabledOpAgentServer +agentServerCfgs :: UserProtocol p => SProtocolType p -> [(Text, ServerOperator)] -> [UserServer' s p] -> [ServerCfg p] +agentServerCfgs p opDomains = mapMaybe agentServer where - fallbackSrvs = L.map (snd . agentServer) randomSrvs - enabledOpAgentServer srv = - let (opEnabled, srvCfg) = agentServer srv - in if opEnabled then Just srvCfg else Nothing - agentServer :: UserServer' s p -> (Bool, ServerCfg p) + agentServer :: UserServer' s p -> Maybe (ServerCfg p) agentServer srv@UserServer {server, enabled} = case find (\(d, _) -> any (matchingHost d) (srvHost srv)) opDomains of - Just (_, op@ServerOperator {operatorId = DBEntityId opId, enabled = opEnabled}) -> - (opEnabled, ServerCfg {server, enabled, operator = Just opId, roles = operatorRoles p op}) + Just (_, op@ServerOperator {operatorId = DBEntityId opId, enabled = opEnabled}) + | opEnabled -> Just ServerCfg {server, enabled, operator = Just opId, roles = operatorRoles p op} + | otherwise -> Nothing Nothing -> - (True, ServerCfg {server, enabled, operator = Nothing, roles = allRoles}) + Just ServerCfg {server, enabled, operator = Nothing, roles = allRoles} matchingHost :: Text -> TransportHost -> Bool matchingHost d = \case THDomainName h -> d `T.isSuffixOf` T.pack h _ -> False -operatorDomains :: [ServerOperator] -> [(Text, ServerOperator)] +operatorDomains :: [ServerOperator' s] -> [(Text, ServerOperator' s)] operatorDomains = foldr (\op ds -> foldr (\d -> ((d, op) :)) ds (serverDomains op)) [] -groupByOperator :: ([ServerOperator], [UserServer 'PSMP], [UserServer 'PXFTP]) -> IO [UserOperatorServers] -groupByOperator (ops, smpSrvs, xftpSrvs) = do - ss <- mapM (\op -> (serverDomains op,) <$> newIORef (UserOperatorServers (Just op) [] [])) ops - custom <- newIORef $ UserOperatorServers Nothing [] [] +class Box b where + box :: a -> b a + unbox :: b a -> a + +instance Box Identity where + box = Identity + unbox = runIdentity + +instance Box ((,) (Maybe a)) where + box = (Nothing,) + unbox = snd + +groupByOperator :: ([Maybe ServerOperator], [UserServer 'PSMP], [UserServer 'PXFTP]) -> IO [UserOperatorServers] +groupByOperator (ops, smpSrvs, xftpSrvs) = map runIdentity <$> groupByOperator_ (map Identity ops, smpSrvs, xftpSrvs) + +-- For the initial app start this function relies on tuple being Functor/Box +-- to preserve the information about operator being DBNew or DBStored +groupByOperator' :: ([(Maybe PresetOperator, Maybe ServerOperator)], [UserServer 'PSMP], [UserServer 'PXFTP]) -> IO [(Maybe PresetOperator, UserOperatorServers)] +groupByOperator' = groupByOperator_ +{-# INLINE groupByOperator' #-} + +groupByOperator_ :: forall f. (Box f, Traversable f) => ([f (Maybe ServerOperator)], [UserServer 'PSMP], [UserServer 'PXFTP]) -> IO [f UserOperatorServers] +groupByOperator_ (ops, smpSrvs, xftpSrvs) = do + let ops' = mapMaybe sequence ops + customOp_ = find (isNothing . unbox) ops + ss <- mapM ((\op -> (serverDomains (unbox op),) <$> newIORef (mkUS . Just <$> op))) ops' + custom <- newIORef $ maybe (box $ mkUS Nothing) (mkUS <$>) customOp_ mapM_ (addServer ss custom addSMP) (reverse smpSrvs) mapM_ (addServer ss custom addXFTP) (reverse xftpSrvs) opSrvs <- mapM (readIORef . snd) ss customSrvs <- readIORef custom pure $ opSrvs <> [customSrvs] where - addServer :: [([Text], IORef UserOperatorServers)] -> IORef UserOperatorServers -> (UserServer p -> UserOperatorServers -> UserOperatorServers) -> UserServer p -> IO () + mkUS op = UserOperatorServers op [] [] + addServer :: [([Text], IORef (f UserOperatorServers))] -> IORef (f UserOperatorServers) -> (UserServer p -> UserOperatorServers -> UserOperatorServers) -> UserServer p -> IO () addServer ss custom add srv = let v = maybe custom snd $ find (\(ds, _) -> any (\d -> any (matchingHost d) (srvHost srv)) ds) ss - in atomicModifyIORef'_ v $ add srv + in atomicModifyIORef'_ v (add srv <$>) addSMP srv s@UserOperatorServers {smpServers} = (s :: UserOperatorServers) {smpServers = srv : smpServers} addXFTP srv s@UserOperatorServers {xftpServers} = (s :: UserOperatorServers) {xftpServers = srv : xftpServers} @@ -434,7 +469,7 @@ validateUserServers curr others = currUserErrs <> concatMap otherUserErrs others | otherwise = [USEStorageMissing p' user | noServers (hasRole storage)] <> [USEProxyMissing p' user | noServers (hasRole proxy)] where p' = AProtocolType p - noServers cond = not $ any srvEnabled $ snd $ partitionValid $ concatMap (`servers'` p) $ filter cond uss + noServers cond = not $ any srvEnabled $ snd $ partitionValid $ concatMap (servers' p) $ filter cond uss opEnabled = maybe True (\ServerOperator {enabled} -> enabled) . operator' hasRole roleSel = maybe True (\op@ServerOperator {enabled} -> enabled && roleSel (operatorRoles p op)) . operator' srvEnabled (AUS _ UserServer {deleted, enabled}) = enabled && not deleted @@ -442,7 +477,7 @@ validateUserServers curr others = currUserErrs <> concatMap otherUserErrs others serverErrs p uss = map (USEInvalidServer p') invalidSrvs <> mapMaybe duplicateErr_ srvs where p' = AProtocolType p - (invalidSrvs, userSrvs) = partitionValid $ concatMap (`servers'` p) uss + (invalidSrvs, userSrvs) = partitionValid $ concatMap (servers' p) uss srvs = filter (\(AUS _ UserServer {deleted}) -> not deleted) userSrvs duplicateErr_ (AUS _ srv@UserServer {server}) = USEDuplicateServer p' (safeDecodeUtf8 $ strEncode server) diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs index daf9a78fca..ec657fd6f7 100644 --- a/src/Simplex/Chat/Store/Profiles.hs +++ b/src/Simplex/Chat/Store/Profiles.hs @@ -50,9 +50,6 @@ module Simplex.Chat.Store.Profiles getContactWithoutConnViaAddress, updateUserAddressAutoAccept, getProtocolServers, - getUpdateUserServers, - -- overwriteOperatorsAndServers, - overwriteProtocolServers, insertProtocolServer, getUpdateServerOperators, getServerOperators, @@ -63,6 +60,7 @@ module Simplex.Chat.Store.Profiles setConditionsNotified, acceptConditions, setUserServers, + setUserServers', createCall, deleteCalls, getCalls, @@ -83,7 +81,7 @@ import Data.Functor (($>)) import Data.Int (Int64) import Data.List.NonEmpty (NonEmpty) import qualified Data.List.NonEmpty as L -import Data.Maybe (fromMaybe) +import Data.Maybe (catMaybes, fromMaybe) import Data.Text (Text) import qualified Data.Text as T import Data.Text.Encoding (decodeLatin1, encodeUtf8) @@ -108,7 +106,7 @@ import qualified Simplex.Messaging.Crypto as C import qualified Simplex.Messaging.Crypto.Ratchet as CR import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON) -import Simplex.Messaging.Protocol (BasicAuth (..), ProtoServerWithAuth (..), ProtocolServer (..), ProtocolType (..), ProtocolTypeI (..), SProtocolType (..), SubscriptionMode, UserProtocol) +import Simplex.Messaging.Protocol (BasicAuth (..), ProtoServerWithAuth (..), ProtocolServer (..), ProtocolType (..), ProtocolTypeI (..), SProtocolType (..), SubscriptionMode) import Simplex.Messaging.Transport.Client (TransportHost) import Simplex.Messaging.Util (eitherToMaybe, safeDecodeUtf8) @@ -532,18 +530,6 @@ updateUserAddressAutoAccept db user@User {userId} autoAccept = do Just AutoAccept {acceptIncognito, autoReply} -> (True, acceptIncognito, autoReply) _ -> (False, False, Nothing) -getUpdateUserServers :: forall p. (ProtocolTypeI p, UserProtocol p) => DB.Connection -> SProtocolType p -> NonEmpty PresetOperator -> NonEmpty (NewUserServer p) -> User -> IO [UserServer p] -getUpdateUserServers db p presetOps randomSrvs user = do - ts <- getCurrentTime - srvs <- getProtocolServers db p user - let srvs' = L.toList $ updatedUserServers p presetOps randomSrvs srvs - mapM (upsertServer ts) srvs' - where - upsertServer :: UTCTime -> AUserServer p -> IO (UserServer p) - upsertServer ts (AUS _ s@UserServer {serverId}) = case serverId of - DBNewEntity -> insertProtocolServer db p user ts s - DBEntityId _ -> updateProtocolServer db p ts s $> s - getProtocolServers :: forall p. ProtocolTypeI p => DB.Connection -> SProtocolType p -> User -> IO [UserServer p] getProtocolServers db p User {userId} = map toUserServer @@ -561,26 +547,6 @@ getProtocolServers db p User {userId} = let server = ProtoServerWithAuth (ProtocolServer p host port keyHash) (BasicAuth . encodeUtf8 <$> auth_) in UserServer {serverId, server, preset, tested, enabled, deleted = False} --- TODO remove --- overwriteOperatorsAndServers :: forall p. ProtocolTypeI p => DB.Connection -> User -> Maybe [ServerOperator] -> [ServerCfg p] -> ExceptT StoreError IO [ServerCfg p] --- overwriteOperatorsAndServers db user@User {userId} operators_ servers = do -overwriteProtocolServers :: ProtocolTypeI p => DB.Connection -> SProtocolType p -> User -> [UserServer p] -> ExceptT StoreError IO () -overwriteProtocolServers db p User {userId} servers = - -- liftIO $ mapM_ (updateServerOperators_ db) operators_ - checkConstraint SEUniqueID . ExceptT $ do - currentTs <- getCurrentTime - DB.execute db "DELETE FROM protocol_servers WHERE user_id = ? AND protocol = ? " (userId, decodeLatin1 $ strEncode p) - forM_ servers $ \UserServer {serverId, server, preset, tested, enabled} -> do - DB.execute - db - [sql| - INSERT INTO protocol_servers - (server_id, protocol, host, port, key_hash, basic_auth, preset, tested, enabled, user_id, created_at, updated_at) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?) - |] - (Only serverId :. serverColumns p server :. (preset, tested, enabled, userId, currentTs, currentTs)) - pure $ Right () - insertProtocolServer :: forall p. ProtocolTypeI p => DB.Connection -> SProtocolType p -> User -> UTCTime -> NewUserServer p -> IO (UserServer p) insertProtocolServer db p User {userId} ts srv@UserServer {server, preset, tested, enabled} = do DB.execute @@ -623,10 +589,10 @@ getServerOperators db = do let conditionsAction = usageConditionsAction ops currentConditions now pure ServerOperatorConditions {serverOperators = ops, currentConditions, conditionsAction} -getUserServers :: DB.Connection -> User -> ExceptT StoreError IO ([ServerOperator], [UserServer 'PSMP], [UserServer 'PXFTP]) +getUserServers :: DB.Connection -> User -> ExceptT StoreError IO ([Maybe ServerOperator], [UserServer 'PSMP], [UserServer 'PXFTP]) getUserServers db user = (,,) - <$> (serverOperators <$> getServerOperators db) + <$> (map Just . serverOperators <$> getServerOperators db) <*> liftIO (getProtocolServers db SPSMP user) <*> liftIO (getProtocolServers db SPXFTP user) @@ -646,7 +612,7 @@ updateServerOperator db currentTs ServerOperator {operatorId, enabled, smpRoles, |] (enabled, storage smpRoles, proxy smpRoles, storage xftpRoles, proxy xftpRoles, currentTs, operatorId) -getUpdateServerOperators :: DB.Connection -> NonEmpty PresetOperator -> Bool -> IO [ServerOperator] +getUpdateServerOperators :: DB.Connection -> NonEmpty PresetOperator -> Bool -> IO [(Maybe PresetOperator, Maybe ServerOperator)] getUpdateServerOperators db presetOps newUser = do conds <- map toUsageConditions <$> DB.query_ db usageCondsQuery now <- getCurrentTime @@ -654,7 +620,7 @@ getUpdateServerOperators db presetOps newUser = do mapM_ insertConditions condsToAdd latestAcceptedConds_ <- getLatestAcceptedConditions db ops <- updatedServerOperators presetOps <$> getServerOperators_ db - forM ops $ \(ASO _ op) -> + forM ops $ traverse $ mapM $ \(ASO _ op) -> -- traverse for tuple, mapM for Maybe case operatorId op of DBNewEntity -> do op' <- insertOperator op @@ -825,22 +791,24 @@ getUsageConditionsById_ db conditionsId = |] (Only conditionsId) -setUserServers :: DB.Connection -> User -> NonEmpty UpdatedUserOperatorServers -> ExceptT StoreError IO () -setUserServers db user@User {userId} userServers = checkConstraint SEUniqueID $ liftIO $ do - ts <- getCurrentTime - forM_ userServers $ \UpdatedUserOperatorServers {operator, smpServers, xftpServers} -> do - mapM_ (updateServerOperator db ts) operator - mapM_ (upsertOrDelete SPSMP ts) smpServers - mapM_ (upsertOrDelete SPXFTP ts) xftpServers +setUserServers :: DB.Connection -> User -> UTCTime -> UpdatedUserOperatorServers -> ExceptT StoreError IO UserOperatorServers +setUserServers db user ts = checkConstraint SEUniqueID . liftIO . setUserServers' db user ts + +setUserServers' :: DB.Connection -> User -> UTCTime -> UpdatedUserOperatorServers -> IO UserOperatorServers +setUserServers' db user@User {userId} ts UpdatedUserOperatorServers {operator, smpServers, xftpServers} = do + mapM_ (updateServerOperator db ts) operator + smpSrvs' <- catMaybes <$> mapM (upsertOrDelete SPSMP) smpServers + xftpSrvs' <- catMaybes <$> mapM (upsertOrDelete SPXFTP) xftpServers + pure UserOperatorServers {operator, smpServers = smpSrvs', xftpServers = xftpSrvs'} where - upsertOrDelete :: ProtocolTypeI p => SProtocolType p -> UTCTime -> AUserServer p -> IO () - upsertOrDelete p ts (AUS _ s@UserServer {serverId, deleted}) = case serverId of + upsertOrDelete :: ProtocolTypeI p => SProtocolType p -> AUserServer p -> IO (Maybe (UserServer p)) + upsertOrDelete p (AUS _ s@UserServer {serverId, deleted}) = case serverId of DBNewEntity - | deleted -> pure () - | otherwise -> void $ insertProtocolServer db p user ts s + | deleted -> pure Nothing + | otherwise -> Just <$> insertProtocolServer db p user ts s DBEntityId srvId - | deleted -> DB.execute db "DELETE FROM protocol_servers WHERE user_id = ? AND smp_server_id = ? AND preset = ?" (userId, srvId, False) - | otherwise -> updateProtocolServer db p ts s + | deleted -> Nothing <$ DB.execute db "DELETE FROM protocol_servers WHERE user_id = ? AND smp_server_id = ? AND preset = ?" (userId, srvId, False) + | otherwise -> Just s <$ updateProtocolServer db p ts s createCall :: DB.Connection -> User -> Call -> UTCTime -> IO () createCall db user@User {userId} Call {contactId, callId, callUUID, chatItemId, callState} callTs = do diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index c2cc44d164..7bf7804472 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -25,10 +25,9 @@ import Data.Maybe (isNothing) import qualified Data.Text as T import Network.Socket import Simplex.Chat -import Simplex.Chat.Controller (ChatCommand (..), ChatConfig (..), ChatController (..), ChatDatabase (..), ChatLogLevel (..), PresetServers (..), defaultSimpleNetCfg) +import Simplex.Chat.Controller (ChatCommand (..), ChatConfig (..), ChatController (..), ChatDatabase (..), ChatLogLevel (..), defaultSimpleNetCfg) import Simplex.Chat.Core import Simplex.Chat.Options -import Simplex.Chat.Operators (PresetOperator (..), presetServer) import Simplex.Chat.Protocol (currentChatVersion, pqEncryptionCompressionVersion) import Simplex.Chat.Store import Simplex.Chat.Store.Profiles @@ -95,8 +94,8 @@ testCoreOpts = { dbFilePrefix = "./simplex_v1", dbKey = "", -- dbKey = "this is a pass-phrase to encrypt the database", - smpServers = [], - xftpServers = [], + smpServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001"], + xftpServers = ["xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002"], simpleNetCfg = defaultSimpleNetCfg, logLevel = CLLImportant, logConnections = False, @@ -150,18 +149,6 @@ testCfg :: ChatConfig testCfg = defaultChatConfig { agentConfig = testAgentCfg, - presetServers = - (presetServers defaultChatConfig) - { operators = - [ PresetOperator - { operator = Nothing, - smp = map (presetServer True) ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001"], - useSMP = 1, - xftp = map (presetServer True) ["xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002"], - useXFTP = 1 - } - ] - }, showReceipts = False, testView = True, tbqSize = 16 diff --git a/tests/ChatTests/Direct.hs b/tests/ChatTests/Direct.hs index bd2a267c3a..6bbf72171e 100644 --- a/tests/ChatTests/Direct.hs +++ b/tests/ChatTests/Direct.hs @@ -240,6 +240,7 @@ testRetryConnecting tmp = testChatCfgOpts2 cfg' opts' aliceProfile bobProfile te bob <##. "smp agent error: BROKER" withSmpServer' serverCfg' $ do alice <## "server connected localhost ()" + threadDelay 250000 bob ##> ("/_connect plan 1 " <> inv) bob <## "invitation link: ok to connect" bob ##> ("/_connect 1 " <> inv) @@ -1144,27 +1145,24 @@ testGetSetSMPServers = alice ##> "/_servers 1" alice <## "Your servers" alice <## " SMP servers" - alice <## " smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001 (preset)" + alice <## " smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001" alice <## " XFTP servers" - alice <## " xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002 (preset)" + alice <## " xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002" alice #$> ("/smp smp://1234-w==@smp1.example.im", id, "ok") alice ##> "/smp" alice <## "Your servers" alice <## " SMP servers" - alice <## " smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001 (preset, disabled)" alice <## " smp://1234-w==@smp1.example.im" alice #$> ("/smp smp://1234-w==:password@smp1.example.im", id, "ok") -- alice #$> ("/smp", id, "smp://1234-w==:password@smp1.example.im") alice ##> "/smp" alice <## "Your servers" alice <## " SMP servers" - alice <## " smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001 (preset, disabled)" alice <## " smp://1234-w==:password@smp1.example.im" alice #$> ("/smp smp://2345-w==@smp2.example.im smp://3456-w==@smp3.example.im:5224", id, "ok") alice ##> "/smp" alice <## "Your servers" alice <## " SMP servers" - alice <## " smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001 (preset, disabled)" alice <## " smp://2345-w==@smp2.example.im" alice <## " smp://3456-w==@smp3.example.im:5224" @@ -1190,26 +1188,23 @@ testGetSetXFTPServers = alice ##> "/_servers 1" alice <## "Your servers" alice <## " SMP servers" - alice <## " smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001 (preset)" + alice <## " smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001" alice <## " XFTP servers" - alice <## " xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002 (preset)" + alice <## " xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002" alice #$> ("/xftp xftp://1234-w==@xftp1.example.im", id, "ok") alice ##> "/xftp" alice <## "Your servers" alice <## " XFTP servers" - alice <## " xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002 (preset, disabled)" alice <## " xftp://1234-w==@xftp1.example.im" alice #$> ("/xftp xftp://1234-w==:password@xftp1.example.im", id, "ok") alice ##> "/xftp" alice <## "Your servers" alice <## " XFTP servers" - alice <## " xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002 (preset, disabled)" alice <## " xftp://1234-w==:password@xftp1.example.im" alice #$> ("/xftp xftp://2345-w==@xftp2.example.im xftp://3456-w==@xftp3.example.im:5224", id, "ok") alice ##> "/xftp" alice <## "Your servers" alice <## " XFTP servers" - alice <## " xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002 (preset, disabled)" alice <## " xftp://2345-w==@xftp2.example.im" alice <## " xftp://3456-w==@xftp3.example.im:5224" @@ -1831,13 +1826,11 @@ testCreateUserSameServers = alice ##> "/smp" alice <## "Your servers" alice <## " SMP servers" - alice <## " smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001 (preset, disabled)" alice <## " smp://2345-w==@smp2.example.im" alice <## " smp://3456-w==@smp3.example.im:5224" alice ##> "/xftp" alice <## "Your servers" alice <## " XFTP servers" - alice <## " xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002 (preset, disabled)" alice <## " xftp://2345-w==@xftp2.example.im" alice <## " xftp://3456-w==@xftp3.example.im:5224" diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index a51f42114b..bdd3b53829 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -1988,7 +1988,6 @@ testGroupAsync tmp = do (bob <## "#team: you joined the group") alice #> "#team hello bob" bob <# "#team alice> hello bob" - print (1 :: Integer) withTestChat tmp "alice" $ \alice -> do withNewTestChat tmp "cath" cathProfile $ \cath -> do alice <## "1 contacts connected (use /cs for the list)" @@ -2008,7 +2007,6 @@ testGroupAsync tmp = do ] alice #> "#team hello cath" cath <# "#team alice> hello cath" - print (2 :: Integer) withTestChat tmp "bob" $ \bob -> do withTestChat tmp "cath" $ \cath -> do concurrentlyN_ @@ -2024,7 +2022,6 @@ testGroupAsync tmp = do cath <## "#team: member bob (Bob) is connected" ] threadDelay 500000 - print (3 :: Integer) withTestChat tmp "bob" $ \bob -> do withNewTestChat tmp "dan" danProfile $ \dan -> do bob <## "2 contacts connected (use /cs for the list)" @@ -2044,7 +2041,6 @@ testGroupAsync tmp = do ] threadDelay 1000000 threadDelay 1000000 - print (4 :: Integer) withTestChat tmp "alice" $ \alice -> do withTestChat tmp "cath" $ \cath -> do withTestChat tmp "dan" $ \dan -> do @@ -2066,7 +2062,6 @@ testGroupAsync tmp = do dan <## "#team: member cath (Catherine) is connected" ] threadDelay 1000000 - print (5 :: Integer) withTestChat tmp "alice" $ \alice -> do withTestChat tmp "bob" $ \bob -> do withTestChat tmp "cath" $ \cath -> do diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index 1d390e1236..3ff8808541 100644 --- a/tests/ChatTests/Profiles.hs +++ b/tests/ChatTests/Profiles.hs @@ -273,6 +273,7 @@ testRetryAcceptingViaContactLink tmp = testChatCfgOpts2 cfg' opts' aliceProfile bob <##. "smp agent error: BROKER" withSmpServer' serverCfg' $ do alice <## "server connected localhost ()" + threadDelay 250000 bob ##> ("/_connect plan 1 " <> cLink) bob <## "contact address: ok to connect" bob ##> ("/_connect 1 " <> cLink) @@ -1737,12 +1738,11 @@ testChangePCCUserDiffSrv tmp = do alice ##> "/smp" alice <## "Your servers" alice <## " SMP servers" - alice <## " smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001 (preset)" + alice <## " smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001" alice #$> ("/smp smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@127.0.0.1:7003", id, "ok") alice ##> "/smp" alice <## "Your servers" alice <## " SMP servers" - alice <## " smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001 (preset, disabled)" alice <## " smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@127.0.0.1:7003" alice ##> "/user alice" showActiveUser alice "alice (Alice)" diff --git a/tests/OperatorTests.hs b/tests/OperatorTests.hs index 4966bfbb97..03cea56133 100644 --- a/tests/OperatorTests.hs +++ b/tests/OperatorTests.hs @@ -1,6 +1,12 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedLists #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TypeApplications #-} {-# OPTIONS_GHC -Wno-orphans #-} @@ -8,8 +14,10 @@ module OperatorTests (operatorTests) where +import Data.Bifunctor (second) import qualified Data.List.NonEmpty as L import Simplex.Chat +import Simplex.Chat.Controller (ChatConfig (..), PresetServers (..)) import Simplex.Chat.Operators import Simplex.Chat.Types import Simplex.FileTransfer.Client.Presets (defaultXFTPServers) @@ -19,10 +27,11 @@ import Test.Hspec operatorTests :: Spec operatorTests = describe "managing server operators" $ do - validateServers + validateServersTest + updatedServersTest -validateServers :: Spec -validateServers = describe "validate user servers" $ do +validateServersTest :: Spec +validateServersTest = describe "validate user servers" $ do it "should pass valid user servers" $ validateUserServers [valid] [] `shouldBe` [] it "should fail without servers" $ do validateUserServers [invalidNoServers] [] `shouldBe` [USENoServers aSMP Nothing] @@ -41,6 +50,50 @@ validateServers = describe "validate user servers" $ do aSMP = AProtocolType SPSMP aXFTP = AProtocolType SPXFTP +updatedServersTest :: Spec +updatedServersTest = describe "validate user servers" $ do + it "adding preset operators on first start" $ do + let ops' :: [(Maybe PresetOperator, Maybe AServerOperator)] = + updatedServerOperators operators [] + length ops' `shouldBe` 2 + all addedPreset ops' `shouldBe` True + let ops'' :: [(Maybe PresetOperator, Maybe ServerOperator)] = + saveOps ops' -- mock getUpdateServerOperators + uss <- groupByOperator' (ops'', [], []) -- no stored servers + length uss `shouldBe` 3 + [op1, op2, op3] <- pure $ map updatedUserServers uss + [p1, p2] <- pure operators -- presets + sameServers p1 op1 + sameServers p2 op2 + null (servers' SPSMP op3) `shouldBe` True + null (servers' SPXFTP op3) `shouldBe` True + it "adding preset operators and assiging servers to operator for existing users" $ do + let ops' = updatedServerOperators operators [] + ops'' = saveOps ops' + uss <- + groupByOperator' + ( ops'', + saveSrvs $ take 3 simplexChatSMPServers <> [newUserServer "smp://abcd@smp.example.im"], + saveSrvs $ map (presetServer True) $ L.take 3 defaultXFTPServers + ) + [op1, op2, op3] <- pure $ map updatedUserServers uss + [p1, p2] <- pure operators -- presets + sameServers p1 op1 + sameServers p2 op2 + map srvHost' (servers' SPSMP op3) `shouldBe` [["smp.example.im"]] + null (servers' SPXFTP op3) `shouldBe` True + where + addedPreset = \case + (Just PresetOperator {operator = Just op}, Just (ASO SDBNew op')) -> operatorTag op == operatorTag op' + _ -> False + saveOps = zipWith (\i -> second ((\(ASO _ op) -> op {operatorId = DBEntityId i}) <$>)) [1..] + saveSrvs = zipWith (\i srv -> srv {serverId = DBEntityId i}) [1..] + sameServers preset op = do + map srvHost (pServers SPSMP preset) `shouldBe` map srvHost' (servers' SPSMP op) + map srvHost (pServers SPXFTP preset) `shouldBe` map srvHost' (servers' SPXFTP op) + srvHost' (AUS _ s) = srvHost s + PresetServers {operators} = presetServers defaultChatConfig + deriving instance Eq User deriving instance Eq UserServersError diff --git a/tests/RandomServers.hs b/tests/RandomServers.hs index 048a2b5e5a..d0d74724d0 100644 --- a/tests/RandomServers.hs +++ b/tests/RandomServers.hs @@ -14,9 +14,8 @@ import Control.Monad (replicateM) import Data.Foldable (foldMap') import Data.List (sortOn) import Data.List.NonEmpty (NonEmpty) -import qualified Data.List.NonEmpty as L import Data.Monoid (Sum (..)) -import Simplex.Chat (defaultChatConfig, randomPresetServers) +import Simplex.Chat (defaultChatConfig, chooseRandomServers) import Simplex.Chat.Controller (ChatConfig (..), PresetServers (..)) import Simplex.Chat.Operators import Simplex.Messaging.Agent.Env.SQLite (ServerRoles (..)) @@ -38,22 +37,25 @@ testRandomSMPServers :: IO () testRandomSMPServers = do [srvs1, srvs2, srvs3] <- replicateM 3 $ - checkEnabled SPSMP 7 False =<< randomPresetServers SPSMP (presetServers defaultChatConfig) + checkEnabled SPSMP 7 False =<< chooseRandomServers (presetServers defaultChatConfig) (srvs1 == srvs2 && srvs2 == srvs3) `shouldBe` False -- && to avoid rare failures testRandomXFTPServers :: IO () testRandomXFTPServers = do [srvs1, srvs2, srvs3] <- replicateM 3 $ - checkEnabled SPXFTP 6 False =<< randomPresetServers SPXFTP (presetServers defaultChatConfig) + checkEnabled SPXFTP 6 False =<< chooseRandomServers (presetServers defaultChatConfig) (srvs1 == srvs2 && srvs2 == srvs3) `shouldBe` False -- && to avoid rare failures -checkEnabled :: UserProtocol p => SProtocolType p -> Int -> Bool -> NonEmpty (NewUserServer p) -> IO [NewUserServer p] -checkEnabled p n allUsed srvs = do - let srvs' = sortOn server' $ L.toList srvs - PresetServers {operators = presetOps} = presetServers defaultChatConfig - presetSrvs = sortOn server' $ concatMap (operatorServers p) presetOps +checkEnabled :: UserProtocol p => SProtocolType p -> Int -> Bool -> NonEmpty (PresetOperator) -> IO [NewUserServer p] +checkEnabled p n allUsed presetOps' = do + let PresetServers {operators = presetOps} = presetServers defaultChatConfig + presetSrvs = sortOn server' $ concatMap (pServers p) presetOps + srvs' = sortOn server' $ concatMap (pServers p) presetOps' Sum toUse = foldMap' (Sum . operatorServersToUse p) presetOps + Sum toUse' = foldMap' (Sum . operatorServersToUse p) presetOps' + length presetOps `shouldBe` length presetOps' + toUse `shouldBe` toUse' srvs' == presetSrvs `shouldBe` allUsed map enable srvs' `shouldBe` map enable presetSrvs let enbldSrvs = filter (\UserServer {enabled} -> enabled) srvs' From fcae5e992582780dcf053b1353d2c615f5e15a1d Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Tue, 19 Nov 2024 00:22:35 +0400 Subject: [PATCH 031/167] core: fix validation of operator servers for non current users (#5205) * core: fix validation of operator servers for non current users * style * refactor --------- Co-authored-by: Evgeny Poberezkin --- src/Simplex/Chat.hs | 13 +++++++++++-- src/Simplex/Chat/Operators.hs | 2 ++ tests/RandomServers.hs | 2 -- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 819832a1ed..11cd8e33ad 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -1611,7 +1611,7 @@ processChatCommand' vr = \case srvs' <- mapM aUserServer srvs processChatCommand $ APISetUserServers userId $ L.map (updatedServers p srvs') userServers where - aUserServer :: AProtoServerWithAuth -> CM (AUserServer p) + aUserServer :: AProtoServerWithAuth -> CM (AUserServer p) aUserServer (AProtoServerWithAuth p' srv) = case testEquality p p' of Just Refl -> pure $ AUS SDBNew $ newUserServer srv Nothing -> throwChatError $ CECommandError $ "incorrect server protocol: " <> B.unpack (strEncode srv) @@ -2949,8 +2949,17 @@ processChatCommand' vr = \case validateAllUsersServers :: UserServersClass u => Int64 -> [u] -> CM [UserServersError] validateAllUsersServers currUserId userServers = withFastStore $ \db -> do users' <- filter (\User {userId} -> userId /= currUserId) <$> liftIO (getUsers db) - others <- mapM (\user -> liftIO . fmap (user,) . groupByOperator =<< getUserServers db user) users' + others <- mapM (getUserOperatorServers db) users' pure $ validateUserServers userServers others + where + getUserOperatorServers :: DB.Connection -> User -> ExceptT StoreError IO (User, [UserOperatorServers]) + getUserOperatorServers db user = do + uss <- liftIO . groupByOperator =<< getUserServers db user + pure (user, map updatedUserServers uss) + updatedUserServers uss = uss {operator = updatedOp <$> operator' uss} :: UserOperatorServers + updatedOp op = fromMaybe op $ find matchingOp $ mapMaybe operator' userServers + where + matchingOp op' = operatorId op' == operatorId op forwardFile :: ChatName -> FileTransferId -> (ChatName -> CryptoFile -> ChatCommand) -> CM ChatResponse forwardFile chatName fileId sendCommand = withUser $ \user -> do withStore (\db -> getFileTransfer db user fileId) >>= \case diff --git a/src/Simplex/Chat/Operators.hs b/src/Simplex/Chat/Operators.hs index f7a07682f9..1f9b79b56b 100644 --- a/src/Simplex/Chat/Operators.hs +++ b/src/Simplex/Chat/Operators.hs @@ -89,6 +89,8 @@ data DBEntityId' (s :: DBStored) where deriving instance Show (DBEntityId' s) +deriving instance Eq (DBEntityId' s) + type DBEntityId = DBEntityId' 'DBStored type DBNewEntity = DBEntityId' 'DBNew diff --git a/tests/RandomServers.hs b/tests/RandomServers.hs index d0d74724d0..9b83be26c4 100644 --- a/tests/RandomServers.hs +++ b/tests/RandomServers.hs @@ -29,8 +29,6 @@ randomServersTests = describe "choosig random servers" $ do deriving instance Eq ServerRoles -deriving instance Eq (DBEntityId' s) - deriving instance Eq (UserServer' s p) testRandomSMPServers :: IO () From 70a29512b76fd9cd6f04c790d4ae13f348e68eda Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Tue, 19 Nov 2024 15:37:00 +0400 Subject: [PATCH 032/167] ios: server operators ui (#5114) * wip * refactor, fix bindings * wip * wip * fixes * wip * information map, logos * global conditions hack * restructure * restructure * texts * text * restructure * wip * restructure * rename * wip * conditions for all * comment * onboarding wip * onboarding wip * fix paddings * fix paddings * wip * fix padding * onboarding wip * nav link instead of sheet * pretty button * large titles * notifications mode button style * reenable demo operator * Revert "reenable demo operator" This reverts commit 42111eb333bd5482100567c2f9855756d364caf3. * padding * reenable demo operator * refactor (removes additional model api) * style * bold * bold * light/dark * fix button * comment * wip * remove preset * new types * api types * apis * smp and xftp servers in single view * test operator servers, refactor * save in main view * better progress * better in progress * remove shadow * update * apis * conditions view wip * load text * remove custom servers button from onboarding, open already conditions in nav link * allow to continue with simplex on onboarding * footer * existing users notice * fix to not show nothing on no action * disable notice * review later * disable notice * wip * wip * wip * wip * optional tag * fix * fix tags * fix * wip * remove coding keys * fix onboarding * rename * rework model wip * wip * wip * wip * fix * wip * wip * delete * simplify * wip * fix delete * ios: server operators ui wip * refactor * edited * save servers on dismiss/back * ios: add address card and remove address from onboarding (#5181) * ios: add address card and remove address from onboarding * allow for address creation in info when open via card * conditions interactions wip * conditions interactions wip * fix * wip * wip * wip * wip * rename * wip * fix * remove operator binding * fix set enabled * rename * cleanup * text * fix info view dark mode * update lib * ios: operators & servers validation * fix * ios: align onboarding style * ios: align onboarding style * ios: operators info (#5207) * ios: operators info * update * update texts * texts --------- Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> --------- Co-authored-by: Diogo Co-authored-by: Evgeny Poberezkin --- .../flux_logo-light.imageset/Contents.json | 21 + .../Flux_logo_blue_white.png | Bin 0 -> 33847 bytes .../flux_logo.imageset/Contents.json | 21 + .../flux_logo.imageset/Flux_logo_blue.png | Bin 0 -> 34876 bytes .../flux_logo_symbol.imageset/Contents.json | 21 + .../Flux_symbol_blue-white.png | Bin 0 -> 17248 bytes apps/ios/Shared/ContentView.swift | 29 +- apps/ios/Shared/Model/ChatModel.swift | 2 + apps/ios/Shared/Model/SimpleXAPI.swift | 74 ++- .../Shared/Views/ChatList/ChatListView.swift | 27 +- .../Views/ChatList/ServersSummaryView.swift | 51 +- .../Onboarding/AddressCreationCard.swift | 116 ++++ .../Onboarding/ChooseServerOperators.swift | 344 +++++++++++ .../Views/Onboarding/CreateProfile.swift | 86 ++- .../Shared/Views/Onboarding/HowItWorks.swift | 21 +- .../Views/Onboarding/OnboardingView.swift | 6 +- .../Onboarding/SetNotificationsMode.swift | 63 +- .../Shared/Views/Onboarding/SimpleXInfo.swift | 174 +++--- .../Views/Onboarding/WhatsNewView.swift | 433 +++++++------ .../NetworkAndServers/NetworkAndServers.swift | 363 ++++++++++- .../NetworkAndServers/NewServerView.swift | 156 +++++ .../NetworkAndServers/OperatorView.swift | 569 ++++++++++++++++++ .../ProtocolServerView.swift | 57 +- .../ProtocolServersView.swift | 472 +++++++-------- .../ScanProtocolServer.swift | 24 +- .../Views/UserSettings/SettingsView.swift | 27 +- .../UserSettings/UserAddressLearnMore.swift | 46 +- .../Views/UserSettings/UserAddressView.swift | 42 +- apps/ios/SimpleX.xcodeproj/project.pbxproj | 66 +- apps/ios/SimpleXChat/APITypes.swift | 443 ++++++++++++-- 30 files changed, 3014 insertions(+), 740 deletions(-) create mode 100644 apps/ios/Shared/Assets.xcassets/flux_logo-light.imageset/Contents.json create mode 100644 apps/ios/Shared/Assets.xcassets/flux_logo-light.imageset/Flux_logo_blue_white.png create mode 100644 apps/ios/Shared/Assets.xcassets/flux_logo.imageset/Contents.json create mode 100644 apps/ios/Shared/Assets.xcassets/flux_logo.imageset/Flux_logo_blue.png create mode 100644 apps/ios/Shared/Assets.xcassets/flux_logo_symbol.imageset/Contents.json create mode 100644 apps/ios/Shared/Assets.xcassets/flux_logo_symbol.imageset/Flux_symbol_blue-white.png create mode 100644 apps/ios/Shared/Views/Onboarding/AddressCreationCard.swift create mode 100644 apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift create mode 100644 apps/ios/Shared/Views/UserSettings/NetworkAndServers/NewServerView.swift create mode 100644 apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift diff --git a/apps/ios/Shared/Assets.xcassets/flux_logo-light.imageset/Contents.json b/apps/ios/Shared/Assets.xcassets/flux_logo-light.imageset/Contents.json new file mode 100644 index 0000000000..d3a15f9a33 --- /dev/null +++ b/apps/ios/Shared/Assets.xcassets/flux_logo-light.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Flux_logo_blue_white.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/ios/Shared/Assets.xcassets/flux_logo-light.imageset/Flux_logo_blue_white.png b/apps/ios/Shared/Assets.xcassets/flux_logo-light.imageset/Flux_logo_blue_white.png new file mode 100644 index 0000000000000000000000000000000000000000..e1d6dda4fedd57195f0ee425a0a28a579f16b266 GIT binary patch literal 33847 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�>Y9E5G~yEbTaSm_@QckJ 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

PL9g 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 F{C_Q?zn%a9 literal 0 HcmV?d00001 diff --git a/apps/ios/Shared/Assets.xcassets/flux_logo_symbol.imageset/Contents.json b/apps/ios/Shared/Assets.xcassets/flux_logo_symbol.imageset/Contents.json new file mode 100644 index 0000000000..16686bdf80 --- /dev/null +++ b/apps/ios/Shared/Assets.xcassets/flux_logo_symbol.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Flux_symbol_blue-white.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/ios/Shared/Assets.xcassets/flux_logo_symbol.imageset/Flux_symbol_blue-white.png b/apps/ios/Shared/Assets.xcassets/flux_logo_symbol.imageset/Flux_symbol_blue-white.png new file mode 100644 index 0000000000000000000000000000000000000000..0793b0ee85dfff83a4e180ebc0cc70230a452c28 GIT binary patch literal 17248 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 E0Eh^ Void> = [:] diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 9297aa7898..c61ad412c0 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -500,18 +500,6 @@ func apiDeleteToken(token: DeviceToken) async throws { try await sendCommandOkResp(.apiDeleteToken(token: token)) } -func getUserProtoServers(_ serverProtocol: ServerProtocol) throws -> UserProtoServers { - let userId = try currentUserId("getUserProtoServers") - let r = chatSendCmdSync(.apiGetUserProtoServers(userId: userId, serverProtocol: serverProtocol)) - if case let .userProtoServers(_, servers) = r { return servers } - throw r -} - -func setUserProtoServers(_ serverProtocol: ServerProtocol, servers: [ServerCfg]) async throws { - let userId = try currentUserId("setUserProtoServers") - try await sendCommandOkResp(.apiSetUserProtoServers(userId: userId, serverProtocol: serverProtocol, servers: servers)) -} - func testProtoServer(server: String) async throws -> Result<(), ProtocolTestFailure> { let userId = try currentUserId("testProtoServer") let r = await chatSendCmd(.apiTestProtoServer(userId: userId, server: server)) @@ -524,6 +512,65 @@ func testProtoServer(server: String) async throws -> Result<(), ProtocolTestFail throw r } +func getServerOperators() throws -> ServerOperatorConditions { + let r = chatSendCmdSync(.apiGetServerOperators) + if case let .serverOperatorConditions(conditions) = r { return conditions } + logger.error("getServerOperators error: \(String(describing: r))") + throw r +} + +func setServerOperators(operators: [ServerOperator]) async throws -> ServerOperatorConditions { + let r = await chatSendCmd(.apiSetServerOperators(operators: operators)) + if case let .serverOperatorConditions(conditions) = r { return conditions } + logger.error("setServerOperators error: \(String(describing: r))") + throw r +} + +func getUserServers() async throws -> [UserOperatorServers] { + let userId = try currentUserId("getUserServers") + let r = await chatSendCmd(.apiGetUserServers(userId: userId)) + if case let .userServers(_, userServers) = r { return userServers } + logger.error("getUserServers error: \(String(describing: r))") + throw r +} + +func setUserServers(userServers: [UserOperatorServers]) async throws { + let userId = try currentUserId("setUserServers") + let r = await chatSendCmd(.apiSetUserServers(userId: userId, userServers: userServers)) + if case .cmdOk = r { return } + logger.error("setUserServers error: \(String(describing: r))") + throw r +} + +func validateServers(userServers: [UserOperatorServers]) async throws -> [UserServersError] { + let userId = try currentUserId("validateServers") + let r = await chatSendCmd(.apiValidateServers(userId: userId, userServers: userServers)) + if case let .userServersValidation(_, serverErrors) = r { return serverErrors } + logger.error("validateServers error: \(String(describing: r))") + throw r +} + +func getUsageConditions() async throws -> (UsageConditions, String?, UsageConditions?) { + let r = await chatSendCmd(.apiGetUsageConditions) + if case let .usageConditions(usageConditions, conditionsText, acceptedConditions) = r { return (usageConditions, conditionsText, acceptedConditions) } + logger.error("getUsageConditions error: \(String(describing: r))") + throw r +} + +func setConditionsNotified(conditionsId: Int64) async throws { + let r = await chatSendCmd(.apiSetConditionsNotified(conditionsId: conditionsId)) + if case .cmdOk = r { return } + logger.error("setConditionsNotified error: \(String(describing: r))") + throw r +} + +func acceptConditions(conditionsId: Int64, operatorIds: [Int64]) async throws -> ServerOperatorConditions { + let r = await chatSendCmd(.apiAcceptConditions(conditionsId: conditionsId, operatorIds: operatorIds)) + if case let .serverOperatorConditions(conditions) = r { return conditions } + logger.error("acceptConditions error: \(String(describing: r))") + throw r +} + func getChatItemTTL() throws -> ChatItemTTL { let userId = try currentUserId("getChatItemTTL") return try chatItemTTLResponse(chatSendCmdSync(.apiGetChatItemTTL(userId: userId))) @@ -1558,6 +1605,7 @@ func initializeChat(start: Bool, confirmStart: Bool = false, dbKey: String? = ni try apiSetEncryptLocalFiles(privacyEncryptLocalFilesGroupDefault.get()) m.chatInitialized = true m.currentUser = try apiGetActiveUser() + m.conditions = try getServerOperators() if m.currentUser == nil { onboardingStageDefault.set(.step1_SimpleXInfo) privacyDeliveryReceiptsSet.set(true) @@ -1624,7 +1672,7 @@ func startChat(refreshInvitations: Bool = true) throws { withAnimation { let savedOnboardingStage = onboardingStageDefault.get() m.onboardingStage = [.step1_SimpleXInfo, .step2_CreateProfile].contains(savedOnboardingStage) && m.users.count == 1 - ? .step3_CreateSimpleXAddress + ? .step3_ChooseServerOperators : savedOnboardingStage if m.onboardingStage == .onboardingComplete && !privacyDeliveryReceiptsSet.get() { m.setDeliveryReceipts = true diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift index 7b24995f62..8e7aec581b 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift @@ -36,6 +36,10 @@ struct UserPickerSheetView: View { @EnvironmentObject var chatModel: ChatModel @State private var loaded = false + @State private var currUserServers: [UserOperatorServers] = [] + @State private var userServers: [UserOperatorServers] = [] + @State private var serverErrors: [UserServersError] = [] + var body: some View { NavigationView { ZStack { @@ -56,7 +60,11 @@ struct UserPickerSheetView: View { case .useFromDesktop: ConnectDesktopView() case .settings: - SettingsView() + SettingsView( + currUserServers: $currUserServers, + userServers: $userServers, + serverErrors: $serverErrors + ) } } Color.clear // Required for list background to be rendered during loading @@ -76,6 +84,16 @@ struct UserPickerSheetView: View { { loaded = true } ) } + .onDisappear { + if serversCanBeSaved(currUserServers, userServers, serverErrors) { + showAlert( + title: NSLocalizedString("Save servers?", comment: "alert title"), + buttonTitle: NSLocalizedString("Save", comment: "alert button"), + buttonAction: { saveServers($currUserServers, $userServers) }, + cancelButton: true + ) + } + } } } @@ -94,6 +112,7 @@ struct ChatListView: View { @AppStorage(DEFAULT_SHOW_UNREAD_AND_FAVORITES) private var showUnreadAndFavorites = false @AppStorage(GROUP_DEFAULT_ONE_HAND_UI, store: groupDefaults) private var oneHandUI = true @AppStorage(DEFAULT_ONE_HAND_UI_CARD_SHOWN) private var oneHandUICardShown = false + @AppStorage(DEFAULT_ADDRESS_CREATION_CARD_SHOWN) private var addressCreationCardShown = false @AppStorage(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial var body: some View { @@ -282,6 +301,12 @@ struct ChatListView: View { .listRowSeparator(.hidden) .listRowBackground(Color.clear) } + if !addressCreationCardShown { + AddressCreationCard() + .scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center) + .listRowSeparator(.hidden) + .listRowBackground(Color.clear) + } if #available(iOS 16.0, *) { ForEach(cs, id: \.viewId) { chat in ChatListNavLink(chat: chat) diff --git a/apps/ios/Shared/Views/ChatList/ServersSummaryView.swift b/apps/ios/Shared/Views/ChatList/ServersSummaryView.swift index 22ea78f27b..a13a159a45 100644 --- a/apps/ios/Shared/Views/ChatList/ServersSummaryView.swift +++ b/apps/ios/Shared/Views/ChatList/ServersSummaryView.swift @@ -20,6 +20,10 @@ struct ServersSummaryView: View { @State private var timer: Timer? = nil @State private var alert: SomeAlert? + @State private var currUserServers: [UserOperatorServers] = [] + @State private var userServers: [UserOperatorServers] = [] + @State private var serverErrors: [UserServersError] = [] + @AppStorage(DEFAULT_SHOW_SUBSCRIPTION_PERCENTAGE) private var showSubscriptionPercentage = false enum PresentedUserCategory { @@ -53,6 +57,15 @@ struct ServersSummaryView: View { } .onDisappear { stopTimer() + + if serversCanBeSaved(currUserServers, userServers, serverErrors) { + showAlert( + title: NSLocalizedString("Save servers?", comment: "alert title"), + buttonTitle: NSLocalizedString("Save", comment: "alert button"), + buttonAction: { saveServers($currUserServers, $userServers) }, + cancelButton: true + ) + } } .alert(item: $alert) { $0.alert } } @@ -275,7 +288,10 @@ struct ServersSummaryView: View { NavigationLink(tag: srvSumm.id, selection: $selectedSMPServer) { SMPServerSummaryView( summary: srvSumm, - statsStartedAt: statsStartedAt + statsStartedAt: statsStartedAt, + currUserServers: $currUserServers, + userServers: $userServers, + serverErrors: $serverErrors ) .navigationBarTitle("SMP server") .navigationBarTitleDisplayMode(.large) @@ -344,7 +360,10 @@ struct ServersSummaryView: View { NavigationLink(tag: srvSumm.id, selection: $selectedXFTPServer) { XFTPServerSummaryView( summary: srvSumm, - statsStartedAt: statsStartedAt + statsStartedAt: statsStartedAt, + currUserServers: $currUserServers, + userServers: $userServers, + serverErrors: $serverErrors ) .navigationBarTitle("XFTP server") .navigationBarTitleDisplayMode(.large) @@ -486,6 +505,10 @@ struct SMPServerSummaryView: View { @AppStorage(DEFAULT_SHOW_SUBSCRIPTION_PERCENTAGE) private var showSubscriptionPercentage = false + @Binding var currUserServers: [UserOperatorServers] + @Binding var userServers: [UserOperatorServers] + @Binding var serverErrors: [UserServersError] + var body: some View { List { Section("Server address") { @@ -493,9 +516,13 @@ struct SMPServerSummaryView: View { .textSelection(.enabled) if summary.known == true { NavigationLink { - ProtocolServersView(serverProtocol: .smp) - .navigationTitle("Your SMP servers") - .modifier(ThemedBackground(grouped: true)) + NetworkAndServers( + currUserServers: $currUserServers, + userServers: $userServers, + serverErrors: $serverErrors + ) + .navigationTitle("Network & servers") + .modifier(ThemedBackground(grouped: true)) } label: { Text("Open server settings") } @@ -674,6 +701,10 @@ struct XFTPServerSummaryView: View { var summary: XFTPServerSummary var statsStartedAt: Date + @Binding var currUserServers: [UserOperatorServers] + @Binding var userServers: [UserOperatorServers] + @Binding var serverErrors: [UserServersError] + var body: some View { List { Section("Server address") { @@ -681,9 +712,13 @@ struct XFTPServerSummaryView: View { .textSelection(.enabled) if summary.known == true { NavigationLink { - ProtocolServersView(serverProtocol: .xftp) - .navigationTitle("Your XFTP servers") - .modifier(ThemedBackground(grouped: true)) + NetworkAndServers( + currUserServers: $currUserServers, + userServers: $userServers, + serverErrors: $serverErrors + ) + .navigationTitle("Network & servers") + .modifier(ThemedBackground(grouped: true)) } label: { Text("Open server settings") } diff --git a/apps/ios/Shared/Views/Onboarding/AddressCreationCard.swift b/apps/ios/Shared/Views/Onboarding/AddressCreationCard.swift new file mode 100644 index 0000000000..e9a8fedaf9 --- /dev/null +++ b/apps/ios/Shared/Views/Onboarding/AddressCreationCard.swift @@ -0,0 +1,116 @@ +// +// AddressCreationCard.swift +// SimpleX (iOS) +// +// Created by Diogo Cunha on 13/11/2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +struct AddressCreationCard: View { + @EnvironmentObject var theme: AppTheme + @EnvironmentObject private var chatModel: ChatModel + @Environment(\.dynamicTypeSize) private var userFont: DynamicTypeSize + @AppStorage(DEFAULT_ADDRESS_CREATION_CARD_SHOWN) private var addressCreationCardShown = false + @State private var showAddressCreationAlert = false + @State private var showAddressSheet = false + @State private var showAddressInfoSheet = false + + var body: some View { + let addressExists = chatModel.userAddress != nil + let chats = chatModel.chats.filter { chat in + !chat.chatInfo.chatDeleted && chatContactType(chat: chat) != ContactType.card + } + ZStack(alignment: .topTrailing) { + HStack(alignment: .top, spacing: 16) { + let envelopeSize = dynamicSize(userFont).profileImageSize + Image(systemName: "envelope.circle.fill") + .resizable() + .frame(width: envelopeSize, height: envelopeSize) + .foregroundColor(.accentColor) + VStack(alignment: .leading) { + Text("Your SimpleX address") + .font(.title3) + Spacer() + HStack(alignment: .center) { + Text("How to use it") + VStack { + Image(systemName: "info.circle") + .foregroundColor(theme.colors.secondary) + } + } + } + } + .frame(maxWidth: .infinity, alignment: .leading) + VStack(alignment: .trailing) { + Image(systemName: "multiply") + .foregroundColor(theme.colors.secondary) + .onTapGesture { + showAddressCreationAlert = true + } + Spacer() + Text("Create") + .foregroundColor(.accentColor) + .onTapGesture { + showAddressSheet = true + } + } + } + .onTapGesture { + showAddressInfoSheet = true + } + .padding() + .background(theme.appColors.sentMessage) + .cornerRadius(12) + .frame(height: dynamicSize(userFont).rowHeight) + .padding(.vertical, 12) + .alert(isPresented: $showAddressCreationAlert) { + Alert( + title: Text("SimpleX address"), + message: Text("You can create it in user picker."), + dismissButton: .default(Text("Ok")) { + withAnimation { + addressCreationCardShown = true + } + } + ) + } + .sheet(isPresented: $showAddressSheet) { + NavigationView { + UserAddressView(autoCreate: true) + .navigationTitle("SimpleX address") + .navigationBarTitleDisplayMode(.large) + .modifier(ThemedBackground(grouped: true)) + } + } + .sheet(isPresented: $showAddressInfoSheet) { + NavigationView { + UserAddressLearnMore(showCreateAddressButton: true) + .navigationTitle("SimpleX address") + .navigationBarTitleDisplayMode(.large) + .modifier(ThemedBackground(grouped: true)) + } + } + .onChange(of: addressExists) { exists in + if exists, !addressCreationCardShown { + addressCreationCardShown = true + } + } + .onChange(of: chats.count) { size in + if size >= 3, !addressCreationCardShown { + addressCreationCardShown = true + } + } + .onAppear { + if addressExists, !addressCreationCardShown { + addressCreationCardShown = true + } + } + } +} + +#Preview { + AddressCreationCard() +} diff --git a/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift b/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift new file mode 100644 index 0000000000..248c1b34c4 --- /dev/null +++ b/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift @@ -0,0 +1,344 @@ +// +// ChooseServerOperators.swift +// SimpleX (iOS) +// +// Created by spaced4ndy on 31.10.2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +struct OnboardingButtonStyle: ButtonStyle { + @EnvironmentObject var theme: AppTheme + var isDisabled: Bool = false + + func makeBody(configuration: Configuration) -> some View { + configuration.label + .font(.system(size: 17, weight: .semibold)) + .padding() + .frame(maxWidth: .infinity) + .background( + isDisabled + ? ( + theme.colors.isLight + ? .gray.opacity(0.17) + : .gray.opacity(0.27) + ) + : theme.colors.primary + ) + .foregroundColor( + isDisabled + ? ( + theme.colors.isLight + ? .gray.opacity(0.4) + : .white.opacity(0.2) + ) + : .white + ) + .cornerRadius(16) + .scaleEffect(configuration.isPressed ? 0.95 : 1.0) + } +} + +struct ChooseServerOperators: View { + @Environment(\.dismiss) var dismiss: DismissAction + @Environment(\.colorScheme) var colorScheme: ColorScheme + @EnvironmentObject var theme: AppTheme + var onboarding: Bool + @State private var showInfoSheet = false + @State private var serverOperators: [ServerOperator] = [] + @State private var selectedOperatorIds = Set() + @State private var reviewConditionsNavLinkActive = false + @State private var justOpened = true + + var selectedOperators: [ServerOperator] { serverOperators.filter { selectedOperatorIds.contains($0.operatorId) } } + + var body: some View { + NavigationView { + GeometryReader { g in + ScrollView { + VStack(alignment: .leading, spacing: 20) { + Text("Choose operators") + .font(.largeTitle) + .bold() + + infoText() + + Spacer() + + ForEach(serverOperators) { srvOperator in + operatorCheckView(srvOperator) + } + + Spacer() + + let reviewForOperators = selectedOperators.filter { !$0.conditionsAcceptance.conditionsAccepted } + let canReviewLater = reviewForOperators.allSatisfy { $0.conditionsAcceptance.usageAllowed } + + VStack(spacing: 8) { + if !reviewForOperators.isEmpty { + reviewConditionsButton() + } else { + continueButton() + } + if onboarding { + Text("You can disable operators and configure your servers in Network & servers settings.") + .multilineTextAlignment(.center) + .font(.footnote) + .padding(.horizontal, 32) + } + } + .padding(.bottom) + + if !onboarding && !reviewForOperators.isEmpty { + VStack(spacing: 8) { + reviewLaterButton() + ( + Text("Conditions will be accepted for enabled operators after 30 days.") + + Text(" ") + + Text("You can configure operators in Network & servers settings.") + ) + .multilineTextAlignment(.center) + .font(.footnote) + .padding(.horizontal, 32) + } + .disabled(!canReviewLater) + .padding(.bottom) + } + } + .frame(minHeight: g.size.height) + } + .onAppear { + if justOpened { + serverOperators = ChatModel.shared.conditions.serverOperators + selectedOperatorIds = Set(serverOperators.filter { $0.enabled }.map { $0.operatorId }) + justOpened = false + } + } + .sheet(isPresented: $showInfoSheet) { + ChooseServerOperatorsInfoView() + } + } + .frame(maxHeight: .infinity) + .padding() + } + } + + private func infoText() -> some View { + HStack(spacing: 12) { + Image(systemName: "info.circle") + .resizable() + .scaledToFit() + .frame(width: 20, height: 20) + .foregroundColor(theme.colors.primary) + .onTapGesture { + showInfoSheet = true + } + + Text("Select operators, whose servers you will be using.") + } + } + + @ViewBuilder private func operatorCheckView(_ serverOperator: ServerOperator) -> some View { + let checked = selectedOperatorIds.contains(serverOperator.operatorId) + let icon = checked ? "checkmark.circle.fill" : "circle" + let iconColor = checked ? theme.colors.primary : Color(uiColor: .tertiaryLabel).asAnotherColorFromSecondary(theme) + HStack(spacing: 10) { + Image(serverOperator.largeLogo(colorScheme)) + .resizable() + .scaledToFit() + .frame(height: 48) + Spacer() + Image(systemName: icon) + .resizable() + .scaledToFit() + .frame(width: 26, height: 26) + .foregroundColor(iconColor) + } + .background(Color(.systemBackground)) + .padding() + .clipShape(RoundedRectangle(cornerRadius: 18)) + .overlay( + RoundedRectangle(cornerRadius: 18) + .stroke(Color(uiColor: .secondarySystemFill), lineWidth: 2) + ) + .padding(.horizontal, 2) + .onTapGesture { + if checked { + selectedOperatorIds.remove(serverOperator.operatorId) + } else { + selectedOperatorIds.insert(serverOperator.operatorId) + } + } + } + + private func reviewConditionsButton() -> some View { + ZStack { + Button { + reviewConditionsNavLinkActive = true + } label: { + Text("Review conditions") + } + .buttonStyle(OnboardingButtonStyle(isDisabled: selectedOperatorIds.isEmpty)) + .disabled(selectedOperatorIds.isEmpty) + + NavigationLink(isActive: $reviewConditionsNavLinkActive) { + reviewConditionsDestinationView() + } label: { + EmptyView() + } + .frame(width: 1, height: 1) + .hidden() + } + } + + private func continueButton() -> some View { + Button { + continueToNextStep() + } label: { + Text("Continue") + } + .buttonStyle(OnboardingButtonStyle(isDisabled: selectedOperatorIds.isEmpty)) + .disabled(selectedOperatorIds.isEmpty) + } + + private func reviewLaterButton() -> some View { + Button { + continueToNextStep() + } label: { + Text("Review later") + } + .buttonStyle(.borderless) + } + + private func continueToNextStep() { + if onboarding { + withAnimation { + onboardingStageDefault.set(.step4_SetNotificationsMode) + ChatModel.shared.onboardingStage = .step4_SetNotificationsMode + } + } else { + dismiss() + } + } + + private func reviewConditionsDestinationView() -> some View { + reviewConditionsView() + .navigationTitle("Conditions of use") + .navigationBarTitleDisplayMode(.large) + .modifier(ThemedBackground(grouped: true)) + } + + @ViewBuilder private func reviewConditionsView() -> some View { + let operatorsWithConditionsAccepted = ChatModel.shared.conditions.serverOperators.filter { $0.conditionsAcceptance.conditionsAccepted } + let acceptForOperators = selectedOperators.filter { !$0.conditionsAcceptance.conditionsAccepted } + VStack(alignment: .leading, spacing: 20) { + if !operatorsWithConditionsAccepted.isEmpty { + Text("Conditions are already accepted for following operator(s): **\(operatorsWithConditionsAccepted.map { $0.legalName_ }.joined(separator: ", "))**.") + Text("Same conditions will apply to operator(s): **\(acceptForOperators.map { $0.legalName_ }.joined(separator: ", "))**.") + } else { + Text("Conditions will be accepted for operator(s): **\(acceptForOperators.map { $0.legalName_ }.joined(separator: ", "))**.") + } + ConditionsTextView() + acceptConditionsButton() + .padding(.bottom) + .padding(.bottom) + } + .padding(.horizontal) + .frame(maxHeight: .infinity) + } + + private func acceptConditionsButton() -> some View { + Button { + Task { + do { + let conditionsId = ChatModel.shared.conditions.currentConditions.conditionsId + let acceptForOperators = selectedOperators.filter { !$0.conditionsAcceptance.conditionsAccepted } + let operatorIds = acceptForOperators.map { $0.operatorId } + let r = try await acceptConditions(conditionsId: conditionsId, operatorIds: operatorIds) + await MainActor.run { + ChatModel.shared.conditions = r + } + if let enabledOperators = enabledOperators(r.serverOperators) { + let r2 = try await setServerOperators(operators: enabledOperators) + await MainActor.run { + ChatModel.shared.conditions = r2 + continueToNextStep() + } + } else { + await MainActor.run { + continueToNextStep() + } + } + } catch let error { + await MainActor.run { + showAlert( + NSLocalizedString("Error accepting conditions", comment: "alert title"), + message: responseError(error) + ) + } + } + } + } label: { + Text("Accept conditions") + } + .buttonStyle(OnboardingButtonStyle()) + } + + private func enabledOperators(_ operators: [ServerOperator]) -> [ServerOperator]? { + var ops = operators + if !ops.isEmpty { + for i in 0.. some View { - HStack { - Button { - hideKeyboard() - withAnimation { - m.onboardingStage = .step1_SimpleXInfo - } - } label: { - HStack { - Image(systemName: "lessthan") - Text("About SimpleX") - } - } - - Spacer() - - Button { - createProfile(displayName, showAlert: showAlert, dismiss: dismiss) - } label: { - HStack { - Text("Create") - Image(systemName: "greaterthan") - } - } - .disabled(!canCreateProfile(displayName)) + func createProfileButton() -> some View { + Button { + createProfile(displayName, showAlert: showAlert, dismiss: dismiss) + } label: { + Text("Create profile") } + .buttonStyle(OnboardingButtonStyle(isDisabled: !canCreateProfile(displayName))) + .disabled(!canCreateProfile(displayName)) } private func showAlert(_ alert: UserProfileAlert) { @@ -176,8 +162,8 @@ private func createProfile(_ displayName: String, showAlert: (UserProfileAlert) if m.users.isEmpty || m.users.allSatisfy({ $0.user.hidden }) { try startChat() withAnimation { - onboardingStageDefault.set(.step3_CreateSimpleXAddress) - m.onboardingStage = .step3_CreateSimpleXAddress + onboardingStageDefault.set(.step3_ChooseServerOperators) + m.onboardingStage = .step3_ChooseServerOperators } } else { onboardingStageDefault.set(.onboardingComplete) diff --git a/apps/ios/Shared/Views/Onboarding/HowItWorks.swift b/apps/ios/Shared/Views/Onboarding/HowItWorks.swift index c1975765d2..f11dbbe7a8 100644 --- a/apps/ios/Shared/Views/Onboarding/HowItWorks.swift +++ b/apps/ios/Shared/Views/Onboarding/HowItWorks.swift @@ -9,8 +9,10 @@ import SwiftUI struct HowItWorks: View { + @Environment(\.dismiss) var dismiss: DismissAction @EnvironmentObject var m: ChatModel var onboarding: Bool + @Binding var createProfileNavLinkActive: Bool var body: some View { VStack(alignment: .leading) { @@ -37,8 +39,8 @@ struct HowItWorks: View { Spacer() if onboarding { - OnboardingActionButton() - .padding(.bottom, 8) + createFirstProfileButton() + .padding(.bottom) } } .lineLimit(10) @@ -46,10 +48,23 @@ struct HowItWorks: View { .frame(maxHeight: .infinity, alignment: .top) .modifier(ThemedBackground()) } + + private func createFirstProfileButton() -> some View { + Button { + dismiss() + createProfileNavLinkActive = true + } label: { + Text("Create your profile") + } + .buttonStyle(OnboardingButtonStyle(isDisabled: false)) + } } struct HowItWorks_Previews: PreviewProvider { static var previews: some View { - HowItWorks(onboarding: true) + HowItWorks( + onboarding: true, + createProfileNavLinkActive: Binding.constant(false) + ) } } diff --git a/apps/ios/Shared/Views/Onboarding/OnboardingView.swift b/apps/ios/Shared/Views/Onboarding/OnboardingView.swift index 438491b5f1..de3dce21bb 100644 --- a/apps/ios/Shared/Views/Onboarding/OnboardingView.swift +++ b/apps/ios/Shared/Views/Onboarding/OnboardingView.swift @@ -16,6 +16,7 @@ struct OnboardingView: View { case .step1_SimpleXInfo: SimpleXInfo(onboarding: true) case .step2_CreateProfile: CreateFirstProfile() case .step3_CreateSimpleXAddress: CreateSimpleXAddress() + case .step3_ChooseServerOperators: ChooseServerOperators(onboarding: true) case .step4_SetNotificationsMode: SetNotificationsMode() case .onboardingComplete: EmptyView() } @@ -24,8 +25,9 @@ struct OnboardingView: View { enum OnboardingStage: String, Identifiable { case step1_SimpleXInfo - case step2_CreateProfile - case step3_CreateSimpleXAddress + case step2_CreateProfile // deprecated + case step3_CreateSimpleXAddress // deprecated + case step3_ChooseServerOperators case step4_SetNotificationsMode case onboardingComplete diff --git a/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift b/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift index 7681a42a77..03ee9c67e0 100644 --- a/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift +++ b/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift @@ -15,41 +15,44 @@ struct SetNotificationsMode: View { @State private var showAlert: NotificationAlert? var body: some View { - ScrollView { - VStack(alignment: .leading, spacing: 16) { - Text("Push notifications") - .font(.largeTitle) - .bold() - .frame(maxWidth: .infinity) - - Text("Send notifications:") - ForEach(NotificationsMode.values) { mode in - NtfModeSelector(mode: mode, selection: $notificationMode) - } - - Spacer() - - Button { - if let token = m.deviceToken { - setNotificationsMode(token, notificationMode) - } else { - AlertManager.shared.showAlertMsg(title: "No device token!") + GeometryReader { g in + ScrollView { + VStack(alignment: .leading, spacing: 16) { + Text("Push notifications") + .font(.largeTitle) + .bold() + .frame(maxWidth: .infinity) + + Text("Send notifications:") + ForEach(NotificationsMode.values) { mode in + NtfModeSelector(mode: mode, selection: $notificationMode) } - onboardingStageDefault.set(.onboardingComplete) - m.onboardingStage = .onboardingComplete - } label: { - if case .off = notificationMode { - Text("Use chat") - } else { - Text("Enable notifications") + + Spacer() + + Button { + if let token = m.deviceToken { + setNotificationsMode(token, notificationMode) + } else { + AlertManager.shared.showAlertMsg(title: "No device token!") + } + onboardingStageDefault.set(.onboardingComplete) + m.onboardingStage = .onboardingComplete + } label: { + if case .off = notificationMode { + Text("Use chat") + } else { + Text("Enable notifications") + } } + .buttonStyle(OnboardingButtonStyle()) + .padding(.bottom) } - .font(.title) - .frame(maxWidth: .infinity) + .padding() + .frame(minHeight: g.size.height) } - .padding() - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom) } + .frame(maxHeight: .infinity) } private func setNotificationsMode(_ token: DeviceToken, _ mode: NotificationsMode) { diff --git a/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift b/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift index ee5a618e68..2e077e9d95 100644 --- a/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift +++ b/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift @@ -13,81 +13,85 @@ struct SimpleXInfo: View { @EnvironmentObject var m: ChatModel @Environment(\.colorScheme) var colorScheme: ColorScheme @State private var showHowItWorks = false + @State private var createProfileNavLinkActive = false var onboarding: Bool var body: some View { - GeometryReader { g in - ScrollView { - VStack(alignment: .leading) { - Image(colorScheme == .light ? "logo" : "logo-light") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: g.size.width * 0.67) - .padding(.bottom, 8) - .frame(maxWidth: .infinity, minHeight: 48, alignment: .top) + NavigationView { + GeometryReader { g in + ScrollView { + VStack(alignment: .leading, spacing: 20) { + Image(colorScheme == .light ? "logo" : "logo-light") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: g.size.width * 0.67) + .padding(.bottom, 8) + .frame(maxWidth: .infinity, minHeight: 48, alignment: .top) - VStack(alignment: .leading) { - Text("The next generation of private messaging") - .font(.title2) - .padding(.bottom, 30) - .padding(.horizontal, 40) - .frame(maxWidth: .infinity) - .multilineTextAlignment(.center) - infoRow("privacy", "Privacy redefined", - "The 1st platform without any user identifiers – private by design.", width: 48) - infoRow("shield", "Immune to spam and abuse", - "People can connect to you only via the links you share.", width: 46) - infoRow(colorScheme == .light ? "decentralized" : "decentralized-light", "Decentralized", - "Open-source protocol and code – anybody can run the servers.", width: 44) - } + VStack(alignment: .leading) { + Text("The next generation of private messaging") + .font(.title2) + .padding(.bottom, 30) + .padding(.horizontal, 40) + .frame(maxWidth: .infinity) + .multilineTextAlignment(.center) + infoRow("privacy", "Privacy redefined", + "The 1st platform without any user identifiers – private by design.", width: 48) + infoRow("shield", "Immune to spam and abuse", + "People can connect to you only via the links you share.", width: 46) + infoRow(colorScheme == .light ? "decentralized" : "decentralized-light", "Decentralized", + "Open-source protocol and code – anybody can run the servers.", width: 44) + } - Spacer() - if onboarding { - OnboardingActionButton() Spacer() + if onboarding { + onboardingActionButton() + + Button { + m.migrationState = .pasteOrScanLink + } label: { + Label("Migrate from another device", systemImage: "tray.and.arrow.down") + .font(.subheadline) + } + .frame(maxWidth: .infinity) + } + Button { - m.migrationState = .pasteOrScanLink + showHowItWorks = true } label: { - Label("Migrate from another device", systemImage: "tray.and.arrow.down") + Label("How it works", systemImage: "info.circle") .font(.subheadline) } - .padding(.bottom, 8) .frame(maxWidth: .infinity) + .padding(.bottom) } - - Button { - showHowItWorks = true - } label: { - Label("How it works", systemImage: "info.circle") - .font(.subheadline) - } - .padding(.bottom, 8) - .frame(maxWidth: .infinity) - + .frame(minHeight: g.size.height) } - .frame(minHeight: g.size.height) - } - .sheet(isPresented: Binding( - get: { m.migrationState != nil }, - set: { _ in - m.migrationState = nil - MigrationToDeviceState.save(nil) } - )) { - NavigationView { - VStack(alignment: .leading) { - MigrateToDevice(migrationState: $m.migrationState) + .sheet(isPresented: Binding( + get: { m.migrationState != nil }, + set: { _ in + m.migrationState = nil + MigrationToDeviceState.save(nil) } + )) { + NavigationView { + VStack(alignment: .leading) { + MigrateToDevice(migrationState: $m.migrationState) + } + .navigationTitle("Migrate here") + .modifier(ThemedBackground(grouped: true)) } - .navigationTitle("Migrate here") - .modifier(ThemedBackground(grouped: true)) + } + .sheet(isPresented: $showHowItWorks) { + HowItWorks( + onboarding: onboarding, + createProfileNavLinkActive: $createProfileNavLinkActive + ) } } - .sheet(isPresented: $showHowItWorks) { - HowItWorks(onboarding: onboarding) - } + .frame(maxHeight: .infinity) + .padding() } - .frame(maxHeight: .infinity) - .padding() } private func infoRow(_ image: String, _ title: LocalizedStringKey, _ text: LocalizedStringKey, width: CGFloat) -> some View { @@ -108,49 +112,51 @@ struct SimpleXInfo: View { .padding(.bottom, 20) .padding(.trailing, 6) } -} -struct OnboardingActionButton: View { - @EnvironmentObject var m: ChatModel - @Environment(\.colorScheme) var colorScheme - - var body: some View { + @ViewBuilder private func onboardingActionButton() -> some View { if m.currentUser == nil { - actionButton("Create your profile", onboarding: .step2_CreateProfile) + createFirstProfileButton() } else { - actionButton("Make a private connection", onboarding: .onboardingComplete) + userExistsFallbackButton() } } - private func actionButton(_ label: LocalizedStringKey, onboarding: OnboardingStage) -> some View { - Button { - withAnimation { - onboardingStageDefault.set(onboarding) - m.onboardingStage = onboarding + private func createFirstProfileButton() -> some View { + ZStack { + Button { + createProfileNavLinkActive = true + } label: { + Text("Create your profile") } - } label: { - HStack { - Text(label).font(.title2) - Image(systemName: "greaterthan") + .buttonStyle(OnboardingButtonStyle(isDisabled: false)) + + NavigationLink(isActive: $createProfileNavLinkActive) { + createProfileDestinationView() + } label: { + EmptyView() } + .frame(width: 1, height: 1) + .hidden() } - .frame(maxWidth: .infinity) - .padding(.bottom) } - private func actionButton(_ label: LocalizedStringKey, action: @escaping () -> Void) -> some View { + private func createProfileDestinationView() -> some View { + CreateFirstProfile() + .navigationTitle("Create your profile") + .navigationBarTitleDisplayMode(.large) + .modifier(ThemedBackground(grouped: true)) + } + + private func userExistsFallbackButton() -> some View { Button { withAnimation { - action() + onboardingStageDefault.set(.onboardingComplete) + m.onboardingStage = .onboardingComplete } } label: { - HStack { - Text(label).font(.title2) - Image(systemName: "greaterthan") - } + Text("Make a private connection") } - .frame(maxWidth: .infinity) - .padding(.bottom) + .buttonStyle(OnboardingButtonStyle(isDisabled: false)) } } diff --git a/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift b/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift index 2ae4aa8c2b..1d1ec5b64c 100644 --- a/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift +++ b/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift @@ -7,190 +7,209 @@ // import SwiftUI +import SimpleXChat private struct VersionDescription { var version: String var post: URL? - var features: [FeatureDescription] + var features: [Feature] } -private struct FeatureDescription { - var icon: String? - var title: LocalizedStringKey - var description: LocalizedStringKey? +private enum Feature: Identifiable { + case feature(Description) + case view(FeatureView) + + var id: LocalizedStringKey { + switch self { + case let .feature(d): d.title + case let .view(v): v.title + } + } +} + +private struct Description { + let icon: String? + let title: LocalizedStringKey + let description: LocalizedStringKey? var subfeatures: [(icon: String, description: LocalizedStringKey)] = [] } +private struct FeatureView { + let icon: String? + let title: LocalizedStringKey + let view: () -> any View +} + private let versionDescriptions: [VersionDescription] = [ VersionDescription( version: "v4.2", post: URL(string: "https://simplex.chat/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html"), features: [ - FeatureDescription( + .feature(Description( icon: "checkmark.shield", title: "Security assessment", description: "SimpleX Chat security was audited by Trail of Bits." - ), - FeatureDescription( + )), + .feature(Description( icon: "person.2", title: "Group links", description: "Admins can create the links to join groups." - ), - FeatureDescription( + )), + .feature(Description( icon: "checkmark", title: "Auto-accept contact requests", description: "With optional welcome message." - ), + )), ] ), VersionDescription( version: "v4.3", post: URL(string: "https://simplex.chat/blog/20221206-simplex-chat-v4.3-voice-messages.html"), features: [ - FeatureDescription( + .feature(Description( icon: "mic", title: "Voice messages", description: "Max 30 seconds, received instantly." - ), - FeatureDescription( + )), + .feature(Description( icon: "trash.slash", title: "Irreversible message deletion", description: "Your contacts can allow full message deletion." - ), - FeatureDescription( + )), + .feature(Description( icon: "externaldrive.connected.to.line.below", title: "Improved server configuration", description: "Add servers by scanning QR codes." - ), - FeatureDescription( + )), + .feature(Description( icon: "eye.slash", title: "Improved privacy and security", description: "Hide app screen in the recent apps." - ), + )), ] ), VersionDescription( version: "v4.4", post: URL(string: "https://simplex.chat/blog/20230103-simplex-chat-v4.4-disappearing-messages.html"), features: [ - FeatureDescription( + .feature(Description( icon: "stopwatch", title: "Disappearing messages", description: "Sent messages will be deleted after set time." - ), - FeatureDescription( + )), + .feature(Description( icon: "ellipsis.circle", title: "Live messages", description: "Recipients see updates as you type them." - ), - FeatureDescription( + )), + .feature(Description( icon: "checkmark.shield", title: "Verify connection security", description: "Compare security codes with your contacts." - ), - FeatureDescription( + )), + .feature(Description( icon: "camera", title: "GIFs and stickers", description: "Send them from gallery or custom keyboards." - ), - FeatureDescription( + )), + .feature(Description( icon: "character", title: "French interface", description: "Thanks to the users – contribute via Weblate!" - ) + )), ] ), VersionDescription( version: "v4.5", post: URL(string: "https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html"), features: [ - FeatureDescription( + .feature(Description( icon: "person.crop.rectangle.stack", title: "Multiple chat profiles", description: "Different names, avatars and transport isolation." - ), - FeatureDescription( + )), + .feature(Description( icon: "rectangle.and.pencil.and.ellipsis", title: "Message draft", description: "Preserve the last message draft, with attachments." - ), - FeatureDescription( + )), + .feature(Description( icon: "network.badge.shield.half.filled", title: "Transport isolation", description: "By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." - ), - FeatureDescription( + )), + .feature(Description( icon: "lock.doc", title: "Private filenames", description: "To protect timezone, image/voice files use UTC." - ), - FeatureDescription( + )), + .feature(Description( icon: "battery.25", title: "Reduced battery usage", description: "More improvements are coming soon!" - ), - FeatureDescription( + )), + .feature(Description( icon: "character", title: "Italian interface", description: "Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" - ) + )), ] ), VersionDescription( version: "v4.6", post: URL(string: "https://simplex.chat/blog/20230328-simplex-chat-v4-6-hidden-profiles.html"), features: [ - FeatureDescription( + .feature(Description( icon: "lock", title: "Hidden chat profiles", description: "Protect your chat profiles with a password!" - ), - FeatureDescription( + )), + .feature(Description( icon: "phone.arrow.up.right", title: "Audio and video calls", description: "Fully re-implemented - work in background!" - ), - FeatureDescription( + )), + .feature(Description( icon: "flag", title: "Group moderation", description: "Now admins can:\n- delete members' messages.\n- disable members (\"observer\" role)" - ), - FeatureDescription( + )), + .feature(Description( icon: "plus.message", title: "Group welcome message", description: "Set the message shown to new members!" - ), - FeatureDescription( + )), + .feature(Description( icon: "battery.50", title: "Further reduced battery usage", description: "More improvements are coming soon!" - ), - FeatureDescription( + )), + .feature(Description( icon: "character", title: "Chinese and Spanish interface", description: "Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" - ), + )), ] ), VersionDescription( version: "v5.0", post: URL(string: "https://simplex.chat/blog/20230422-simplex-chat-vision-funding-v5-videos-files-passcode.html"), features: [ - FeatureDescription( + .feature(Description( icon: "arrow.up.doc", title: "Videos and files up to 1gb", description: "Fast and no wait until the sender is online!" - ), - FeatureDescription( + )), + .feature(Description( icon: "lock", title: "App passcode", description: "Set it instead of system authentication." - ), - FeatureDescription( + )), + .feature(Description( icon: "character", title: "Polish interface", description: "Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" - ), + )), ] ), // Also @@ -200,240 +219,240 @@ private let versionDescriptions: [VersionDescription] = [ version: "v5.1", post: URL(string: "https://simplex.chat/blog/20230523-simplex-chat-v5-1-message-reactions-self-destruct-passcode.html"), features: [ - FeatureDescription( + .feature(Description( icon: "face.smiling", title: "Message reactions", description: "Finally, we have them! 🚀" - ), - FeatureDescription( + )), + .feature(Description( icon: "arrow.up.message", title: "Better messages", description: "- voice messages up to 5 minutes.\n- custom time to disappear.\n- editing history." - ), - FeatureDescription( + )), + .feature(Description( icon: "lock", title: "Self-destruct passcode", description: "All data is erased when it is entered." - ), - FeatureDescription( + )), + .feature(Description( icon: "character", title: "Japanese interface", description: "Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" - ), + )), ] ), VersionDescription( version: "v5.2", post: URL(string: "https://simplex.chat/blog/20230722-simplex-chat-v5-2-message-delivery-receipts.html"), features: [ - FeatureDescription( + .feature(Description( icon: "checkmark", title: "Message delivery receipts!", description: "The second tick we missed! ✅" - ), - FeatureDescription( + )), + .feature(Description( icon: "star", title: "Find chats faster", description: "Filter unread and favorite chats." - ), - FeatureDescription( + )), + .feature(Description( icon: "exclamationmark.arrow.triangle.2.circlepath", title: "Keep your connections", description: "Fix encryption after restoring backups." - ), - FeatureDescription( + )), + .feature(Description( icon: "stopwatch", title: "Make one message disappear", description: "Even when disabled in the conversation." - ), - FeatureDescription( + )), + .feature(Description( icon: "gift", title: "A few more things", description: "- more stable message delivery.\n- a bit better groups.\n- and more!" - ), + )), ] ), VersionDescription( version: "v5.3", post: URL(string: "https://simplex.chat/blog/20230925-simplex-chat-v5-3-desktop-app-local-file-encryption-directory-service.html"), features: [ - FeatureDescription( + .feature(Description( icon: "desktopcomputer", title: "New desktop app!", description: "Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" - ), - FeatureDescription( + )), + .feature(Description( icon: "lock", title: "Encrypt stored files & media", description: "App encrypts new local files (except videos)." - ), - FeatureDescription( + )), + .feature(Description( icon: "magnifyingglass", title: "Discover and join groups", description: "- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)!\n- delivery receipts (up to 20 members).\n- faster and more stable." - ), - FeatureDescription( + )), + .feature(Description( icon: "theatermasks", title: "Simplified incognito mode", description: "Toggle incognito when connecting." - ), - FeatureDescription( + )), + .feature(Description( icon: "character", title: "\(4) new interface languages", description: "Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" - ), + )), ] ), VersionDescription( version: "v5.4", post: URL(string: "https://simplex.chat/blog/20231125-simplex-chat-v5-4-link-mobile-desktop-quantum-resistant-better-groups.html"), features: [ - FeatureDescription( + .feature(Description( icon: "desktopcomputer", title: "Link mobile and desktop apps! 🔗", description: "Via secure quantum resistant protocol." - ), - FeatureDescription( + )), + .feature(Description( icon: "person.2", title: "Better groups", description: "Faster joining and more reliable messages." - ), - FeatureDescription( + )), + .feature(Description( icon: "theatermasks", title: "Incognito groups", description: "Create a group using a random profile." - ), - FeatureDescription( + )), + .feature(Description( icon: "hand.raised", title: "Block group members", description: "To hide unwanted messages." - ), - FeatureDescription( + )), + .feature(Description( icon: "gift", title: "A few more things", description: "- optionally notify deleted contacts.\n- profile names with spaces.\n- and more!" - ), + )), ] ), VersionDescription( version: "v5.5", post: URL(string: "https://simplex.chat/blog/20240124-simplex-chat-infrastructure-costs-v5-5-simplex-ux-private-notes-group-history.html"), features: [ - FeatureDescription( + .feature(Description( icon: "folder", title: "Private notes", description: "With encrypted files and media." - ), - FeatureDescription( + )), + .feature(Description( icon: "link", title: "Paste link to connect!", description: "Search bar accepts invitation links." - ), - FeatureDescription( + )), + .feature(Description( icon: "bubble.left.and.bubble.right", title: "Join group conversations", description: "Recent history and improved [directory bot](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion)." - ), - FeatureDescription( + )), + .feature(Description( icon: "battery.50", title: "Improved message delivery", description: "With reduced battery usage." - ), - FeatureDescription( + )), + .feature(Description( icon: "character", title: "Turkish interface", description: "Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" - ), + )), ] ), VersionDescription( version: "v5.6", post: URL(string: "https://simplex.chat/blog/20240323-simplex-network-privacy-non-profit-v5-6-quantum-resistant-e2e-encryption-simple-migration.html"), features: [ - FeatureDescription( + .feature(Description( icon: "key", title: "Quantum resistant encryption", description: "Enable in direct chats (BETA)!" - ), - FeatureDescription( + )), + .feature(Description( icon: "tray.and.arrow.up", title: "App data migration", description: "Migrate to another device via QR code." - ), - FeatureDescription( + )), + .feature(Description( icon: "phone", title: "Picture-in-picture calls", description: "Use the app while in the call." - ), - FeatureDescription( + )), + .feature(Description( icon: "hand.raised", title: "Safer groups", description: "Admins can block a member for all." - ), - FeatureDescription( + )), + .feature(Description( icon: "character", title: "Hungarian interface", description: "Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" - ), + )), ] ), VersionDescription( version: "v5.7", post: URL(string: "https://simplex.chat/blog/20240426-simplex-legally-binding-transparency-v5-7-better-user-experience.html"), features: [ - FeatureDescription( + .feature(Description( icon: "key", title: "Quantum resistant encryption", description: "Will be enabled in direct chats!" - ), - FeatureDescription( + )), + .feature(Description( icon: "arrowshape.turn.up.forward", title: "Forward and save messages", description: "Message source remains private." - ), - FeatureDescription( + )), + .feature(Description( icon: "music.note", title: "In-call sounds", description: "When connecting audio and video calls." - ), - FeatureDescription( + )), + .feature(Description( icon: "person.crop.square", title: "Shape profile images", description: "Square, circle, or anything in between." - ), - FeatureDescription( + )), + .feature(Description( icon: "antenna.radiowaves.left.and.right", title: "Network management", description: "More reliable network connection." - ) + )), ] ), VersionDescription( version: "v5.8", post: URL(string: "https://simplex.chat/blog/20240604-simplex-chat-v5.8-private-message-routing-chat-themes.html"), features: [ - FeatureDescription( + .feature(Description( icon: "arrow.forward", title: "Private message routing 🚀", description: "Protect your IP address from the messaging relays chosen by your contacts.\nEnable in *Network & servers* settings." - ), - FeatureDescription( + )), + .feature(Description( icon: "network.badge.shield.half.filled", title: "Safely receive files", description: "Confirm files from unknown servers." - ), - FeatureDescription( + )), + .feature(Description( icon: "battery.50", title: "Improved message delivery", description: "With reduced battery usage." - ) + )), ] ), VersionDescription( version: "v6.0", post: URL(string: "https://simplex.chat/blog/20240814-simplex-chat-vision-funding-v6-private-routing-new-user-experience.html"), features: [ - FeatureDescription( + .feature(Description( icon: nil, title: "New chat experience 🎉", description: nil, @@ -444,8 +463,8 @@ private let versionDescriptions: [VersionDescription] = [ ("platter.filled.bottom.and.arrow.down.iphone", "Use the app with one hand."), ("paintpalette", "Color chats with the new themes."), ] - ), - FeatureDescription( + )), + .feature(Description( icon: nil, title: "New media options", description: nil, @@ -454,39 +473,39 @@ private let versionDescriptions: [VersionDescription] = [ ("play.circle", "Play from the chat list."), ("circle.filled.pattern.diagonalline.rectangle", "Blur for better privacy.") ] - ), - FeatureDescription( + )), + .feature(Description( icon: "arrow.forward", title: "Private message routing 🚀", description: "It protects your IP address and connections." - ), - FeatureDescription( + )), + .feature(Description( icon: "network", title: "Better networking", description: "Connection and servers status." - ) + )), ] ), VersionDescription( version: "v6.1", post: URL(string: "https://simplex.chat/blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.html"), features: [ - FeatureDescription( + .feature(Description( icon: "checkmark.shield", title: "Better security ✅", description: "SimpleX protocols reviewed by Trail of Bits." - ), - FeatureDescription( + )), + .feature(Description( icon: "video", title: "Better calls", description: "Switch audio and video during the call." - ), - FeatureDescription( + )), + .feature(Description( icon: "bolt", title: "Better notifications", description: "Improved delivery, reduced traffic usage.\nMore improvements are coming soon!" - ), - FeatureDescription( + )), + .feature(Description( icon: nil, title: "Better user experience", description: nil, @@ -497,9 +516,25 @@ private let versionDescriptions: [VersionDescription] = [ ("arrowshape.turn.up.right", "Forward up to 20 messages at once."), ("flag", "Delete or moderate up to 200 messages.") ] - ), + )), ] ), + VersionDescription( + version: "v6.2 (beta.1)", + post: URL(string: "https://simplex.chat/blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.html"), + features: [ + .view(FeatureView( + icon: nil, + title: "Network decentralization", + view: newOperatorsView + )), + .feature(Description( + icon: "text.quote", + title: "Improved chat navigation", + description: "- Open chat on the first unread message.\n- Jump to quoted messages." + )), + ] + ) ] private let lastVersion = versionDescriptions.last!.version @@ -514,14 +549,56 @@ func shouldShowWhatsNew() -> Bool { return v != lastVersion } +fileprivate func newOperatorsView() -> some View { + VStack(alignment: .leading) { + Image((operatorsInfo[.flux] ?? ServerOperator.dummyOperatorInfo).largeLogo) + .resizable() + .scaledToFit() + .frame(height: 48) + Text("The second preset operator in the app!") + .multilineTextAlignment(.leading) + .lineLimit(10) + HStack { + Button("Enable Flux") { + + } + Text("for better metadata privacy.") + } + } +} + struct WhatsNewView: View { @Environment(\.dismiss) var dismiss: DismissAction @EnvironmentObject var theme: AppTheme @State var currentVersion = versionDescriptions.count - 1 @State var currentVersionNav = versionDescriptions.count - 1 var viaSettings = false + @State var showWhatsNew: Bool + var showOperatorsNotice: Bool var body: some View { + viewBody() + .task { + if showOperatorsNotice { + do { + let conditionsId = ChatModel.shared.conditions.currentConditions.conditionsId + try await setConditionsNotified(conditionsId: conditionsId) + } catch let error { + logger.error("WhatsNewView setConditionsNotified error: \(responseError(error))") + } + } + } + } + + @ViewBuilder private func viewBody() -> some View { + if showWhatsNew { + whatsNewView() + } else if showOperatorsNotice { + ChooseServerOperators(onboarding: false) + } + } + + private func whatsNewView() -> some View { VStack { TabView(selection: $currentVersion) { ForEach(Array(versionDescriptions.enumerated()), id: \.0) { (i, v) in @@ -532,9 +609,11 @@ struct WhatsNewView: View { .foregroundColor(theme.colors.secondary) .frame(maxWidth: .infinity) .padding(.vertical) - ForEach(v.features, id: \.title) { f in - featureDescription(f) - .padding(.bottom, 8) + ForEach(v.features) { f in + switch f { + case let .feature(d): featureDescription(d).padding(.bottom, 8) + case let .view(v): AnyView(v.view()).padding(.bottom, 8) + } } if let post = v.post { Link(destination: post) { @@ -546,11 +625,21 @@ struct WhatsNewView: View { } if !viaSettings { Spacer() - Button("Ok") { - dismiss() + + if showOperatorsNotice { + Button("View updated conditions") { + showWhatsNew = false + } + .font(.title3) + .frame(maxWidth: .infinity, alignment: .center) + } else { + Button("Ok") { + dismiss() + } + .font(.title3) + .frame(maxWidth: .infinity, alignment: .center) } - .font(.title3) - .frame(maxWidth: .infinity, alignment: .center) + Spacer() } } @@ -568,20 +657,24 @@ struct WhatsNewView: View { currentVersionNav = currentVersion } } - - private func featureDescription(_ f: FeatureDescription) -> some View { - VStack(alignment: .leading, spacing: 4) { - if let icon = f.icon { - HStack(alignment: .center, spacing: 4) { - Image(systemName: icon) - .symbolRenderingMode(.monochrome) - .foregroundColor(theme.colors.secondary) - .frame(minWidth: 30, alignment: .center) - Text(f.title).font(.title3).bold() - } - } else { - Text(f.title).font(.title3).bold() + + @ViewBuilder private func featureHeader(_ icon: String?, _ title: LocalizedStringKey) -> some View { + if let icon { + HStack(alignment: .center, spacing: 4) { + Image(systemName: icon) + .symbolRenderingMode(.monochrome) + .foregroundColor(theme.colors.secondary) + .frame(minWidth: 30, alignment: .center) + Text(title).font(.title3).bold() } + } else { + Text(title).font(.title3).bold() + } + } + + private func featureDescription(_ f: Description) -> some View { + VStack(alignment: .leading, spacing: 4) { + featureHeader(f.icon, f.title) if let d = f.description { Text(d) .multilineTextAlignment(.leading) @@ -636,6 +729,6 @@ struct WhatsNewView: View { struct NewFeaturesView_Previews: PreviewProvider { static var previews: some View { - WhatsNewView() + WhatsNewView(showWhatsNew: true, showOperatorsNotice: false) } } diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift index 155a3956be..2247e3d8d5 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift @@ -19,28 +19,80 @@ private enum NetworkAlert: Identifiable { } } +private enum NetworkAndServersSheet: Identifiable { + case showConditions(conditionsAction: UsageConditionsAction) + + var id: String { + switch self { + case .showConditions: return "showConditions" + } + } +} + struct NetworkAndServers: View { + @Environment(\.dismiss) var dismiss: DismissAction @EnvironmentObject var m: ChatModel + @Environment(\.colorScheme) var colorScheme: ColorScheme @EnvironmentObject var theme: AppTheme + @Binding var currUserServers: [UserOperatorServers] + @Binding var userServers: [UserOperatorServers] + @Binding var serverErrors: [UserServersError] + @State private var sheetItem: NetworkAndServersSheet? = nil + @State private var justOpened = true + @State private var showSaveDialog = false var body: some View { VStack { List { + let conditionsAction = m.conditions.conditionsAction + let anyOperatorEnabled = userServers.contains(where: { $0.operator?.enabled ?? false }) Section { - NavigationLink { - ProtocolServersView(serverProtocol: .smp) - .navigationTitle("Your SMP servers") - .modifier(ThemedBackground(grouped: true)) - } label: { - Text("Message servers") + ForEach(userServers.enumerated().map { $0 }, id: \.element.id) { idx, userOperatorServers in + if let serverOperator = userOperatorServers.operator { + serverOperatorView(idx, serverOperator) + } else { + EmptyView() + } } - NavigationLink { - ProtocolServersView(serverProtocol: .xftp) - .navigationTitle("Your XFTP servers") + if let conditionsAction = conditionsAction, anyOperatorEnabled { + conditionsButton(conditionsAction) + } + } header: { + Text("Preset servers") + .foregroundColor(theme.colors.secondary) + } footer: { + switch conditionsAction { + case let .review(_, deadline, _): + if let deadline = deadline, anyOperatorEnabled { + Text("Conditions will be considered accepted on: \(conditionsTimestamp(deadline)).") + .foregroundColor(theme.colors.secondary) + } + default: + EmptyView() + } + } + + Section { + if let idx = userServers.firstIndex(where: { $0.operator == nil }) { + NavigationLink { + YourServersView( + userServers: $userServers, + serverErrors: $serverErrors, + operatorIndex: idx + ) + .navigationTitle("Your servers") .modifier(ThemedBackground(grouped: true)) - } label: { - Text("Media & file servers") + } label: { + HStack { + Text("Your servers") + + if userServers[idx] != currUserServers[idx] { + Spacer() + unsavedChangesIndicator() + } + } + } } NavigationLink { @@ -55,6 +107,17 @@ struct NetworkAndServers: View { .foregroundColor(theme.colors.secondary) } + Section { + Button("Save servers", action: { saveServers($currUserServers, $userServers) }) + .disabled(!serversCanBeSaved(currUserServers, userServers, serverErrors)) + } footer: { + if let errStr = globalServersError(serverErrors) { + ServersErrorView(errStr: errStr) + } else if !serverErrors.isEmpty { + ServersErrorView(errStr: NSLocalizedString("Errors in servers configuration.", comment: "servers error")) + } + } + Section(header: Text("Calls").foregroundColor(theme.colors.secondary)) { NavigationLink { RTCServers() @@ -74,11 +137,287 @@ struct NetworkAndServers: View { } } } + .task { + // this condition is needed to prevent re-setting the servers when exiting single server view + if justOpened { + do { + currUserServers = try await getUserServers() + userServers = currUserServers + serverErrors = [] + } catch let error { + await MainActor.run { + showAlert( + NSLocalizedString("Error loading servers", comment: "alert title"), + message: responseError(error) + ) + } + } + justOpened = false + } + } + .modifier(BackButton(disabled: Binding.constant(false)) { + if serversCanBeSaved(currUserServers, userServers, serverErrors) { + showSaveDialog = true + } else { + dismiss() + } + }) + .confirmationDialog("Save servers?", isPresented: $showSaveDialog, titleVisibility: .visible) { + Button("Save") { + saveServers($currUserServers, $userServers) + dismiss() + } + Button("Exit without saving") { dismiss() } + } + .sheet(item: $sheetItem) { item in + switch item { + case let .showConditions(conditionsAction): + UsageConditionsView( + conditionsAction: conditionsAction, + currUserServers: $currUserServers, + userServers: $userServers + ) + .modifier(ThemedBackground(grouped: true)) + } + } + } + + private func serverOperatorView(_ operatorIndex: Int, _ serverOperator: ServerOperator) -> some View { + NavigationLink() { + OperatorView( + currUserServers: $currUserServers, + userServers: $userServers, + serverErrors: $serverErrors, + operatorIndex: operatorIndex, + useOperator: serverOperator.enabled + ) + .navigationBarTitle("\(serverOperator.tradeName) servers") + .modifier(ThemedBackground(grouped: true)) + .navigationBarTitleDisplayMode(.large) + } label: { + HStack { + Image(serverOperator.logo(colorScheme)) + .resizable() + .scaledToFit() + .grayscale(serverOperator.enabled ? 0.0 : 1.0) + .frame(width: 24, height: 24) + Text(serverOperator.tradeName) + .foregroundColor(serverOperator.enabled ? theme.colors.onBackground : theme.colors.secondary) + + if userServers[operatorIndex] != currUserServers[operatorIndex] { + Spacer() + unsavedChangesIndicator() + } + } + } + } + + private func unsavedChangesIndicator() -> some View { + Image(systemName: "pencil") + .foregroundColor(theme.colors.secondary) + .symbolRenderingMode(.monochrome) + .frame(maxWidth: 24, maxHeight: 24, alignment: .center) + } + + private func conditionsButton(_ conditionsAction: UsageConditionsAction) -> some View { + Button { + sheetItem = .showConditions(conditionsAction: conditionsAction) + } label: { + switch conditionsAction { + case .review: + Text("Review conditions") + case .accepted: + Text("Accepted conditions") + } + } + } +} + +struct UsageConditionsView: View { + @Environment(\.dismiss) var dismiss: DismissAction + @EnvironmentObject var theme: AppTheme + var conditionsAction: UsageConditionsAction + @Binding var currUserServers: [UserOperatorServers] + @Binding var userServers: [UserOperatorServers] + + var body: some View { + VStack(alignment: .leading, spacing: 20) { + Text("Conditions of use") + .font(.largeTitle) + .bold() + .padding(.top) + .padding(.top) + + switch conditionsAction { + + case let .review(operators, _, _): + Text("Conditions will be accepted for the operator(s): **\(operators.map { $0.legalName_ }.joined(separator: ", "))**.") + ConditionsTextView() + acceptConditionsButton(operators.map { $0.operatorId }) + .padding(.bottom) + .padding(.bottom) + + case let .accepted(operators): + Text("Conditions are accepted for the operator(s): **\(operators.map { $0.legalName_ }.joined(separator: ", "))**.") + ConditionsTextView() + .padding(.bottom) + .padding(.bottom) + } + } + .padding(.horizontal) + .frame(maxHeight: .infinity) + } + + private func acceptConditionsButton(_ operatorIds: [Int64]) -> some View { + Button { + acceptForOperators(operatorIds) + } label: { + Text("Accept conditions") + } + .buttonStyle(OnboardingButtonStyle()) + } + + func acceptForOperators(_ operatorIds: [Int64]) { + Task { + do { + let conditionsId = ChatModel.shared.conditions.currentConditions.conditionsId + let r = try await acceptConditions(conditionsId: conditionsId, operatorIds: operatorIds) + await MainActor.run { + ChatModel.shared.conditions = r + updateOperatorsConditionsAcceptance($currUserServers, r.serverOperators) + updateOperatorsConditionsAcceptance($userServers, r.serverOperators) + dismiss() + } + } catch let error { + await MainActor.run { + showAlert( + NSLocalizedString("Error accepting conditions", comment: "alert title"), + message: responseError(error) + ) + } + } + } + } +} + +func validateServers_(_ userServers: Binding<[UserOperatorServers]>, _ serverErrors: Binding<[UserServersError]>) { + let userServersToValidate = userServers.wrappedValue + Task { + do { + let errs = try await validateServers(userServers: userServersToValidate) + await MainActor.run { + serverErrors.wrappedValue = errs + } + } catch let error { + logger.error("validateServers error: \(responseError(error))") + } + } +} + +func serversCanBeSaved( + _ currUserServers: [UserOperatorServers], + _ userServers: [UserOperatorServers], + _ serverErrors: [UserServersError] +) -> Bool { + return userServers != currUserServers && serverErrors.isEmpty +} + +struct ServersErrorView: View { + @EnvironmentObject var theme: AppTheme + var errStr: String + + var body: some View { + HStack { + Image(systemName: "exclamationmark.circle") + .foregroundColor(.red) + Text(errStr) + .foregroundColor(theme.colors.secondary) + } + } +} + +func globalServersError(_ serverErrors: [UserServersError]) -> String? { + for err in serverErrors { + if let errStr = err.globalError { + return errStr + } + } + return nil +} + +func globalSMPServersError(_ serverErrors: [UserServersError]) -> String? { + for err in serverErrors { + if let errStr = err.globalSMPError { + return errStr + } + } + return nil +} + +func globalXFTPServersError(_ serverErrors: [UserServersError]) -> String? { + for err in serverErrors { + if let errStr = err.globalXFTPError { + return errStr + } + } + return nil +} + +func findDuplicateHosts(_ serverErrors: [UserServersError]) -> Set { + let duplicateHostsList = serverErrors.compactMap { err in + if case let .duplicateServer(_, _, duplicateHost) = err { + return duplicateHost + } else { + return nil + } + } + return Set(duplicateHostsList) +} + +func saveServers(_ currUserServers: Binding<[UserOperatorServers]>, _ userServers: Binding<[UserOperatorServers]>) { + let userServersToSave = userServers.wrappedValue + Task { + do { + try await setUserServers(userServers: userServersToSave) + // Get updated servers to learn new server ids (otherwise it messes up delete of newly added and saved servers) + do { + let updatedServers = try await getUserServers() + await MainActor.run { + currUserServers.wrappedValue = updatedServers + userServers.wrappedValue = updatedServers + } + } catch let error { + logger.error("saveServers getUserServers error: \(responseError(error))") + await MainActor.run { + currUserServers.wrappedValue = userServersToSave + } + } + } catch let error { + logger.error("saveServers setUserServers error: \(responseError(error))") + await MainActor.run { + showAlert( + NSLocalizedString("Error saving servers", comment: "alert title"), + message: responseError(error) + ) + } + } + } +} + +func updateOperatorsConditionsAcceptance(_ usvs: Binding<[UserOperatorServers]>, _ updatedOperators: [ServerOperator]) { + for i in 0.. some View { + VStack { + let serverAddress = parseServerAddress(serverToEdit.server) + let valid = serverAddress?.valid == true + List { + Section { + TextEditor(text: $serverToEdit.server) + .multilineTextAlignment(.leading) + .autocorrectionDisabled(true) + .autocapitalization(.none) + .allowsTightening(true) + .lineLimit(10) + .frame(height: 144) + .padding(-6) + } header: { + HStack { + Text("Your server address") + .foregroundColor(theme.colors.secondary) + if !valid { + Spacer() + Image(systemName: "exclamationmark.circle").foregroundColor(.red) + } + } + } + useServerSection(valid) + if valid { + Section(header: Text("Add to another device").foregroundColor(theme.colors.secondary)) { + MutableQRCode(uri: $serverToEdit.server) + .listRowInsets(EdgeInsets(top: 12, leading: 12, bottom: 12, trailing: 12)) + } + } + } + } + } + + private func useServerSection(_ valid: Bool) -> some View { + Section(header: Text("Use server").foregroundColor(theme.colors.secondary)) { + HStack { + Button("Test server") { + testing = true + serverToEdit.tested = nil + Task { + if let f = await testServerConnection(server: $serverToEdit) { + showTestFailure = true + testFailure = f + } + await MainActor.run { testing = false } + } + } + .disabled(!valid || testing) + Spacer() + showTestStatus(server: serverToEdit) + } + Toggle("Use for new connections", isOn: $serverToEdit.enabled) + } + } +} + +func serverProtocolAndOperator(_ server: UserServer, _ userServers: [UserOperatorServers]) -> (ServerProtocol, ServerOperator?)? { + if let serverAddress = parseServerAddress(server.server) { + let serverProtocol = serverAddress.serverProtocol + let hostnames = serverAddress.hostnames + let matchingOperator = userServers.compactMap { $0.operator }.first { op in + op.serverDomains.contains { domain in + hostnames.contains { hostname in + hostname.hasSuffix(domain) + } + } + } + return (serverProtocol, matchingOperator) + } else { + return nil + } +} + +func addServer( + _ server: UserServer, + _ userServers: Binding<[UserOperatorServers]>, + _ serverErrors: Binding<[UserServersError]>, + _ dismiss: DismissAction +) { + if let (serverProtocol, matchingOperator) = serverProtocolAndOperator(server, userServers.wrappedValue) { + if let i = userServers.wrappedValue.firstIndex(where: { $0.operator?.operatorId == matchingOperator?.operatorId }) { + switch serverProtocol { + case .smp: userServers[i].wrappedValue.smpServers.append(server) + case .xftp: userServers[i].wrappedValue.xftpServers.append(server) + } + validateServers_(userServers, serverErrors) + dismiss() + if let op = matchingOperator { + showAlert( + NSLocalizedString("Operator server", comment: "alert title"), + message: String.localizedStringWithFormat(NSLocalizedString("Server added to operator %@.", comment: "alert message"), op.tradeName) + ) + } + } else { // Shouldn't happen + dismiss() + showAlert(NSLocalizedString("Error adding server", comment: "alert title")) + } + } else { + dismiss() + if server.server.trimmingCharacters(in: .whitespaces) != "" { + showAlert( + NSLocalizedString("Invalid server address!", comment: "alert title"), + message: NSLocalizedString("Check server address and try again.", comment: "alert title") + ) + } + } +} + +#Preview { + NewServerView( + userServers: Binding.constant([UserOperatorServers.sampleDataNilOperator]), + serverErrors: Binding.constant([]) + ) +} diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift new file mode 100644 index 0000000000..ef02e94e3f --- /dev/null +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift @@ -0,0 +1,569 @@ +// +// OperatorView.swift +// SimpleX (iOS) +// +// Created by spaced4ndy on 28.10.2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +struct OperatorView: View { + @Environment(\.dismiss) var dismiss: DismissAction + @Environment(\.colorScheme) var colorScheme: ColorScheme + @EnvironmentObject var theme: AppTheme + @Environment(\.editMode) private var editMode + @Binding var currUserServers: [UserOperatorServers] + @Binding var userServers: [UserOperatorServers] + @Binding var serverErrors: [UserServersError] + var operatorIndex: Int + @State var useOperator: Bool + @State private var useOperatorToggleReset: Bool = false + @State private var showConditionsSheet: Bool = false + @State private var selectedServer: String? = nil + @State private var testing = false + + var body: some View { + operatorView() + .opacity(testing ? 0.4 : 1) + .overlay { + if testing { + ProgressView() + .scaleEffect(2) + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + } + .allowsHitTesting(!testing) + } + + @ViewBuilder private func operatorView() -> some View { + let duplicateHosts = findDuplicateHosts(serverErrors) + VStack { + List { + Section { + infoViewLink() + useOperatorToggle() + } header: { + Text("Operator") + .foregroundColor(theme.colors.secondary) + } footer: { + if let errStr = globalServersError(serverErrors) { + ServersErrorView(errStr: errStr) + } else { + switch (userServers[operatorIndex].operator_.conditionsAcceptance) { + case let .accepted(acceptedAt): + if let acceptedAt = acceptedAt { + Text("Conditions accepted on: \(conditionsTimestamp(acceptedAt)).") + .foregroundColor(theme.colors.secondary) + } + case let .required(deadline): + if userServers[operatorIndex].operator_.enabled, let deadline = deadline { + Text("Conditions will be accepted on: \(conditionsTimestamp(deadline)).") + .foregroundColor(theme.colors.secondary) + } + } + } + } + + if userServers[operatorIndex].operator_.enabled { + if !userServers[operatorIndex].smpServers.filter({ !$0.deleted }).isEmpty { + Section { + Toggle("To receive", isOn: $userServers[operatorIndex].operator_.smpRoles.storage) + .onChange(of: userServers[operatorIndex].operator_.smpRoles.storage) { _ in + validateServers_($userServers, $serverErrors) + } + Toggle("For private routing", isOn: $userServers[operatorIndex].operator_.smpRoles.proxy) + .onChange(of: userServers[operatorIndex].operator_.smpRoles.proxy) { _ in + validateServers_($userServers, $serverErrors) + } + } header: { + Text("Use for messages") + .foregroundColor(theme.colors.secondary) + } footer: { + if let errStr = globalSMPServersError(serverErrors) { + ServersErrorView(errStr: errStr) + } + } + } + + // Preset servers can't be deleted + if !userServers[operatorIndex].smpServers.filter({ $0.preset }).isEmpty { + Section { + ForEach($userServers[operatorIndex].smpServers) { srv in + if srv.wrappedValue.preset { + ProtocolServerViewLink( + userServers: $userServers, + serverErrors: $serverErrors, + duplicateHosts: duplicateHosts, + server: srv, + serverProtocol: .smp, + backLabel: "\(userServers[operatorIndex].operator_.tradeName) servers", + selectedServer: $selectedServer + ) + } else { + EmptyView() + } + } + } header: { + Text("Message servers") + .foregroundColor(theme.colors.secondary) + } footer: { + if let errStr = globalSMPServersError(serverErrors) { + ServersErrorView(errStr: errStr) + } else { + Text("The servers for new connections of your current chat profile **\(ChatModel.shared.currentUser?.displayName ?? "")**.") + .foregroundColor(theme.colors.secondary) + .lineLimit(10) + } + } + } + + if !userServers[operatorIndex].smpServers.filter({ !$0.preset && !$0.deleted }).isEmpty { + Section { + ForEach($userServers[operatorIndex].smpServers) { srv in + if !srv.wrappedValue.preset && !srv.wrappedValue.deleted { + ProtocolServerViewLink( + userServers: $userServers, + serverErrors: $serverErrors, + duplicateHosts: duplicateHosts, + server: srv, + serverProtocol: .smp, + backLabel: "\(userServers[operatorIndex].operator_.tradeName) servers", + selectedServer: $selectedServer + ) + } else { + EmptyView() + } + } + .onDelete { indexSet in + deleteSMPServer($userServers, operatorIndex, indexSet) + validateServers_($userServers, $serverErrors) + } + } header: { + Text("Added message servers") + .foregroundColor(theme.colors.secondary) + } + } + + if !userServers[operatorIndex].xftpServers.filter({ !$0.deleted }).isEmpty { + Section { + Toggle("To send", isOn: $userServers[operatorIndex].operator_.xftpRoles.storage) + .onChange(of: userServers[operatorIndex].operator_.xftpRoles.storage) { _ in + validateServers_($userServers, $serverErrors) + } + } header: { + Text("Use for files") + .foregroundColor(theme.colors.secondary) + } footer: { + if let errStr = globalXFTPServersError(serverErrors) { + ServersErrorView(errStr: errStr) + } + } + } + + // Preset servers can't be deleted + if !userServers[operatorIndex].xftpServers.filter({ $0.preset }).isEmpty { + Section { + ForEach($userServers[operatorIndex].xftpServers) { srv in + if srv.wrappedValue.preset { + ProtocolServerViewLink( + userServers: $userServers, + serverErrors: $serverErrors, + duplicateHosts: duplicateHosts, + server: srv, + serverProtocol: .xftp, + backLabel: "\(userServers[operatorIndex].operator_.tradeName) servers", + selectedServer: $selectedServer + ) + } else { + EmptyView() + } + } + } header: { + Text("Media & file servers") + .foregroundColor(theme.colors.secondary) + } footer: { + if let errStr = globalXFTPServersError(serverErrors) { + ServersErrorView(errStr: errStr) + } else { + Text("The servers for new files of your current chat profile **\(ChatModel.shared.currentUser?.displayName ?? "")**.") + .foregroundColor(theme.colors.secondary) + .lineLimit(10) + } + } + } + + if !userServers[operatorIndex].xftpServers.filter({ !$0.preset && !$0.deleted }).isEmpty { + Section { + ForEach($userServers[operatorIndex].xftpServers) { srv in + if !srv.wrappedValue.preset && !srv.wrappedValue.deleted { + ProtocolServerViewLink( + userServers: $userServers, + serverErrors: $serverErrors, + duplicateHosts: duplicateHosts, + server: srv, + serverProtocol: .xftp, + backLabel: "\(userServers[operatorIndex].operator_.tradeName) servers", + selectedServer: $selectedServer + ) + } else { + EmptyView() + } + } + .onDelete { indexSet in + deleteXFTPServer($userServers, operatorIndex, indexSet) + validateServers_($userServers, $serverErrors) + } + } header: { + Text("Added media & file servers") + .foregroundColor(theme.colors.secondary) + } + } + + Section { + TestServersButton( + smpServers: $userServers[operatorIndex].smpServers, + xftpServers: $userServers[operatorIndex].xftpServers, + testing: $testing + ) + } + } + } + } + .toolbar { + if ( + !userServers[operatorIndex].smpServers.filter({ !$0.preset && !$0.deleted }).isEmpty || + !userServers[operatorIndex].xftpServers.filter({ !$0.preset && !$0.deleted }).isEmpty + ) { + EditButton() + } + } + .sheet(isPresented: $showConditionsSheet, onDismiss: onUseToggleSheetDismissed) { + SingleOperatorUsageConditionsView( + currUserServers: $currUserServers, + userServers: $userServers, + serverErrors: $serverErrors, + operatorIndex: operatorIndex + ) + .modifier(ThemedBackground(grouped: true)) + } + } + + private func infoViewLink() -> some View { + NavigationLink() { + OperatorInfoView(serverOperator: userServers[operatorIndex].operator_) + .navigationBarTitle("Network operator") + .modifier(ThemedBackground(grouped: true)) + .navigationBarTitleDisplayMode(.large) + } label: { + HStack { + Image(userServers[operatorIndex].operator_.logo(colorScheme)) + .resizable() + .scaledToFit() + .grayscale(userServers[operatorIndex].operator_.enabled ? 0.0 : 1.0) + .frame(width: 24, height: 24) + Text(userServers[operatorIndex].operator_.tradeName) + } + } + } + + private func useOperatorToggle() -> some View { + Toggle("Use servers", isOn: $useOperator) + .onChange(of: useOperator) { useOperatorToggle in + if useOperatorToggleReset { + useOperatorToggleReset = false + } else if useOperatorToggle { + switch userServers[operatorIndex].operator_.conditionsAcceptance { + case .accepted: + userServers[operatorIndex].operator_.enabled = true + validateServers_($userServers, $serverErrors) + case let .required(deadline): + if deadline == nil { + showConditionsSheet = true + } else { + userServers[operatorIndex].operator_.enabled = true + validateServers_($userServers, $serverErrors) + } + } + } else { + userServers[operatorIndex].operator_.enabled = false + validateServers_($userServers, $serverErrors) + } + } + } + + private func onUseToggleSheetDismissed() { + if useOperator && !userServers[operatorIndex].operator_.conditionsAcceptance.usageAllowed { + useOperatorToggleReset = true + useOperator = false + } + } +} + +func conditionsTimestamp(_ date: Date) -> String { + let localDateFormatter = DateFormatter() + localDateFormatter.dateStyle = .medium + localDateFormatter.timeStyle = .none + return localDateFormatter.string(from: date) +} + +struct OperatorInfoView: View { + @EnvironmentObject var theme: AppTheme + @Environment(\.colorScheme) var colorScheme: ColorScheme + var serverOperator: ServerOperator + + var body: some View { + VStack { + List { + Section { + VStack(alignment: .leading) { + Image(serverOperator.largeLogo(colorScheme)) + .resizable() + .scaledToFit() + .frame(height: 48) + if let legalName = serverOperator.legalName { + Text(legalName) + } + } + } + Section { + VStack(alignment: .leading, spacing: 12) { + ForEach(serverOperator.info.description, id: \.self) { d in + Text(d) + } + } + } + Section { + Link("\(serverOperator.info.website)", destination: URL(string: serverOperator.info.website)!) + } + } + } + } +} + +struct ConditionsTextView: View { + @State private var conditionsData: (UsageConditions, String?, UsageConditions?)? + @State private var failedToLoad: Bool = false + + let defaultConditionsLink = "https://github.com/simplex-chat/simplex-chat/blob/stable/PRIVACY.md" + + var body: some View { + viewBody() + .frame(maxWidth: .infinity, maxHeight: .infinity) + .task { + do { + conditionsData = try await getUsageConditions() + } catch let error { + logger.error("ConditionsTextView getUsageConditions error: \(responseError(error))") + failedToLoad = true + } + } + } + + // TODO Markdown & diff rendering + @ViewBuilder private func viewBody() -> some View { + if let (usageConditions, conditionsText, acceptedConditions) = conditionsData { + if let conditionsText = conditionsText { + ScrollView { + Text(conditionsText.trimmingCharacters(in: .whitespacesAndNewlines)) + .padding() + } + .background( + RoundedRectangle(cornerRadius: 12, style: .continuous) + .fill(Color(uiColor: .secondarySystemGroupedBackground)) + ) + } else { + let conditionsLink = "https://github.com/simplex-chat/simplex-chat/blob/\(usageConditions.conditionsCommit)/PRIVACY.md" + conditionsLinkView(conditionsLink) + } + } else if failedToLoad { + conditionsLinkView(defaultConditionsLink) + } else { + ProgressView() + .scaleEffect(2) + } + } + + private func conditionsLinkView(_ conditionsLink: String) -> some View { + VStack(alignment: .leading, spacing: 20) { + Text("Current conditions text couldn't be loaded, you can review conditions via this link:") + Link(destination: URL(string: conditionsLink)!) { + Text(conditionsLink) + .multilineTextAlignment(.leading) + } + } + } +} + +struct SingleOperatorUsageConditionsView: View { + @Environment(\.dismiss) var dismiss: DismissAction + @EnvironmentObject var theme: AppTheme + @Binding var currUserServers: [UserOperatorServers] + @Binding var userServers: [UserOperatorServers] + @Binding var serverErrors: [UserServersError] + var operatorIndex: Int + @State private var usageConditionsNavLinkActive: Bool = false + + var body: some View { + viewBody() + } + + @ViewBuilder private func viewBody() -> some View { + let operatorsWithConditionsAccepted = ChatModel.shared.conditions.serverOperators.filter { $0.conditionsAcceptance.conditionsAccepted } + if case .accepted = userServers[operatorIndex].operator_.conditionsAcceptance { + + // In current UI implementation this branch doesn't get shown - as conditions can't be opened from inside operator once accepted + VStack(alignment: .leading, spacing: 20) { + Group { + viewHeader() + ConditionsTextView() + .padding(.bottom) + .padding(.bottom) + } + .padding(.horizontal) + } + .frame(maxHeight: .infinity) + + } else if !operatorsWithConditionsAccepted.isEmpty { + + NavigationView { + VStack(alignment: .leading, spacing: 20) { + Group { + viewHeader() + Text("Conditions are already accepted for following operator(s): **\(operatorsWithConditionsAccepted.map { $0.legalName_ }.joined(separator: ", "))**.") + Text("Same conditions will apply to operator **\(userServers[operatorIndex].operator_.legalName_)**.") + conditionsAppliedToOtherOperatorsText() + usageConditionsNavLinkButton() + + Spacer() + + acceptConditionsButton() + .padding(.bottom) + .padding(.bottom) + } + .padding(.horizontal) + } + .frame(maxHeight: .infinity) + } + + } else { + + VStack(alignment: .leading, spacing: 20) { + Group { + viewHeader() + Text("To use the servers of **\(userServers[operatorIndex].operator_.legalName_)**, accept conditions of use.") + conditionsAppliedToOtherOperatorsText() + ConditionsTextView() + acceptConditionsButton() + .padding(.bottom) + .padding(.bottom) + } + .padding(.horizontal) + } + .frame(maxHeight: .infinity) + + } + } + + private func viewHeader() -> some View { + Text("Use servers of \(userServers[operatorIndex].operator_.tradeName)") + .font(.largeTitle) + .bold() + .padding(.top) + .padding(.top) + } + + @ViewBuilder private func conditionsAppliedToOtherOperatorsText() -> some View { + let otherOperatorsToApply = ChatModel.shared.conditions.serverOperators.filter { + $0.enabled && + !$0.conditionsAcceptance.conditionsAccepted && + $0.operatorId != userServers[operatorIndex].operator_.operatorId + } + if !otherOperatorsToApply.isEmpty { + Text("These conditions will also apply for: **\(otherOperatorsToApply.map { $0.legalName_ }.joined(separator: ", "))**.") + } + } + + @ViewBuilder private func acceptConditionsButton() -> some View { + let operatorIds = ChatModel.shared.conditions.serverOperators + .filter { + $0.operatorId == userServers[operatorIndex].operator_.operatorId || // Opened operator + ($0.enabled && !$0.conditionsAcceptance.conditionsAccepted) // Other enabled operators with conditions not accepted + } + .map { $0.operatorId } + Button { + acceptForOperators(operatorIds, operatorIndex) + } label: { + Text("Accept conditions") + } + .buttonStyle(OnboardingButtonStyle()) + } + + func acceptForOperators(_ operatorIds: [Int64], _ operatorIndexToEnable: Int) { + Task { + do { + let conditionsId = ChatModel.shared.conditions.currentConditions.conditionsId + let r = try await acceptConditions(conditionsId: conditionsId, operatorIds: operatorIds) + await MainActor.run { + ChatModel.shared.conditions = r + updateOperatorsConditionsAcceptance($currUserServers, r.serverOperators) + updateOperatorsConditionsAcceptance($userServers, r.serverOperators) + userServers[operatorIndexToEnable].operator?.enabled = true + validateServers_($userServers, $serverErrors) + dismiss() + } + } catch let error { + await MainActor.run { + showAlert( + NSLocalizedString("Error accepting conditions", comment: "alert title"), + message: responseError(error) + ) + } + } + } + } + + private func usageConditionsNavLinkButton() -> some View { + ZStack { + Button { + usageConditionsNavLinkActive = true + } label: { + Text("View conditions") + } + + NavigationLink(isActive: $usageConditionsNavLinkActive) { + usageConditionsDestinationView() + } label: { + EmptyView() + } + .frame(width: 1, height: 1) + .hidden() + } + } + + private func usageConditionsDestinationView() -> some View { + VStack(spacing: 20) { + ConditionsTextView() + .padding(.top) + + acceptConditionsButton() + .padding(.bottom) + .padding(.bottom) + } + .padding(.horizontal) + .navigationTitle("Conditions of use") + .navigationBarTitleDisplayMode(.large) + .modifier(ThemedBackground(grouped: true)) + } +} + +#Preview { + OperatorView( + currUserServers: Binding.constant([UserOperatorServers.sampleData1, UserOperatorServers.sampleDataNilOperator]), + userServers: Binding.constant([UserOperatorServers.sampleData1, UserOperatorServers.sampleDataNilOperator]), + serverErrors: Binding.constant([]), + operatorIndex: 1, + useOperator: ServerOperator.sampleData1.enabled + ) +} diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServerView.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServerView.swift index da29dfac29..13d01874ed 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServerView.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServerView.swift @@ -12,15 +12,15 @@ import SimpleXChat struct ProtocolServerView: View { @Environment(\.dismiss) var dismiss: DismissAction @EnvironmentObject var theme: AppTheme - let serverProtocol: ServerProtocol - @Binding var server: ServerCfg - @State var serverToEdit: ServerCfg + @Binding var userServers: [UserOperatorServers] + @Binding var serverErrors: [UserServersError] + @Binding var server: UserServer + @State var serverToEdit: UserServer + var backLabel: LocalizedStringKey @State private var showTestFailure = false @State private var testing = false @State private var testFailure: ProtocolTestFailure? - var proto: String { serverProtocol.rawValue.uppercased() } - var body: some View { ZStack { if server.preset { @@ -32,9 +32,33 @@ struct ProtocolServerView: View { ProgressView().scaleEffect(2) } } - .modifier(BackButton(label: "Your \(proto) servers", disabled: Binding.constant(false)) { - server = serverToEdit - dismiss() + .modifier(BackButton(label: backLabel, disabled: Binding.constant(false)) { + if let (serverToEditProtocol, serverToEditOperator) = serverProtocolAndOperator(serverToEdit, userServers), + let (serverProtocol, serverOperator) = serverProtocolAndOperator(server, userServers) { + if serverToEditProtocol != serverProtocol { + dismiss() + showAlert( + NSLocalizedString("Error updating server", comment: "alert title"), + message: NSLocalizedString("Server protocol changed.", comment: "alert title") + ) + } else if serverToEditOperator != serverOperator { + dismiss() + showAlert( + NSLocalizedString("Error updating server", comment: "alert title"), + message: NSLocalizedString("Server operator changed.", comment: "alert title") + ) + } else { + server = serverToEdit + validateServers_($userServers, $serverErrors) + dismiss() + } + } else { + dismiss() + showAlert( + NSLocalizedString("Invalid server address!", comment: "alert title"), + message: NSLocalizedString("Check server address and try again.", comment: "alert title") + ) + } }) .alert(isPresented: $showTestFailure) { Alert( @@ -62,7 +86,7 @@ struct ProtocolServerView: View { private func customServer() -> some View { VStack { let serverAddress = parseServerAddress(serverToEdit.server) - let valid = serverAddress?.valid == true && serverAddress?.serverProtocol == serverProtocol + let valid = serverAddress?.valid == true List { Section { TextEditor(text: $serverToEdit.server) @@ -112,10 +136,7 @@ struct ProtocolServerView: View { Spacer() showTestStatus(server: serverToEdit) } - let useForNewDisabled = serverToEdit.tested != true && !serverToEdit.preset Toggle("Use for new connections", isOn: $serverToEdit.enabled) - .disabled(useForNewDisabled) - .foregroundColor(useForNewDisabled ? theme.colors.secondary : theme.colors.onBackground) } } } @@ -142,7 +163,7 @@ struct BackButton: ViewModifier { } } -@ViewBuilder func showTestStatus(server: ServerCfg) -> some View { +@ViewBuilder func showTestStatus(server: UserServer) -> some View { switch server.tested { case .some(true): Image(systemName: "checkmark") @@ -155,7 +176,7 @@ struct BackButton: ViewModifier { } } -func testServerConnection(server: Binding) async -> ProtocolTestFailure? { +func testServerConnection(server: Binding) async -> ProtocolTestFailure? { do { let r = try await testProtoServer(server: server.wrappedValue.server) switch r { @@ -178,9 +199,11 @@ func testServerConnection(server: Binding) async -> ProtocolTestFailu struct ProtocolServerView_Previews: PreviewProvider { static var previews: some View { ProtocolServerView( - serverProtocol: .smp, - server: Binding.constant(ServerCfg.sampleData.custom), - serverToEdit: ServerCfg.sampleData.custom + userServers: Binding.constant([UserOperatorServers.sampleDataNilOperator]), + serverErrors: Binding.constant([]), + server: Binding.constant(UserServer.sampleData.custom), + serverToEdit: UserServer.sampleData.custom, + backLabel: "Your SMP servers" ) } } diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServersView.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServersView.swift index 0fb37d5c49..ed3c5c773c 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServersView.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServersView.swift @@ -11,238 +11,166 @@ import SimpleXChat private let howToUrl = URL(string: "https://simplex.chat/docs/server.html")! -struct ProtocolServersView: View { +struct YourServersView: View { @Environment(\.dismiss) var dismiss: DismissAction @EnvironmentObject private var m: ChatModel @EnvironmentObject var theme: AppTheme @Environment(\.editMode) private var editMode - let serverProtocol: ServerProtocol - @State private var currServers: [ServerCfg] = [] - @State private var presetServers: [ServerCfg] = [] - @State private var configuredServers: [ServerCfg] = [] - @State private var otherServers: [ServerCfg] = [] + @Binding var userServers: [UserOperatorServers] + @Binding var serverErrors: [UserServersError] + var operatorIndex: Int @State private var selectedServer: String? = nil @State private var showAddServer = false + @State private var newServerNavLinkActive = false @State private var showScanProtoServer = false - @State private var justOpened = true @State private var testing = false - @State private var alert: ServerAlert? = nil - @State private var showSaveDialog = false - - var proto: String { serverProtocol.rawValue.uppercased() } var body: some View { - ZStack { - protocolServersView() - if testing { - ProgressView().scaleEffect(2) + yourServersView() + .opacity(testing ? 0.4 : 1) + .overlay { + if testing { + ProgressView() + .scaleEffect(2) + .frame(maxWidth: .infinity, maxHeight: .infinity) + } } - } + .allowsHitTesting(!testing) } - enum ServerAlert: Identifiable { - case testsFailed(failures: [String: ProtocolTestFailure]) - case error(title: LocalizedStringKey, error: LocalizedStringKey = "") - - var id: String { - switch self { - case .testsFailed: return "testsFailed" - case let .error(title, _): return "error \(title)" - } - } - } - - private func protocolServersView() -> some View { + @ViewBuilder private func yourServersView() -> some View { + let duplicateHosts = findDuplicateHosts(serverErrors) List { - if !configuredServers.isEmpty { + if !userServers[operatorIndex].smpServers.filter({ !$0.deleted }).isEmpty { Section { - ForEach($configuredServers) { srv in - protocolServerView(srv) - } - .onMove { indexSet, offset in - configuredServers.move(fromOffsets: indexSet, toOffset: offset) + ForEach($userServers[operatorIndex].smpServers) { srv in + if !srv.wrappedValue.deleted { + ProtocolServerViewLink( + userServers: $userServers, + serverErrors: $serverErrors, + duplicateHosts: duplicateHosts, + server: srv, + serverProtocol: .smp, + backLabel: "Your servers", + selectedServer: $selectedServer + ) + } else { + EmptyView() + } } .onDelete { indexSet in - configuredServers.remove(atOffsets: indexSet) + deleteSMPServer($userServers, operatorIndex, indexSet) + validateServers_($userServers, $serverErrors) } } header: { - Text("Configured \(proto) servers") + Text("Message servers") .foregroundColor(theme.colors.secondary) } footer: { - Text("The servers for new connections of your current chat profile **\(m.currentUser?.displayName ?? "")**.") - .foregroundColor(theme.colors.secondary) - .lineLimit(10) + if let errStr = globalSMPServersError(serverErrors) { + ServersErrorView(errStr: errStr) + } else { + Text("The servers for new connections of your current chat profile **\(m.currentUser?.displayName ?? "")**.") + .foregroundColor(theme.colors.secondary) + .lineLimit(10) + } } } - if !otherServers.isEmpty { + if !userServers[operatorIndex].xftpServers.filter({ !$0.deleted }).isEmpty { Section { - ForEach($otherServers) { srv in - protocolServerView(srv) - } - .onMove { indexSet, offset in - otherServers.move(fromOffsets: indexSet, toOffset: offset) + ForEach($userServers[operatorIndex].xftpServers) { srv in + if !srv.wrappedValue.deleted { + ProtocolServerViewLink( + userServers: $userServers, + serverErrors: $serverErrors, + duplicateHosts: duplicateHosts, + server: srv, + serverProtocol: .xftp, + backLabel: "Your servers", + selectedServer: $selectedServer + ) + } else { + EmptyView() + } } .onDelete { indexSet in - otherServers.remove(atOffsets: indexSet) + deleteXFTPServer($userServers, operatorIndex, indexSet) + validateServers_($userServers, $serverErrors) } } header: { - Text("Other \(proto) servers") + Text("Media & file servers") .foregroundColor(theme.colors.secondary) + } footer: { + if let errStr = globalXFTPServersError(serverErrors) { + ServersErrorView(errStr: errStr) + } else { + Text("The servers for new files of your current chat profile **\(m.currentUser?.displayName ?? "")**.") + .foregroundColor(theme.colors.secondary) + .lineLimit(10) + } } } Section { - Button("Add server") { - showAddServer = true + ZStack { + Button("Add server") { + showAddServer = true + } + + NavigationLink(isActive: $newServerNavLinkActive) { + newServerDestinationView() + } label: { + EmptyView() + } + .frame(width: 1, height: 1) + .hidden() + } + } footer: { + if let errStr = globalServersError(serverErrors) { + ServersErrorView(errStr: errStr) } } Section { - Button("Reset") { partitionServers(currServers) } - .disabled(Set(allServers) == Set(currServers) || testing) - Button("Test servers", action: testServers) - .disabled(testing || allServersDisabled) - Button("Save servers", action: saveServers) - .disabled(saveDisabled) + TestServersButton( + smpServers: $userServers[operatorIndex].smpServers, + xftpServers: $userServers[operatorIndex].xftpServers, + testing: $testing + ) howToButton() } } - .toolbar { EditButton() } - .confirmationDialog("Add server", isPresented: $showAddServer, titleVisibility: .hidden) { - Button("Enter server manually") { - otherServers.append(ServerCfg.empty) - selectedServer = allServers.last?.id + .toolbar { + if ( + !userServers[operatorIndex].smpServers.filter({ !$0.deleted }).isEmpty || + !userServers[operatorIndex].xftpServers.filter({ !$0.deleted }).isEmpty + ) { + EditButton() } + } + .confirmationDialog("Add server", isPresented: $showAddServer, titleVisibility: .hidden) { + Button("Enter server manually") { newServerNavLinkActive = true } Button("Scan server QR code") { showScanProtoServer = true } - Button("Add preset servers", action: addAllPresets) - .disabled(hasAllPresets()) } .sheet(isPresented: $showScanProtoServer) { - ScanProtocolServer(servers: $otherServers) - .modifier(ThemedBackground(grouped: true)) - } - .modifier(BackButton(disabled: Binding.constant(false)) { - if saveDisabled { - dismiss() - justOpened = false - } else { - showSaveDialog = true - } - }) - .confirmationDialog("Save servers?", isPresented: $showSaveDialog, titleVisibility: .visible) { - Button("Save") { - saveServers() - dismiss() - justOpened = false - } - Button("Exit without saving") { dismiss() } - } - .alert(item: $alert) { a in - switch a { - case let .testsFailed(fs): - let msg = fs.map { (srv, f) in - "\(srv): \(f.localizedDescription)" - }.joined(separator: "\n") - return Alert( - title: Text("Tests failed!"), - message: Text("Some servers failed the test:\n" + msg) - ) - case .error: - return Alert( - title: Text("Error") - ) - } - } - .onAppear { - // this condition is needed to prevent re-setting the servers when exiting single server view - if justOpened { - do { - let r = try getUserProtoServers(serverProtocol) - currServers = r.protoServers - presetServers = r.presetServers - partitionServers(currServers) - } catch let error { - alert = .error( - title: "Error loading \(proto) servers", - error: "Error: \(responseError(error))" - ) - } - justOpened = false - } else { - partitionServers(allServers) - } - } - } - - private func partitionServers(_ servers: [ServerCfg]) { - configuredServers = servers.filter { $0.preset || $0.enabled } - otherServers = servers.filter { !($0.preset || $0.enabled) } - } - - private var allServers: [ServerCfg] { - configuredServers + otherServers - } - - private var saveDisabled: Bool { - allServers.isEmpty || - Set(allServers) == Set(currServers) || - testing || - !allServers.allSatisfy { srv in - if let address = parseServerAddress(srv.server) { - return uniqueAddress(srv, address) - } - return false - } || - allServersDisabled - } - - private var allServersDisabled: Bool { - allServers.allSatisfy { !$0.enabled } - } - - private func protocolServerView(_ server: Binding) -> some View { - let srv = server.wrappedValue - return NavigationLink(tag: srv.id, selection: $selectedServer) { - ProtocolServerView( - serverProtocol: serverProtocol, - server: server, - serverToEdit: srv + ScanProtocolServer( + userServers: $userServers, + serverErrors: $serverErrors ) - .navigationBarTitle(srv.preset ? "Preset server" : "Your server") .modifier(ThemedBackground(grouped: true)) - .navigationBarTitleDisplayMode(.large) - } label: { - let address = parseServerAddress(srv.server) - HStack { - Group { - if let address = address { - if !address.valid || address.serverProtocol != serverProtocol { - invalidServer() - } else if !uniqueAddress(srv, address) { - Image(systemName: "exclamationmark.circle").foregroundColor(.red) - } else if !srv.enabled { - Image(systemName: "slash.circle").foregroundColor(theme.colors.secondary) - } else { - showTestStatus(server: srv) - } - } else { - invalidServer() - } - } - .frame(width: 16, alignment: .center) - .padding(.trailing, 4) - - let v = Text(address?.hostnames.first ?? srv.server).lineLimit(1) - if srv.enabled { - v - } else { - v.foregroundColor(theme.colors.secondary) - } - } } } + private func newServerDestinationView() -> some View { + NewServerView( + userServers: $userServers, + serverErrors: $serverErrors + ) + .navigationTitle("New server") + .navigationBarTitleDisplayMode(.large) + .modifier(ThemedBackground(grouped: true)) + } + func howToButton() -> some View { Button { DispatchQueue.main.async { @@ -255,33 +183,114 @@ struct ProtocolServersView: View { } } } +} + +struct ProtocolServerViewLink: View { + @EnvironmentObject var theme: AppTheme + @Binding var userServers: [UserOperatorServers] + @Binding var serverErrors: [UserServersError] + var duplicateHosts: Set + @Binding var server: UserServer + var serverProtocol: ServerProtocol + var backLabel: LocalizedStringKey + @Binding var selectedServer: String? + + var body: some View { + let proto = serverProtocol.rawValue.uppercased() + + NavigationLink(tag: server.id, selection: $selectedServer) { + ProtocolServerView( + userServers: $userServers, + serverErrors: $serverErrors, + server: $server, + serverToEdit: server, + backLabel: backLabel + ) + .navigationBarTitle("\(proto) server") + .modifier(ThemedBackground(grouped: true)) + .navigationBarTitleDisplayMode(.large) + } label: { + let address = parseServerAddress(server.server) + HStack { + Group { + if let address = address { + if !address.valid || address.serverProtocol != serverProtocol { + invalidServer() + } else if address.hostnames.contains(where: duplicateHosts.contains) { + Image(systemName: "exclamationmark.circle").foregroundColor(.red) + } else if !server.enabled { + Image(systemName: "slash.circle").foregroundColor(theme.colors.secondary) + } else { + showTestStatus(server: server) + } + } else { + invalidServer() + } + } + .frame(width: 16, alignment: .center) + .padding(.trailing, 4) + + let v = Text(address?.hostnames.first ?? server.server).lineLimit(1) + if server.enabled { + v + } else { + v.foregroundColor(theme.colors.secondary) + } + } + } + } private func invalidServer() -> some View { Image(systemName: "exclamationmark.circle").foregroundColor(.red) } +} - private func uniqueAddress(_ s: ServerCfg, _ address: ServerAddress) -> Bool { - allServers.allSatisfy { srv in - address.hostnames.allSatisfy { host in - srv.id == s.id || !srv.server.contains(host) - } +func deleteSMPServer( + _ userServers: Binding<[UserOperatorServers]>, + _ operatorServersIndex: Int, + _ serverIndexSet: IndexSet +) { + if let idx = serverIndexSet.first { + let server = userServers[operatorServersIndex].wrappedValue.smpServers[idx] + if server.serverId == nil { + userServers[operatorServersIndex].wrappedValue.smpServers.remove(at: idx) + } else { + var updatedServer = server + updatedServer.deleted = true + userServers[operatorServersIndex].wrappedValue.smpServers[idx] = updatedServer } } +} - private func hasAllPresets() -> Bool { - presetServers.allSatisfy { hasPreset($0) } - } - - private func addAllPresets() { - for srv in presetServers { - if !hasPreset(srv) { - configuredServers.append(srv) - } +func deleteXFTPServer( + _ userServers: Binding<[UserOperatorServers]>, + _ operatorServersIndex: Int, + _ serverIndexSet: IndexSet +) { + if let idx = serverIndexSet.first { + let server = userServers[operatorServersIndex].wrappedValue.xftpServers[idx] + if server.serverId == nil { + userServers[operatorServersIndex].wrappedValue.xftpServers.remove(at: idx) + } else { + var updatedServer = server + updatedServer.deleted = true + userServers[operatorServersIndex].wrappedValue.xftpServers[idx] = updatedServer } } +} - private func hasPreset(_ srv: ServerCfg) -> Bool { - allServers.contains(where: { $0.server == srv.server }) +struct TestServersButton: View { + @Binding var smpServers: [UserServer] + @Binding var xftpServers: [UserServer] + @Binding var testing: Bool + + var body: some View { + Button("Test servers", action: testServers) + .disabled(testing || allServersDisabled) + } + + private var allServersDisabled: Bool { + smpServers.allSatisfy { !$0.enabled } && xftpServers.allSatisfy { !$0.enabled } } private func testServers() { @@ -292,68 +301,59 @@ struct ProtocolServersView: View { await MainActor.run { testing = false if !fs.isEmpty { - alert = .testsFailed(failures: fs) + let msg = fs.map { (srv, f) in + "\(srv): \(f.localizedDescription)" + }.joined(separator: "\n") + showAlert( + NSLocalizedString("Tests failed!", comment: "alert title"), + message: String.localizedStringWithFormat(NSLocalizedString("Some servers failed the test:\n%@", comment: "alert message"), msg) + ) } } } } private func resetTestStatus() { - for i in 0.. [String: ProtocolTestFailure] { var fs: [String: ProtocolTestFailure] = [:] - for i in 0..) { switch resp { case let .success(r): - if parseServerAddress(r.string) != nil { - servers.append(ServerCfg(server: r.string, preset: false, tested: nil, enabled: false)) - dismiss() - } else { - showAddressError = true - } + var server: UserServer = .empty + server.server = r.string + addServer(server, $userServers, $serverErrors, dismiss) case let .failure(e): logger.error("ScanProtocolServer.processQRCode QR code error: \(e.localizedDescription)") dismiss() @@ -54,6 +45,9 @@ struct ScanProtocolServer: View { struct ScanProtocolServer_Previews: PreviewProvider { static var previews: some View { - ScanProtocolServer(servers: Binding.constant([])) + ScanProtocolServer( + userServers: Binding.constant([UserOperatorServers.sampleDataNilOperator]), + serverErrors: Binding.constant([]) + ) } } diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift index 12a982e76b..e73697e42a 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift @@ -50,6 +50,7 @@ let DEFAULT_PROFILE_IMAGE_CORNER_RADIUS = "profileImageCornerRadius" let DEFAULT_CHAT_ITEM_ROUNDNESS = "chatItemRoundness" let DEFAULT_CHAT_ITEM_TAIL = "chatItemTail" let DEFAULT_ONE_HAND_UI_CARD_SHOWN = "oneHandUICardShown" +let DEFAULT_ADDRESS_CREATION_CARD_SHOWN = "addressCreationCardShown" let DEFAULT_TOOLBAR_MATERIAL = "toolbarMaterial" let DEFAULT_CONNECT_VIA_LINK_TAB = "connectViaLinkTab" let DEFAULT_LIVE_MESSAGE_ALERT_SHOWN = "liveMessageAlertShown" @@ -107,6 +108,7 @@ let appDefaults: [String: Any] = [ DEFAULT_CHAT_ITEM_ROUNDNESS: defaultChatItemRoundness, DEFAULT_CHAT_ITEM_TAIL: true, DEFAULT_ONE_HAND_UI_CARD_SHOWN: false, + DEFAULT_ADDRESS_CREATION_CARD_SHOWN: false, DEFAULT_TOOLBAR_MATERIAL: ToolbarMaterial.defaultMaterial, DEFAULT_CONNECT_VIA_LINK_TAB: ConnectViaLinkTab.scan.rawValue, DEFAULT_LIVE_MESSAGE_ALERT_SHOWN: false, @@ -135,6 +137,7 @@ let appDefaults: [String: Any] = [ let hintDefaults = [ DEFAULT_LA_NOTICE_SHOWN, DEFAULT_ONE_HAND_UI_CARD_SHOWN, + DEFAULT_ADDRESS_CREATION_CARD_SHOWN, DEFAULT_LIVE_MESSAGE_ALERT_SHOWN, DEFAULT_SHOW_HIDDEN_PROFILES_NOTICE, DEFAULT_SHOW_MUTE_PROFILE_ALERT, @@ -263,6 +266,10 @@ struct SettingsView: View { @EnvironmentObject var theme: AppTheme @State private var showProgress: Bool = false + @Binding var currUserServers: [UserOperatorServers] + @Binding var userServers: [UserOperatorServers] + @Binding var serverErrors: [UserServersError] + var body: some View { ZStack { settingsView() @@ -289,9 +296,13 @@ struct SettingsView: View { .disabled(chatModel.chatRunning != true) NavigationLink { - NetworkAndServers() - .navigationTitle("Network & servers") - .modifier(ThemedBackground(grouped: true)) + NetworkAndServers( + currUserServers: $currUserServers, + userServers: $userServers, + serverErrors: $serverErrors + ) + .navigationTitle("Network & servers") + .modifier(ThemedBackground(grouped: true)) } label: { settingsRow("externaldrive.connected.to.line.below", color: theme.colors.secondary) { Text("Network & servers") } } @@ -356,7 +367,7 @@ struct SettingsView: View { } } NavigationLink { - WhatsNewView(viaSettings: true) + WhatsNewView(viaSettings: true, showWhatsNew: true, showOperatorsNotice: false) .modifier(ThemedBackground()) .navigationBarTitleDisplayMode(.inline) } label: { @@ -525,7 +536,11 @@ struct SettingsView_Previews: PreviewProvider { static var previews: some View { let chatModel = ChatModel() chatModel.currentUser = User.sampleData - return SettingsView() - .environmentObject(chatModel) + return SettingsView( + currUserServers: Binding.constant([UserOperatorServers.sampleData1, UserOperatorServers.sampleDataNilOperator]), + userServers: Binding.constant([UserOperatorServers.sampleData1, UserOperatorServers.sampleDataNilOperator]), + serverErrors: Binding.constant([]) + ) + .environmentObject(chatModel) } } diff --git a/apps/ios/Shared/Views/UserSettings/UserAddressLearnMore.swift b/apps/ios/Shared/Views/UserSettings/UserAddressLearnMore.swift index 15f6a1c7d7..d4bc0959c9 100644 --- a/apps/ios/Shared/Views/UserSettings/UserAddressLearnMore.swift +++ b/apps/ios/Shared/Views/UserSettings/UserAddressLearnMore.swift @@ -9,15 +9,47 @@ import SwiftUI struct UserAddressLearnMore: View { + @State var showCreateAddressButton = false + @State private var createAddressLinkActive = false + var body: some View { - List { - VStack(alignment: .leading, spacing: 18) { - Text("You can share your address as a link or QR code - anybody can connect to you.") - Text("You won't lose your contacts if you later delete your address.") - Text("When people request to connect, you can accept or reject it.") - Text("Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address).") + VStack { + List { + VStack(alignment: .leading, spacing: 18) { + Text("You can share your address as a link or QR code - anybody can connect to you.") + Text("You won't lose your contacts if you later delete your address.") + Text("When people request to connect, you can accept or reject it.") + Text("Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address).") + } + .listRowBackground(Color.clear) } - .listRowBackground(Color.clear) + .frame(maxHeight: .infinity) + + if showCreateAddressButton { + addressCreationButton() + .padding() + } + } + } + + private func addressCreationButton() -> some View { + ZStack { + Button { + createAddressLinkActive = true + } label: { + Text("Create SimpleX address") + } + .buttonStyle(OnboardingButtonStyle()) + + NavigationLink(isActive: $createAddressLinkActive) { + UserAddressView(autoCreate: true) + .navigationTitle("SimpleX address") + .navigationBarTitleDisplayMode(.large) + } label: { + EmptyView() + } + .frame(width: 1, height: 1) + .hidden() } } } diff --git a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift index 2469dc59db..cbc3e9b79e 100644 --- a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift +++ b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift @@ -15,6 +15,7 @@ struct UserAddressView: View { @EnvironmentObject private var chatModel: ChatModel @EnvironmentObject var theme: AppTheme @State var shareViaProfile = false + @State var autoCreate = false @State private var aas = AutoAcceptState() @State private var savedAAS = AutoAcceptState() @State private var ignoreShareViaProfileChange = false @@ -67,6 +68,11 @@ struct UserAddressView: View { } } } + .onAppear { + if chatModel.userAddress == nil, autoCreate { + createAddress() + } + } } @Namespace private var bottomID @@ -212,26 +218,30 @@ struct UserAddressView: View { private func createAddressButton() -> some View { Button { - progressIndicator = true - Task { - do { - let connReqContact = try await apiCreateUserAddress() - DispatchQueue.main.async { - chatModel.userAddress = UserContactLink(connReqContact: connReqContact) - alert = .shareOnCreate - progressIndicator = false - } - } catch let error { - logger.error("UserAddressView apiCreateUserAddress: \(responseError(error))") - let a = getErrorAlert(error, "Error creating address") - alert = .error(title: a.title, error: a.message) - await MainActor.run { progressIndicator = false } - } - } + createAddress() } label: { Label("Create SimpleX address", systemImage: "qrcode") } } + + private func createAddress() { + progressIndicator = true + Task { + do { + let connReqContact = try await apiCreateUserAddress() + DispatchQueue.main.async { + chatModel.userAddress = UserContactLink(connReqContact: connReqContact) + alert = .shareOnCreate + progressIndicator = false + } + } catch let error { + logger.error("UserAddressView apiCreateUserAddress: \(responseError(error))") + let a = getErrorAlert(error, "Error creating address") + alert = .error(title: a.title, error: a.message) + await MainActor.run { progressIndicator = false } + } + } + } private func deleteAddressButton() -> some View { Button(role: .destructive) { diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 8a63cd3309..8dc195e17f 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -144,20 +144,22 @@ 5CFE0922282EEAF60002594B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */; }; 640417CD2B29B8C200CCB412 /* NewChatMenuButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 640417CB2B29B8C200CCB412 /* NewChatMenuButton.swift */; }; 640417CE2B29B8C200CCB412 /* NewChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 640417CC2B29B8C200CCB412 /* NewChatView.swift */; }; + 640743612CD360E600158442 /* ChooseServerOperators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 640743602CD360E600158442 /* ChooseServerOperators.swift */; }; 6407BA83295DA85D0082BA18 /* CIInvalidJSONView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6407BA82295DA85D0082BA18 /* CIInvalidJSONView.swift */; }; 6419EC562AB8BC8B004A607A /* ContextInvitingContactMemberView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6419EC552AB8BC8B004A607A /* ContextInvitingContactMemberView.swift */; }; 6419EC582AB97507004A607A /* CIMemberCreatedContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6419EC572AB97507004A607A /* CIMemberCreatedContactView.swift */; }; + 642BA82D2CE50495005E9412 /* NewServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 642BA82C2CE50495005E9412 /* NewServerView.swift */; }; + 642BA8332CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.0-1FOg7s6V4oE9PrQV6sHPkH.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 642BA82E2CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.0-1FOg7s6V4oE9PrQV6sHPkH.a */; }; + 642BA8342CEB3D4B005E9412 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 642BA82F2CEB3D4B005E9412 /* libffi.a */; }; + 642BA8352CEB3D4B005E9412 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 642BA8302CEB3D4B005E9412 /* libgmp.a */; }; + 642BA8362CEB3D4B005E9412 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 642BA8312CEB3D4B005E9412 /* libgmpxx.a */; }; + 642BA8372CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.0-1FOg7s6V4oE9PrQV6sHPkH-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 642BA8322CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.0-1FOg7s6V4oE9PrQV6sHPkH-ghc9.6.3.a */; }; 6432857C2925443C00FBE5C8 /* GroupPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */; }; - 643B3B452CCBEB080083A2CF /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643B3B402CCBEB080083A2CF /* libgmpxx.a */; }; - 643B3B462CCBEB080083A2CF /* libHSsimplex-chat-6.2.0.0-ICCEDdz3q5b2XylcUTCFFs-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643B3B412CCBEB080083A2CF /* libHSsimplex-chat-6.2.0.0-ICCEDdz3q5b2XylcUTCFFs-ghc9.6.3.a */; }; - 643B3B472CCBEB080083A2CF /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643B3B422CCBEB080083A2CF /* libffi.a */; }; - 643B3B482CCBEB080083A2CF /* libHSsimplex-chat-6.2.0.0-ICCEDdz3q5b2XylcUTCFFs.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643B3B432CCBEB080083A2CF /* libHSsimplex-chat-6.2.0.0-ICCEDdz3q5b2XylcUTCFFs.a */; }; - 643B3B492CCBEB080083A2CF /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643B3B442CCBEB080083A2CF /* libgmp.a */; }; + 643B3B4E2CCFD6400083A2CF /* OperatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 643B3B4D2CCFD6400083A2CF /* OperatorView.swift */; }; 6440CA00288857A10062C672 /* CIEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440C9FF288857A10062C672 /* CIEventView.swift */; }; 6440CA03288AECA70062C672 /* AddGroupMembersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440CA02288AECA70062C672 /* AddGroupMembersView.swift */; }; 6442E0BA287F169300CEC0F9 /* AddGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6442E0B9287F169300CEC0F9 /* AddGroupView.swift */; }; 6442E0BE2880182D00CEC0F9 /* GroupChatInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6442E0BD2880182D00CEC0F9 /* GroupChatInfoView.swift */; }; - 64466DC829FC2B3B00E3D48D /* CreateSimpleXAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64466DC729FC2B3B00E3D48D /* CreateSimpleXAddress.swift */; }; 64466DCC29FFE3E800E3D48D /* MailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64466DCB29FFE3E800E3D48D /* MailView.swift */; }; 6448BBB628FA9D56000D2AB9 /* GroupLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6448BBB528FA9D56000D2AB9 /* GroupLinkView.swift */; }; 644EFFDE292BCD9D00525D5B /* ComposeVoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EFFDD292BCD9D00525D5B /* ComposeVoiceView.swift */; }; @@ -200,7 +202,9 @@ 8CC4ED902BD7B8530078AEE8 /* CallAudioDeviceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CC4ED8F2BD7B8530078AEE8 /* CallAudioDeviceManager.swift */; }; 8CC956EE2BC0041000412A11 /* NetworkObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CC956ED2BC0041000412A11 /* NetworkObserver.swift */; }; 8CE848A32C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CE848A22C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift */; }; + B73EFE532CE5FA3500C778EA /* CreateSimpleXAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = B73EFE522CE5FA3500C778EA /* CreateSimpleXAddress.swift */; }; B76E6C312C5C41D900EC11AA /* ContactListNavLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = B76E6C302C5C41D900EC11AA /* ContactListNavLink.swift */; }; + B79ADAFF2CE4EF930083DFFD /* AddressCreationCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = B79ADAFE2CE4EF930083DFFD /* AddressCreationCard.swift */; }; CE176F202C87014C00145DBC /* InvertedForegroundStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE176F1F2C87014C00145DBC /* InvertedForegroundStyle.swift */; }; CE1EB0E42C459A660099D896 /* ShareAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1EB0E32C459A660099D896 /* ShareAPI.swift */; }; CE2AD9CE2C452A4D00E844E3 /* ChatUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2AD9CD2C452A4D00E844E3 /* ChatUtils.swift */; }; @@ -436,7 +440,7 @@ 5CB634AC29E46CF70066AD6B /* LocalAuthView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAuthView.swift; sourceTree = ""; }; 5CB634AE29E4BB7D0066AD6B /* SetAppPasscodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetAppPasscodeView.swift; sourceTree = ""; }; 5CB634B029E5EFEA0066AD6B /* PasscodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasscodeView.swift; sourceTree = ""; }; - 5CB924D627A8563F00ACCCDD /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; + 5CB924D627A8563F00ACCCDD /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; wrapsLines = 0; }; 5CB924E027A867BA00ACCCDD /* UserProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfile.swift; sourceTree = ""; }; 5CB9250C27A9432000ACCCDD /* ChatListNavLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListNavLink.swift; sourceTree = ""; }; 5CBD285529565CAE00EC2CF4 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; @@ -487,20 +491,22 @@ 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ZoomableScrollView.swift; path = Shared/Views/ZoomableScrollView.swift; sourceTree = SOURCE_ROOT; }; 640417CB2B29B8C200CCB412 /* NewChatMenuButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewChatMenuButton.swift; sourceTree = ""; }; 640417CC2B29B8C200CCB412 /* NewChatView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewChatView.swift; sourceTree = ""; }; + 640743602CD360E600158442 /* ChooseServerOperators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChooseServerOperators.swift; sourceTree = ""; }; 6407BA82295DA85D0082BA18 /* CIInvalidJSONView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIInvalidJSONView.swift; sourceTree = ""; }; 6419EC552AB8BC8B004A607A /* ContextInvitingContactMemberView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextInvitingContactMemberView.swift; sourceTree = ""; }; 6419EC572AB97507004A607A /* CIMemberCreatedContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIMemberCreatedContactView.swift; sourceTree = ""; }; + 642BA82C2CE50495005E9412 /* NewServerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewServerView.swift; sourceTree = ""; }; + 642BA82E2CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.0-1FOg7s6V4oE9PrQV6sHPkH.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.2.0.0-1FOg7s6V4oE9PrQV6sHPkH.a"; sourceTree = ""; }; + 642BA82F2CEB3D4B005E9412 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; + 642BA8302CEB3D4B005E9412 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; + 642BA8312CEB3D4B005E9412 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; + 642BA8322CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.0-1FOg7s6V4oE9PrQV6sHPkH-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.2.0.0-1FOg7s6V4oE9PrQV6sHPkH-ghc9.6.3.a"; sourceTree = ""; }; 6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupPreferencesView.swift; sourceTree = ""; }; - 643B3B402CCBEB080083A2CF /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libgmpxx.a; path = Libraries/libgmpxx.a; sourceTree = ""; }; - 643B3B412CCBEB080083A2CF /* libHSsimplex-chat-6.2.0.0-ICCEDdz3q5b2XylcUTCFFs-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libHSsimplex-chat-6.2.0.0-ICCEDdz3q5b2XylcUTCFFs-ghc9.6.3.a"; path = "Libraries/libHSsimplex-chat-6.2.0.0-ICCEDdz3q5b2XylcUTCFFs-ghc9.6.3.a"; sourceTree = ""; }; - 643B3B422CCBEB080083A2CF /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libffi.a; path = Libraries/libffi.a; sourceTree = ""; }; - 643B3B432CCBEB080083A2CF /* libHSsimplex-chat-6.2.0.0-ICCEDdz3q5b2XylcUTCFFs.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libHSsimplex-chat-6.2.0.0-ICCEDdz3q5b2XylcUTCFFs.a"; path = "Libraries/libHSsimplex-chat-6.2.0.0-ICCEDdz3q5b2XylcUTCFFs.a"; sourceTree = ""; }; - 643B3B442CCBEB080083A2CF /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libgmp.a; path = Libraries/libgmp.a; sourceTree = ""; }; + 643B3B4D2CCFD6400083A2CF /* OperatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperatorView.swift; sourceTree = ""; }; 6440C9FF288857A10062C672 /* CIEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIEventView.swift; sourceTree = ""; }; 6440CA02288AECA70062C672 /* AddGroupMembersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGroupMembersView.swift; sourceTree = ""; }; 6442E0B9287F169300CEC0F9 /* AddGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGroupView.swift; sourceTree = ""; }; 6442E0BD2880182D00CEC0F9 /* GroupChatInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupChatInfoView.swift; sourceTree = ""; }; - 64466DC729FC2B3B00E3D48D /* CreateSimpleXAddress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateSimpleXAddress.swift; sourceTree = ""; }; 64466DCB29FFE3E800E3D48D /* MailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MailView.swift; sourceTree = ""; }; 6448BBB528FA9D56000D2AB9 /* GroupLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupLinkView.swift; sourceTree = ""; }; 644EFFDD292BCD9D00525D5B /* ComposeVoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeVoiceView.swift; sourceTree = ""; }; @@ -544,7 +550,9 @@ 8CC4ED8F2BD7B8530078AEE8 /* CallAudioDeviceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallAudioDeviceManager.swift; sourceTree = ""; }; 8CC956ED2BC0041000412A11 /* NetworkObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkObserver.swift; sourceTree = ""; }; 8CE848A22C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableChatItemToolbars.swift; sourceTree = ""; }; + B73EFE522CE5FA3500C778EA /* CreateSimpleXAddress.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateSimpleXAddress.swift; sourceTree = ""; }; B76E6C302C5C41D900EC11AA /* ContactListNavLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactListNavLink.swift; sourceTree = ""; }; + B79ADAFE2CE4EF930083DFFD /* AddressCreationCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressCreationCard.swift; sourceTree = ""; }; CE176F1F2C87014C00145DBC /* InvertedForegroundStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvertedForegroundStyle.swift; sourceTree = ""; }; CE1EB0E32C459A660099D896 /* ShareAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareAPI.swift; sourceTree = ""; }; CE2AD9CD2C452A4D00E844E3 /* ChatUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatUtils.swift; sourceTree = ""; }; @@ -657,14 +665,14 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 643B3B452CCBEB080083A2CF /* libgmpxx.a in Frameworks */, - 643B3B472CCBEB080083A2CF /* libffi.a in Frameworks */, - 643B3B492CCBEB080083A2CF /* libgmp.a in Frameworks */, 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */, - 643B3B482CCBEB080083A2CF /* libHSsimplex-chat-6.2.0.0-ICCEDdz3q5b2XylcUTCFFs.a in Frameworks */, - 643B3B462CCBEB080083A2CF /* libHSsimplex-chat-6.2.0.0-ICCEDdz3q5b2XylcUTCFFs-ghc9.6.3.a in Frameworks */, + 642BA8342CEB3D4B005E9412 /* libffi.a in Frameworks */, + 642BA8352CEB3D4B005E9412 /* libgmp.a in Frameworks */, + 642BA8372CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.0-1FOg7s6V4oE9PrQV6sHPkH-ghc9.6.3.a in Frameworks */, + 642BA8332CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.0-1FOg7s6V4oE9PrQV6sHPkH.a in Frameworks */, + 642BA8362CEB3D4B005E9412 /* libgmpxx.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -741,6 +749,11 @@ 5C764E5C279C70B7000C6508 /* Libraries */ = { isa = PBXGroup; children = ( + 642BA82F2CEB3D4B005E9412 /* libffi.a */, + 642BA8302CEB3D4B005E9412 /* libgmp.a */, + 642BA8312CEB3D4B005E9412 /* libgmpxx.a */, + 642BA8322CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.0-1FOg7s6V4oE9PrQV6sHPkH-ghc9.6.3.a */, + 642BA82E2CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.0-1FOg7s6V4oE9PrQV6sHPkH.a */, ); path = Libraries; sourceTree = ""; @@ -812,11 +825,6 @@ 5CC2C0FA2809BF11000C35E3 /* Localizable.strings */, 5C422A7C27A9A6FA0097A1E1 /* SimpleX (iOS).entitlements */, 5C764E5C279C70B7000C6508 /* Libraries */, - 643B3B422CCBEB080083A2CF /* libffi.a */, - 643B3B442CCBEB080083A2CF /* libgmp.a */, - 643B3B402CCBEB080083A2CF /* libgmpxx.a */, - 643B3B412CCBEB080083A2CF /* libHSsimplex-chat-6.2.0.0-ICCEDdz3q5b2XylcUTCFFs-ghc9.6.3.a */, - 643B3B432CCBEB080083A2CF /* libHSsimplex-chat-6.2.0.0-ICCEDdz3q5b2XylcUTCFFs.a */, 5CA059C2279559F40002BEB4 /* Shared */, 5CDCAD462818589900503DA2 /* SimpleX NSE */, CEE723A82C3BD3D70009AE93 /* SimpleX SE */, @@ -875,13 +883,15 @@ 5CB0BA8C282711BC00B3292C /* Onboarding */ = { isa = PBXGroup; children = ( + B73EFE522CE5FA3500C778EA /* CreateSimpleXAddress.swift */, 5CB0BA8D2827126500B3292C /* OnboardingView.swift */, 5CB0BA8F282713D900B3292C /* SimpleXInfo.swift */, 5CB0BA992827FD8800B3292C /* HowItWorks.swift */, 5CB0BA91282713FD00B3292C /* CreateProfile.swift */, - 64466DC729FC2B3B00E3D48D /* CreateSimpleXAddress.swift */, 5C9A5BDA2871E05400A5B906 /* SetNotificationsMode.swift */, 5CBD285B29575B8E00EC2CF4 /* WhatsNewView.swift */, + 640743602CD360E600158442 /* ChooseServerOperators.swift */, + B79ADAFE2CE4EF930083DFFD /* AddressCreationCard.swift */, ); path = Onboarding; sourceTree = ""; @@ -1056,8 +1066,10 @@ isa = PBXGroup; children = ( 5C9329402929248A0090FFF9 /* ScanProtocolServer.swift */, + 642BA82C2CE50495005E9412 /* NewServerView.swift */, 5C93293029239BED0090FFF9 /* ProtocolServerView.swift */, 5C93292E29239A170090FFF9 /* ProtocolServersView.swift */, + 643B3B4D2CCFD6400083A2CF /* OperatorView.swift */, 5C9C2DA6289957AE00CC63B1 /* AdvancedNetworkSettings.swift */, 5C9C2DA82899DA6F00CC63B1 /* NetworkAndServers.swift */, ); @@ -1383,10 +1395,12 @@ 64C06EB52A0A4A7C00792D4D /* ChatItemInfoView.swift in Sources */, 640417CE2B29B8C200CCB412 /* NewChatView.swift in Sources */, 6440CA03288AECA70062C672 /* AddGroupMembersView.swift in Sources */, + 640743612CD360E600158442 /* ChooseServerOperators.swift in Sources */, 5C3F1D58284363C400EC8A82 /* PrivacySettings.swift in Sources */, 5C55A923283CEDE600C4E99E /* SoundPlayer.swift in Sources */, 5C93292F29239A170090FFF9 /* ProtocolServersView.swift in Sources */, 5CB924D727A8563F00ACCCDD /* SettingsView.swift in Sources */, + B79ADAFF2CE4EF930083DFFD /* AddressCreationCard.swift in Sources */, 5CEACCE327DE9246000BD591 /* ComposeView.swift in Sources */, E51CC1E62C62085600DB91FE /* OneHandUICard.swift in Sources */, 5C65DAF929D0CC20003CEE45 /* DeveloperView.swift in Sources */, @@ -1413,12 +1427,12 @@ 644EFFE2292D089800525D5B /* FramedCIVoiceView.swift in Sources */, 5C4B3B0A285FB130003915F2 /* DatabaseView.swift in Sources */, 5CB2084F28DA4B4800D024EC /* RTCServers.swift in Sources */, + B73EFE532CE5FA3500C778EA /* CreateSimpleXAddress.swift in Sources */, 5CB634AF29E4BB7D0066AD6B /* SetAppPasscodeView.swift in Sources */, 5C10D88828EED12E00E58BF0 /* ContactConnectionInfo.swift in Sources */, 5CBE6C12294487F7002D9531 /* VerifyCodeView.swift in Sources */, 3CDBCF4227FAE51000354CDD /* ComposeLinkView.swift in Sources */, 648679AB2BC96A74006456E7 /* ChatItemForwardingView.swift in Sources */, - 64466DC829FC2B3B00E3D48D /* CreateSimpleXAddress.swift in Sources */, 3CDBCF4827FF621E00354CDD /* CILinkView.swift in Sources */, 5C7505A827B6D34800BE3227 /* ChatInfoToolbar.swift in Sources */, B76E6C312C5C41D900EC11AA /* ContactListNavLink.swift in Sources */, @@ -1536,7 +1550,9 @@ 5CB634AD29E46CF70066AD6B /* LocalAuthView.swift in Sources */, 18415FEFE153C5920BFB7828 /* GroupWelcomeView.swift in Sources */, 18415F9A2D551F9757DA4654 /* CIVideoView.swift in Sources */, + 642BA82D2CE50495005E9412 /* NewServerView.swift in Sources */, 184158C131FDB829D8A117EA /* VideoPlayerView.swift in Sources */, + 643B3B4E2CCFD6400083A2CF /* OperatorView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 3c9b91fa0b..5470059e92 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -72,9 +72,15 @@ public enum ChatCommand { case apiGetGroupLink(groupId: Int64) case apiCreateMemberContact(groupId: Int64, groupMemberId: Int64) case apiSendMemberContactInvitation(contactId: Int64, msg: MsgContent) - case apiGetUserProtoServers(userId: Int64, serverProtocol: ServerProtocol) - case apiSetUserProtoServers(userId: Int64, serverProtocol: ServerProtocol, servers: [ServerCfg]) case apiTestProtoServer(userId: Int64, server: String) + case apiGetServerOperators + case apiSetServerOperators(operators: [ServerOperator]) + case apiGetUserServers(userId: Int64) + case apiSetUserServers(userId: Int64, userServers: [UserOperatorServers]) + case apiValidateServers(userId: Int64, userServers: [UserOperatorServers]) + case apiGetUsageConditions + case apiSetConditionsNotified(conditionsId: Int64) + case apiAcceptConditions(conditionsId: Int64, operatorIds: [Int64]) case apiSetChatItemTTL(userId: Int64, seconds: Int64?) case apiGetChatItemTTL(userId: Int64) case apiSetNetworkConfig(networkConfig: NetCfg) @@ -231,9 +237,15 @@ public enum ChatCommand { case let .apiGetGroupLink(groupId): return "/_get link #\(groupId)" case let .apiCreateMemberContact(groupId, groupMemberId): return "/_create member contact #\(groupId) \(groupMemberId)" case let .apiSendMemberContactInvitation(contactId, mc): return "/_invite member contact @\(contactId) \(mc.cmdString)" - case let .apiGetUserProtoServers(userId, serverProtocol): return "/_servers \(userId) \(serverProtocol)" - case let .apiSetUserProtoServers(userId, serverProtocol, servers): return "/_servers \(userId) \(serverProtocol) \(protoServersStr(servers))" case let .apiTestProtoServer(userId, server): return "/_server test \(userId) \(server)" + case .apiGetServerOperators: return "/_operators" + case let .apiSetServerOperators(operators): return "/_operators \(encodeJSON(operators))" + case let .apiGetUserServers(userId): return "/_servers \(userId)" + case let .apiSetUserServers(userId, userServers): return "/_servers \(userId) \(encodeJSON(userServers))" + case let .apiValidateServers(userId, userServers): return "/_validate_servers \(userId) \(encodeJSON(userServers))" + case .apiGetUsageConditions: return "/_conditions" + case let .apiSetConditionsNotified(conditionsId): return "/_conditions_notified \(conditionsId)" + case let .apiAcceptConditions(conditionsId, operatorIds): return "/_accept_conditions \(conditionsId) \(joinedIds(operatorIds))" case let .apiSetChatItemTTL(userId, seconds): return "/_ttl \(userId) \(chatItemTTLStr(seconds: seconds))" case let .apiGetChatItemTTL(userId): return "/_ttl \(userId)" case let .apiSetNetworkConfig(networkConfig): return "/_network \(encodeJSON(networkConfig))" @@ -386,9 +398,15 @@ public enum ChatCommand { case .apiGetGroupLink: return "apiGetGroupLink" case .apiCreateMemberContact: return "apiCreateMemberContact" case .apiSendMemberContactInvitation: return "apiSendMemberContactInvitation" - case .apiGetUserProtoServers: return "apiGetUserProtoServers" - case .apiSetUserProtoServers: return "apiSetUserProtoServers" case .apiTestProtoServer: return "apiTestProtoServer" + case .apiGetServerOperators: return "apiGetServerOperators" + case .apiSetServerOperators: return "apiSetServerOperators" + case .apiGetUserServers: return "apiGetUserServers" + case .apiSetUserServers: return "apiSetUserServers" + case .apiValidateServers: return "apiValidateServers" + case .apiGetUsageConditions: return "apiGetUsageConditions" + case .apiSetConditionsNotified: return "apiSetConditionsNotified" + case .apiAcceptConditions: return "apiAcceptConditions" case .apiSetChatItemTTL: return "apiSetChatItemTTL" case .apiGetChatItemTTL: return "apiGetChatItemTTL" case .apiSetNetworkConfig: return "apiSetNetworkConfig" @@ -475,10 +493,6 @@ public enum ChatCommand { func joinedIds(_ ids: [Int64]) -> String { ids.map { "\($0)" }.joined(separator: ",") } - - func protoServersStr(_ servers: [ServerCfg]) -> String { - encodeJSON(ProtoServersConfig(servers: servers)) - } func chatItemTTLStr(seconds: Int64?) -> String { if let seconds = seconds { @@ -548,8 +562,11 @@ public enum ChatResponse: Decodable, Error { case apiChats(user: UserRef, chats: [ChatData]) case apiChat(user: UserRef, chat: ChatData) case chatItemInfo(user: UserRef, chatItem: AChatItem, chatItemInfo: ChatItemInfo) - case userProtoServers(user: UserRef, servers: UserProtoServers) case serverTestResult(user: UserRef, testServer: String, testFailure: ProtocolTestFailure?) + case serverOperatorConditions(conditions: ServerOperatorConditions) + case userServers(user: UserRef, userServers: [UserOperatorServers]) + case userServersValidation(user: UserRef, serverErrors: [UserServersError]) + case usageConditions(usageConditions: UsageConditions, conditionsText: String, acceptedConditions: UsageConditions?) case chatItemTTL(user: UserRef, chatItemTTL: Int64?) case networkConfig(networkConfig: NetCfg) case contactInfo(user: UserRef, contact: Contact, connectionStats_: ConnectionStats?, customUserProfile: Profile?) @@ -721,8 +738,11 @@ public enum ChatResponse: Decodable, Error { case .apiChats: return "apiChats" case .apiChat: return "apiChat" case .chatItemInfo: return "chatItemInfo" - case .userProtoServers: return "userProtoServers" case .serverTestResult: return "serverTestResult" + case .serverOperatorConditions: return "serverOperators" + case .userServers: return "userServers" + case .userServersValidation: return "userServersValidation" + case .usageConditions: return "usageConditions" case .chatItemTTL: return "chatItemTTL" case .networkConfig: return "networkConfig" case .contactInfo: return "contactInfo" @@ -890,8 +910,11 @@ public enum ChatResponse: Decodable, Error { case let .apiChats(u, chats): return withUser(u, String(describing: chats)) case let .apiChat(u, chat): return withUser(u, String(describing: chat)) case let .chatItemInfo(u, chatItem, chatItemInfo): return withUser(u, "chatItem: \(String(describing: chatItem))\nchatItemInfo: \(String(describing: chatItemInfo))") - case let .userProtoServers(u, servers): return withUser(u, "servers: \(String(describing: servers))") case let .serverTestResult(u, server, testFailure): return withUser(u, "server: \(server)\nresult: \(String(describing: testFailure))") + case let .serverOperatorConditions(conditions): return "conditions: \(String(describing: conditions))" + case let .userServers(u, userServers): return withUser(u, "userServers: \(String(describing: userServers))") + case let .userServersValidation(u, serverErrors): return withUser(u, "serverErrors: \(String(describing: serverErrors))") + case let .usageConditions(usageConditions, _, acceptedConditions): return "usageConditions: \(String(describing: usageConditions))\nacceptedConditions: \(String(describing: acceptedConditions))" case let .chatItemTTL(u, chatItemTTL): return withUser(u, String(describing: chatItemTTL)) case let .networkConfig(networkConfig): return String(describing: networkConfig) case let .contactInfo(u, contact, connectionStats_, customUserProfile): return withUser(u, "contact: \(String(describing: contact))\nconnectionStats_: \(String(describing: connectionStats_))\ncustomUserProfile: \(String(describing: customUserProfile))") @@ -1175,86 +1198,426 @@ public struct DBEncryptionConfig: Codable { public var newKey: String } -struct SMPServersConfig: Encodable { - var smpServers: [ServerCfg] -} - public enum ServerProtocol: String, Decodable { case smp case xftp } -public struct ProtoServersConfig: Codable { - public var servers: [ServerCfg] +public enum OperatorTag: String, Codable { + case simplex = "simplex" + case flux = "flux" + case xyz = "xyz" + case demo = "demo" } -public struct UserProtoServers: Decodable { - public var serverProtocol: ServerProtocol - public var protoServers: [ServerCfg] - public var presetServers: [ServerCfg] +public struct ServerOperatorInfo: Decodable { + public var description: [String] + public var website: String + public var logo: String + public var largeLogo: String + public var logoDarkMode: String + public var largeLogoDarkMode: String } -public struct ServerCfg: Identifiable, Equatable, Codable, Hashable { +public let operatorsInfo: Dictionary = [ + .simplex: ServerOperatorInfo( + description: ["SimpleX Chat preset servers"], + website: "https://simplex.chat", + logo: "decentralized", + largeLogo: "logo", + logoDarkMode: "decentralized-light", + largeLogoDarkMode: "logo-light" + ), + .flux: ServerOperatorInfo( + description: [ + "Flux is the largest decentralized cloud infrastructure, leveraging a global network of user-operated computational nodes.", + "Flux offers a powerful, scalable, and affordable platform designed to support individuals, businesses, and cutting-edge technologies like AI. With high uptime and worldwide distribution, Flux ensures reliable, accessible cloud computing for all." + ], + website: "https://runonflux.com", + logo: "flux_logo_symbol", + largeLogo: "flux_logo", + logoDarkMode: "flux_logo_symbol", + largeLogoDarkMode: "flux_logo-light" + ), + .xyz: ServerOperatorInfo( + description: ["XYZ servers"], + website: "XYZ website", + logo: "shield", + largeLogo: "logo", + logoDarkMode: "shield", + largeLogoDarkMode: "logo-light" + ), + .demo: ServerOperatorInfo( + description: ["Demo operator"], + website: "Demo website", + logo: "decentralized", + largeLogo: "logo", + logoDarkMode: "decentralized-light", + largeLogoDarkMode: "logo-light" + ) +] + +public struct UsageConditions: Decodable { + public var conditionsId: Int64 + public var conditionsCommit: String + public var notifiedAt: Date? + public var createdAt: Date + + public static var sampleData = UsageConditions( + conditionsId: 1, + conditionsCommit: "11a44dc1fd461a93079f897048b46998db55da5c", + notifiedAt: nil, + createdAt: Date.now + ) +} + +public enum UsageConditionsAction: Decodable { + case review(operators: [ServerOperator], deadline: Date?, showNotice: Bool) + case accepted(operators: [ServerOperator]) + + public var showNotice: Bool { + switch self { + case let .review(_, _, showNotice): showNotice + case .accepted: false + } + } +} + +public struct ServerOperatorConditions: Decodable { + public var serverOperators: [ServerOperator] + public var currentConditions: UsageConditions + public var conditionsAction: UsageConditionsAction? + + public static var empty = ServerOperatorConditions( + serverOperators: [], + currentConditions: UsageConditions(conditionsId: 0, conditionsCommit: "empty", notifiedAt: nil, createdAt: .now), + conditionsAction: nil + ) +} + +public enum ConditionsAcceptance: Equatable, Codable, Hashable { + case accepted(acceptedAt: Date?) + // If deadline is present, it means there's a grace period to review and accept conditions during which user can continue to use the operator. + // No deadline indicates it's required to accept conditions for the operator to start using it. + case required(deadline: Date?) + + public var conditionsAccepted: Bool { + switch self { + case .accepted: true + case .required: false + } + } + + public var usageAllowed: Bool { + switch self { + case .accepted: true + case let .required(deadline): deadline != nil + } + } +} + +public struct ServerOperator: Identifiable, Equatable, Codable { + public var operatorId: Int64 + public var operatorTag: OperatorTag? + public var tradeName: String + public var legalName: String? + public var serverDomains: [String] + public var conditionsAcceptance: ConditionsAcceptance + public var enabled: Bool + public var smpRoles: ServerRoles + public var xftpRoles: ServerRoles + + public var id: Int64 { operatorId } + + public static func == (l: ServerOperator, r: ServerOperator) -> Bool { + l.operatorId == r.operatorId && l.operatorTag == r.operatorTag && l.tradeName == r.tradeName && l.legalName == r.legalName && + l.serverDomains == r.serverDomains && l.conditionsAcceptance == r.conditionsAcceptance && l.enabled == r.enabled && + l.smpRoles == r.smpRoles && l.xftpRoles == r.xftpRoles + } + + public var legalName_: String { + legalName ?? tradeName + } + + public var info: ServerOperatorInfo { + return if let operatorTag = operatorTag { + operatorsInfo[operatorTag] ?? ServerOperator.dummyOperatorInfo + } else { + ServerOperator.dummyOperatorInfo + } + } + + public static let dummyOperatorInfo = ServerOperatorInfo( + description: ["Default"], + website: "Default", + logo: "decentralized", + largeLogo: "logo", + logoDarkMode: "decentralized-light", + largeLogoDarkMode: "logo-light" + ) + + public func logo(_ colorScheme: ColorScheme) -> String { + colorScheme == .light ? info.logo : info.logoDarkMode + } + + public func largeLogo(_ colorScheme: ColorScheme) -> String { + colorScheme == .light ? info.largeLogo : info.largeLogoDarkMode + } + + public static var sampleData1 = ServerOperator( + operatorId: 1, + operatorTag: .simplex, + tradeName: "SimpleX Chat", + legalName: "SimpleX Chat Ltd", + serverDomains: ["simplex.im"], + conditionsAcceptance: .accepted(acceptedAt: nil), + enabled: true, + smpRoles: ServerRoles(storage: true, proxy: true), + xftpRoles: ServerRoles(storage: true, proxy: true) + ) + + public static var sampleData2 = ServerOperator( + operatorId: 2, + operatorTag: .xyz, + tradeName: "XYZ", + legalName: nil, + serverDomains: ["xyz.com"], + conditionsAcceptance: .required(deadline: nil), + enabled: false, + smpRoles: ServerRoles(storage: false, proxy: true), + xftpRoles: ServerRoles(storage: false, proxy: true) + ) + + public static var sampleData3 = ServerOperator( + operatorId: 3, + operatorTag: .demo, + tradeName: "Demo", + legalName: nil, + serverDomains: ["demo.com"], + conditionsAcceptance: .required(deadline: nil), + enabled: false, + smpRoles: ServerRoles(storage: true, proxy: false), + xftpRoles: ServerRoles(storage: true, proxy: false) + ) +} + +public struct ServerRoles: Equatable, Codable { + public var storage: Bool + public var proxy: Bool +} + +public struct UserOperatorServers: Identifiable, Equatable, Codable { + public var `operator`: ServerOperator? + public var smpServers: [UserServer] + public var xftpServers: [UserServer] + + public var id: String { + if let op = self.operator { + "\(op.operatorId)" + } else { + "nil operator" + } + } + + public var operator_: ServerOperator { + get { + self.operator ?? ServerOperator( + operatorId: 0, + operatorTag: nil, + tradeName: "", + legalName: "", + serverDomains: [], + conditionsAcceptance: .accepted(acceptedAt: nil), + enabled: false, + smpRoles: ServerRoles(storage: true, proxy: true), + xftpRoles: ServerRoles(storage: true, proxy: true) + ) + } + set { `operator` = newValue } + } + + public static var sampleData1 = UserOperatorServers( + operator: ServerOperator.sampleData1, + smpServers: [UserServer.sampleData.preset], + xftpServers: [UserServer.sampleData.xftpPreset] + ) + + public static var sampleDataNilOperator = UserOperatorServers( + operator: nil, + smpServers: [UserServer.sampleData.preset], + xftpServers: [UserServer.sampleData.xftpPreset] + ) +} + +public enum UserServersError: Decodable { + case noServers(protocol: ServerProtocol, user: UserRef?) + case storageMissing(protocol: ServerProtocol, user: UserRef?) + case proxyMissing(protocol: ServerProtocol, user: UserRef?) + case invalidServer(protocol: ServerProtocol, invalidServer: String) + case duplicateServer(protocol: ServerProtocol, duplicateServer: String, duplicateHost: String) + + public var globalError: String? { + switch self { + case let .noServers(`protocol`, _): + switch `protocol` { + case .smp: return globalSMPError + case .xftp: return globalXFTPError + } + case let .storageMissing(`protocol`, _): + switch `protocol` { + case .smp: return globalSMPError + case .xftp: return globalXFTPError + } + case let .proxyMissing(`protocol`, _): + switch `protocol` { + case .smp: return globalSMPError + case .xftp: return globalXFTPError + } + default: return nil + } + } + + public var globalSMPError: String? { + switch self { + case let .noServers(.smp, user): + let text = NSLocalizedString("No message servers.", comment: "servers error") + if let user = user { + return userStr(user) + " " + text + } else { + return text + } + case let .storageMissing(.smp, user): + let text = NSLocalizedString("No servers to receive messages.", comment: "servers error") + if let user = user { + return userStr(user) + " " + text + } else { + return text + } + case let .proxyMissing(.smp, user): + let text = NSLocalizedString("No servers for private message routing.", comment: "servers error") + if let user = user { + return userStr(user) + " " + text + } else { + return text + } + default: + return nil + } + } + + public var globalXFTPError: String? { + switch self { + case let .noServers(.xftp, user): + let text = NSLocalizedString("No media & file servers.", comment: "servers error") + if let user = user { + return userStr(user) + " " + text + } else { + return text + } + case let .storageMissing(.xftp, user): + let text = NSLocalizedString("No servers to send files.", comment: "servers error") + if let user = user { + return userStr(user) + " " + text + } else { + return text + } + case let .proxyMissing(.xftp, user): + let text = NSLocalizedString("No servers to receive files.", comment: "servers error") + if let user = user { + return userStr(user) + " " + text + } else { + return text + } + default: + return nil + } + } + + private func userStr(_ user: UserRef) -> String { + String.localizedStringWithFormat(NSLocalizedString("For chat profile %@:", comment: "servers error"), user.localDisplayName) + } +} + +public struct UserServer: Identifiable, Equatable, Codable, Hashable { + public var serverId: Int64? public var server: String public var preset: Bool public var tested: Bool? public var enabled: Bool + public var deleted: Bool var createdAt = Date() -// public var sendEnabled: Bool // can we potentially want to prevent sending on the servers we use to receive? -// Even if we don't see the use case, it's probably better to allow it in the model -// In any case, "trusted/known" servers are out of scope of this change - public init(server: String, preset: Bool, tested: Bool?, enabled: Bool) { + public init(serverId: Int64?, server: String, preset: Bool, tested: Bool?, enabled: Bool, deleted: Bool) { + self.serverId = serverId self.server = server self.preset = preset self.tested = tested self.enabled = enabled + self.deleted = deleted } - public static func == (l: ServerCfg, r: ServerCfg) -> Bool { - l.server == r.server && l.preset == r.preset && l.tested == r.tested && l.enabled == r.enabled + public static func == (l: UserServer, r: UserServer) -> Bool { + l.serverId == r.serverId && l.server == r.server && l.preset == r.preset && l.tested == r.tested && + l.enabled == r.enabled && l.deleted == r.deleted } public var id: String { "\(server) \(createdAt)" } - public static var empty = ServerCfg(server: "", preset: false, tested: nil, enabled: false) + public static var empty = UserServer(serverId: nil, server: "", preset: false, tested: nil, enabled: false, deleted: false) public var isEmpty: Bool { server.trimmingCharacters(in: .whitespaces) == "" } public struct SampleData { - public var preset: ServerCfg - public var custom: ServerCfg - public var untested: ServerCfg + public var preset: UserServer + public var custom: UserServer + public var untested: UserServer + public var xftpPreset: UserServer } public static var sampleData = SampleData( - preset: ServerCfg( + preset: UserServer( + serverId: 1, server: "smp://abcd@smp8.simplex.im", preset: true, tested: true, - enabled: true + enabled: true, + deleted: false ), - custom: ServerCfg( + custom: UserServer( + serverId: 2, server: "smp://abcd@smp9.simplex.im", preset: false, tested: false, - enabled: false + enabled: false, + deleted: false ), - untested: ServerCfg( + untested: UserServer( + serverId: 3, server: "smp://abcd@smp10.simplex.im", preset: false, tested: nil, - enabled: true + enabled: true, + deleted: false + ), + xftpPreset: UserServer( + serverId: 4, + server: "xftp://abcd@xftp8.simplex.im", + preset: true, + tested: true, + enabled: true, + deleted: false ) ) enum CodingKeys: CodingKey { + case serverId case server case preset case tested case enabled + case deleted } } From 4b9c618ae36b7751217d59cbfb65352c7289c8d1 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Tue, 19 Nov 2024 14:10:33 +0000 Subject: [PATCH 033/167] core: remove a separate type to validate servers with invalid addresses (they are prevented by the UI) (#5211) --- src/Simplex/Chat/Controller.hs | 2 +- src/Simplex/Chat/Operators.hs | 67 +++++++--------------------------- tests/OperatorTests.hs | 13 ------- 3 files changed, 15 insertions(+), 67 deletions(-) diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index b6229e07ba..e44ea2ac18 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -360,7 +360,7 @@ data ChatCommand | APISetServerOperators (NonEmpty ServerOperator) | APIGetUserServers UserId | APISetUserServers UserId (NonEmpty UpdatedUserOperatorServers) - | APIValidateServers UserId [ValidatedUserOperatorServers] -- response is CRUserServersValidation + | APIValidateServers UserId [UpdatedUserOperatorServers] -- response is CRUserServersValidation | APIGetUsageConditions | APISetConditionsNotified Int64 | APIAcceptConditions Int64 (NonEmpty Int64) diff --git a/src/Simplex/Chat/Operators.hs b/src/Simplex/Chat/Operators.hs index 1f9b79b56b..ebe1da8176 100644 --- a/src/Simplex/Chat/Operators.hs +++ b/src/Simplex/Chat/Operators.hs @@ -24,7 +24,6 @@ import Data.Aeson (FromJSON (..), ToJSON (..)) import qualified Data.Aeson as J import qualified Data.Aeson.Encoding as JE import qualified Data.Aeson.TH as JQ -import Data.Either (partitionEithers) import Data.FileEmbed import Data.Foldable (foldMap') import Data.Functor.Identity @@ -217,32 +216,19 @@ data UpdatedUserOperatorServers = UpdatedUserOperatorServers } deriving (Show) -data ValidatedUserOperatorServers = ValidatedUserOperatorServers - { operator :: Maybe ServerOperator, - smpServers :: [AValidatedServer 'PSMP], - xftpServers :: [AValidatedServer 'PXFTP] - } - deriving (Show) - -data AValidatedServer p = forall s. AVS (SDBStored s) (ValidatedServer s p) - -deriving instance Show (AValidatedServer p) - -type ValidatedServer s p = UserServer_ s ValidatedProtoServer p - data ValidatedProtoServer p = ValidatedProtoServer {unVPS :: Either Text (ProtoServerWithAuth p)} deriving (Show) class UserServersClass u where type AServer u = (s :: ProtocolType -> Type) | s -> u operator' :: u -> Maybe ServerOperator - partitionValid :: [AServer u p] -> ([Text], [AUserServer p]) + aUserServer' :: AServer u p -> AUserServer p servers' :: UserProtocol p => SProtocolType p -> u -> [AServer u p] instance UserServersClass UserOperatorServers where - type AServer UserOperatorServers = UserServer_ 'DBStored ProtoServerWithAuth + type AServer UserOperatorServers = UserServer' 'DBStored operator' UserOperatorServers {operator} = operator - partitionValid ss = ([], map (AUS SDBStored) ss) + aUserServer' = AUS SDBStored servers' p UserOperatorServers {smpServers, xftpServers} = case p of SPSMP -> smpServers SPXFTP -> xftpServers @@ -250,24 +236,11 @@ instance UserServersClass UserOperatorServers where instance UserServersClass UpdatedUserOperatorServers where type AServer UpdatedUserOperatorServers = AUserServer operator' UpdatedUserOperatorServers {operator} = operator - partitionValid = ([],) + aUserServer' = id servers' p UpdatedUserOperatorServers {smpServers, xftpServers} = case p of SPSMP -> smpServers SPXFTP -> xftpServers -instance UserServersClass ValidatedUserOperatorServers where - type AServer ValidatedUserOperatorServers = AValidatedServer - operator' ValidatedUserOperatorServers {operator} = operator - partitionValid = partitionEithers . map serverOrErr - where - serverOrErr :: AValidatedServer p -> Either Text (AUserServer p) - serverOrErr (AVS s srv@UserServer {server = server'}) = (\server -> AUS s srv {server}) <$> unVPS server' - servers' p ValidatedUserOperatorServers {smpServers, xftpServers} = case p of - SPSMP -> smpServers - SPXFTP -> xftpServers - -type UserServer' s p = UserServer_ s ProtoServerWithAuth p - type UserServer p = UserServer' 'DBStored p type NewUserServer p = UserServer' 'DBNew p @@ -276,9 +249,9 @@ data AUserServer p = forall s. AUS (SDBStored s) (UserServer' s p) deriving instance Show (AUserServer p) -data UserServer_ s (srv :: ProtocolType -> Type) (p :: ProtocolType) = UserServer +data UserServer' s (p :: ProtocolType) = UserServer { serverId :: DBEntityId' s, - server :: srv p, + server :: ProtoServerWithAuth p, preset :: Bool, tested :: Maybe Bool, enabled :: Bool, @@ -456,7 +429,6 @@ data UserServersError = USENoServers {protocol :: AProtocolType, user :: Maybe User} | USEStorageMissing {protocol :: AProtocolType, user :: Maybe User} | USEProxyMissing {protocol :: AProtocolType, user :: Maybe User} - | USEInvalidServer {protocol :: AProtocolType, invalidServer :: Text} | USEDuplicateServer {protocol :: AProtocolType, duplicateServer :: Text, duplicateHost :: TransportHost} deriving (Show) @@ -471,16 +443,15 @@ validateUserServers curr others = currUserErrs <> concatMap otherUserErrs others | otherwise = [USEStorageMissing p' user | noServers (hasRole storage)] <> [USEProxyMissing p' user | noServers (hasRole proxy)] where p' = AProtocolType p - noServers cond = not $ any srvEnabled $ snd $ partitionValid $ concatMap (servers' p) $ filter cond uss + noServers cond = not $ any srvEnabled $ userServers p $ filter cond uss opEnabled = maybe True (\ServerOperator {enabled} -> enabled) . operator' hasRole roleSel = maybe True (\op@ServerOperator {enabled} -> enabled && roleSel (operatorRoles p op)) . operator' srvEnabled (AUS _ UserServer {deleted, enabled}) = enabled && not deleted serverErrs :: (UserServersClass u, ProtocolTypeI p, UserProtocol p) => SProtocolType p -> [u] -> [UserServersError] - serverErrs p uss = map (USEInvalidServer p') invalidSrvs <> mapMaybe duplicateErr_ srvs + serverErrs p uss = mapMaybe duplicateErr_ srvs where p' = AProtocolType p - (invalidSrvs, userSrvs) = partitionValid $ concatMap (servers' p) uss - srvs = filter (\(AUS _ UserServer {deleted}) -> not deleted) userSrvs + srvs = filter (\(AUS _ UserServer {deleted}) -> not deleted) $ userServers p uss duplicateErr_ (AUS _ srv@UserServer {server}) = USEDuplicateServer p' (safeDecodeUtf8 $ strEncode server) <$> find (`S.member` duplicateHosts) (srvHost srv) @@ -489,6 +460,8 @@ validateUserServers curr others = currUserErrs <> concatMap otherUserErrs others addHost (hs, dups) h | h `S.member` hs = (hs, S.insert h dups) | otherwise = (S.insert h hs, dups) + userServers :: (UserServersClass u, UserProtocol p) => SProtocolType p -> [u] -> [AUserServer p] + userServers p = map aUserServer' . concatMap (servers' p) instance ToJSON (DBEntityId' s) where toEncoding = \case @@ -525,30 +498,18 @@ $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "UCA") ''UsageConditionsAction) $(JQ.deriveJSON defaultJSON ''ServerOperatorConditions) instance ProtocolTypeI p => ToJSON (UserServer' s p) where - toEncoding = $(JQ.mkToEncoding defaultJSON ''UserServer_) - toJSON = $(JQ.mkToJSON defaultJSON ''UserServer_) + toEncoding = $(JQ.mkToEncoding defaultJSON ''UserServer') + toJSON = $(JQ.mkToJSON defaultJSON ''UserServer') instance (DBStoredI s, ProtocolTypeI p) => FromJSON (UserServer' s p) where - parseJSON = $(JQ.mkParseJSON defaultJSON ''UserServer_) + parseJSON = $(JQ.mkParseJSON defaultJSON ''UserServer') instance ProtocolTypeI p => FromJSON (AUserServer p) where parseJSON v = (AUS SDBStored <$> parseJSON v) <|> (AUS SDBNew <$> parseJSON v) -instance ProtocolTypeI p => FromJSON (ValidatedProtoServer p) where - parseJSON v = ValidatedProtoServer <$> ((Right <$> parseJSON v) <|> (Left <$> parseJSON v)) - -instance (DBStoredI s, ProtocolTypeI p) => FromJSON (ValidatedServer s p) where - parseJSON = $(JQ.mkParseJSON defaultJSON ''UserServer_) - -instance ProtocolTypeI p => FromJSON (AValidatedServer p) where - parseJSON v = (AVS SDBStored <$> parseJSON v) <|> (AVS SDBNew <$> parseJSON v) - $(JQ.deriveJSON defaultJSON ''UserOperatorServers) instance FromJSON UpdatedUserOperatorServers where parseJSON = $(JQ.mkParseJSON defaultJSON ''UpdatedUserOperatorServers) -instance FromJSON ValidatedUserOperatorServers where - parseJSON = $(JQ.mkParseJSON defaultJSON ''ValidatedUserOperatorServers) - $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "USE") ''UserServersError) diff --git a/tests/OperatorTests.hs b/tests/OperatorTests.hs index 03cea56133..0a00d7b83c 100644 --- a/tests/OperatorTests.hs +++ b/tests/OperatorTests.hs @@ -44,8 +44,6 @@ validateServersTest = describe "validate user servers" $ do [ USEDuplicateServer aSMP "smp://0YuTwO05YJWS8rkjn9eLJDjQhFKvIYd8d4xG8X1blIU=@smp8.simplex.im,beccx4yfxxbvyhqypaavemqurytl6hozr47wfc7uuecacjqdvwpw2xid.onion" "smp8.simplex.im", USEDuplicateServer aSMP "smp://abcd@smp8.simplex.im" "smp8.simplex.im" ] - it "should fail with invalid host" $ do - validateUserServers [invalidHost] [] `shouldBe` [USENoServers aXFTP Nothing, USEInvalidServer aSMP "smp:abcd@smp8.simplex.im"] where aSMP = AProtocolType SPSMP aXFTP = AProtocolType SPXFTP @@ -132,14 +130,3 @@ invalidDuplicate = (valid :: UpdatedUserOperatorServers) { smpServers = map (AUS SDBNew) $ simplexChatSMPServers <> [presetServer True "smp://abcd@smp8.simplex.im"] } - -invalidHost :: ValidatedUserOperatorServers -invalidHost = - ValidatedUserOperatorServers - { operator = Just operatorSimpleXChat {operatorId = DBEntityId 1}, - smpServers = [validatedServer (Left "smp:abcd@smp8.simplex.im"), validatedServer (Right "smp://abcd@smp8.simplex.im")], - xftpServers = [] - } - where - validatedServer srv = - AVS SDBNew (presetServer @'PSMP True "smp://abcd@smp8.simplex.im") {server = ValidatedProtoServer srv} From 181f72fa1f735904232bf26b21a7d222f6acdfab Mon Sep 17 00:00:00 2001 From: Evgeny Date: Tue, 19 Nov 2024 15:26:41 +0000 Subject: [PATCH 034/167] ios: texts about operators (#5213) * ios: texts about operators * remove comment * button for conditions --- .../Onboarding/ChooseServerOperators.swift | 24 ++++++++++++------- .../Views/Onboarding/CreateProfile.swift | 6 +++-- .../Shared/Views/Onboarding/SimpleXInfo.swift | 1 - .../NetworkAndServers/OperatorView.swift | 13 ++++------ apps/ios/SimpleXChat/APITypes.swift | 5 +++- 5 files changed, 29 insertions(+), 20 deletions(-) diff --git a/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift b/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift index 248c1b34c4..4b886ad9be 100644 --- a/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift +++ b/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift @@ -70,6 +70,11 @@ struct ChooseServerOperators: View { ForEach(serverOperators) { srvOperator in operatorCheckView(srvOperator) } + Text("You can configure servers via settings.") + .font(.footnote) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity, alignment: .center) +// .padding(.horizontal, 32) Spacer() @@ -83,10 +88,12 @@ struct ChooseServerOperators: View { continueButton() } if onboarding { - Text("You can disable operators and configure your servers in Network & servers settings.") - .multilineTextAlignment(.center) - .font(.footnote) - .padding(.horizontal, 32) + Button("Conditions of use") { + // TODO open accepted conditions + } + .font(.callout) + .foregroundColor(reviewForOperators.isEmpty ? .accentColor : .clear) + .padding(.top) } } .padding(.bottom) @@ -136,7 +143,7 @@ struct ChooseServerOperators: View { showInfoSheet = true } - Text("Select operators, whose servers you will be using.") + Text("Select network operators to use.") } } @@ -320,14 +327,15 @@ struct ChooseServerOperators: View { struct ChooseServerOperatorsInfoView: View { var body: some View { VStack(alignment: .leading) { - Text("Why choose multiple operators") + Text("Network operators") .font(.largeTitle) + .bold() .padding(.vertical) ScrollView { VStack(alignment: .leading) { Group { - Text("Selecting multiple operators improves protection of your communication graph.") - Text("TODO Better explanation") + Text("When more than one network operator is enabled, the app will use the servers of different operators for each conversation.") + Text("For example, if you receive messages via SimpleX Chat server, the app will use one of Flux servers for private routing.") } .padding(.bottom) } diff --git a/apps/ios/Shared/Views/Onboarding/CreateProfile.swift b/apps/ios/Shared/Views/Onboarding/CreateProfile.swift index 9d1f9f4709..b9f569e96d 100644 --- a/apps/ios/Shared/Views/Onboarding/CreateProfile.swift +++ b/apps/ios/Shared/Views/Onboarding/CreateProfile.swift @@ -90,8 +90,10 @@ struct CreateFirstProfile: View { var body: some View { VStack(alignment: .leading, spacing: 20) { Text("Your profile, contacts and delivered messages are stored on your device.") + .font(.callout) .foregroundColor(theme.colors.secondary) Text("The profile is only shared with your contacts.") + .font(.callout) .foregroundColor(theme.colors.secondary) HStack { @@ -114,8 +116,8 @@ struct CreateFirstProfile: View { .padding(.horizontal) .padding(.vertical, 10) .background( - RoundedRectangle(cornerRadius: 16, style: .continuous) - .fill(Color(uiColor: .secondarySystemGroupedBackground)) + RoundedRectangle(cornerRadius: 10, style: .continuous) + .fill(Color(uiColor: .tertiarySystemFill)) ) } .padding(.top) diff --git a/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift b/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift index 2e077e9d95..ea3627871e 100644 --- a/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift +++ b/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift @@ -144,7 +144,6 @@ struct SimpleXInfo: View { CreateFirstProfile() .navigationTitle("Create your profile") .navigationBarTitleDisplayMode(.large) - .modifier(ThemedBackground(grouped: true)) } private func userExistsFallbackButton() -> some View { diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift index ef02e94e3f..63586e2121 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift @@ -257,14 +257,11 @@ struct OperatorView: View { .modifier(ThemedBackground(grouped: true)) .navigationBarTitleDisplayMode(.large) } label: { - HStack { - Image(userServers[operatorIndex].operator_.logo(colorScheme)) - .resizable() - .scaledToFit() - .grayscale(userServers[operatorIndex].operator_.enabled ? 0.0 : 1.0) - .frame(width: 24, height: 24) - Text(userServers[operatorIndex].operator_.tradeName) - } + Image(userServers[operatorIndex].operator_.largeLogo(colorScheme)) + .resizable() + .scaledToFit() + .grayscale(userServers[operatorIndex].operator_.enabled ? 0.0 : 1.0) + .frame(height: 40) } } diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 5470059e92..016a8213c3 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -1221,7 +1221,10 @@ public struct ServerOperatorInfo: Decodable { public let operatorsInfo: Dictionary = [ .simplex: ServerOperatorInfo( - description: ["SimpleX Chat preset servers"], + description: [ + "SimpleX Chat is the first communication network that has no user profile IDs of any kind, not even random numbers or keys that identify the users.", + "SimpleX Chat Ltd develops the communication software for SimpleX network." + ], website: "https://simplex.chat", logo: "decentralized", largeLogo: "logo", From 58c92ed004b01937ef6c233a6243c1db57669320 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Tue, 19 Nov 2024 20:48:51 +0400 Subject: [PATCH 035/167] ios: rework existing users notice, condition views (#5214) --- apps/ios/Shared/ContentView.swift | 26 +++-- .../Onboarding/ChooseServerOperators.swift | 68 ++++++++++++-- .../Shared/Views/Onboarding/HowItWorks.swift | 1 + .../Views/Onboarding/WhatsNewView.swift | 94 +++++++++++-------- .../NetworkAndServers/NetworkAndServers.swift | 35 ++++--- .../Views/UserSettings/SettingsView.swift | 2 +- 6 files changed, 159 insertions(+), 67 deletions(-) diff --git a/apps/ios/Shared/ContentView.swift b/apps/ios/Shared/ContentView.swift index 62de4bc1c6..ac699d4a2c 100644 --- a/apps/ios/Shared/ContentView.swift +++ b/apps/ios/Shared/ContentView.swift @@ -10,11 +10,13 @@ import Intents import SimpleXChat private enum NoticesSheet: Identifiable { - case notices(showWhatsNew: Bool, showOperatorsNotice: Bool) + case whatsNew(updatedConditions: Bool) + case updatedConditions var id: String { switch self { - case .notices: return "notices" + case .whatsNew: return "whatsNew" + case .updatedConditions: return "updatedConditions" } } } @@ -274,10 +276,12 @@ struct ContentView: View { DispatchQueue.main.asyncAfter(deadline: .now() + 1) { if !noticesShown { let showWhatsNew = shouldShowWhatsNew() - let showOperatorsNotice = chatModel.conditions.conditionsAction?.showNotice ?? false - noticesShown = showWhatsNew || showOperatorsNotice - if noticesShown { - noticesSheetItem = .notices(showWhatsNew: showWhatsNew, showOperatorsNotice: showOperatorsNotice) + let showUpdatedConditions = chatModel.conditions.conditionsAction?.showNotice ?? false + noticesShown = showWhatsNew || showUpdatedConditions + if showWhatsNew { + noticesSheetItem = .whatsNew(updatedConditions: showUpdatedConditions) + } else if showUpdatedConditions { + noticesSheetItem = .updatedConditions } } } @@ -288,8 +292,14 @@ struct ContentView: View { .onChange(of: chatModel.appOpenUrl) { _ in connectViaUrl() } .sheet(item: $noticesSheetItem) { item in switch item { - case let .notices(showWhatsNew, showOperatorsNotice): - WhatsNewView(showWhatsNew: showWhatsNew, showOperatorsNotice: showOperatorsNotice) + case let .whatsNew(updatedConditions): + WhatsNewView(updatedConditions: updatedConditions) + case .updatedConditions: + UsageConditionsView( + currUserServers: Binding.constant([]), + userServers: Binding.constant([]) + ) + .modifier(ThemedBackground(grouped: true)) } } if chatModel.setDeliveryReceipts { diff --git a/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift b/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift index 4b886ad9be..09e0060c22 100644 --- a/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift +++ b/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift @@ -41,15 +41,27 @@ struct OnboardingButtonStyle: ButtonStyle { } } +private enum ChooseServerOperatorsSheet: Identifiable { + case showInfo + case showConditions + + var id: String { + switch self { + case .showInfo: return "showInfo" + case .showConditions: return "showConditions" + } + } +} + struct ChooseServerOperators: View { @Environment(\.dismiss) var dismiss: DismissAction @Environment(\.colorScheme) var colorScheme: ColorScheme @EnvironmentObject var theme: AppTheme var onboarding: Bool - @State private var showInfoSheet = false @State private var serverOperators: [ServerOperator] = [] @State private var selectedOperatorIds = Set() @State private var reviewConditionsNavLinkActive = false + @State private var sheetItem: ChooseServerOperatorsSheet? = nil @State private var justOpened = true var selectedOperators: [ServerOperator] { serverOperators.filter { selectedOperatorIds.contains($0.operatorId) } } @@ -74,25 +86,34 @@ struct ChooseServerOperators: View { .font(.footnote) .multilineTextAlignment(.center) .frame(maxWidth: .infinity, alignment: .center) -// .padding(.horizontal, 32) + .padding(.horizontal, 32) Spacer() let reviewForOperators = selectedOperators.filter { !$0.conditionsAcceptance.conditionsAccepted } let canReviewLater = reviewForOperators.allSatisfy { $0.conditionsAcceptance.usageAllowed } + let currEnabledOperatorIds = Set(serverOperators.filter { $0.enabled }.map { $0.operatorId }) VStack(spacing: 8) { if !reviewForOperators.isEmpty { reviewConditionsButton() + } else if selectedOperatorIds != currEnabledOperatorIds && !selectedOperatorIds.isEmpty { + setOperatorsButton() } else { continueButton() } if onboarding { - Button("Conditions of use") { - // TODO open accepted conditions + Group { + if reviewForOperators.isEmpty { + Button("Conditions of use") { + sheetItem = .showConditions + } + } else { + Text("Conditions of use") + .foregroundColor(.clear) + } } .font(.callout) - .foregroundColor(reviewForOperators.isEmpty ? .accentColor : .clear) .padding(.top) } } @@ -123,8 +144,17 @@ struct ChooseServerOperators: View { justOpened = false } } - .sheet(isPresented: $showInfoSheet) { - ChooseServerOperatorsInfoView() + .sheet(item: $sheetItem) { item in + switch item { + case .showInfo: + ChooseServerOperatorsInfoView() + case .showConditions: + UsageConditionsView( + currUserServers: Binding.constant([]), + userServers: Binding.constant([]) + ) + .modifier(ThemedBackground(grouped: true)) + } } } .frame(maxHeight: .infinity) @@ -140,7 +170,7 @@ struct ChooseServerOperators: View { .frame(width: 20, height: 20) .foregroundColor(theme.colors.primary) .onTapGesture { - showInfoSheet = true + sheetItem = .showInfo } Text("Select network operators to use.") @@ -200,6 +230,28 @@ struct ChooseServerOperators: View { } } + private func setOperatorsButton() -> some View { + Button { + Task { + if let enabledOperators = enabledOperators(serverOperators) { + let r = try await setServerOperators(operators: enabledOperators) + await MainActor.run { + ChatModel.shared.conditions = r + continueToNextStep() + } + } else { + await MainActor.run { + continueToNextStep() + } + } + } + } label: { + Text("Update") + } + .buttonStyle(OnboardingButtonStyle(isDisabled: selectedOperatorIds.isEmpty)) + .disabled(selectedOperatorIds.isEmpty) + } + private func continueButton() -> some View { Button { continueToNextStep() diff --git a/apps/ios/Shared/Views/Onboarding/HowItWorks.swift b/apps/ios/Shared/Views/Onboarding/HowItWorks.swift index f11dbbe7a8..9a0ee4ddeb 100644 --- a/apps/ios/Shared/Views/Onboarding/HowItWorks.swift +++ b/apps/ios/Shared/Views/Onboarding/HowItWorks.swift @@ -18,6 +18,7 @@ struct HowItWorks: View { VStack(alignment: .leading) { Text("How SimpleX works") .font(.largeTitle) + .bold() .padding(.vertical) ScrollView { VStack(alignment: .leading) { diff --git a/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift b/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift index 1d1ec5b64c..c078fb23b1 100644 --- a/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift +++ b/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift @@ -526,7 +526,7 @@ private let versionDescriptions: [VersionDescription] = [ .view(FeatureView( icon: nil, title: "Network decentralization", - view: newOperatorsView + view: { NewOperatorsView() } )), .feature(Description( icon: "text.quote", @@ -549,20 +549,37 @@ func shouldShowWhatsNew() -> Bool { return v != lastVersion } -fileprivate func newOperatorsView() -> some View { - VStack(alignment: .leading) { - Image((operatorsInfo[.flux] ?? ServerOperator.dummyOperatorInfo).largeLogo) - .resizable() - .scaledToFit() - .frame(height: 48) - Text("The second preset operator in the app!") - .multilineTextAlignment(.leading) - .lineLimit(10) - HStack { - Button("Enable Flux") { - +fileprivate struct NewOperatorsView: View { + @State private var showOperatorsSheet = false + + var body: some View { + VStack(alignment: .leading) { + Image((operatorsInfo[.flux] ?? ServerOperator.dummyOperatorInfo).largeLogo) + .resizable() + .scaledToFit() + .frame(height: 48) + Text("The second preset operator in the app!") + .multilineTextAlignment(.leading) + .lineLimit(10) + HStack { + Button("Enable Flux") { + showOperatorsSheet = true + } + Text("for better metadata privacy.") } - Text("for better metadata privacy.") + } + .sheet(isPresented: $showOperatorsSheet) { + ChooseServerOperators(onboarding: false) + } + } +} + +private enum WhatsNewViewSheet: Identifiable { + case showConditions + + var id: String { + switch self { + case .showConditions: return "showConditions" } } } @@ -573,13 +590,13 @@ struct WhatsNewView: View { @State var currentVersion = versionDescriptions.count - 1 @State var currentVersionNav = versionDescriptions.count - 1 var viaSettings = false - @State var showWhatsNew: Bool - var showOperatorsNotice: Bool + var updatedConditions: Bool + @State private var sheetItem: WhatsNewViewSheet? = nil var body: some View { - viewBody() + whatsNewView() .task { - if showOperatorsNotice { + if updatedConditions { do { let conditionsId = ChatModel.shared.conditions.currentConditions.conditionsId try await setConditionsNotified(conditionsId: conditionsId) @@ -588,14 +605,16 @@ struct WhatsNewView: View { } } } - } - - @ViewBuilder private func viewBody() -> some View { - if showWhatsNew { - whatsNewView() - } else if showOperatorsNotice { - ChooseServerOperators(onboarding: false) - } + .sheet(item: $sheetItem) { item in + switch item { + case .showConditions: + UsageConditionsView( + currUserServers: Binding.constant([]), + userServers: Binding.constant([]) + ) + .modifier(ThemedBackground(grouped: true)) + } + } } private func whatsNewView() -> some View { @@ -623,22 +642,19 @@ struct WhatsNewView: View { } } } + if updatedConditions { + Button("View updated conditions") { + sheetItem = .showConditions + } + } if !viaSettings { Spacer() - if showOperatorsNotice { - Button("View updated conditions") { - showWhatsNew = false - } - .font(.title3) - .frame(maxWidth: .infinity, alignment: .center) - } else { - Button("Ok") { - dismiss() - } - .font(.title3) - .frame(maxWidth: .infinity, alignment: .center) + Button("Ok") { + dismiss() } + .font(.title3) + .frame(maxWidth: .infinity, alignment: .center) Spacer() } @@ -729,6 +745,6 @@ struct WhatsNewView: View { struct NewFeaturesView_Previews: PreviewProvider { static var previews: some View { - WhatsNewView(showWhatsNew: true, showOperatorsNotice: false) + WhatsNewView(updatedConditions: false) } } diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift index 2247e3d8d5..c668ad3858 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift @@ -20,7 +20,7 @@ private enum NetworkAlert: Identifiable { } private enum NetworkAndServersSheet: Identifiable { - case showConditions(conditionsAction: UsageConditionsAction) + case showConditions var id: String { switch self { @@ -65,7 +65,7 @@ struct NetworkAndServers: View { switch conditionsAction { case let .review(_, deadline, _): if let deadline = deadline, anyOperatorEnabled { - Text("Conditions will be considered accepted on: \(conditionsTimestamp(deadline)).") + Text("Conditions will be accepted on: \(conditionsTimestamp(deadline)).") .foregroundColor(theme.colors.secondary) } default: @@ -171,9 +171,8 @@ struct NetworkAndServers: View { } .sheet(item: $sheetItem) { item in switch item { - case let .showConditions(conditionsAction): + case .showConditions: UsageConditionsView( - conditionsAction: conditionsAction, currUserServers: $currUserServers, userServers: $userServers ) @@ -221,7 +220,7 @@ struct NetworkAndServers: View { private func conditionsButton(_ conditionsAction: UsageConditionsAction) -> some View { Button { - sheetItem = .showConditions(conditionsAction: conditionsAction) + sheetItem = .showConditions } label: { switch conditionsAction { case .review: @@ -236,7 +235,6 @@ struct NetworkAndServers: View { struct UsageConditionsView: View { @Environment(\.dismiss) var dismiss: DismissAction @EnvironmentObject var theme: AppTheme - var conditionsAction: UsageConditionsAction @Binding var currUserServers: [UserOperatorServers] @Binding var userServers: [UserOperatorServers] @@ -248,14 +246,29 @@ struct UsageConditionsView: View { .padding(.top) .padding(.top) - switch conditionsAction { + switch ChatModel.shared.conditions.conditionsAction { - case let .review(operators, _, _): + case .none: + ConditionsTextView() + .padding(.bottom) + .padding(.bottom) + + case let .review(operators, deadline, _): Text("Conditions will be accepted for the operator(s): **\(operators.map { $0.legalName_ }.joined(separator: ", "))**.") ConditionsTextView() - acceptConditionsButton(operators.map { $0.operatorId }) - .padding(.bottom) - .padding(.bottom) + VStack(spacing: 8) { + acceptConditionsButton(operators.map { $0.operatorId }) + if let deadline = deadline { + Text("Conditions will be automatically accepted for enabled operators on: \(conditionsTimestamp(deadline)).") + .foregroundColor(theme.colors.secondary) + .font(.footnote) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity, alignment: .center) + .padding(.horizontal, 32) + } + } + .padding(.bottom) + .padding(.bottom) case let .accepted(operators): Text("Conditions are accepted for the operator(s): **\(operators.map { $0.legalName_ }.joined(separator: ", "))**.") diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift index e73697e42a..f2a1a56d01 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift @@ -367,7 +367,7 @@ struct SettingsView: View { } } NavigationLink { - WhatsNewView(viaSettings: true, showWhatsNew: true, showOperatorsNotice: false) + WhatsNewView(viaSettings: true, updatedConditions: false) .modifier(ThemedBackground()) .navigationBarTitleDisplayMode(.inline) } label: { From 4e37efdc4a68ce817f00015d66a2cc1f99007fcf Mon Sep 17 00:00:00 2001 From: Evgeny Date: Wed, 20 Nov 2024 07:23:25 +0000 Subject: [PATCH 036/167] core: update agent servers (#5215) --- src/Simplex/Chat.hs | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 11cd8e33ad..0daf9fa394 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -1620,9 +1620,26 @@ processChatCommand' vr = \case TestProtoServer srv -> withUser $ \User {userId} -> processChatCommand $ APITestProtoServer userId srv APIGetServerOperators -> CRServerOperatorConditions <$> withFastStore getServerOperators - APISetServerOperators operatorsEnabled -> withFastStore $ \db -> do - liftIO $ setServerOperators db operatorsEnabled - CRServerOperatorConditions <$> getServerOperators db + APISetServerOperators operators -> do + as <- asks randomAgentServers + (opsConds, srvs) <- withFastStore $ \db -> do + liftIO $ setServerOperators db operators + opsConds <- getServerOperators db + let ops = serverOperators opsConds + ops' = map Just ops <> [Nothing] + opDomains = operatorDomains ops + liftIO $ fmap (opsConds,) . mapM (getServers db as ops' opDomains) =<< getUsers db + lift $ withAgent' $ \a -> forM_ srvs $ \(auId, (smp', xftp')) -> do + setProtocolServers a auId smp' + setProtocolServers a auId xftp' + pure $ CRServerOperatorConditions opsConds + where + getServers :: DB.Connection -> RandomAgentServers -> [Maybe ServerOperator] -> [(Text, ServerOperator)] -> User -> IO (UserId, (NonEmpty (ServerCfg 'PSMP), NonEmpty (ServerCfg 'PXFTP))) + getServers db as ops opDomains user = do + smpSrvs <- getProtocolServers db SPSMP user + xftpSrvs <- getProtocolServers db SPXFTP user + uss <- groupByOperator (ops, smpSrvs, xftpSrvs) + pure $ (aUserId user,) $ useServers as opDomains uss APIGetUserServers userId -> withUserId userId $ \user -> withFastStore $ \db -> do CRUserServers user <$> (liftIO . groupByOperator =<< getUserServers db user) APISetUserServers userId userServers -> withUserId userId $ \user -> do @@ -2955,8 +2972,8 @@ processChatCommand' vr = \case getUserOperatorServers :: DB.Connection -> User -> ExceptT StoreError IO (User, [UserOperatorServers]) getUserOperatorServers db user = do uss <- liftIO . groupByOperator =<< getUserServers db user - pure (user, map updatedUserServers uss) - updatedUserServers uss = uss {operator = updatedOp <$> operator' uss} :: UserOperatorServers + pure (user, map updatedUserSrvs uss) + updatedUserSrvs uss = uss {operator = updatedOp <$> operator' uss} :: UserOperatorServers updatedOp op = fromMaybe op $ find matchingOp $ mapMaybe operator' userServers where matchingOp op' = operatorId op' == operatorId op From e5534c0402e606ad9aa1c9e39566690fcaf4d9e1 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Wed, 20 Nov 2024 14:28:36 +0400 Subject: [PATCH 037/167] ios: improve onboarding animations (#5216) --- apps/ios/Shared/Model/SimpleXAPI.swift | 18 +- .../Onboarding/ChooseServerOperators.swift | 299 ++++++++++-------- .../Views/Onboarding/CreateProfile.swift | 147 ++++++--- .../Views/Onboarding/OnboardingView.swift | 31 +- .../Onboarding/SetNotificationsMode.swift | 7 +- .../Shared/Views/Onboarding/SimpleXInfo.swift | 141 ++++----- .../Views/Onboarding/WhatsNewView.swift | 5 +- 7 files changed, 359 insertions(+), 289 deletions(-) diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index c61ad412c0..13b11568d8 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -1650,7 +1650,7 @@ private func chatInitialized(start: Bool, refreshInvitations: Bool) throws { } } -func startChat(refreshInvitations: Bool = true) throws { +func startChat(refreshInvitations: Bool = true, onboarding: Bool = false) throws { logger.debug("startChat") let m = ChatModel.shared try setNetworkConfig(getNetCfg()) @@ -1669,13 +1669,15 @@ func startChat(refreshInvitations: Bool = true) throws { if let token = m.deviceToken { registerToken(token: token) } - withAnimation { - let savedOnboardingStage = onboardingStageDefault.get() - m.onboardingStage = [.step1_SimpleXInfo, .step2_CreateProfile].contains(savedOnboardingStage) && m.users.count == 1 - ? .step3_ChooseServerOperators - : savedOnboardingStage - if m.onboardingStage == .onboardingComplete && !privacyDeliveryReceiptsSet.get() { - m.setDeliveryReceipts = true + if !onboarding { + withAnimation { + let savedOnboardingStage = onboardingStageDefault.get() + m.onboardingStage = [.step1_SimpleXInfo, .step2_CreateProfile].contains(savedOnboardingStage) && m.users.count == 1 + ? .step3_ChooseServerOperators + : savedOnboardingStage + if m.onboardingStage == .onboardingComplete && !privacyDeliveryReceiptsSet.get() { + m.setDeliveryReceipts = true + } } } } diff --git a/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift b/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift index 09e0060c22..45c7a94bae 100644 --- a/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift +++ b/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift @@ -62,104 +62,105 @@ struct ChooseServerOperators: View { @State private var selectedOperatorIds = Set() @State private var reviewConditionsNavLinkActive = false @State private var sheetItem: ChooseServerOperatorsSheet? = nil + @State private var notificationsModeNavLinkActive = false @State private var justOpened = true var selectedOperators: [ServerOperator] { serverOperators.filter { selectedOperatorIds.contains($0.operatorId) } } var body: some View { - NavigationView { - GeometryReader { g in - ScrollView { - VStack(alignment: .leading, spacing: 20) { + GeometryReader { g in + ScrollView { + VStack(alignment: .leading, spacing: 20) { + if !onboarding { Text("Choose operators") .font(.largeTitle) .bold() + } - infoText() - - Spacer() - - ForEach(serverOperators) { srvOperator in - operatorCheckView(srvOperator) + infoText() + + Spacer() + + ForEach(serverOperators) { srvOperator in + operatorCheckView(srvOperator) + } + Text("You can configure servers via settings.") + .font(.footnote) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity, alignment: .center) + .padding(.horizontal, 32) + + Spacer() + + let reviewForOperators = selectedOperators.filter { !$0.conditionsAcceptance.conditionsAccepted } + let canReviewLater = reviewForOperators.allSatisfy { $0.conditionsAcceptance.usageAllowed } + let currEnabledOperatorIds = Set(serverOperators.filter { $0.enabled }.map { $0.operatorId }) + + VStack(spacing: 8) { + if !reviewForOperators.isEmpty { + reviewConditionsButton() + } else if selectedOperatorIds != currEnabledOperatorIds && !selectedOperatorIds.isEmpty { + setOperatorsButton() + } else { + continueButton() } - Text("You can configure servers via settings.") - .font(.footnote) - .multilineTextAlignment(.center) - .frame(maxWidth: .infinity, alignment: .center) - .padding(.horizontal, 32) - - Spacer() - - let reviewForOperators = selectedOperators.filter { !$0.conditionsAcceptance.conditionsAccepted } - let canReviewLater = reviewForOperators.allSatisfy { $0.conditionsAcceptance.usageAllowed } - let currEnabledOperatorIds = Set(serverOperators.filter { $0.enabled }.map { $0.operatorId }) - - VStack(spacing: 8) { - if !reviewForOperators.isEmpty { - reviewConditionsButton() - } else if selectedOperatorIds != currEnabledOperatorIds && !selectedOperatorIds.isEmpty { - setOperatorsButton() - } else { - continueButton() - } - if onboarding { - Group { - if reviewForOperators.isEmpty { - Button("Conditions of use") { - sheetItem = .showConditions - } - } else { - Text("Conditions of use") - .foregroundColor(.clear) + if onboarding { + Group { + if reviewForOperators.isEmpty { + Button("Conditions of use") { + sheetItem = .showConditions } + } else { + Text("Conditions of use") + .foregroundColor(.clear) } - .font(.callout) - .padding(.top) } + .font(.callout) + .padding(.top) } + } + .padding(.bottom) + + if !onboarding && !reviewForOperators.isEmpty { + VStack(spacing: 8) { + reviewLaterButton() + ( + Text("Conditions will be accepted for enabled operators after 30 days.") + + Text(" ") + + Text("You can configure operators in Network & servers settings.") + ) + .multilineTextAlignment(.center) + .font(.footnote) + .padding(.horizontal, 32) + } + .disabled(!canReviewLater) .padding(.bottom) - - if !onboarding && !reviewForOperators.isEmpty { - VStack(spacing: 8) { - reviewLaterButton() - ( - Text("Conditions will be accepted for enabled operators after 30 days.") - + Text(" ") - + Text("You can configure operators in Network & servers settings.") - ) - .multilineTextAlignment(.center) - .font(.footnote) - .padding(.horizontal, 32) - } - .disabled(!canReviewLater) - .padding(.bottom) - } - } - .frame(minHeight: g.size.height) - } - .onAppear { - if justOpened { - serverOperators = ChatModel.shared.conditions.serverOperators - selectedOperatorIds = Set(serverOperators.filter { $0.enabled }.map { $0.operatorId }) - justOpened = false } } - .sheet(item: $sheetItem) { item in - switch item { - case .showInfo: - ChooseServerOperatorsInfoView() - case .showConditions: - UsageConditionsView( - currUserServers: Binding.constant([]), - userServers: Binding.constant([]) - ) - .modifier(ThemedBackground(grouped: true)) - } + .frame(minHeight: g.size.height) + } + .onAppear { + if justOpened { + serverOperators = ChatModel.shared.conditions.serverOperators + selectedOperatorIds = Set(serverOperators.filter { $0.enabled }.map { $0.operatorId }) + justOpened = false + } + } + .sheet(item: $sheetItem) { item in + switch item { + case .showInfo: + ChooseServerOperatorsInfoView() + case .showConditions: + UsageConditionsView( + currUserServers: Binding.constant([]), + userServers: Binding.constant([]) + ) + .modifier(ThemedBackground(grouped: true)) } } - .frame(maxHeight: .infinity) - .padding() } + .frame(maxHeight: .infinity) + .padding() } private func infoText() -> some View { @@ -193,7 +194,7 @@ struct ChooseServerOperators: View { .frame(width: 26, height: 26) .foregroundColor(iconColor) } - .background(Color(.systemBackground)) + .background(theme.colors.background) .padding() .clipShape(RoundedRectangle(cornerRadius: 18)) .overlay( @@ -231,57 +232,83 @@ struct ChooseServerOperators: View { } private func setOperatorsButton() -> some View { - Button { - Task { - if let enabledOperators = enabledOperators(serverOperators) { - let r = try await setServerOperators(operators: enabledOperators) - await MainActor.run { - ChatModel.shared.conditions = r - continueToNextStep() - } - } else { - await MainActor.run { - continueToNextStep() + notificationsModeNavLinkButton { + Button { + Task { + if let enabledOperators = enabledOperators(serverOperators) { + let r = try await setServerOperators(operators: enabledOperators) + await MainActor.run { + ChatModel.shared.conditions = r + continueToNextStep() + } + } else { + await MainActor.run { + continueToNextStep() + } } } + } label: { + Text("Update") } - } label: { - Text("Update") + .buttonStyle(OnboardingButtonStyle(isDisabled: selectedOperatorIds.isEmpty)) + .disabled(selectedOperatorIds.isEmpty) } - .buttonStyle(OnboardingButtonStyle(isDisabled: selectedOperatorIds.isEmpty)) - .disabled(selectedOperatorIds.isEmpty) } private func continueButton() -> some View { - Button { - continueToNextStep() - } label: { - Text("Continue") + notificationsModeNavLinkButton { + Button { + continueToNextStep() + } label: { + Text("Continue") + } + .buttonStyle(OnboardingButtonStyle(isDisabled: selectedOperatorIds.isEmpty)) + .disabled(selectedOperatorIds.isEmpty) } - .buttonStyle(OnboardingButtonStyle(isDisabled: selectedOperatorIds.isEmpty)) - .disabled(selectedOperatorIds.isEmpty) } private func reviewLaterButton() -> some View { - Button { - continueToNextStep() - } label: { - Text("Review later") + notificationsModeNavLinkButton { + Button { + continueToNextStep() + } label: { + Text("Review later") + } + .buttonStyle(.borderless) } - .buttonStyle(.borderless) } private func continueToNextStep() { if onboarding { - withAnimation { - onboardingStageDefault.set(.step4_SetNotificationsMode) - ChatModel.shared.onboardingStage = .step4_SetNotificationsMode - } + onboardingStageDefault.set(.step4_SetNotificationsMode) + notificationsModeNavLinkActive = true } else { dismiss() } } + func notificationsModeNavLinkButton(_ button: @escaping (() -> some View)) -> some View { + ZStack { + button() + + NavigationLink(isActive: $notificationsModeNavLinkActive) { + notificationsModeDestinationView() + } label: { + EmptyView() + } + .frame(width: 1, height: 1) + .hidden() + } + } + + private func notificationsModeDestinationView() -> some View { + SetNotificationsMode() + .navigationTitle("Push notifications") + .navigationBarTitleDisplayMode(.large) + .navigationBarBackButtonHidden(true) + .modifier(ThemedBackground(grouped: false)) + } + private func reviewConditionsDestinationView() -> some View { reviewConditionsView() .navigationTitle("Conditions of use") @@ -309,40 +336,42 @@ struct ChooseServerOperators: View { } private func acceptConditionsButton() -> some View { - Button { - Task { - do { - let conditionsId = ChatModel.shared.conditions.currentConditions.conditionsId - let acceptForOperators = selectedOperators.filter { !$0.conditionsAcceptance.conditionsAccepted } - let operatorIds = acceptForOperators.map { $0.operatorId } - let r = try await acceptConditions(conditionsId: conditionsId, operatorIds: operatorIds) - await MainActor.run { - ChatModel.shared.conditions = r - } - if let enabledOperators = enabledOperators(r.serverOperators) { - let r2 = try await setServerOperators(operators: enabledOperators) + notificationsModeNavLinkButton { + Button { + Task { + do { + let conditionsId = ChatModel.shared.conditions.currentConditions.conditionsId + let acceptForOperators = selectedOperators.filter { !$0.conditionsAcceptance.conditionsAccepted } + let operatorIds = acceptForOperators.map { $0.operatorId } + let r = try await acceptConditions(conditionsId: conditionsId, operatorIds: operatorIds) await MainActor.run { - ChatModel.shared.conditions = r2 - continueToNextStep() + ChatModel.shared.conditions = r } - } else { + if let enabledOperators = enabledOperators(r.serverOperators) { + let r2 = try await setServerOperators(operators: enabledOperators) + await MainActor.run { + ChatModel.shared.conditions = r2 + continueToNextStep() + } + } else { + await MainActor.run { + continueToNextStep() + } + } + } catch let error { await MainActor.run { - continueToNextStep() + showAlert( + NSLocalizedString("Error accepting conditions", comment: "alert title"), + message: responseError(error) + ) } } - } catch let error { - await MainActor.run { - showAlert( - NSLocalizedString("Error accepting conditions", comment: "alert title"), - message: responseError(error) - ) - } } + } label: { + Text("Accept conditions") } - } label: { - Text("Accept conditions") + .buttonStyle(OnboardingButtonStyle()) } - .buttonStyle(OnboardingButtonStyle()) } private func enabledOperators(_ operators: [ServerOperator]) -> [ServerOperator]? { diff --git a/apps/ios/Shared/Views/Onboarding/CreateProfile.swift b/apps/ios/Shared/Views/Onboarding/CreateProfile.swift index b9f569e96d..30e7d4dc83 100644 --- a/apps/ios/Shared/Views/Onboarding/CreateProfile.swift +++ b/apps/ios/Shared/Views/Onboarding/CreateProfile.swift @@ -38,7 +38,7 @@ struct CreateProfile: View { TextField("Enter your name…", text: $displayName) .focused($focusDisplayName) Button { - createProfile(displayName, showAlert: { alert = $0 }, dismiss: dismiss) + createProfile() } label: { Label("Create profile", systemImage: "checkmark") } @@ -78,6 +78,35 @@ struct CreateProfile: View { } } } + + private func createProfile() { + hideKeyboard() + let profile = Profile( + displayName: displayName.trimmingCharacters(in: .whitespaces), + fullName: "" + ) + let m = ChatModel.shared + do { + AppChatState.shared.set(.active) + m.currentUser = try apiCreateActiveUser(profile) + // .isEmpty check is redundant here, but it makes it clearer what is going on + if m.users.isEmpty || m.users.allSatisfy({ $0.user.hidden }) { + try startChat() + withAnimation { + onboardingStageDefault.set(.step3_ChooseServerOperators) + m.onboardingStage = .step3_ChooseServerOperators + } + } else { + onboardingStageDefault.set(.onboardingComplete) + m.onboardingStage = .onboardingComplete + dismiss() + m.users = try listUsers() + try getUserChatData() + } + } catch let error { + showCreateProfileAlert(showAlert: { alert = $0 }, error) + } + } } struct CreateFirstProfile: View { @@ -86,6 +115,7 @@ struct CreateFirstProfile: View { @Environment(\.dismiss) var dismiss @State private var displayName: String = "" @FocusState private var focusDisplayName + @State private var nextStepNavLinkActive = false var body: some View { VStack(alignment: .leading, spacing: 20) { @@ -136,69 +166,84 @@ struct CreateFirstProfile: View { } func createProfileButton() -> some View { - Button { - createProfile(displayName, showAlert: showAlert, dismiss: dismiss) - } label: { - Text("Create profile") + ZStack { + Button { + createProfile() + } label: { + Text("Create profile") + } + .buttonStyle(OnboardingButtonStyle(isDisabled: !canCreateProfile(displayName))) + .disabled(!canCreateProfile(displayName)) + + NavigationLink(isActive: $nextStepNavLinkActive) { + nextStepDestinationView() + } label: { + EmptyView() + } + .frame(width: 1, height: 1) + .hidden() } - .buttonStyle(OnboardingButtonStyle(isDisabled: !canCreateProfile(displayName))) - .disabled(!canCreateProfile(displayName)) } private func showAlert(_ alert: UserProfileAlert) { AlertManager.shared.showAlert(userProfileAlert(alert, $displayName)) } + + private func nextStepDestinationView() -> some View { + ChooseServerOperators(onboarding: true) + .navigationTitle("Choose operators") + .navigationBarTitleDisplayMode(.large) + .navigationBarBackButtonHidden(true) + .modifier(ThemedBackground(grouped: false)) + } + + private func createProfile() { + hideKeyboard() + let profile = Profile( + displayName: displayName.trimmingCharacters(in: .whitespaces), + fullName: "" + ) + let m = ChatModel.shared + do { + AppChatState.shared.set(.active) + m.currentUser = try apiCreateActiveUser(profile) + try startChat(onboarding: true) + onboardingStageDefault.set(.step3_ChooseServerOperators) + nextStepNavLinkActive = true + } catch let error { + showCreateProfileAlert(showAlert: showAlert, error) + } + } } -private func createProfile(_ displayName: String, showAlert: (UserProfileAlert) -> Void, dismiss: DismissAction) { - hideKeyboard() - let profile = Profile( - displayName: displayName.trimmingCharacters(in: .whitespaces), - fullName: "" - ) +private func showCreateProfileAlert( + showAlert: (UserProfileAlert) -> Void, + _ error: Error +) { let m = ChatModel.shared - do { - AppChatState.shared.set(.active) - m.currentUser = try apiCreateActiveUser(profile) - // .isEmpty check is redundant here, but it makes it clearer what is going on - if m.users.isEmpty || m.users.allSatisfy({ $0.user.hidden }) { - try startChat() - withAnimation { - onboardingStageDefault.set(.step3_ChooseServerOperators) - m.onboardingStage = .step3_ChooseServerOperators - } + switch error as? ChatResponse { + case .chatCmdError(_, .errorStore(.duplicateName)), + .chatCmdError(_, .error(.userExists)): + if m.currentUser == nil { + AlertManager.shared.showAlert(duplicateUserAlert) } else { - onboardingStageDefault.set(.onboardingComplete) - m.onboardingStage = .onboardingComplete - dismiss() - m.users = try listUsers() - try getUserChatData() + showAlert(.duplicateUserError) } - } catch let error { - switch error as? ChatResponse { - case .chatCmdError(_, .errorStore(.duplicateName)), - .chatCmdError(_, .error(.userExists)): - if m.currentUser == nil { - AlertManager.shared.showAlert(duplicateUserAlert) - } else { - showAlert(.duplicateUserError) - } - case .chatCmdError(_, .error(.invalidDisplayName)): - if m.currentUser == nil { - AlertManager.shared.showAlert(invalidDisplayNameAlert) - } else { - showAlert(.invalidDisplayNameError) - } - default: - let err: LocalizedStringKey = "Error: \(responseError(error))" - if m.currentUser == nil { - AlertManager.shared.showAlert(creatUserErrorAlert(err)) - } else { - showAlert(.createUserError(error: err)) - } + case .chatCmdError(_, .error(.invalidDisplayName)): + if m.currentUser == nil { + AlertManager.shared.showAlert(invalidDisplayNameAlert) + } else { + showAlert(.invalidDisplayNameError) + } + default: + let err: LocalizedStringKey = "Error: \(responseError(error))" + if m.currentUser == nil { + AlertManager.shared.showAlert(creatUserErrorAlert(err)) + } else { + showAlert(.createUserError(error: err)) } - logger.error("Failed to create user or start chat: \(responseError(error))") } + logger.error("Failed to create user or start chat: \(responseError(error))") } private func canCreateProfile(_ displayName: String) -> Bool { diff --git a/apps/ios/Shared/Views/Onboarding/OnboardingView.swift b/apps/ios/Shared/Views/Onboarding/OnboardingView.swift index de3dce21bb..172db25315 100644 --- a/apps/ios/Shared/Views/Onboarding/OnboardingView.swift +++ b/apps/ios/Shared/Views/Onboarding/OnboardingView.swift @@ -12,13 +12,30 @@ struct OnboardingView: View { var onboarding: OnboardingStage var body: some View { - switch onboarding { - case .step1_SimpleXInfo: SimpleXInfo(onboarding: true) - case .step2_CreateProfile: CreateFirstProfile() - case .step3_CreateSimpleXAddress: CreateSimpleXAddress() - case .step3_ChooseServerOperators: ChooseServerOperators(onboarding: true) - case .step4_SetNotificationsMode: SetNotificationsMode() - case .onboardingComplete: EmptyView() + NavigationView { + switch onboarding { + case .step1_SimpleXInfo: + SimpleXInfo(onboarding: true) + .modifier(ThemedBackground(grouped: false)) + case .step2_CreateProfile: // deprecated + CreateFirstProfile() + .modifier(ThemedBackground(grouped: false)) + case .step3_CreateSimpleXAddress: // deprecated + CreateSimpleXAddress() + case .step3_ChooseServerOperators: + ChooseServerOperators(onboarding: true) + .navigationTitle("Choose operators") + .navigationBarTitleDisplayMode(.large) + .navigationBarBackButtonHidden(true) + .modifier(ThemedBackground(grouped: false)) + case .step4_SetNotificationsMode: + SetNotificationsMode() + .navigationTitle("Push notifications") + .navigationBarTitleDisplayMode(.large) + .navigationBarBackButtonHidden(true) + .modifier(ThemedBackground(grouped: false)) + case .onboardingComplete: EmptyView() + } } } } diff --git a/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift b/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift index 03ee9c67e0..91a755459a 100644 --- a/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift +++ b/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift @@ -17,12 +17,7 @@ struct SetNotificationsMode: View { var body: some View { GeometryReader { g in ScrollView { - VStack(alignment: .leading, spacing: 16) { - Text("Push notifications") - .font(.largeTitle) - .bold() - .frame(maxWidth: .infinity) - + VStack(alignment: .leading, spacing: 20) { Text("Send notifications:") ForEach(NotificationsMode.values) { mode in NtfModeSelector(mode: mode, selection: $notificationMode) diff --git a/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift b/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift index ea3627871e..2229f47a49 100644 --- a/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift +++ b/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift @@ -17,81 +17,79 @@ struct SimpleXInfo: View { var onboarding: Bool var body: some View { - NavigationView { - GeometryReader { g in - ScrollView { - VStack(alignment: .leading, spacing: 20) { - Image(colorScheme == .light ? "logo" : "logo-light") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: g.size.width * 0.67) - .padding(.bottom, 8) - .frame(maxWidth: .infinity, minHeight: 48, alignment: .top) + GeometryReader { g in + ScrollView { + VStack(alignment: .leading, spacing: 20) { + Image(colorScheme == .light ? "logo" : "logo-light") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: g.size.width * 0.67) + .padding(.bottom, 8) + .frame(maxWidth: .infinity, minHeight: 48, alignment: .top) - VStack(alignment: .leading) { - Text("The next generation of private messaging") - .font(.title2) - .padding(.bottom, 30) - .padding(.horizontal, 40) - .frame(maxWidth: .infinity) - .multilineTextAlignment(.center) - infoRow("privacy", "Privacy redefined", - "The 1st platform without any user identifiers – private by design.", width: 48) - infoRow("shield", "Immune to spam and abuse", - "People can connect to you only via the links you share.", width: 46) - infoRow(colorScheme == .light ? "decentralized" : "decentralized-light", "Decentralized", - "Open-source protocol and code – anybody can run the servers.", width: 44) - } - - Spacer() - - if onboarding { - onboardingActionButton() - - Button { - m.migrationState = .pasteOrScanLink - } label: { - Label("Migrate from another device", systemImage: "tray.and.arrow.down") - .font(.subheadline) - } + VStack(alignment: .leading) { + Text("The next generation of private messaging") + .font(.title2) + .padding(.bottom, 30) + .padding(.horizontal, 40) .frame(maxWidth: .infinity) - } + .multilineTextAlignment(.center) + infoRow("privacy", "Privacy redefined", + "The 1st platform without any user identifiers – private by design.", width: 48) + infoRow("shield", "Immune to spam and abuse", + "People can connect to you only via the links you share.", width: 46) + infoRow(colorScheme == .light ? "decentralized" : "decentralized-light", "Decentralized", + "Open-source protocol and code – anybody can run the servers.", width: 44) + } + + Spacer() + + if onboarding { + createFirstProfileButton() Button { - showHowItWorks = true + m.migrationState = .pasteOrScanLink } label: { - Label("How it works", systemImage: "info.circle") + Label("Migrate from another device", systemImage: "tray.and.arrow.down") .font(.subheadline) } .frame(maxWidth: .infinity) - .padding(.bottom) } - .frame(minHeight: g.size.height) - } - .sheet(isPresented: Binding( - get: { m.migrationState != nil }, - set: { _ in - m.migrationState = nil - MigrationToDeviceState.save(nil) } - )) { - NavigationView { - VStack(alignment: .leading) { - MigrateToDevice(migrationState: $m.migrationState) - } - .navigationTitle("Migrate here") - .modifier(ThemedBackground(grouped: true)) + + Button { + showHowItWorks = true + } label: { + Label("How it works", systemImage: "info.circle") + .font(.subheadline) } + .frame(maxWidth: .infinity) + .padding(.bottom) } - .sheet(isPresented: $showHowItWorks) { - HowItWorks( - onboarding: onboarding, - createProfileNavLinkActive: $createProfileNavLinkActive - ) + .frame(minHeight: g.size.height) + } + .sheet(isPresented: Binding( + get: { m.migrationState != nil }, + set: { _ in + m.migrationState = nil + MigrationToDeviceState.save(nil) } + )) { + NavigationView { + VStack(alignment: .leading) { + MigrateToDevice(migrationState: $m.migrationState) + } + .navigationTitle("Migrate here") + .modifier(ThemedBackground(grouped: true)) } } - .frame(maxHeight: .infinity) - .padding() + .sheet(isPresented: $showHowItWorks) { + HowItWorks( + onboarding: onboarding, + createProfileNavLinkActive: $createProfileNavLinkActive + ) + } } + .frame(maxHeight: .infinity) + .padding() } private func infoRow(_ image: String, _ title: LocalizedStringKey, _ text: LocalizedStringKey, width: CGFloat) -> some View { @@ -113,14 +111,6 @@ struct SimpleXInfo: View { .padding(.trailing, 6) } - @ViewBuilder private func onboardingActionButton() -> some View { - if m.currentUser == nil { - createFirstProfileButton() - } else { - userExistsFallbackButton() - } - } - private func createFirstProfileButton() -> some View { ZStack { Button { @@ -144,18 +134,7 @@ struct SimpleXInfo: View { CreateFirstProfile() .navigationTitle("Create your profile") .navigationBarTitleDisplayMode(.large) - } - - private func userExistsFallbackButton() -> some View { - Button { - withAnimation { - onboardingStageDefault.set(.onboardingComplete) - m.onboardingStage = .onboardingComplete - } - } label: { - Text("Make a private connection") - } - .buttonStyle(OnboardingButtonStyle(isDisabled: false)) + .modifier(ThemedBackground(grouped: false)) } } diff --git a/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift b/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift index c078fb23b1..82497a7922 100644 --- a/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift +++ b/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift @@ -569,7 +569,10 @@ fileprivate struct NewOperatorsView: View { } } .sheet(isPresented: $showOperatorsSheet) { - ChooseServerOperators(onboarding: false) + NavigationView { + ChooseServerOperators(onboarding: false) + .modifier(ThemedBackground(grouped: false)) + } } } } From 313acefb193e6256d44a6f0b735b292f0d8efe44 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Wed, 20 Nov 2024 17:18:24 +0400 Subject: [PATCH 038/167] ios: remove crashing accept button (#5217) --- apps/ios/Shared/ContentView.swift | 1 + .../Onboarding/ChooseServerOperators.swift | 2 +- .../Views/Onboarding/CreateProfile.swift | 2 +- .../Views/Onboarding/OnboardingView.swift | 8 ++++---- .../Shared/Views/Onboarding/SimpleXInfo.swift | 2 +- .../Shared/Views/Onboarding/WhatsNewView.swift | 2 +- .../NetworkAndServers/OperatorView.swift | 18 ++++++------------ 7 files changed, 15 insertions(+), 20 deletions(-) diff --git a/apps/ios/Shared/ContentView.swift b/apps/ios/Shared/ContentView.swift index ac699d4a2c..c5a7a6f20b 100644 --- a/apps/ios/Shared/ContentView.swift +++ b/apps/ios/Shared/ContentView.swift @@ -294,6 +294,7 @@ struct ContentView: View { switch item { case let .whatsNew(updatedConditions): WhatsNewView(updatedConditions: updatedConditions) + .modifier(ThemedBackground()) case .updatedConditions: UsageConditionsView( currUserServers: Binding.constant([]), diff --git a/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift b/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift index 45c7a94bae..471d27ea50 100644 --- a/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift +++ b/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift @@ -306,7 +306,7 @@ struct ChooseServerOperators: View { .navigationTitle("Push notifications") .navigationBarTitleDisplayMode(.large) .navigationBarBackButtonHidden(true) - .modifier(ThemedBackground(grouped: false)) + .modifier(ThemedBackground()) } private func reviewConditionsDestinationView() -> some View { diff --git a/apps/ios/Shared/Views/Onboarding/CreateProfile.swift b/apps/ios/Shared/Views/Onboarding/CreateProfile.swift index 30e7d4dc83..c6760319b1 100644 --- a/apps/ios/Shared/Views/Onboarding/CreateProfile.swift +++ b/apps/ios/Shared/Views/Onboarding/CreateProfile.swift @@ -194,7 +194,7 @@ struct CreateFirstProfile: View { .navigationTitle("Choose operators") .navigationBarTitleDisplayMode(.large) .navigationBarBackButtonHidden(true) - .modifier(ThemedBackground(grouped: false)) + .modifier(ThemedBackground()) } private func createProfile() { diff --git a/apps/ios/Shared/Views/Onboarding/OnboardingView.swift b/apps/ios/Shared/Views/Onboarding/OnboardingView.swift index 172db25315..d004e0306f 100644 --- a/apps/ios/Shared/Views/Onboarding/OnboardingView.swift +++ b/apps/ios/Shared/Views/Onboarding/OnboardingView.swift @@ -16,10 +16,10 @@ struct OnboardingView: View { switch onboarding { case .step1_SimpleXInfo: SimpleXInfo(onboarding: true) - .modifier(ThemedBackground(grouped: false)) + .modifier(ThemedBackground()) case .step2_CreateProfile: // deprecated CreateFirstProfile() - .modifier(ThemedBackground(grouped: false)) + .modifier(ThemedBackground()) case .step3_CreateSimpleXAddress: // deprecated CreateSimpleXAddress() case .step3_ChooseServerOperators: @@ -27,13 +27,13 @@ struct OnboardingView: View { .navigationTitle("Choose operators") .navigationBarTitleDisplayMode(.large) .navigationBarBackButtonHidden(true) - .modifier(ThemedBackground(grouped: false)) + .modifier(ThemedBackground()) case .step4_SetNotificationsMode: SetNotificationsMode() .navigationTitle("Push notifications") .navigationBarTitleDisplayMode(.large) .navigationBarBackButtonHidden(true) - .modifier(ThemedBackground(grouped: false)) + .modifier(ThemedBackground()) case .onboardingComplete: EmptyView() } } diff --git a/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift b/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift index 2229f47a49..2d90fb2fb2 100644 --- a/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift +++ b/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift @@ -134,7 +134,7 @@ struct SimpleXInfo: View { CreateFirstProfile() .navigationTitle("Create your profile") .navigationBarTitleDisplayMode(.large) - .modifier(ThemedBackground(grouped: false)) + .modifier(ThemedBackground()) } } diff --git a/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift b/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift index 82497a7922..4208c4a068 100644 --- a/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift +++ b/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift @@ -571,7 +571,7 @@ fileprivate struct NewOperatorsView: View { .sheet(isPresented: $showOperatorsSheet) { NavigationView { ChooseServerOperators(onboarding: false) - .modifier(ThemedBackground(grouped: false)) + .modifier(ThemedBackground()) } } } diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift index 63586e2121..6cebfdcde6 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift @@ -540,18 +540,12 @@ struct SingleOperatorUsageConditionsView: View { } private func usageConditionsDestinationView() -> some View { - VStack(spacing: 20) { - ConditionsTextView() - .padding(.top) - - acceptConditionsButton() - .padding(.bottom) - .padding(.bottom) - } - .padding(.horizontal) - .navigationTitle("Conditions of use") - .navigationBarTitleDisplayMode(.large) - .modifier(ThemedBackground(grouped: true)) + ConditionsTextView() + .padding() + .padding(.bottom) + .navigationTitle("Conditions of use") + .navigationBarTitleDisplayMode(.large) + .modifier(ThemedBackground(grouped: true)) } } From 29b54ec5b296926c56882f3db5c2f12774efbfc8 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Wed, 20 Nov 2024 17:58:13 +0400 Subject: [PATCH 039/167] ios: rework saving settings (#5219) * ios: rework saving settings * fix * shorter names --------- Co-authored-by: Evgeny Poberezkin --- .../Shared/Views/ChatList/ChatListView.swift | 31 ++++++---- .../Views/ChatList/ServersSummaryView.swift | 57 +------------------ .../NetworkAndServers/NetworkAndServers.swift | 52 ++++++++--------- .../Views/UserSettings/SettingsView.swift | 22 ++----- 4 files changed, 50 insertions(+), 112 deletions(-) diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift index 8e7aec581b..6da17fb312 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift @@ -31,14 +31,22 @@ enum UserPickerSheet: Identifiable { } } +class SaveableSettings: ObservableObject { + @Published var servers: ServerSettings = ServerSettings(currUserServers: [], userServers: [], serverErrors: []) +} + +struct ServerSettings { + public var currUserServers: [UserOperatorServers] + public var userServers: [UserOperatorServers] + public var serverErrors: [UserServersError] +} + struct UserPickerSheetView: View { let sheet: UserPickerSheet @EnvironmentObject var chatModel: ChatModel - @State private var loaded = false + @StateObject private var ss = SaveableSettings() - @State private var currUserServers: [UserOperatorServers] = [] - @State private var userServers: [UserOperatorServers] = [] - @State private var serverErrors: [UserServersError] = [] + @State private var loaded = false var body: some View { NavigationView { @@ -60,11 +68,7 @@ struct UserPickerSheetView: View { case .useFromDesktop: ConnectDesktopView() case .settings: - SettingsView( - currUserServers: $currUserServers, - userServers: $userServers, - serverErrors: $serverErrors - ) + SettingsView() } } Color.clear // Required for list background to be rendered during loading @@ -85,15 +89,20 @@ struct UserPickerSheetView: View { ) } .onDisappear { - if serversCanBeSaved(currUserServers, userServers, serverErrors) { + if serversCanBeSaved( + ss.servers.currUserServers, + ss.servers.userServers, + ss.servers.serverErrors + ) { showAlert( title: NSLocalizedString("Save servers?", comment: "alert title"), buttonTitle: NSLocalizedString("Save", comment: "alert button"), - buttonAction: { saveServers($currUserServers, $userServers) }, + buttonAction: { saveServers($ss.servers.currUserServers, $ss.servers.userServers) }, cancelButton: true ) } } + .environmentObject(ss) } } diff --git a/apps/ios/Shared/Views/ChatList/ServersSummaryView.swift b/apps/ios/Shared/Views/ChatList/ServersSummaryView.swift index a13a159a45..b87b84ebc0 100644 --- a/apps/ios/Shared/Views/ChatList/ServersSummaryView.swift +++ b/apps/ios/Shared/Views/ChatList/ServersSummaryView.swift @@ -20,10 +20,6 @@ struct ServersSummaryView: View { @State private var timer: Timer? = nil @State private var alert: SomeAlert? - @State private var currUserServers: [UserOperatorServers] = [] - @State private var userServers: [UserOperatorServers] = [] - @State private var serverErrors: [UserServersError] = [] - @AppStorage(DEFAULT_SHOW_SUBSCRIPTION_PERCENTAGE) private var showSubscriptionPercentage = false enum PresentedUserCategory { @@ -57,15 +53,6 @@ struct ServersSummaryView: View { } .onDisappear { stopTimer() - - if serversCanBeSaved(currUserServers, userServers, serverErrors) { - showAlert( - title: NSLocalizedString("Save servers?", comment: "alert title"), - buttonTitle: NSLocalizedString("Save", comment: "alert button"), - buttonAction: { saveServers($currUserServers, $userServers) }, - cancelButton: true - ) - } } .alert(item: $alert) { $0.alert } } @@ -288,10 +275,7 @@ struct ServersSummaryView: View { NavigationLink(tag: srvSumm.id, selection: $selectedSMPServer) { SMPServerSummaryView( summary: srvSumm, - statsStartedAt: statsStartedAt, - currUserServers: $currUserServers, - userServers: $userServers, - serverErrors: $serverErrors + statsStartedAt: statsStartedAt ) .navigationBarTitle("SMP server") .navigationBarTitleDisplayMode(.large) @@ -360,10 +344,7 @@ struct ServersSummaryView: View { NavigationLink(tag: srvSumm.id, selection: $selectedXFTPServer) { XFTPServerSummaryView( summary: srvSumm, - statsStartedAt: statsStartedAt, - currUserServers: $currUserServers, - userServers: $userServers, - serverErrors: $serverErrors + statsStartedAt: statsStartedAt ) .navigationBarTitle("XFTP server") .navigationBarTitleDisplayMode(.large) @@ -505,28 +486,11 @@ struct SMPServerSummaryView: View { @AppStorage(DEFAULT_SHOW_SUBSCRIPTION_PERCENTAGE) private var showSubscriptionPercentage = false - @Binding var currUserServers: [UserOperatorServers] - @Binding var userServers: [UserOperatorServers] - @Binding var serverErrors: [UserServersError] - var body: some View { List { Section("Server address") { Text(summary.smpServer) .textSelection(.enabled) - if summary.known == true { - NavigationLink { - NetworkAndServers( - currUserServers: $currUserServers, - userServers: $userServers, - serverErrors: $serverErrors - ) - .navigationTitle("Network & servers") - .modifier(ThemedBackground(grouped: true)) - } label: { - Text("Open server settings") - } - } } if let stats = summary.stats { @@ -701,28 +665,11 @@ struct XFTPServerSummaryView: View { var summary: XFTPServerSummary var statsStartedAt: Date - @Binding var currUserServers: [UserOperatorServers] - @Binding var userServers: [UserOperatorServers] - @Binding var serverErrors: [UserServersError] - var body: some View { List { Section("Server address") { Text(summary.xftpServer) .textSelection(.enabled) - if summary.known == true { - NavigationLink { - NetworkAndServers( - currUserServers: $currUserServers, - userServers: $userServers, - serverErrors: $serverErrors - ) - .navigationTitle("Network & servers") - .modifier(ThemedBackground(grouped: true)) - } label: { - Text("Open server settings") - } - } } if let stats = summary.stats { diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift index c668ad3858..8b07c9a519 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift @@ -34,9 +34,7 @@ struct NetworkAndServers: View { @EnvironmentObject var m: ChatModel @Environment(\.colorScheme) var colorScheme: ColorScheme @EnvironmentObject var theme: AppTheme - @Binding var currUserServers: [UserOperatorServers] - @Binding var userServers: [UserOperatorServers] - @Binding var serverErrors: [UserServersError] + @EnvironmentObject var ss: SaveableSettings @State private var sheetItem: NetworkAndServersSheet? = nil @State private var justOpened = true @State private var showSaveDialog = false @@ -45,9 +43,9 @@ struct NetworkAndServers: View { VStack { List { let conditionsAction = m.conditions.conditionsAction - let anyOperatorEnabled = userServers.contains(where: { $0.operator?.enabled ?? false }) + let anyOperatorEnabled = ss.servers.userServers.contains(where: { $0.operator?.enabled ?? false }) Section { - ForEach(userServers.enumerated().map { $0 }, id: \.element.id) { idx, userOperatorServers in + ForEach(ss.servers.userServers.enumerated().map { $0 }, id: \.element.id) { idx, userOperatorServers in if let serverOperator = userOperatorServers.operator { serverOperatorView(idx, serverOperator) } else { @@ -74,11 +72,11 @@ struct NetworkAndServers: View { } Section { - if let idx = userServers.firstIndex(where: { $0.operator == nil }) { + if let idx = ss.servers.userServers.firstIndex(where: { $0.operator == nil }) { NavigationLink { YourServersView( - userServers: $userServers, - serverErrors: $serverErrors, + userServers: $ss.servers.userServers, + serverErrors: $ss.servers.serverErrors, operatorIndex: idx ) .navigationTitle("Your servers") @@ -87,7 +85,7 @@ struct NetworkAndServers: View { HStack { Text("Your servers") - if userServers[idx] != currUserServers[idx] { + if ss.servers.userServers[idx] != ss.servers.currUserServers[idx] { Spacer() unsavedChangesIndicator() } @@ -108,12 +106,12 @@ struct NetworkAndServers: View { } Section { - Button("Save servers", action: { saveServers($currUserServers, $userServers) }) - .disabled(!serversCanBeSaved(currUserServers, userServers, serverErrors)) + Button("Save servers", action: { saveServers($ss.servers.currUserServers, $ss.servers.userServers) }) + .disabled(!serversCanBeSaved(ss.servers.currUserServers, ss.servers.userServers, ss.servers.serverErrors)) } footer: { - if let errStr = globalServersError(serverErrors) { + if let errStr = globalServersError(ss.servers.serverErrors) { ServersErrorView(errStr: errStr) - } else if !serverErrors.isEmpty { + } else if !ss.servers.serverErrors.isEmpty { ServersErrorView(errStr: NSLocalizedString("Errors in servers configuration.", comment: "servers error")) } } @@ -141,9 +139,9 @@ struct NetworkAndServers: View { // this condition is needed to prevent re-setting the servers when exiting single server view if justOpened { do { - currUserServers = try await getUserServers() - userServers = currUserServers - serverErrors = [] + ss.servers.currUserServers = try await getUserServers() + ss.servers.userServers = ss.servers.currUserServers + ss.servers.serverErrors = [] } catch let error { await MainActor.run { showAlert( @@ -156,7 +154,7 @@ struct NetworkAndServers: View { } } .modifier(BackButton(disabled: Binding.constant(false)) { - if serversCanBeSaved(currUserServers, userServers, serverErrors) { + if serversCanBeSaved(ss.servers.currUserServers, ss.servers.userServers, ss.servers.serverErrors) { showSaveDialog = true } else { dismiss() @@ -164,7 +162,7 @@ struct NetworkAndServers: View { }) .confirmationDialog("Save servers?", isPresented: $showSaveDialog, titleVisibility: .visible) { Button("Save") { - saveServers($currUserServers, $userServers) + saveServers($ss.servers.currUserServers, $ss.servers.userServers) dismiss() } Button("Exit without saving") { dismiss() } @@ -173,8 +171,8 @@ struct NetworkAndServers: View { switch item { case .showConditions: UsageConditionsView( - currUserServers: $currUserServers, - userServers: $userServers + currUserServers: $ss.servers.currUserServers, + userServers: $ss.servers.userServers ) .modifier(ThemedBackground(grouped: true)) } @@ -184,9 +182,9 @@ struct NetworkAndServers: View { private func serverOperatorView(_ operatorIndex: Int, _ serverOperator: ServerOperator) -> some View { NavigationLink() { OperatorView( - currUserServers: $currUserServers, - userServers: $userServers, - serverErrors: $serverErrors, + currUserServers: $ss.servers.currUserServers, + userServers: $ss.servers.userServers, + serverErrors: $ss.servers.serverErrors, operatorIndex: operatorIndex, useOperator: serverOperator.enabled ) @@ -203,7 +201,7 @@ struct NetworkAndServers: View { Text(serverOperator.tradeName) .foregroundColor(serverOperator.enabled ? theme.colors.onBackground : theme.colors.secondary) - if userServers[operatorIndex] != currUserServers[operatorIndex] { + if ss.servers.userServers[operatorIndex] != ss.servers.currUserServers[operatorIndex] { Spacer() unsavedChangesIndicator() } @@ -427,10 +425,6 @@ func updateOperatorsConditionsAcceptance(_ usvs: Binding<[UserOperatorServers]>, struct NetworkServersView_Previews: PreviewProvider { static var previews: some View { - NetworkAndServers( - currUserServers: Binding.constant([UserOperatorServers.sampleData1, UserOperatorServers.sampleDataNilOperator]), - userServers: Binding.constant([UserOperatorServers.sampleData1, UserOperatorServers.sampleDataNilOperator]), - serverErrors: Binding.constant([]) - ) + NetworkAndServers() } } diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift index f2a1a56d01..95bf327f1b 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift @@ -266,10 +266,6 @@ struct SettingsView: View { @EnvironmentObject var theme: AppTheme @State private var showProgress: Bool = false - @Binding var currUserServers: [UserOperatorServers] - @Binding var userServers: [UserOperatorServers] - @Binding var serverErrors: [UserServersError] - var body: some View { ZStack { settingsView() @@ -296,13 +292,9 @@ struct SettingsView: View { .disabled(chatModel.chatRunning != true) NavigationLink { - NetworkAndServers( - currUserServers: $currUserServers, - userServers: $userServers, - serverErrors: $serverErrors - ) - .navigationTitle("Network & servers") - .modifier(ThemedBackground(grouped: true)) + NetworkAndServers() + .navigationTitle("Network & servers") + .modifier(ThemedBackground(grouped: true)) } label: { settingsRow("externaldrive.connected.to.line.below", color: theme.colors.secondary) { Text("Network & servers") } } @@ -536,11 +528,7 @@ struct SettingsView_Previews: PreviewProvider { static var previews: some View { let chatModel = ChatModel() chatModel.currentUser = User.sampleData - return SettingsView( - currUserServers: Binding.constant([UserOperatorServers.sampleData1, UserOperatorServers.sampleDataNilOperator]), - userServers: Binding.constant([UserOperatorServers.sampleData1, UserOperatorServers.sampleDataNilOperator]), - serverErrors: Binding.constant([]) - ) - .environmentObject(chatModel) + return SettingsView() + .environmentObject(chatModel) } } From f3cef7ce12ef76c4e2d9dd7329593ce6d5c86815 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Wed, 20 Nov 2024 18:23:51 +0400 Subject: [PATCH 040/167] ios: remove unused type --- apps/ios/SimpleXChat/APITypes.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 016a8213c3..8014600d47 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -1456,7 +1456,6 @@ public enum UserServersError: Decodable { case noServers(protocol: ServerProtocol, user: UserRef?) case storageMissing(protocol: ServerProtocol, user: UserRef?) case proxyMissing(protocol: ServerProtocol, user: UserRef?) - case invalidServer(protocol: ServerProtocol, invalidServer: String) case duplicateServer(protocol: ServerProtocol, duplicateServer: String, duplicateHost: String) public var globalError: String? { From 2b155db57d7885f64ea28f0742a8c8519ddc1dbc Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Thu, 21 Nov 2024 02:23:55 +0700 Subject: [PATCH 041/167] android, desktop: open chat on first unread, "scroll" to quoted items that were not loaded (#5140) * android, desktop: infinity scroll rework * group corrections * scroll to quote/unread/top/bottom * changes * changes * changes * changes * better * changes * fix chat closing on desktop * fix reading items counter, scrolling to newly appeared message, removed unneeded items loading, only partially visible items marked read * workaround of showing buttom with arrow down on new messages receiving * rename param * fix tests * comments and removed unused code * performance optimization * optimization for loading more items in small chat * fix loading prev items in loop * workaround to blinking button with counter * terminal scroll fix * different click events for floating buttons * refactor * change * WIP * refactor * refactor * renames * refactor * refactor * change * mark read problem fix * fix tests * fix auto scroll in some situations * fix scroll to quote when it's near the top loaded area * refactor * refactor * rename * rename * fix * alert when quoted message doesn't exist * refactor --------- Co-authored-by: Evgeny Poberezkin --- .../simplex/common/platform/UI.android.kt | 3 +- .../views/chat/item/CIVideoView.android.kt | 7 + .../chat/simplex/common/model/ChatModel.kt | 155 +++- .../chat/simplex/common/model/SimpleXAPI.kt | 25 +- .../simplex/common/platform/NtfManager.kt | 2 +- .../chat/simplex/common/views/TerminalView.kt | 14 +- .../common/views/chat/ChatItemInfoView.kt | 2 +- .../common/views/chat/ChatItemsLoader.kt | 301 +++++++ .../common/views/chat/ChatItemsMerger.kt | 379 +++++++++ .../simplex/common/views/chat/ChatView.kt | 738 ++++++++++++------ .../simplex/common/views/chat/ComposeView.kt | 4 +- .../views/chat/group/GroupMemberInfoView.kt | 16 +- .../views/chat/item/CIChatFeatureView.kt | 2 +- .../common/views/chat/item/CIVideoView.kt | 3 + .../common/views/chat/item/ChatItemView.kt | 49 +- .../common/views/chat/item/FramedItemView.kt | 16 +- .../views/chat/item/MarkedDeletedItemView.kt | 4 +- .../views/chatlist/ChatListNavLinkView.kt | 64 +- .../common/views/chatlist/ChatPreviewView.kt | 2 +- .../views/contacts/ContactListNavView.kt | 6 +- .../common/views/database/DatabaseView.kt | 2 +- .../common/views/newchat/AddGroupView.kt | 2 +- .../common/views/newchat/ConnectPlan.kt | 4 +- .../commonMain/resources/MR/base/strings.xml | 2 + .../chat/simplex/app/ChatItemsMergerTest.kt | 158 ++++ .../kotlin/chat/simplex/common/DesktopApp.kt | 2 +- .../views/chat/item/CIVideoView.desktop.kt | 4 + .../views/chatlist/ChatListView.desktop.kt | 4 +- 28 files changed, 1575 insertions(+), 395 deletions(-) create mode 100644 apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemsLoader.kt create mode 100644 apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemsMerger.kt create mode 100644 apps/multiplatform/common/src/commonTest/kotlin/chat/simplex/app/ChatItemsMergerTest.kt diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/UI.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/UI.android.kt index 7ab6bf525f..ae5966b20f 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/UI.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/UI.android.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalView import chat.simplex.common.AppScreen import chat.simplex.common.model.clear +import chat.simplex.common.model.clearAndNotify import chat.simplex.common.views.helpers.* import androidx.compose.ui.platform.LocalContext as LocalContext1 import chat.simplex.res.MR @@ -75,7 +76,7 @@ actual class GlobalExceptionsHandler: Thread.UncaughtExceptionHandler { } else if (chatModel.chatId.value != null) { // Since no modals are open, the problem is probably in ChatView chatModel.chatId.value = null - chatModel.chatItems.clear() + chatModel.chatItems.clearAndNotify() } else { // ChatList, nothing to do. Maybe to show other view except ChatList } diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/CIVideoView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/CIVideoView.android.kt index f2f3e27766..a8c084bbad 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/CIVideoView.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/CIVideoView.android.kt @@ -44,3 +44,10 @@ actual fun LocalWindowWidth(): Dp { (rect.width() / density).dp } } + +@Composable +actual fun LocalWindowHeight(): Dp { + val view = LocalView.current + val density = LocalDensity.current + return with(density) { view.height.toDp() } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index 422eb1e77f..ef777f151f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -8,10 +8,11 @@ import androidx.compose.ui.graphics.* import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.font.* import androidx.compose.ui.text.style.TextDecoration +import chat.simplex.common.model.ChatModel.chatItemsChangesListener import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.call.* -import chat.simplex.common.views.chat.ComposeState +import chat.simplex.common.views.chat.* import chat.simplex.common.views.helpers.* import chat.simplex.common.views.migration.MigrationToDeviceState import chat.simplex.common.views.migration.MigrationToState @@ -22,6 +23,7 @@ import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import kotlin.collections.removeAll as remAll import kotlinx.datetime.* import kotlinx.datetime.TimeZone import kotlinx.serialization.* @@ -35,6 +37,7 @@ import java.net.URI import java.time.format.DateTimeFormatter import java.time.format.FormatStyle import java.util.* +import kotlin.collections.ArrayList import kotlin.random.Random import kotlin.time.* @@ -64,7 +67,14 @@ object ChatModel { // current chat val chatId = mutableStateOf(null) + /** if you modify the items by adding/removing them, use helpers methods like [addAndNotify], [removeLastAndNotify], [removeAllAndNotify], [clearAndNotify] and so on. + * If some helper is missing, create it. Notify is needed to track state of items that we added manually (not via api call). See [apiLoadMessages]. + * If you use api call to get the items, use just [add] instead of [addAndNotify]. + * Never modify underlying list directly because it produces unexpected results in ChatView's LazyColumn (setting by index is ok) */ val chatItems = mutableStateOf(SnapshotStateList()) + // set listener here that will be notified on every add/delete of a chat item + var chatItemsChangesListener: ChatItemsChangesListener? = null + val chatState = ActiveChatState() // rhId, chatId val deletedChats = mutableStateOf>>(emptyList()) val chatItemStatuses = mutableMapOf() @@ -216,6 +226,15 @@ object ChatModel { popChatCollector.throttlePopChat(chat.remoteHostId, chat.id, currentPosition = 0) } + private suspend fun reorderChat(chat: Chat, toIndex: Int) { + val newChats = SnapshotStateList() + newChats.addAll(chats.value) + newChats.remove(chat) + newChats.add(index = toIndex, chat) + chats.replaceAll(newChats) + popChatCollector.throttlePopChat(chat.remoteHostId, chat.id, currentPosition = toIndex) + } + fun updateChatInfo(rhId: Long?, cInfo: ChatInfo) { val i = getChatIndex(rhId, cInfo.id) if (i >= 0) { @@ -317,7 +336,7 @@ object ChatModel { chat.chatStats ) if (appPlatform.isDesktop && cItem.chatDir.sent) { - addChat(chats.removeAt(i)) + reorderChat(chats[i], 0) } else { popChatCollector.throttlePopChat(chat.remoteHostId, chat.id, currentPosition = i) } @@ -330,9 +349,9 @@ object ChatModel { // Prevent situation when chat item already in the list received from backend if (chatItems.value.none { it.id == cItem.id }) { if (chatItems.value.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) { - chatItems.add(kotlin.math.max(0, chatItems.value.lastIndex), cItem) + chatItems.addAndNotify(kotlin.math.max(0, chatItems.value.lastIndex), cItem) } else { - chatItems.add(cItem) + chatItems.addAndNotify(cItem) } } } @@ -377,7 +396,7 @@ object ChatModel { } else { cItem } - chatItems.add(ci) + chatItems.addAndNotify(ci) true } } else { @@ -416,7 +435,7 @@ object ChatModel { } // remove from current chat if (chatId.value == cInfo.id) { - chatItems.removeAll { + chatItems.removeAllAndNotify { // 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 @@ -436,7 +455,7 @@ object ChatModel { // clear current chat if (chatId.value == cInfo.id) { chatItemStatuses.clear() - chatItems.clear() + chatItems.clearAndNotify() } } @@ -607,14 +626,14 @@ object ChatModel { suspend fun addLiveDummy(chatInfo: ChatInfo): ChatItem { val cItem = ChatItem.liveDummy(chatInfo is ChatInfo.Direct) withContext(Dispatchers.Main) { - chatItems.add(cItem) + chatItems.addAndNotify(cItem) } return cItem } fun removeLiveDummy() { if (chatItems.value.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) { - chatItems.removeLast() + chatItems.removeLastAndNotify() } } @@ -622,11 +641,17 @@ object ChatModel { val cInfo = chatInfo var markedRead = 0 if (chatId.value == cInfo.id) { - var i = 0 val items = chatItems.value - while (i < items.size) { + var i = items.lastIndex + val itemIdsFromRange = if (range != null) { + (range.from .. range.to).toMutableSet() + } else { + mutableSetOf() + } + val markedReadIds = mutableSetOf() + while (i >= 0) { val item = items[i] - if (item.meta.itemStatus is CIStatus.RcvNew && (range == null || (range.from <= item.id && item.id <= range.to))) { + if (item.meta.itemStatus is CIStatus.RcvNew && (range == null || itemIdsFromRange.contains(item.id))) { val newItem = item.withStatus(CIStatus.RcvRead()) items[i] = newItem if (newItem.meta.itemLive != true && newItem.meta.itemTimed?.ttl != null) { @@ -634,10 +659,17 @@ object ChatModel { deleteAt = Clock.System.now() + newItem.meta.itemTimed.ttl.toDuration(DurationUnit.SECONDS))) ) } + markedReadIds.add(item.id) markedRead++ + if (range != null) { + itemIdsFromRange.remove(item.id) + // already set all needed items as read, can finish the loop + if (itemIdsFromRange.isEmpty()) break + } } - i += 1 + i-- } + chatItemsChangesListener?.read(if (range != null) markedReadIds else null, items) } return markedRead } @@ -684,17 +716,6 @@ object ChatModel { return count to ns } - // returns the index of the passed item and the next item (it has smaller index) - fun getNextChatItem(ci: ChatItem): Pair { - val i = getChatItemIndexOrNull(ci) - return if (i != null) { - val reversedChatItems = chatItems.asReversed() - i to if (i > 0) reversedChatItems[i - 1] else null - } else { - null to null - } - } - // returns the index of the first item in the same merged group (the first hidden item) // and the previous visible item with another merge category fun getPrevShownChatItem(ciIndex: Int?, ciCategory: CIMergeCategory?): Pair { @@ -738,7 +759,7 @@ object ChatModel { fun replaceConnReqView(id: String, withId: String) { if (id == showingInvitation.value?.connId) { showingInvitation.value = null - chatModel.chatItems.clear() + chatModel.chatItems.clearAndNotify() chatModel.chatId.value = withId ModalManager.start.closeModals() ModalManager.end.closeModals() @@ -748,7 +769,7 @@ object ChatModel { fun dismissConnReqView(id: String) { if (id == showingInvitation.value?.connId) { showingInvitation.value = null - chatModel.chatItems.clear() + chatModel.chatItems.clearAndNotify() chatModel.chatId.value = null // Close NewChatView ModalManager.start.closeModals() @@ -798,6 +819,15 @@ object ChatModel { fun connectedToRemote(): Boolean = currentRemoteHost.value != null || remoteCtrlSession.value?.active == true } +interface ChatItemsChangesListener { + // pass null itemIds if the whole chat now read + fun read(itemIds: Set?, newItems: List) + fun added(item: Pair, index: Int) + // itemId, index in old chatModel.chatItems (before the update), isRcvNew (is item unread or not) + fun removed(itemIds: List>, newItems: List) + fun cleared() +} + data class ShowingInvitation( val connId: String, val connReq: String, @@ -1293,6 +1323,12 @@ data class Contact( } } +@Serializable +data class NavigationInfo( + val afterUnread: Int = 0, + val afterTotal: Int = 0 +) + @Serializable enum class ContactStatus { @SerialName("active") Active, @@ -2279,12 +2315,24 @@ data class ChatItem ( } } -fun MutableState>.add(index: Int, elem: T) { - value = SnapshotStateList().apply { addAll(value); add(index, elem) } +fun MutableState>.add(index: Int, elem: Chat) { + value = SnapshotStateList().apply { addAll(value); add(index, elem) } } -fun MutableState>.add(elem: T) { - value = SnapshotStateList().apply { addAll(value); add(elem) } +fun MutableState>.addAndNotify(index: Int, elem: ChatItem) { + value = SnapshotStateList().apply { addAll(value); add(index, elem); chatItemsChangesListener?.added(elem.id to elem.isRcvNew, index) } +} + +fun MutableState>.add(elem: Chat) { + value = SnapshotStateList().apply { addAll(value); add(elem) } +} + +// For some reason, Kotlin version crashes if the list is empty +fun MutableList.removeAll(predicate: (T) -> Boolean): Boolean = if (isEmpty()) false else remAll(predicate) + +// Adds item to chatItems and notifies a listener about newly added item +fun MutableState>.addAndNotify(elem: ChatItem) { + value = SnapshotStateList().apply { addAll(value); add(elem); chatItemsChangesListener?.added(elem.id to elem.isRcvNew, lastIndex) } } fun MutableState>.addAll(index: Int, elems: List) { @@ -2295,28 +2343,59 @@ fun MutableState>.addAll(elems: List) { value = SnapshotStateList().apply { addAll(value); addAll(elems) } } -fun MutableState>.removeAll(block: (T) -> Boolean) { - value = SnapshotStateList().apply { addAll(value); removeAll(block) } +fun MutableState>.removeAll(block: (Chat) -> Boolean) { + value = SnapshotStateList().apply { addAll(value); removeAll(block) } } -fun MutableState>.removeAt(index: Int): T { - val new = SnapshotStateList() +// Removes item(s) from chatItems and notifies a listener about removed item(s) +fun MutableState>.removeAllAndNotify(block: (ChatItem) -> Boolean) { + val toRemove = ArrayList>() + value = SnapshotStateList().apply { + addAll(value) + var i = 0 + removeAll { + val remove = block(it) + if (remove) toRemove.add(Triple(it.id, i, it.isRcvNew)) + i++ + remove + } + } + if (toRemove.isNotEmpty()) { + chatItemsChangesListener?.removed(toRemove, value) + } +} + +fun MutableState>.removeAt(index: Int): Chat { + val new = SnapshotStateList() new.addAll(value) val res = new.removeAt(index) value = new return res } -fun MutableState>.removeLast() { - value = SnapshotStateList().apply { addAll(value); removeLast() } +fun MutableState>.removeLastAndNotify() { + val removed: Triple + value = SnapshotStateList().apply { + addAll(value) + val remIndex = lastIndex + val rem = removeLast() + removed = Triple(rem.id, remIndex, rem.isRcvNew) + } + chatItemsChangesListener?.removed(listOf(removed), value) } fun MutableState>.replaceAll(elems: List) { value = SnapshotStateList().apply { addAll(elems) } } -fun MutableState>.clear() { - value = SnapshotStateList() +fun MutableState>.clear() { + value = SnapshotStateList() +} + +// Removes all chatItems and notifies a listener about it +fun MutableState>.clearAndNotify() { + value = SnapshotStateList() + chatItemsChangesListener?.cleared() } fun State>.asReversed(): MutableList = value.asReversed() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index 5674920914..7f7f8a6e58 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -22,6 +22,7 @@ import dev.icerock.moko.resources.compose.painterResource import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.call.* +import chat.simplex.common.views.chat.item.showQuotedItemDoesNotExistAlert import chat.simplex.common.views.migration.MigrationFileLinkData import chat.simplex.common.views.onboarding.OnboardingStage import chat.simplex.common.views.usersettings.* @@ -868,11 +869,15 @@ object ChatController { return emptyList() } - suspend fun apiGetChat(rh: Long?, type: ChatType, id: Long, pagination: ChatPagination = ChatPagination.Last(ChatPagination.INITIAL_COUNT), search: String = ""): Chat? { + suspend fun apiGetChat(rh: Long?, type: ChatType, id: Long, pagination: ChatPagination, search: String = ""): Pair? { val r = sendCmd(rh, CC.ApiGetChat(type, id, pagination, search)) - if (r is CR.ApiChat) return if (rh == null) r.chat else r.chat.copy(remoteHostId = rh) + if (r is CR.ApiChat) return if (rh == null) r.chat to r.navInfo else r.chat.copy(remoteHostId = rh) to r.navInfo Log.e(TAG, "apiGetChat bad response: ${r.responseType} ${r.details}") - AlertManager.shared.showAlertMsg(generalGetString(MR.strings.failed_to_parse_chat_title), generalGetString(MR.strings.contact_developers)) + if (pagination is ChatPagination.Around && r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorStore && r.chatError.storeError is StoreError.ChatItemNotFound) { + showQuotedItemDoesNotExistAlert() + } else { + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.failed_to_parse_chat_title), generalGetString(MR.strings.contact_developers)) + } return null } @@ -2861,7 +2866,7 @@ object ChatController { chatModel.users.addAll(users) chatModel.currentUser.value = user if (user == null) { - chatModel.chatItems.clear() + chatModel.chatItems.clearAndNotify() withChats { chats.clear() popChatCollector.clear() @@ -3423,7 +3428,7 @@ sealed class CC { is GetAgentServersSummary -> "getAgentServersSummary" } - class ItemRange(val from: Long, val to: Long) + data class ItemRange(val from: Long, val to: Long) fun chatItemTTLStr(seconds: Long?): String { if (seconds == null) return "none" @@ -3471,15 +3476,19 @@ sealed class ChatPagination { class Last(val count: Int): ChatPagination() class After(val chatItemId: Long, val count: Int): ChatPagination() class Before(val chatItemId: Long, val count: Int): ChatPagination() + class Around(val chatItemId: Long, val count: Int): ChatPagination() + class Initial(val count: Int): ChatPagination() val cmdString: String get() = when (this) { is Last -> "count=${this.count}" is After -> "after=${this.chatItemId} count=${this.count}" is Before -> "before=${this.chatItemId} count=${this.count}" + is Around -> "around=${this.chatItemId} count=${this.count}" + is Initial -> "initial=${this.count}" } companion object { - const val INITIAL_COUNT = 100 + val INITIAL_COUNT = if (appPlatform.isDesktop) 100 else 75 const val PRELOAD_COUNT = 100 const val UNTIL_PRELOAD_COUNT = 50 } @@ -4917,7 +4926,7 @@ sealed class CR { @Serializable @SerialName("chatRunning") class ChatRunning: CR() @Serializable @SerialName("chatStopped") class ChatStopped: CR() @Serializable @SerialName("apiChats") class ApiChats(val user: UserRef, val chats: List): CR() - @Serializable @SerialName("apiChat") class ApiChat(val user: UserRef, val chat: Chat): CR() + @Serializable @SerialName("apiChat") class ApiChat(val user: UserRef, val chat: Chat, val navInfo: NavigationInfo = NavigationInfo()): CR() @Serializable @SerialName("chatItemInfo") class ApiChatItemInfo(val user: UserRef, val chatItem: AChatItem, val chatItemInfo: ChatItemInfo): CR() @Serializable @SerialName("userProtoServers") class UserProtoServers(val user: UserRef, val servers: UserProtocolServers): CR() @Serializable @SerialName("serverTestResult") class ServerTestResult(val user: UserRef, val testServer: String, val testFailure: ProtocolTestFailure? = null): CR() @@ -5267,7 +5276,7 @@ sealed class CR { is ChatRunning -> noDetails() is ChatStopped -> noDetails() is ApiChats -> withUser(user, json.encodeToString(chats)) - is ApiChat -> withUser(user, json.encodeToString(chat)) + is ApiChat -> withUser(user, "chat: ${json.encodeToString(chat)}\nnavInfo: ${navInfo}") is ApiChatItemInfo -> withUser(user, "chatItem: ${json.encodeToString(chatItem)}\n${json.encodeToString(chatItemInfo)}") is UserProtoServers -> withUser(user, "servers: ${json.encodeToString(servers)}") is ServerTestResult -> withUser(user, "server: $testServer\nresult: ${json.encodeToString(testFailure)}") diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt index e7c653e1b9..1f1cb45d48 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt @@ -65,7 +65,7 @@ abstract class NtfManager { } val cInfo = chatModel.getChat(chatId)?.chatInfo chatModel.clearOverlays.value = true - if (cInfo != null && (cInfo is ChatInfo.Direct || cInfo is ChatInfo.Group)) openChat(null, cInfo, chatModel) + if (cInfo != null && (cInfo is ChatInfo.Direct || cInfo is ChatInfo.Group)) openChat(null, cInfo) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt index d4eb416081..b6eb4c8996 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.lazy.* import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.* import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.layoutId @@ -25,6 +26,7 @@ import chat.simplex.common.platform.* import chat.simplex.common.views.chat.item.CONSOLE_COMPOSE_LAYOUT_ID import chat.simplex.common.views.chat.item.AdaptingBottomPaddingLayout import chat.simplex.common.views.chatlist.NavigationBarBackground +import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch @@ -125,11 +127,11 @@ fun TerminalLog(floating: Boolean, composeViewHeight: State) { derivedStateOf { chatModel.terminalItems.value.asReversed() } } val listState = LocalAppBarHandler.current?.listState ?: rememberLazyListState() + var autoScrollToBottom = rememberSaveable { mutableStateOf(true) } LaunchedEffect(Unit) { - var autoScrollToBottom = listState.firstVisibleItemIndex <= 1 launch { snapshotFlow { listState.layoutInfo.totalItemsCount } - .filter { autoScrollToBottom } + .filter { autoScrollToBottom.value } .collect { try { listState.scrollToItem(0) @@ -138,10 +140,16 @@ fun TerminalLog(floating: Boolean, composeViewHeight: State) { } } } + var oldNumberOfElements = listState.layoutInfo.totalItemsCount launch { snapshotFlow { listState.firstVisibleItemIndex } + .drop(1) .collect { - autoScrollToBottom = it == 0 + if (oldNumberOfElements != listState.layoutInfo.totalItemsCount) { + oldNumberOfElements = listState.layoutInfo.totalItemsCount + return@collect + } + autoScrollToBottom.value = it == 0 } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt index 30bbe72a72..8b4f7f8ec9 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt @@ -201,7 +201,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools SectionItemView( click = { withBGApi { - openChat(chatRh, forwardedFromItem.chatInfo, chatModel) + openChat(chatRh, forwardedFromItem.chatInfo) ModalManager.end.closeModals() } }, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemsLoader.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemsLoader.kt new file mode 100644 index 0000000000..22fba59004 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemsLoader.kt @@ -0,0 +1,301 @@ +package chat.simplex.common.views.chat + +import androidx.compose.runtime.snapshots.SnapshotStateList +import chat.simplex.common.model.* +import chat.simplex.common.model.ChatModel.withChats +import chat.simplex.common.platform.chatModel +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.StateFlow +import kotlin.math.min + +const val TRIM_KEEP_COUNT = 200 + +suspend fun apiLoadMessages( + rhId: Long?, + chatType: ChatType, + apiId: Long, + pagination: ChatPagination, + chatState: ActiveChatState, + search: String = "", + visibleItemIndexesNonReversed: () -> IntRange = { 0 .. 0 } +) = coroutineScope { + val (chat, navInfo) = chatModel.controller.apiGetChat(rhId, chatType, apiId, pagination, search) ?: return@coroutineScope + // For .initial allow the chatItems to be empty as well as chatModel.chatId to not match this chat because these values become set after .initial finishes + if (((chatModel.chatId.value != chat.id || chat.chatItems.isEmpty()) && pagination !is ChatPagination.Initial && pagination !is ChatPagination.Last) + || !isActive) return@coroutineScope + + val (splits, unreadAfterItemId, totalAfter, unreadTotal, unreadAfter, unreadAfterNewestLoaded) = chatState + val oldItems = chatModel.chatItems.value + val newItems = SnapshotStateList() + when (pagination) { + is ChatPagination.Initial -> { + val newSplits = if (chat.chatItems.isNotEmpty() && navInfo.afterTotal > 0) listOf(chat.chatItems.last().id) else emptyList() + withChats { + if (chatModel.getChat(chat.id) == null) { + addChat(chat) + } + } + withContext(Dispatchers.Main) { + chatModel.chatItemStatuses.clear() + chatModel.chatItems.replaceAll(chat.chatItems) + chatModel.chatId.value = chat.chatInfo.id + splits.value = newSplits + if (chat.chatItems.isNotEmpty()) { + unreadAfterItemId.value = chat.chatItems.last().id + } + totalAfter.value = navInfo.afterTotal + unreadTotal.value = chat.chatStats.unreadCount + unreadAfter.value = navInfo.afterUnread + unreadAfterNewestLoaded.value = navInfo.afterUnread + } + } + is ChatPagination.Before -> { + newItems.addAll(oldItems) + val indexInCurrentItems: Int = oldItems.indexOfFirst { it.id == pagination.chatItemId } + if (indexInCurrentItems == -1) return@coroutineScope + val (newIds, _) = mapItemsToIds(chat.chatItems) + val wasSize = newItems.size + val (oldUnreadSplitIndex, newUnreadSplitIndex, trimmedIds, newSplits) = removeDuplicatesAndModifySplitsOnBeforePagination( + unreadAfterItemId, newItems, newIds, splits, visibleItemIndexesNonReversed + ) + val insertAt = (indexInCurrentItems - (wasSize - newItems.size) + trimmedIds.size).coerceAtLeast(0) + newItems.addAll(insertAt, chat.chatItems) + withContext(Dispatchers.Main) { + chatModel.chatItems.replaceAll(newItems) + splits.value = newSplits + chatState.moveUnreadAfterItem(oldUnreadSplitIndex, newUnreadSplitIndex, oldItems) + } + } + is ChatPagination.After -> { + newItems.addAll(oldItems) + val indexInCurrentItems: Int = oldItems.indexOfFirst { it.id == pagination.chatItemId } + if (indexInCurrentItems == -1) return@coroutineScope + + val mappedItems = mapItemsToIds(chat.chatItems) + val newIds = mappedItems.first + val (newSplits, unreadInLoaded) = removeDuplicatesAndModifySplitsOnAfterPagination( + mappedItems.second, pagination.chatItemId, newItems, newIds, chat, splits + ) + val indexToAdd = min(indexInCurrentItems + 1, newItems.size) + val indexToAddIsLast = indexToAdd == newItems.size + newItems.addAll(indexToAdd, chat.chatItems) + withContext(Dispatchers.Main) { + chatModel.chatItems.replaceAll(newItems) + splits.value = newSplits + chatState.moveUnreadAfterItem(splits.value.firstOrNull() ?: newItems.last().id, newItems) + // loading clear bottom area, updating number of unread items after the newest loaded item + if (indexToAddIsLast) { + unreadAfterNewestLoaded.value -= unreadInLoaded + } + } + } + is ChatPagination.Around -> { + newItems.addAll(oldItems) + val newSplits = removeDuplicatesAndUpperSplits(newItems, chat, splits, visibleItemIndexesNonReversed) + // currently, items will always be added on top, which is index 0 + newItems.addAll(0, chat.chatItems) + withContext(Dispatchers.Main) { + chatModel.chatItems.replaceAll(newItems) + splits.value = listOf(chat.chatItems.last().id) + newSplits + unreadAfterItemId.value = chat.chatItems.last().id + totalAfter.value = navInfo.afterTotal + unreadTotal.value = chat.chatStats.unreadCount + unreadAfter.value = navInfo.afterUnread + // no need to set it, count will be wrong + // unreadAfterNewestLoaded.value = navInfo.afterUnread + } + } + is ChatPagination.Last -> { + newItems.addAll(oldItems) + removeDuplicates(newItems, chat) + newItems.addAll(chat.chatItems) + withContext(Dispatchers.Main) { + chatModel.chatItems.replaceAll(newItems) + unreadAfterNewestLoaded.value = 0 + } + } + } +} + +private data class ModifiedSplits ( + val oldUnreadSplitIndex: Int, + val newUnreadSplitIndex: Int, + val trimmedIds: Set, + val newSplits: List, +) + +private fun removeDuplicatesAndModifySplitsOnBeforePagination( + unreadAfterItemId: StateFlow, + newItems: SnapshotStateList, + newIds: Set, + splits: StateFlow>, + visibleItemIndexesNonReversed: () -> IntRange +): ModifiedSplits { + var oldUnreadSplitIndex: Int = -1 + var newUnreadSplitIndex: Int = -1 + val visibleItemIndexes = visibleItemIndexesNonReversed() + var lastSplitIndexTrimmed = -1 + var allowedTrimming = true + var index = 0 + /** keep the newest [TRIM_KEEP_COUNT] items (bottom area) and oldest [TRIM_KEEP_COUNT] items, trim others */ + val trimRange = visibleItemIndexes.last + TRIM_KEEP_COUNT .. newItems.size - TRIM_KEEP_COUNT + val trimmedIds = mutableSetOf() + val prevItemTrimRange = visibleItemIndexes.last + TRIM_KEEP_COUNT + 1 .. newItems.size - TRIM_KEEP_COUNT + var newSplits = splits.value + + newItems.removeAll { + val invisibleItemToTrim = trimRange.contains(index) && allowedTrimming + val prevItemWasTrimmed = prevItemTrimRange.contains(index) && allowedTrimming + // may disable it after clearing the whole split range + if (splits.value.isNotEmpty() && it.id == splits.value.firstOrNull()) { + // trim only in one split range + allowedTrimming = false + } + val indexInSplits = splits.value.indexOf(it.id) + if (indexInSplits != -1) { + lastSplitIndexTrimmed = indexInSplits + } + if (invisibleItemToTrim) { + if (prevItemWasTrimmed) { + trimmedIds.add(it.id) + } else { + newUnreadSplitIndex = index + // prev item is not supposed to be trimmed, so exclude current one from trimming and set a split here instead. + // this allows to define splitRange of the oldest items and to start loading trimmed items when user scrolls in the opposite direction + if (lastSplitIndexTrimmed == -1) { + newSplits = listOf(it.id) + newSplits + } else { + val new = ArrayList(newSplits) + new[lastSplitIndexTrimmed] = it.id + newSplits = new + } + } + } + if (unreadAfterItemId.value == it.id) { + oldUnreadSplitIndex = index + } + index++ + (invisibleItemToTrim && prevItemWasTrimmed) || newIds.contains(it.id) + } + // will remove any splits that now becomes obsolete because items were merged + newSplits = newSplits.filterNot { split -> newIds.contains(split) || trimmedIds.contains(split) } + return ModifiedSplits(oldUnreadSplitIndex, newUnreadSplitIndex, trimmedIds, newSplits) +} + +private fun removeDuplicatesAndModifySplitsOnAfterPagination( + unreadInLoaded: Int, + paginationChatItemId: Long, + newItems: SnapshotStateList, + newIds: Set, + chat: Chat, + splits: StateFlow> +): Pair, Int> { + var unreadInLoaded = unreadInLoaded + var firstItemIdBelowAllSplits: Long? = null + val splitsToRemove = ArrayList() + val indexInSplitRanges = splits.value.indexOf(paginationChatItemId) + // Currently, it should always load from split range + val loadingFromSplitRange = indexInSplitRanges != -1 + val splitsToMerge = if (loadingFromSplitRange && indexInSplitRanges + 1 <= splits.value.size) ArrayList(splits.value.subList(indexInSplitRanges + 1, splits.value.size)) else ArrayList() + newItems.removeAll { + val duplicate = newIds.contains(it.id) + if (loadingFromSplitRange && duplicate) { + if (splitsToMerge.contains(it.id)) { + splitsToMerge.remove(it.id) + splitsToRemove.add(it.id) + } else if (firstItemIdBelowAllSplits == null && splitsToMerge.isEmpty()) { + // we passed all splits and found duplicated item below all of them, which means no splits anymore below the loaded items + firstItemIdBelowAllSplits = it.id + } + } + if (duplicate && it.isRcvNew) { + unreadInLoaded-- + } + duplicate + } + var newSplits: List = emptyList() + if (firstItemIdBelowAllSplits != null) { + // no splits anymore, all were merged with bottom items + newSplits = emptyList() + } else { + if (splitsToRemove.isNotEmpty()) { + val new = ArrayList(splits.value) + new.removeAll(splitsToRemove.toSet()) + newSplits = new + } + val enlargedSplit = splits.value.indexOf(paginationChatItemId) + if (enlargedSplit != -1) { + // move the split to the end of loaded items + val new = ArrayList(splits.value) + new[enlargedSplit] = chat.chatItems.last().id + newSplits = new + // Log.d(TAG, "Enlarged split range $newSplits") + } + } + return newSplits to unreadInLoaded +} + +private fun removeDuplicatesAndUpperSplits( + newItems: SnapshotStateList, + chat: Chat, + splits: StateFlow>, + visibleItemIndexesNonReversed: () -> IntRange +): List { + if (splits.value.isEmpty()) { + removeDuplicates(newItems, chat) + return splits.value + } + + val newSplits = splits.value.toMutableList() + val visibleItemIndexes = visibleItemIndexesNonReversed() + val (newIds, _) = mapItemsToIds(chat.chatItems) + val idsToTrim = ArrayList>() + idsToTrim.add(mutableSetOf()) + var index = 0 + newItems.removeAll { + val duplicate = newIds.contains(it.id) + if (!duplicate && visibleItemIndexes.first > index) { + idsToTrim.last().add(it.id) + } + if (visibleItemIndexes.first > index && splits.value.contains(it.id)) { + newSplits -= it.id + // closing previous range. All items in idsToTrim that ends with empty set should be deleted. + // Otherwise, the last set should be excluded from trimming because it is in currently visible split range + idsToTrim.add(mutableSetOf()) + } + + index++ + duplicate + } + if (idsToTrim.last().isNotEmpty()) { + // it has some elements to trim from currently visible range which means the items shouldn't be trimmed + // Otherwise, the last set would be empty + idsToTrim.removeLast() + } + val allItemsToDelete = idsToTrim.flatten() + if (allItemsToDelete.isNotEmpty()) { + newItems.removeAll { allItemsToDelete.contains(it.id) } + } + return newSplits +} + +// ids, number of unread items +private fun mapItemsToIds(items: List): Pair, Int> { + var unreadInLoaded = 0 + val ids = mutableSetOf() + var i = 0 + while (i < items.size) { + val item = items[i] + ids.add(item.id) + if (item.isRcvNew) { + unreadInLoaded++ + } + i++ + } + return ids to unreadInLoaded +} + +private fun removeDuplicates(newItems: SnapshotStateList, chat: Chat) { + val (newIds, _) = mapItemsToIds(chat.chatItems) + newItems.removeAll { newIds.contains(it.id) } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemsMerger.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemsMerger.kt new file mode 100644 index 0000000000..fda5c35e01 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemsMerger.kt @@ -0,0 +1,379 @@ +package chat.simplex.common.views.chat + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.* +import chat.simplex.common.model.* +import chat.simplex.common.platform.chatModel +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.MutableStateFlow + +data class MergedItems ( + val items: List, + val splits: List, + // chat item id, index in list + val indexInParentItems: Map, +) { + companion object { + fun create(items: List, unreadCount: State, revealedItems: Set, chatState: ActiveChatState): MergedItems { + if (items.isEmpty()) return MergedItems(emptyList(), emptyList(), emptyMap()) + + val unreadAfterItemId = chatState.unreadAfterItemId + val itemSplits = chatState.splits.value + val mergedItems = ArrayList() + // Indexes of splits here will be related to reversedChatItems, not chatModel.chatItems + val splitRanges = ArrayList() + val indexInParentItems = mutableMapOf() + var index = 0 + var unclosedSplitIndex: Int? = null + var unclosedSplitIndexInParent: Int? = null + var visibleItemIndexInParent = -1 + var unreadBefore = unreadCount.value - chatState.unreadAfterNewestLoaded.value + var lastRevealedIdsInMergedItems: MutableList? = null + var lastRangeInReversedForMergedItems: MutableStateFlow? = null + var recent: MergedItem? = null + while (index < items.size) { + val item = items[index] + val prev = items.getOrNull(index - 1) + val next = items.getOrNull(index + 1) + val category = item.mergeCategory + val itemIsSplit = itemSplits.contains(item.id) + + if (item.id == unreadAfterItemId.value) { + unreadBefore = unreadCount.value - chatState.unreadAfter.value + } + if (item.isRcvNew) unreadBefore-- + + val revealed = item.mergeCategory == null || revealedItems.contains(item.id) + if (recent is MergedItem.Grouped && recent.mergeCategory == category && !revealedItems.contains(recent.items.first().item.id) && !itemIsSplit) { + val listItem = ListItem(item, prev, next, unreadBefore) + recent.items.add(listItem) + + if (item.isRcvNew) { + recent.unreadIds.add(item.id) + } + if (lastRevealedIdsInMergedItems != null && lastRangeInReversedForMergedItems != null) { + if (revealed) { + lastRevealedIdsInMergedItems += item.id + } + lastRangeInReversedForMergedItems.value = lastRangeInReversedForMergedItems.value.first..index + } + } else { + visibleItemIndexInParent++ + val listItem = ListItem(item, prev, next, unreadBefore) + recent = if (item.mergeCategory != null) { + if (item.mergeCategory != prev?.mergeCategory || lastRevealedIdsInMergedItems == null) { + lastRevealedIdsInMergedItems = if (revealedItems.contains(item.id)) mutableListOf(item.id) else mutableListOf() + } else if (revealed) { + lastRevealedIdsInMergedItems += item.id + } + lastRangeInReversedForMergedItems = MutableStateFlow(index .. index) + MergedItem.Grouped( + items = arrayListOf(listItem), + revealed = revealed, + revealedIdsWithinGroup = lastRevealedIdsInMergedItems, + rangeInReversed = lastRangeInReversedForMergedItems, + mergeCategory = item.mergeCategory, + startIndexInReversedItems = index, + unreadIds = if (item.isRcvNew) mutableSetOf(item.id) else mutableSetOf() + ) + } else { + lastRangeInReversedForMergedItems = null + MergedItem.Single( + item = listItem, + startIndexInReversedItems = index + ) + } + mergedItems.add(recent) + } + if (itemIsSplit) { + // found item that is considered as a split + if (unclosedSplitIndex != null && unclosedSplitIndexInParent != null) { + // it was at least second split in the list + splitRanges.add(SplitRange(unclosedSplitIndex until index, unclosedSplitIndexInParent until visibleItemIndexInParent)) + } + unclosedSplitIndex = index + unclosedSplitIndexInParent = visibleItemIndexInParent + } else if (index + 1 == items.size && unclosedSplitIndex != null && unclosedSplitIndexInParent != null) { + // just one split for the whole list, there will be no more, it's the end + splitRanges.add(SplitRange(unclosedSplitIndex .. index, unclosedSplitIndexInParent .. visibleItemIndexInParent)) + } + indexInParentItems[item.id] = visibleItemIndexInParent + index++ + } + return MergedItems( + mergedItems, + splitRanges, + indexInParentItems + ) + } + } +} + +sealed class MergedItem { + abstract val startIndexInReversedItems: Int + + // the item that is always single, cannot be grouped and always revealed + data class Single( + val item: ListItem, + override val startIndexInReversedItems: Int, + ): MergedItem() + + /** The item that can contain multiple items or just one depending on revealed state. When the whole group of merged items is revealed, + * there will be multiple [Grouped] items with revealed flag set to true. When the whole group is collapsed, it will be just one instance + * of [Grouped] item with all grouped items inside [items]. In other words, number of [MergedItem] will always be equal to number of + * visible rows in ChatView LazyColumn */ + @Stable + data class Grouped ( + val items: ArrayList, + val revealed: Boolean, + // it stores ids for all consecutive revealed items from the same group in order to hide them all on user's action + // it's the same list instance for all Grouped items within revealed group + /** @see reveal */ + val revealedIdsWithinGroup: MutableList, + val rangeInReversed: MutableStateFlow, + val mergeCategory: CIMergeCategory?, + val unreadIds: MutableSet, + override val startIndexInReversedItems: Int, + ): MergedItem() { + fun reveal(reveal: Boolean, revealedItems: MutableState>) { + val newRevealed = revealedItems.value.toMutableSet() + var i = 0 + if (reveal) { + while (i < items.size) { + newRevealed.add(items[i].item.id) + i++ + } + } else { + while (i < revealedIdsWithinGroup.size) { + newRevealed.remove(revealedIdsWithinGroup[i]) + i++ + } + revealedIdsWithinGroup.clear() + } + revealedItems.value = newRevealed + } + } + + fun hasUnread(): Boolean = when (this) { + is Single -> item.item.isRcvNew + is Grouped -> unreadIds.isNotEmpty() + } + + fun newest(): ListItem = when (this) { + is Single -> item + is Grouped -> items.first() + } + + fun oldest(): ListItem = when (this) { + is Single -> item + is Grouped -> items.last() + } + + fun lastIndexInReversed(): Int = when (this) { + is Single -> startIndexInReversedItems + is Grouped -> startIndexInReversedItems + items.lastIndex + } +} + +data class SplitRange( + /** range of indexes inside reversedChatItems where the first element is the split (it's index is [indexRangeInReversed.first]) + * so [0, 1, 2, -100-, 101] if the 3 is a split, SplitRange(indexRange = 3 .. 4) will be this SplitRange instance + * (3, 4 indexes of the splitRange with the split itself at index 3) + * */ + val indexRangeInReversed: IntRange, + /** range of indexes inside LazyColumn where the first element is the split (it's index is [indexRangeInParentItems.first]) */ + val indexRangeInParentItems: IntRange +) + +data class ListItem( + val item: ChatItem, + val prevItem: ChatItem?, + val nextItem: ChatItem?, + // how many unread items before (older than) this one (excluding this one) + val unreadBefore: Int +) + +data class ActiveChatState ( + val splits: MutableStateFlow> = MutableStateFlow(emptyList()), + val unreadAfterItemId: MutableStateFlow = MutableStateFlow(-1L), + // total items after unread after item (exclusive) + val totalAfter: MutableStateFlow = MutableStateFlow(0), + val unreadTotal: MutableStateFlow = MutableStateFlow(0), + // exclusive + val unreadAfter: MutableStateFlow = MutableStateFlow(0), + // exclusive + val unreadAfterNewestLoaded: MutableStateFlow = MutableStateFlow(0) +) { + fun moveUnreadAfterItem(toItemId: Long?, nonReversedItems: List) { + toItemId ?: return + val currentIndex = nonReversedItems.indexOfFirst { it.id == unreadAfterItemId.value } + val newIndex = nonReversedItems.indexOfFirst { it.id == toItemId } + if (currentIndex == -1 || newIndex == -1) return + unreadAfterItemId.value = toItemId + val unreadDiff = if (newIndex > currentIndex) { + -nonReversedItems.subList(currentIndex + 1, newIndex + 1).count { it.isRcvNew } + } else { + nonReversedItems.subList(newIndex + 1, currentIndex + 1).count { it.isRcvNew } + } + unreadAfter.value += unreadDiff + } + + fun moveUnreadAfterItem(fromIndex: Int, toIndex: Int, nonReversedItems: List) { + if (fromIndex == -1 || toIndex == -1) return + unreadAfterItemId.value = nonReversedItems[toIndex].id + val unreadDiff = if (toIndex > fromIndex) { + -nonReversedItems.subList(fromIndex + 1, toIndex + 1).count { it.isRcvNew } + } else { + nonReversedItems.subList(toIndex + 1, fromIndex + 1).count { it.isRcvNew } + } + unreadAfter.value += unreadDiff + } + + fun clear() { + splits.value = emptyList() + unreadAfterItemId.value = -1L + totalAfter.value = 0 + unreadTotal.value = 0 + unreadAfter.value = 0 + unreadAfterNewestLoaded.value = 0 + } +} + +fun visibleItemIndexesNonReversed(mergedItems: State, listState: LazyListState): IntRange { + val zero = 0 .. 0 + if (listState.layoutInfo.totalItemsCount == 0) return zero + val newest = mergedItems.value.items.getOrNull(listState.firstVisibleItemIndex)?.startIndexInReversedItems + val oldest = mergedItems.value.items.getOrNull(listState.layoutInfo.visibleItemsInfo.last().index)?.lastIndexInReversed() + if (newest == null || oldest == null) return zero + val size = chatModel.chatItems.value.size + val range = size - oldest .. size - newest + if (range.first < 0 || range.last < 0) return zero + + // visible items mapped to their underlying data structure which is chatModel.chatItems + return range +} + +fun recalculateChatStatePositions(chatState: ActiveChatState) = object: ChatItemsChangesListener { + override fun read(itemIds: Set?, newItems: List) { + val (_, unreadAfterItemId, _, unreadTotal, unreadAfter) = chatState + if (itemIds == null) { + // special case when the whole chat became read + unreadTotal.value = 0 + unreadAfter.value = 0 + return + } + var unreadAfterItemIndex: Int = -1 + // since it's more often that the newest items become read, it's logical to loop from the end of the list to finish it faster + var i = newItems.lastIndex + val ids = itemIds.toMutableSet() + // intermediate variables to prevent re-setting state value a lot of times without reason + var newUnreadTotal = unreadTotal.value + var newUnreadAfter = unreadAfter.value + while (i >= 0) { + val item = newItems[i] + if (item.id == unreadAfterItemId.value) { + unreadAfterItemIndex = i + } + if (ids.contains(item.id)) { + // was unread, now this item is read + if (unreadAfterItemIndex == -1) { + newUnreadAfter-- + } + newUnreadTotal-- + ids.remove(item.id) + if (ids.isEmpty()) break + } + i-- + } + unreadTotal.value = newUnreadTotal + unreadAfter.value = newUnreadAfter + } + override fun added(item: Pair, index: Int) { + if (item.second) { + chatState.unreadAfter.value++ + chatState.unreadTotal.value++ + } + } + override fun removed(itemIds: List>, newItems: List) { + val (splits, unreadAfterItemId, totalAfter, unreadTotal, unreadAfter) = chatState + val newSplits = ArrayList() + for (split in splits.value) { + val index = itemIds.indexOfFirst { it.first == split } + // deleted the item that was right before the split between items, find newer item so it will act like the split + if (index != -1) { + val newSplit = newItems.getOrNull(itemIds[index].second - itemIds.count { it.second <= index })?.id + // it the whole section is gone and splits overlap, don't add it at all + if (newSplit != null && !newSplits.contains(newSplit)) { + newSplits.add(newSplit) + } + } else { + newSplits.add(split) + } + } + splits.value = newSplits + + val index = itemIds.indexOfFirst { it.first == unreadAfterItemId.value } + // unread after item was removed + if (index != -1) { + var newUnreadAfterItemId = newItems.getOrNull(itemIds[index].second - itemIds.count { it.second <= index })?.id + val newUnreadAfterItemWasNull = newUnreadAfterItemId == null + if (newUnreadAfterItemId == null) { + // everything on top (including unread after item) were deleted, take top item as unread after id + newUnreadAfterItemId = newItems.firstOrNull()?.id + } + if (newUnreadAfterItemId != null) { + unreadAfterItemId.value = newUnreadAfterItemId + totalAfter.value -= itemIds.count { it.second > index } + unreadTotal.value -= itemIds.count { it.second <= index && it.third } + unreadAfter.value -= itemIds.count { it.second > index && it.third } + if (newUnreadAfterItemWasNull) { + // since the unread after item was moved one item after initial position, adjust counters accordingly + if (newItems.firstOrNull()?.isRcvNew == true) { + unreadTotal.value++ + unreadAfter.value-- + } + } + } else { + // all items were deleted, 0 items in chatItems + unreadAfterItemId.value = -1L + totalAfter.value = 0 + unreadTotal.value = 0 + unreadAfter.value = 0 + } + } else { + totalAfter.value -= itemIds.size + } + } + override fun cleared() { chatState.clear() } +} + +/** Helps in debugging */ +//@Composable +//fun BoxScope.ShowChatState() { +// Box(Modifier.align(Alignment.Center).size(200.dp).background(Color.Black)) { +// val s = chatModel.chatState +// Text( +// "itemId ${s.unreadAfterItemId.value} / ${chatModel.chatItems.value.firstOrNull { it.id == s.unreadAfterItemId.value }?.text}, \nunreadAfter ${s.unreadAfter.value}, afterNewest ${s.unreadAfterNewestLoaded.value}", +// color = Color.White +// ) +// } +//} +//// Returns items mapping for easy checking the structure +//fun MergedItems.mappingToString(): String = items.mapIndexed { index, g -> +// when (g) { +// is MergedItem.Single -> +// "\nstartIndexInParentItems $index, startIndexInReversedItems ${g.startIndexInReversedItems}, " + +// "revealed true, " + +// "mergeCategory null " + +// "\nunreadBefore ${g.item.unreadBefore}" +// +// is MergedItem.Grouped -> +// "\nstartIndexInParentItems $index, startIndexInReversedItems ${g.startIndexInReversedItems}, " + +// "revealed ${g.revealed}, " + +// "mergeCategory ${g.items[0].item.mergeCategory} " + +// g.items.mapIndexed { i, it -> +// "\nunreadBefore ${it.unreadBefore} ${Triple(index, g.startIndexInReversedItems + i, it.item.id)}" +// } +// } +//}.toString() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index 2c43a81f7d..8dd3e42440 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -47,9 +47,9 @@ import kotlinx.coroutines.flow.* import kotlinx.datetime.* import java.io.File import java.net.URI -import kotlin.math.abs -import kotlin.math.sign +import kotlin.math.* +@Stable data class ItemSeparation(val timestamp: Boolean, val largeGap: Boolean, val date: Instant?) @Composable @@ -292,14 +292,11 @@ fun ChatView(staleChatId: State, onComposed: suspend (chatId: String) - } } }, - loadPrevMessages = { chatId -> + loadMessages = { chatId, pagination, chatState, visibleItemIndexes -> val c = chatModel.getChat(chatId) if (chatModel.chatId.value != chatId) return@ChatLayout - val firstId = chatModel.chatItems.value.firstOrNull()?.id - if (c != null && firstId != null) { - withBGApi { - apiLoadPrevMessages(c, chatModel, firstId, searchText.value) - } + if (c != null) { + apiLoadMessages(c.remoteHostId, c.chatInfo.chatType, c.chatInfo.apiId, pagination, chatState, searchText.value, visibleItemIndexes) } }, deleteMessage = { itemId, mode -> @@ -376,7 +373,7 @@ fun ChatView(staleChatId: State, onComposed: suspend (chatId: String) - }, openDirectChat = { contactId -> scope.launch { - openDirectChat(chatRh, contactId, chatModel) + openDirectChat(chatRh, contactId) } }, forwardItem = { cInfo, cItem -> @@ -516,7 +513,7 @@ fun ChatView(staleChatId: State, onComposed: suspend (chatId: String) - val c = chatModel.getChat(chatInfo.id) ?: return@ChatLayout if (chatModel.chatId.value != chatInfo.id) return@ChatLayout withBGApi { - apiFindMessages(c, chatModel, value) + apiFindMessages(c, value) searchText.value = value } }, @@ -535,7 +532,7 @@ fun ChatView(staleChatId: State, onComposed: suspend (chatId: String) - LaunchedEffect(chatInfo.id) { onComposed(chatInfo.id) ModalManager.end.closeModals() - chatModel.chatItems.clear() + chatModel.chatItems.clearAndNotify() } } is ChatInfo.InvalidJSON -> { @@ -546,7 +543,7 @@ fun ChatView(staleChatId: State, onComposed: suspend (chatId: String) - LaunchedEffect(chatInfo.id) { onComposed(chatInfo.id) ModalManager.end.closeModals() - chatModel.chatItems.clear() + chatModel.chatItems.clearAndNotify() } } else -> {} @@ -583,7 +580,7 @@ fun ChatLayout( back: () -> Unit, info: () -> Unit, showMemberInfo: (GroupInfo, GroupMember) -> Unit, - loadPrevMessages: (ChatId) -> Unit, + loadMessages: suspend (ChatId, ChatPagination, ActiveChatState, visibleItemIndexesNonReversed: () -> IntRange) -> Unit, deleteMessage: (Long, CIDeleteMode) -> Unit, deleteMessages: (List) -> Unit, receiveFile: (Long) -> Unit, @@ -649,7 +646,7 @@ fun ChatLayout( Box(Modifier.fillMaxSize()) { ChatItemsList( remoteHostId, chatInfo, unreadCount, composeState, composeViewHeight, searchValue, - useLinkPreviews, linkMode, selectedChatItems, showMemberInfo, loadPrevMessages, deleteMessage, deleteMessages, + useLinkPreviews, linkMode, selectedChatItems, showMemberInfo, loadMessages, deleteMessage, deleteMessages, receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, forwardItem, updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember, setReaction, showItemDetails, markRead, remember { { onComposed(it) } }, developerTools, showViaProxy, @@ -922,7 +919,7 @@ fun BoxScope.ChatItemsList( linkMode: SimplexLinkMode, selectedChatItems: MutableState?>, showMemberInfo: (GroupInfo, GroupMember) -> Unit, - loadPrevMessages: (ChatId) -> Unit, + loadMessages: suspend (ChatId, ChatPagination, ActiveChatState, visibleItemIndexesNonReversed: () -> IntRange) -> Unit, deleteMessage: (Long, CIDeleteMode) -> Unit, deleteMessages: (List) -> Unit, receiveFile: (Long) -> Unit, @@ -945,65 +942,73 @@ fun BoxScope.ChatItemsList( developerTools: Boolean, showViaProxy: Boolean ) { - val listState = rememberLazyListState() - val scope = rememberCoroutineScope() - ScrollToBottom(chatInfo.id, listState, chatModel.chatItems) - var prevSearchEmptiness by rememberSaveable { mutableStateOf(searchValue.value.isEmpty()) } - // Scroll to bottom when search value changes from something to nothing and back - LaunchedEffect(searchValue.value.isEmpty()) { - // They are equal when orientation was changed, don't need to scroll. - // LaunchedEffect unaware of this event since it uses remember, not rememberSaveable - if (prevSearchEmptiness == searchValue.value.isEmpty()) return@LaunchedEffect - prevSearchEmptiness = searchValue.value.isEmpty() - - if (listState.firstVisibleItemIndex != 0) { - scope.launch { listState.scrollToItem(0) } + val searchValueIsEmpty = remember { derivedStateOf { searchValue.value.isEmpty() } } + val reversedChatItems = remember { derivedStateOf { chatModel.chatItems.asReversed() } } + val revealedItems = rememberSaveable(stateSaver = serializableSaver()) { mutableStateOf(setOf()) } + val mergedItems = remember { derivedStateOf { MergedItems.create(reversedChatItems.value, unreadCount, revealedItems.value, chatModel.chatState) } } + val topPaddingToContentPx = rememberUpdatedState(with(LocalDensity.current) { topPaddingToContent().roundToPx() }) + /** determines height based on window info and static height of two AppBars. It's needed because in the first graphic frame height of + * [composeViewHeight] is unknown, but we need to set scroll position for unread messages already so it will be correct before the first frame appears + * */ + val maxHeightForList = rememberUpdatedState( + with(LocalDensity.current) { LocalWindowHeight().roundToPx() - topPaddingToContentPx.value - (AppBarHeight * fontSizeSqrtMultiplier * 2).roundToPx() } + ) + val listState = rememberUpdatedState(rememberSaveable(chatInfo.id, searchValueIsEmpty.value, saver = LazyListState.Saver) { + val index = mergedItems.value.items.indexOfLast { it.hasUnread() } + if (index <= 0) { + LazyListState(0, 0) + } else { + LazyListState(index + 1, -maxHeightForList.value) + } + }) + val maxHeight = remember { derivedStateOf { listState.value.layoutInfo.viewportEndOffset - topPaddingToContentPx.value } } + val loadingMoreItems = remember { mutableStateOf(false) } + val ignoreLoadingRequests = remember(remoteHostId) { mutableSetOf() } + if (!loadingMoreItems.value) { + PreloadItems(chatInfo.id, if (searchValueIsEmpty.value) ignoreLoadingRequests else mutableSetOf(), mergedItems, listState, ChatPagination.UNTIL_PRELOAD_COUNT) { chatId, pagination -> + if (loadingMoreItems.value) return@PreloadItems false + try { + loadingMoreItems.value = true + loadMessages(chatId, pagination, chatModel.chatState) { + visibleItemIndexesNonReversed(mergedItems, listState.value) + } + } finally { + loadingMoreItems.value = false + } + true } } - PreloadItems(chatInfo.id, listState, ChatPagination.UNTIL_PRELOAD_COUNT, loadPrevMessages) + val chatInfoUpdated = rememberUpdatedState(chatInfo) + val scope = rememberCoroutineScope() + val scrollToItem: (Long) -> Unit = remember { scrollToItem(loadingMoreItems, chatInfoUpdated, maxHeight, scope, reversedChatItems, mergedItems, listState, loadMessages) } + + LoadLastItems(loadingMoreItems, remoteHostId, chatInfo) + SmallScrollOnNewMessage(listState, chatModel.chatItems) + val finishedInitialComposition = remember { mutableStateOf(false) } + NotifyChatListOnFinishingComposition(finishedInitialComposition, chatInfo, revealedItems, listState, onComposed) - Spacer(Modifier.size(8.dp)) - val reversedChatItems = remember { derivedStateOf { chatModel.chatItems.asReversed() } } - val topPaddingToContentPx = rememberUpdatedState(with(LocalDensity.current) { topPaddingToContent().roundToPx() }) - val maxHeight = remember { derivedStateOf { listState.layoutInfo.viewportEndOffset - topPaddingToContentPx.value } } - val scrollToItem: State<(Long) -> Unit> = remember { - mutableStateOf( - { itemId: Long -> - val index = reversedChatItems.value.indexOfFirst { it.id == itemId } - if (index != -1) { - scope.launch { listState.animateScrollToItem(kotlin.math.min(reversedChatItems.value.lastIndex, index + 1), -maxHeight.value) } - } - } - ) - } - // TODO: Having this block on desktop makes ChatItemsList() to recompose twice on chatModel.chatId update instead of once - LaunchedEffect(chatInfo.id) { - var stopListening = false - snapshotFlow { listState.layoutInfo.visibleItemsInfo.lastIndex } - .distinctUntilChanged() - .filter { !stopListening } - .collect { - onComposed(chatInfo.id) - stopListening = true - } - } DisposableEffectOnGone( + always = { + chatModel.chatItemsChangesListener = recalculateChatStatePositions(chatModel.chatState) + }, whenGone = { VideoPlayerHolder.releaseAll() + chatModel.chatItemsChangesListener = null } ) - LazyColumnWithScrollBar( - Modifier.align(Alignment.BottomCenter), - state = listState, - reverseLayout = true, - contentPadding = PaddingValues( - top = topPaddingToContent(), - bottom = composeViewHeight.value - ), - additionalBarOffset = composeViewHeight - ) { - itemsIndexed(reversedChatItems.value, key = { _, item -> item.id to item.meta.createdAt.toEpochMilliseconds() }) { i, cItem -> + + @Composable + fun ChatViewListItem( + itemAtZeroIndexInWholeList: Boolean, + range: State, + showAvatar: Boolean, + cItem: ChatItem, + itemSeparation: ItemSeparation, + previousItemSeparationLargeGap: Boolean, + revealed: State, + reveal: (Boolean) -> Unit + ) { val itemScope = rememberCoroutineScope() CompositionLocalProvider( // Makes horizontal and vertical scrolling to coexist nicely. @@ -1011,29 +1016,27 @@ fun BoxScope.ChatItemsList( LocalViewConfiguration provides LocalViewConfiguration.current.bigTouchSlop() ) { val provider = { - providerForGallery(i, chatModel.chatItems.value, cItem.id) { indexInReversed -> + providerForGallery(chatModel.chatItems.value, cItem.id) { indexInReversed -> itemScope.launch { - listState.scrollToItem( - kotlin.math.min(reversedChatItems.value.lastIndex, indexInReversed + 1), + listState.value.scrollToItem( + min(reversedChatItems.value.lastIndex, indexInReversed + 1), -maxHeight.value ) } } } - val revealed = remember { mutableStateOf(false) } - @Composable - fun ChatItemViewShortHand(cItem: ChatItem, itemSeparation: ItemSeparation, range: IntRange?, fillMaxWidth: Boolean = true) { + fun ChatItemViewShortHand(cItem: ChatItem, itemSeparation: ItemSeparation, range: State, fillMaxWidth: Boolean = true) { tryOrShowError("${cItem.id}ChatItem", error = { CIBrokenComposableView(if (cItem.chatDir.sent) Alignment.CenterEnd else Alignment.CenterStart) }) { - ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, range = range, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem.value, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp) + ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, range = range, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, reveal = reveal, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp) } } @Composable - fun ChatItemView(cItem: ChatItem, range: IntRange?, prevItem: ChatItem?, itemSeparation: ItemSeparation, previousItemSeparation: ItemSeparation?) { + fun ChatItemView(cItem: ChatItem, range: State, itemSeparation: ItemSeparation, previousItemSeparationLargeGap: Boolean) { val dismissState = rememberDismissState(initialValue = DismissValue.Default) { if (it == DismissValue.DismissedToStart) { itemScope.launch { @@ -1060,12 +1063,12 @@ fun BoxScope.ChatItemsList( Box( modifier = modifier.padding( bottom = if (itemSeparation.largeGap) { - if (i == 0) { + if (itemAtZeroIndexInWholeList) { 8.dp } else { 4.dp } - } else 1.dp, top = if (previousItemSeparation?.largeGap == true) 4.dp else 1.dp + } else 1.dp, top = if (previousItemSeparationLargeGap) 4.dp else 1.dp ), contentAlignment = Alignment.CenterStart ) { @@ -1089,14 +1092,7 @@ fun BoxScope.ChatItemsList( val swipeableOrSelectionModifier = (if (selectionVisible) Modifier else swipeableModifier).graphicsLayer { translationX = selectionOffset.toPx() } if (chatInfo is ChatInfo.Group) { if (cItem.chatDir is CIDirection.GroupRcv) { - val member = cItem.chatDir.groupMember - val (prevMember, memCount) = - if (range != null) { - chatModel.getPrevHiddenMember(member, range) - } else { - null to 1 - } - if (prevItem == null || showMemberImage(member, prevItem) || prevMember != null) { + if (showAvatar) { Column( Modifier .padding(top = 8.dp) @@ -1107,8 +1103,16 @@ fun BoxScope.ChatItemsList( horizontalAlignment = Alignment.Start ) { @Composable - fun MemberNameAndRole() { + fun MemberNameAndRole(range: State) { Row(Modifier.padding(bottom = 2.dp).graphicsLayer { translationX = selectionOffset.toPx() }, horizontalArrangement = Arrangement.SpaceBetween) { + val member = cItem.chatDir.groupMember + val rangeValue = range.value + val (prevMember, memCount) = + if (rangeValue != null) { + chatModel.getPrevHiddenMember(member, rangeValue) + } else { + null to 1 + } Text( memberNames(member, prevMember, memCount), Modifier @@ -1143,6 +1147,7 @@ fun BoxScope.ChatItemsList( SelectedChatItem(Modifier, cItem.id, selectedChatItems) } Row(Modifier.graphicsLayer { translationX = selectionOffset.toPx() }) { + val member = cItem.chatDir.groupMember Box(Modifier.clickable { showMemberInfo(chatInfo.groupInfo, member) }) { MemberImage(member) } @@ -1154,7 +1159,7 @@ fun BoxScope.ChatItemsList( } if (cItem.content.showMemberName) { DependentLayout(Modifier, CHAT_BUBBLE_LAYOUT_ID) { - MemberNameAndRole() + MemberNameAndRole(range) Item() } } else { @@ -1217,58 +1222,68 @@ fun BoxScope.ChatItemsList( } } } - - val (currIndex, nextItem) = chatModel.getNextChatItem(cItem) - val ciCategory = cItem.mergeCategory - if (ciCategory != null && ciCategory == nextItem?.mergeCategory) { - // memberConnected events and deleted items are aggregated at the last chat item in a row, see ChatItemView - } else { - val (prevHidden, prevItem) = chatModel.getPrevShownChatItem(currIndex, ciCategory) - - val itemSeparation = getItemSeparation(cItem, nextItem) - val previousItemSeparation = if (prevItem != null) getItemSeparation(prevItem, cItem) else null - - if (itemSeparation.date != null) { - DateSeparator(itemSeparation.date) - } - - val range = chatViewItemsRange(currIndex, prevHidden) - val reversed = reversedChatItems.value - if (revealed.value && range != null) { - reversed.subList(range.first, range.last + 1).forEachIndexed { index, ci -> - val prev = if (index + range.first == prevHidden) prevItem else reversed[index + range.first + 1] - ChatItemView(ci, null, prev, itemSeparation, previousItemSeparation) - } - } else { - ChatItemView(cItem, range, prevItem, itemSeparation, previousItemSeparation) - } - - if (i == reversed.lastIndex) { - DateSeparator(cItem.meta.itemTs) - } + if (itemSeparation.date != null) { + DateSeparator(itemSeparation.date) } + ChatItemView(cItem, range, itemSeparation, previousItemSeparationLargeGap) + } + } + LazyColumnWithScrollBar( + Modifier.align(Alignment.BottomCenter), + state = listState.value, + reverseLayout = true, + contentPadding = PaddingValues( + top = topPaddingToContent(), + bottom = composeViewHeight.value + ), + additionalBarOffset = composeViewHeight + ) { + val mergedItemsValue = mergedItems.value + itemsIndexed(mergedItemsValue.items, key = { _, merged -> keyForItem(merged.newest().item) }) { index, merged -> + val isLastItem = index == mergedItemsValue.items.lastIndex + val last = if (isLastItem) reversedChatItems.value.lastOrNull() else null + val listItem = merged.newest() + val item = listItem.item + val range = if (merged is MergedItem.Grouped) { + merged.rangeInReversed.value + } else { + null + } + val showAvatar = if (merged is MergedItem.Grouped) shouldShowAvatar(item, listItem.nextItem) else true + val isRevealed = remember { derivedStateOf { revealedItems.value.contains(item.id) } } + val itemSeparation: ItemSeparation + val prevItemSeparationLargeGap: Boolean + if (merged is MergedItem.Single || isRevealed.value) { + val prev = listItem.prevItem + itemSeparation = getItemSeparation(item, prev) + val nextForGap = if ((item.mergeCategory != null && item.mergeCategory == prev?.mergeCategory) || isLastItem) null else listItem.nextItem + prevItemSeparationLargeGap = if (nextForGap == null) false else getItemSeparationLargeGap(nextForGap, item) + } else { + itemSeparation = getItemSeparation(item, null) + prevItemSeparationLargeGap = false + } + ChatViewListItem(index == 0, rememberUpdatedState(range), showAvatar, item, itemSeparation, prevItemSeparationLargeGap, isRevealed) { + if (merged is MergedItem.Grouped) merged.reveal(it, revealedItems) + } - - if (cItem.isRcvNew && chatInfo.id == ChatModel.chatId.value) { - LaunchedEffect(cItem.id) { - itemScope.launch { - delay(600) - markRead(CC.ItemRange(cItem.id, cItem.id), null) - } - } + if (last != null) { + // no using separate item(){} block in order to have total number of items in LazyColumn match number of merged items + DateSeparator(last.meta.itemTs) + } + if (item.isRcvNew) { + val (itemIdStart, itemIdEnd) = when (merged) { + is MergedItem.Single -> merged.item.item.id to merged.item.item.id + is MergedItem.Grouped -> merged.items.last().item.id to merged.items.first().item.id } + MarkItemsReadAfterDelay(keyForItem(item), itemIdStart, itemIdEnd, finishedInitialComposition, chatInfo.id, listState, markRead) } } } - FloatingButtons(chatModel.chatItems, unreadCount, composeViewHeight, remoteHostId, chatInfo, searchValue, markRead, listState) - - FloatingDate( - Modifier.padding(top = 10.dp + topPaddingToContent()).align(Alignment.TopCenter), - listState, - ) + FloatingButtons(loadingMoreItems, mergedItems, unreadCount, maxHeight, composeViewHeight, remoteHostId, chatInfo, searchValue, markRead, listState) + FloatingDate(Modifier.padding(top = 10.dp + topPaddingToContent()).align(Alignment.TopCenter), mergedItems, listState) LaunchedEffect(Unit) { - snapshotFlow { listState.isScrollInProgress } + snapshotFlow { listState.value.isScrollInProgress } .collect { chatViewScrollState.value = it } @@ -1276,33 +1291,43 @@ fun BoxScope.ChatItemsList( } @Composable -private fun ScrollToBottom(chatId: ChatId, listState: LazyListState, chatItems: State>) { - val scope = rememberCoroutineScope() - // Helps to scroll to bottom after moving from Group to Direct chat - // and prevents scrolling to bottom on orientation change - var shouldAutoScroll by rememberSaveable { mutableStateOf(true to chatId) } - LaunchedEffect(chatId, shouldAutoScroll) { - if ((shouldAutoScroll.first || shouldAutoScroll.second != chatId) && listState.firstVisibleItemIndex != 0) { - scope.launch { listState.scrollToItem(0) } +private fun LoadLastItems(loadingMoreItems: MutableState, remoteHostId: Long?, chatInfo: ChatInfo) { + LaunchedEffect(remoteHostId, chatInfo.id) { + try { + loadingMoreItems.value = true + if (chatModel.chatState.totalAfter.value <= 0) return@LaunchedEffect + delay(500) + withContext(Dispatchers.Default) { + apiLoadMessages(remoteHostId, chatInfo.chatType, chatInfo.apiId, ChatPagination.Last(ChatPagination.INITIAL_COUNT), chatModel.chatState) + } + } finally { + loadingMoreItems.value = false } - // Don't autoscroll next time until it will be needed - shouldAutoScroll = false to chatId } +} + +@Composable +private fun SmallScrollOnNewMessage(listState: State, chatItems: State>) { val scrollDistance = with(LocalDensity.current) { -39.dp.toPx() } - /* - * Since we use key with each item in LazyColumn, LazyColumn will not autoscroll to bottom item. We need to do it ourselves. - * When the first visible item (from bottom) is visible (even partially) we can autoscroll to 0 item. Or just scrollBy small distance otherwise - * */ LaunchedEffect(Unit) { - snapshotFlow { chatItems.value.lastOrNull()?.id } + var lastTotalItems = listState.value.layoutInfo.totalItemsCount + var lastItemId = chatItems.value.lastOrNull()?.id + snapshotFlow { listState.value.layoutInfo.totalItemsCount } .distinctUntilChanged() - .filter { listState.layoutInfo.visibleItemsInfo.firstOrNull()?.key != it } + .drop(1) .collect { + val diff = listState.value.layoutInfo.totalItemsCount - lastTotalItems + val sameLastItem = lastItemId == chatItems.value.lastOrNull()?.id + lastTotalItems = listState.value.layoutInfo.totalItemsCount + lastItemId = chatItems.value.lastOrNull()?.id + if (diff < 1 || diff > 2 || sameLastItem) { + return@collect + } try { - if (listState.firstVisibleItemIndex == 0 || (listState.firstVisibleItemIndex == 1 && listState.layoutInfo.totalItemsCount == chatItems.value.size)) { - if (appPlatform.isAndroid) listState.animateScrollToItem(0) else listState.scrollToItem(0) + if (listState.value.firstVisibleItemIndex == 0 || listState.value.firstVisibleItemIndex == 1) { + if (appPlatform.isAndroid) listState.value.animateScrollToItem(0) else listState.value.scrollToItem(0) } else { - if (appPlatform.isAndroid) listState.animateScrollBy(scrollDistance) else listState.scrollBy(scrollDistance) + if (appPlatform.isAndroid) listState.value.animateScrollBy(scrollDistance) else listState.value.scrollBy(scrollDistance) } } catch (e: CancellationException) { /** @@ -1317,55 +1342,89 @@ private fun ScrollToBottom(chatId: ChatId, listState: LazyListState, chatItems: } } +@Composable +private fun NotifyChatListOnFinishingComposition( + finishedInitialComposition: MutableState, + chatInfo: ChatInfo, + revealedItems: MutableState>, + listState: State, + onComposed: suspend (chatId: String) -> Unit +) { + LaunchedEffect(chatInfo.id) { + revealedItems.value = emptySet() + snapshotFlow { listState.value.layoutInfo.visibleItemsInfo.lastIndex } + .distinctUntilChanged() + .collect { + onComposed(chatInfo.id) + finishedInitialComposition.value = true + cancel() + } + } +} + @Composable fun BoxScope.FloatingButtons( - chatItems: State>, + loadingMoreItems: MutableState, + mergedItems: State, unreadCount: State, + maxHeight: State, composeViewHeight: State, remoteHostId: Long?, chatInfo: ChatInfo, searchValue: State, markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit, - listState: LazyListState + listState: State ) { val scope = rememberCoroutineScope() - val maxHeight = remember { derivedStateOf { listState.layoutInfo.viewportSize.height } } + val topPaddingToContentPx = rememberUpdatedState(with(LocalDensity.current) { topPaddingToContent().roundToPx() }) val bottomUnreadCount = remember { derivedStateOf { if (unreadCount.value == 0) return@derivedStateOf 0 - val items = chatItems.value - val from = items.lastIndex - listState.firstVisibleItemIndex - listState.layoutInfo.visibleItemsInfo.lastIndex - if (items.size <= from || from < 0) return@derivedStateOf 0 - - items.subList(from, items.size).count { it.isRcvNew } + val lastVisibleItem = oldestPartiallyVisibleListItemInListStateOrNull(topPaddingToContentPx, mergedItems, listState) ?: return@derivedStateOf -1 + unreadCount.value - lastVisibleItem.unreadBefore } } - val showBottomButtonWithCounter = remember { derivedStateOf { bottomUnreadCount.value > 0 && listState.firstVisibleItemIndex != 0 && searchValue.value.isEmpty() } } - val showBottomButtonWithArrow = remember { derivedStateOf { !showBottomButtonWithCounter.value && listState.firstVisibleItemIndex != 0 } } + val allowToShowBottomWithCounter = remember { mutableStateOf(true) } + val showBottomButtonWithCounter = remember { derivedStateOf { + val allow = allowToShowBottomWithCounter.value + val shouldShow = bottomUnreadCount.value > 0 && listState.value.firstVisibleItemIndex != 0 && searchValue.value.isEmpty() + // this tricky idea is to prevent showing button with arrow in the next frame after creating/receiving new message because the list will + // scroll to that message but before this happens, that button will show up and then will hide itself after scroll finishes. + // This workaround prevents it + allowToShowBottomWithCounter.value = shouldShow + shouldShow && allow + } } + val allowToShowBottomWithArrow = remember { mutableStateOf(true) } + val showBottomButtonWithArrow = remember { derivedStateOf { + val allow = allowToShowBottomWithArrow.value + val shouldShow = !showBottomButtonWithCounter.value && listState.value.firstVisibleItemIndex != 0 + allowToShowBottomWithArrow.value = shouldShow + shouldShow && allow + } } BottomEndFloatingButton( bottomUnreadCount, showBottomButtonWithCounter, showBottomButtonWithArrow, composeViewHeight, - onClickArrowDown = { - scope.launch { listState.animateScrollToItem(0) } - }, - onClickCounter = { - val firstVisibleOffset = (-maxHeight.value * 0.8).toInt() - scope.launch { listState.animateScrollToItem(kotlin.math.max(0, bottomUnreadCount.value - 1), firstVisibleOffset) } - } + onClick = { scope.launch { tryBlockAndSetLoadingMore(loadingMoreItems) { listState.value.animateScrollToItem(0) } } } ) // Don't show top FAB if is in search if (searchValue.value.isNotEmpty()) return val fabSize = 56.dp - val topUnreadCount = remember { derivedStateOf { unreadCount.value - bottomUnreadCount.value } } + val topUnreadCount = remember { derivedStateOf { if (bottomUnreadCount.value >= 0) (unreadCount.value - bottomUnreadCount.value).coerceAtLeast(0) else 0 } } val showDropDown = remember { mutableStateOf(false) } TopEndFloatingButton( Modifier.padding(end = DEFAULT_PADDING, top = 24.dp + topPaddingToContent()).align(Alignment.TopEnd), topUnreadCount, - onClick = { scope.launch { listState.animateScrollBy(maxHeight.value.toFloat()) } }, + onClick = { + val index = mergedItems.value.items.indexOfLast { it.hasUnread() } + if (index != -1) { + // scroll to the top unread item + scope.launch { tryBlockAndSetLoadingMore(loadingMoreItems) { listState.value.animateScrollToItem(index + 1, -maxHeight.value) } } + } + }, onLongClick = { showDropDown.value = true } ) @@ -1381,10 +1440,11 @@ fun BoxScope.FloatingButtons( generalGetString(MR.strings.mark_read), painterResource(MR.images.ic_check), onClick = { - val minUnreadItemId = chatModel.chats.value.firstOrNull { it.remoteHostId == remoteHostId && it.id == chatInfo.id }?.chatStats?.minUnreadItemId ?: return@ItemAction + val chat = chatModel.chats.value.firstOrNull { it.remoteHostId == remoteHostId && it.id == chatInfo.id } ?: return@ItemAction + val minUnreadItemId = chat.chatStats.minUnreadItemId markRead( - CC.ItemRange(minUnreadItemId, chatItems.value[chatItems.value.size - listState.layoutInfo.visibleItemsInfo.lastIndex - 1].id - 1), - bottomUnreadCount.value + CC.ItemRange(minUnreadItemId, chat.chatItems.lastOrNull()?.id ?: return@ItemAction), + 0 ) showDropDown.value = false }) @@ -1395,46 +1455,106 @@ fun BoxScope.FloatingButtons( @Composable fun PreloadItems( chatId: String, - listState: LazyListState, - remaining: Int = 10, - onLoadMore: (ChatId) -> Unit, + ignoreLoadingRequests: MutableSet, + mergedItems: State, + listState: State, + remaining: Int, + loadItems: suspend (ChatId, ChatPagination) -> Boolean, ) { // Prevent situation when initial load and load more happens one after another after selecting a chat with long scroll position from previous selection val allowLoad = remember { mutableStateOf(false) } val chatId = rememberUpdatedState(chatId) - val onLoadMore = rememberUpdatedState(onLoadMore) - LaunchedEffect(Unit) { - snapshotFlow { chatId.value } - .filterNotNull() - .collect { - allowLoad.value = listState.layoutInfo.totalItemsCount == listState.layoutInfo.visibleItemsInfo.size - delay(500) - allowLoad.value = true + val loadItems = rememberUpdatedState(loadItems) + val ignoreLoadingRequests = rememberUpdatedState(ignoreLoadingRequests) + PreloadItemsBefore(allowLoad, chatId, ignoreLoadingRequests, mergedItems, listState, remaining, loadItems) + PreloadItemsAfter(allowLoad, chatId, mergedItems, listState, remaining, loadItems) +} + +@Composable +private fun PreloadItemsBefore( + allowLoad: State, + chatId: State, + ignoreLoadingRequests: State>, + mergedItems: State, + listState: State, + remaining: Int, + loadItems: State Boolean>, +) { + KeyChangeEffect(allowLoad.value, chatId.value) { + snapshotFlow { listState.value.firstVisibleItemIndex } + .distinctUntilChanged() + .map { firstVisibleIndex -> + val splits = mergedItems.value.splits + val lastVisibleIndex = (listState.value.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0) + var lastIndexToLoadFrom: Int? = findLastIndexToLoadFromInSplits(firstVisibleIndex, lastVisibleIndex, remaining, splits) + val items = chatModel.chatItems.value + if (splits.isEmpty() && items.isNotEmpty() && lastVisibleIndex > mergedItems.value.items.size - remaining && items.size >= ChatPagination.INITIAL_COUNT) { + lastIndexToLoadFrom = items.lastIndex + } + if (allowLoad.value && lastIndexToLoadFrom != null) { + items.getOrNull(items.lastIndex - lastIndexToLoadFrom)?.id + } else { + null + } } - } - KeyChangeEffect(allowLoad.value) { - snapshotFlow { - val lInfo = listState.layoutInfo - val totalItemsNumber = lInfo.totalItemsCount - val lastVisibleItemIndex = (lInfo.visibleItemsInfo.lastOrNull()?.index ?: 0) + 1 - if (allowLoad.value && lastVisibleItemIndex > (totalItemsNumber - remaining) && totalItemsNumber >= ChatPagination.INITIAL_COUNT) - totalItemsNumber + ChatPagination.PRELOAD_COUNT - else - 0 - } - .filter { it > 0 } - .collect { - onLoadMore.value(chatId.value) + .filterNotNull() + .filter { !ignoreLoadingRequests.value.contains(it) } + .collect { loadFromItemId -> + withBGApi { + val sizeWas = chatModel.chatItems.value.size + val firstItemIdWas = chatModel.chatItems.value.firstOrNull()?.id + val triedToLoad = loadItems.value(chatId.value, ChatPagination.Before(loadFromItemId, ChatPagination.PRELOAD_COUNT)) + if (triedToLoad && sizeWas == chatModel.chatItems.value.size && firstItemIdWas == chatModel.chatItems.value.firstOrNull()?.id) { + ignoreLoadingRequests.value.add(loadFromItemId) + } + } } } } -private fun showMemberImage(member: GroupMember, prevItem: ChatItem?): Boolean = - when (val dir = prevItem?.chatDir) { - is CIDirection.GroupSnd -> true - is CIDirection.GroupRcv -> dir.groupMember.groupMemberId != member.groupMemberId - else -> false +@Composable +private fun PreloadItemsAfter( + allowLoad: MutableState, + chatId: State, + mergedItems: State, + listState: State, + remaining: Int, + loadItems: State Boolean>, +) { + LaunchedEffect(Unit) { + snapshotFlow { chatId.value } + .distinctUntilChanged() + .filterNotNull() + .collect { + allowLoad.value = listState.value.layoutInfo.totalItemsCount == listState.value.layoutInfo.visibleItemsInfo.size + delay(500) + allowLoad.value = true + } } + LaunchedEffect(chatId.value) { + launch { + snapshotFlow { listState.value.firstVisibleItemIndex } + .distinctUntilChanged() + .map { firstVisibleIndex -> + val items = chatModel.chatItems.value + val splits = mergedItems.value.splits + val split = splits.lastOrNull { it.indexRangeInParentItems.contains(firstVisibleIndex) } + // we're inside a splitRange (top --- [end of the splitRange --- we're here --- start of the splitRange] --- bottom) + if (split != null && split.indexRangeInParentItems.first + remaining > firstVisibleIndex) { + items.getOrNull(items.lastIndex - split.indexRangeInReversed.first)?.id + } else { + null + } + } + .filterNotNull() + .collect { loadFromItemId -> + withBGApi { + loadItems.value(chatId.value, ChatPagination.After(loadFromItemId, ChatPagination.PRELOAD_COUNT)) + } + } + } + } +} val MEMBER_IMAGE_SIZE: Dp = 37.dp @@ -1449,8 +1569,8 @@ private fun TopEndFloatingButton( unreadCount: State, onClick: () -> Unit, onLongClick: () -> Unit -) = when { - unreadCount.value > 0 -> { +) { + if (unreadCount.value > 0) { val interactionSource = interactionSourceWithDetection(onClick, onLongClick) FloatingActionButton( {}, // no action here @@ -1466,8 +1586,6 @@ private fun TopEndFloatingButton( ) } } - else -> { - } } @Composable @@ -1483,52 +1601,43 @@ fun topPaddingToContent(): Dp { @Composable private fun FloatingDate( modifier: Modifier, - listState: LazyListState, + mergedItems: State, + listState: State, ) { - var nearBottomIndex by remember { mutableStateOf(-1) } - var isNearBottom by remember { mutableStateOf(true) } + val isNearBottom = remember(chatModel.chatId) { mutableStateOf(listState.value.firstVisibleItemIndex == 0) } + val nearBottomIndex = remember(chatModel.chatId) { mutableStateOf(if (isNearBottom.value) -1 else 0) } + val showDate = remember(chatModel.chatId) { mutableStateOf(false) } + val density = LocalDensity.current.density + val topPaddingToContentPx = rememberUpdatedState(with(LocalDensity.current) { topPaddingToContent().roundToPx() }) + val fontSizeSqrtMultiplier = fontSizeSqrtMultiplier val lastVisibleItemDate = remember { derivedStateOf { - if (listState.layoutInfo.visibleItemsInfo.lastIndex >= 0) { - val lastFullyVisibleOffset = listState.layoutInfo.viewportEndOffset - val lastVisibleChatItemIndex = chatModel.chatItems.value.lastIndex - (listState.layoutInfo.visibleItemsInfo.lastOrNull { item -> item.offset + item.size <= lastFullyVisibleOffset && item.size > 0 }?.index ?: 0) - val item = chatModel.chatItems.value.getOrNull(lastVisibleChatItemIndex) + if (listState.value.layoutInfo.visibleItemsInfo.lastIndex >= 0) { + val lastVisibleChatItem = lastFullyVisibleIemInListState(topPaddingToContentPx, density, fontSizeSqrtMultiplier, mergedItems, listState) val timeZone = TimeZone.currentSystemDefault() - item?.meta?.itemTs?.toLocalDateTime(timeZone)?.date?.atStartOfDayIn(timeZone) + lastVisibleChatItem?.meta?.itemTs?.toLocalDateTime(timeZone)?.date?.atStartOfDayIn(timeZone) } else { null } } } - val showDate = remember { mutableStateOf(false) } - LaunchedEffect(Unit) { - launch { - snapshotFlow { chatModel.chatId.value } - .distinctUntilChanged() - .collect { - showDate.value = false - isNearBottom = true - nearBottomIndex = -1 - } - } - } LaunchedEffect(Unit) { - snapshotFlow { listState.layoutInfo.visibleItemsInfo } + snapshotFlow { listState.value.layoutInfo.visibleItemsInfo } .collect { visibleItemsInfo -> if (visibleItemsInfo.find { it.index == 0 } != null) { var elapsedOffset = 0 for (it in visibleItemsInfo) { - if (elapsedOffset >= listState.layoutInfo.viewportSize.height / 2.5) { - nearBottomIndex = it.index + if (elapsedOffset >= listState.value.layoutInfo.viewportSize.height / 2.5) { + nearBottomIndex.value = it.index break; } elapsedOffset += it.size } } - isNearBottom = if (nearBottomIndex == -1) true else (visibleItemsInfo.firstOrNull()?.index ?: 0) <= nearBottomIndex + isNearBottom.value = if (nearBottomIndex.value == -1) true else (visibleItemsInfo.firstOrNull()?.index ?: 0) <= nearBottomIndex.value } } @@ -1536,7 +1645,7 @@ private fun FloatingDate( if (isVisible) { val now = Clock.System.now() val date = lastVisibleItemDate.value - if (!isNearBottom && !showDate.value && date != null && getTimestampDateText(date) != getTimestampDateText(now)) { + if (!isNearBottom.value && !showDate.value && date != null && getTimestampDateText(date) != getTimestampDateText(now)) { showDate.value = true } } else if (showDate.value) { @@ -1546,7 +1655,7 @@ private fun FloatingDate( LaunchedEffect(Unit) { var hideDateWhenNotScrolling: Job = Job() - snapshotFlow { listState.firstVisibleItemScrollOffset } + snapshotFlow { listState.value.firstVisibleItemScrollOffset } .collect { setDateVisibility(true) hideDateWhenNotScrolling.cancel() @@ -1654,6 +1763,93 @@ private fun DateSeparator(date: Instant) { ) } +@Composable +private fun MarkItemsReadAfterDelay( + itemKey: String, + itemIdStart: Long, + itemIdEnd: Long, + finishedInitialComposition: State, + chatId: ChatId, + listState: State, + markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit +) { + // items can be "visible" in terms of LazyColumn but hidden behind compose view/appBar. So don't count such item as visible and not mark read + val itemIsPartiallyAboveCompose = remember { derivedStateOf { + val item = listState.value.layoutInfo.visibleItemsInfo.firstOrNull { it.key == itemKey } + if (item != null) { + item.offset >= 0 || -item.offset < item.size + } else { + false + } + } } + LaunchedEffect(itemIsPartiallyAboveCompose.value, itemIdStart, itemIdEnd, finishedInitialComposition.value, chatId) { + if (chatId != ChatModel.chatId.value || !itemIsPartiallyAboveCompose.value || !finishedInitialComposition.value) return@LaunchedEffect + + delay(600L) + markRead(CC.ItemRange(itemIdStart, itemIdEnd), null) + } +} + +private fun oldestPartiallyVisibleListItemInListStateOrNull(topPaddingToContentPx: State, mergedItems: State, listState: State): ListItem? { + val lastFullyVisibleOffset = listState.value.layoutInfo.viewportEndOffset - topPaddingToContentPx.value + return mergedItems.value.items.getOrNull((listState.value.layoutInfo.visibleItemsInfo.lastOrNull { item -> + item.offset <= lastFullyVisibleOffset + }?.index ?: listState.value.layoutInfo.visibleItemsInfo.lastOrNull()?.index) ?: -1)?.oldest() +} + +private fun lastFullyVisibleIemInListState(topPaddingToContentPx: State, density: Float, fontSizeSqrtMultiplier: Float, mergedItems: State, listState: State): ChatItem? { + val lastFullyVisibleOffsetMinusFloatingHeight = listState.value.layoutInfo.viewportEndOffset - topPaddingToContentPx.value - 50 * density * fontSizeSqrtMultiplier + return mergedItems.value.items.getOrNull( + (listState.value.layoutInfo.visibleItemsInfo.lastOrNull { item -> + item.offset <= lastFullyVisibleOffsetMinusFloatingHeight && item.size > 0 + } + ?.index + ?: listState.value.layoutInfo.visibleItemsInfo.lastOrNull()?.index) + ?: -1)?.newest()?.item +} + +private fun scrollToItem( + loadingMoreItems: MutableState, + chatInfo: State, + maxHeight: State, + scope: CoroutineScope, + reversedChatItems: State>, + mergedItems: State, + listState: State, + loadMessages: suspend (ChatId, ChatPagination, ActiveChatState, visibleItemIndexesNonReversed: () -> IntRange) -> Unit, +): (Long) -> Unit = { itemId: Long -> + withApi { + try { + var index = mergedItems.value.indexInParentItems[itemId] ?: -1 + // setting it to 'loading' even if the item is loaded because in rare cases when the resulting item is near the top, scrolling to + // it will trigger loading more items and will scroll to incorrect position (because of trimming) + loadingMoreItems.value = true + if (index == -1) { + val pagination = ChatPagination.Around(itemId, ChatPagination.PRELOAD_COUNT * 2) + val oldSize = reversedChatItems.value.size + withContext(Dispatchers.Default) { + loadMessages(chatInfo.value.id, pagination, chatModel.chatState) { + visibleItemIndexesNonReversed(mergedItems, listState.value) + } + } + var repeatsLeft = 50 + while (oldSize == reversedChatItems.value.size && repeatsLeft > 0) { + delay(20) + repeatsLeft-- + } + index = mergedItems.value.indexInParentItems[itemId] ?: -1 + } + if (index != -1) { + withContext(scope.coroutineContext) { + listState.value.animateScrollToItem(min(reversedChatItems.value.lastIndex, index + 1), -maxHeight.value) + } + } + } finally { + loadingMoreItems.value = false + } + } +} + val chatViewScrollState = MutableStateFlow(false) fun addGroupMembers(groupInfo: GroupInfo, rhId: Long?, view: Any? = null, close: (() -> Unit)? = null) { @@ -1684,12 +1880,11 @@ private fun BoxScope.BottomEndFloatingButton( showButtonWithCounter: State, showButtonWithArrow: State, composeViewHeight: State, - onClickArrowDown: () -> Unit, - onClickCounter: () -> Unit + onClick: () -> Unit ) = when { showButtonWithCounter.value -> { FloatingActionButton( - onClick = onClickCounter, + onClick = onClick, elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp), modifier = Modifier.padding(end = DEFAULT_PADDING, bottom = DEFAULT_PADDING + composeViewHeight.value).align(Alignment.BottomEnd).size(48.dp), backgroundColor = MaterialTheme.colors.secondaryVariant, @@ -1703,7 +1898,7 @@ private fun BoxScope.BottomEndFloatingButton( } showButtonWithArrow.value -> { FloatingActionButton( - onClick = onClickArrowDown, + onClick = onClick, elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp), modifier = Modifier.padding(end = DEFAULT_PADDING, bottom = DEFAULT_PADDING + composeViewHeight.value).align(Alignment.BottomEnd).size(48.dp), backgroundColor = MaterialTheme.colors.secondaryVariant, @@ -1861,6 +2056,26 @@ fun Modifier.chatViewBackgroundModifier( ) } +private fun findLastIndexToLoadFromInSplits(firstVisibleIndex: Int, lastVisibleIndex: Int, remaining: Int, splits: List): Int? { + for (split in splits) { + // before any split + if (split.indexRangeInParentItems.first > firstVisibleIndex) { + if (lastVisibleIndex > (split.indexRangeInParentItems.first - remaining)) { + return split.indexRangeInReversed.first - 1 + } + break + } + val containsInRange = split.indexRangeInParentItems.contains(firstVisibleIndex) + if (containsInRange) { + if (lastVisibleIndex > (split.indexRangeInParentItems.last - remaining)) { + return split.indexRangeInReversed.last + } + break + } + } + return null +} + fun chatViewItemsRange(currIndex: Int?, prevHidden: Int?): IntRange? = if (currIndex != null && prevHidden != null && prevHidden > currIndex) { currIndex..prevHidden @@ -1868,6 +2083,16 @@ fun chatViewItemsRange(currIndex: Int?, prevHidden: Int?): IntRange? = null } +private suspend fun tryBlockAndSetLoadingMore(loadingMoreItems: MutableState, block: suspend () -> Unit) { + try { + loadingMoreItems.value = true + block() + } catch (e: Exception) { + Log.e(TAG, e.stackTraceToString()) + } finally { + loadingMoreItems.value = false + } +} sealed class ProviderMedia { data class Image(val data: ByteArray, val image: ImageBitmap): ProviderMedia() @@ -1875,7 +2100,6 @@ sealed class ProviderMedia { } fun providerForGallery( - listStateIndex: Int, chatItems: List, cItemId: Long, scrollTo: (Int) -> Unit @@ -1943,15 +2167,18 @@ fun providerForGallery( override fun onDismiss(index: Int) { val internalIndex = initialIndex - index - val indexInChatItems = item(internalIndex, initialChatId)?.first ?: return + val item = item(internalIndex, initialChatId) + val indexInChatItems = item?.first ?: return val indexInReversed = chatItems.lastIndex - indexInChatItems // Do not scroll to active item, just to different items - if (indexInReversed == listStateIndex) return + if (item.second.id == cItemId) return scrollTo(indexInReversed) } } } +private fun keyForItem(item: ChatItem): String = (item.id to item.meta.createdAt.toEpochMilliseconds()).toString() + private fun ViewConfiguration.bigTouchSlop(slop: Float = 50f) = object: ViewConfiguration { override val longPressTimeoutMillis get() = @@ -2042,23 +2269,38 @@ private fun handleForwardConfirmation( ) } -private fun getItemSeparation(chatItem: ChatItem, nextItem: ChatItem?): ItemSeparation { - if (nextItem == null) { +private fun getItemSeparation(chatItem: ChatItem, prevItem: ChatItem?): ItemSeparation { + if (prevItem == null) { return ItemSeparation(timestamp = true, largeGap = true, date = null) } + val sameMemberAndDirection = if (prevItem.chatDir is GroupRcv && chatItem.chatDir is GroupRcv) { + chatItem.chatDir.groupMember.groupMemberId == prevItem.chatDir.groupMember.groupMemberId + } else chatItem.chatDir.sent == prevItem.chatDir.sent + val largeGap = !sameMemberAndDirection || (abs(prevItem.meta.createdAt.epochSeconds - chatItem.meta.createdAt.epochSeconds) >= 60) + + return ItemSeparation( + timestamp = largeGap || prevItem.meta.timestampText != chatItem.meta.timestampText, + largeGap = largeGap, + date = if (getTimestampDateText(chatItem.meta.itemTs) == getTimestampDateText(prevItem.meta.itemTs)) null else prevItem.meta.itemTs + ) +} + +private fun getItemSeparationLargeGap(chatItem: ChatItem, nextItem: ChatItem?): Boolean { + if (nextItem == null) { + return true + } + val sameMemberAndDirection = if (nextItem.chatDir is GroupRcv && chatItem.chatDir is GroupRcv) { chatItem.chatDir.groupMember.groupMemberId == nextItem.chatDir.groupMember.groupMemberId } else chatItem.chatDir.sent == nextItem.chatDir.sent - val largeGap = !sameMemberAndDirection || (abs(nextItem.meta.createdAt.epochSeconds - chatItem.meta.createdAt.epochSeconds) >= 60) - - return ItemSeparation( - timestamp = largeGap || nextItem.meta.timestampText != chatItem.meta.timestampText, - largeGap = largeGap, - date = if (getTimestampDateText(chatItem.meta.itemTs) == getTimestampDateText(nextItem.meta.itemTs)) null else nextItem.meta.itemTs - ) + return !sameMemberAndDirection || (abs(nextItem.meta.createdAt.epochSeconds - chatItem.meta.createdAt.epochSeconds) >= 60) } +private fun shouldShowAvatar(current: ChatItem, older: ChatItem?) = + current.chatDir is CIDirection.GroupRcv && (older == null || (older.chatDir !is CIDirection.GroupRcv || older.chatDir.groupMember.memberId != current.chatDir.groupMember.memberId)) + + @Preview/*( uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, @@ -2102,7 +2344,7 @@ fun PreviewChatLayout() { back = {}, info = {}, showMemberInfo = { _, _ -> }, - loadPrevMessages = {}, + loadMessages = { _, _, _, _ -> }, deleteMessage = { _, _ -> }, deleteMessages = { _ -> }, receiveFile = { _ -> }, @@ -2174,7 +2416,7 @@ fun PreviewGroupChatLayout() { back = {}, info = {}, showMemberInfo = { _, _ -> }, - loadPrevMessages = {}, + loadMessages = { _, _, _, _ -> }, deleteMessage = { _, _ -> }, deleteMessages = {}, receiveFile = { _ -> }, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt index fd1d3ab92d..6cd46d49a6 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt @@ -429,8 +429,8 @@ fun ComposeView( ttl = ttl ) - chatItems?.forEach { chatItem -> - withChats { + withChats { + chatItems?.forEach { chatItem -> addChatItem(rhId, chat.chatInfo, chatItem) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt index a03cff2bb0..a78dd36887 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt @@ -70,17 +70,9 @@ fun GroupMemberInfoView( getContactChat = { chatModel.getContactChat(it) }, openDirectChat = { withBGApi { - val c = chatModel.controller.apiGetChat(rhId, ChatType.Direct, it) - if (c != null) { - withChats { - if (chatModel.getContactChat(it) == null) { - addChat(c) - } - chatModel.chatItemStatuses.clear() - chatModel.chatItems.replaceAll(c.chatItems) - chatModel.chatId.value = c.id - closeAll() - } + apiLoadMessages(rhId, ChatType.Direct, it, ChatPagination.Initial(ChatPagination.INITIAL_COUNT), chatModel.chatState) + if (chatModel.getContactChat(it) != null) { + closeAll() } } }, @@ -92,7 +84,7 @@ fun GroupMemberInfoView( val memberChat = Chat(remoteHostId = rhId, ChatInfo.Direct(memberContact), chatItems = arrayListOf()) withChats { addChat(memberChat) - openLoadedChat(memberChat, chatModel) + openLoadedChat(memberChat) } closeAll() chatModel.setContactNetworkStatus(memberContact, NetworkStatus.Connected()) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIChatFeatureView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIChatFeatureView.kt index 19cc949543..9bb3cef1d7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIChatFeatureView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIChatFeatureView.kt @@ -21,7 +21,7 @@ fun CIChatFeatureView( feature: Feature, iconColor: Color, icon: Painter? = null, - revealed: MutableState, + revealed: State, showMenu: MutableState, ) { val merged = if (!revealed.value) mergedFeatures(chatItem, chatInfo) else emptyList() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVideoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVideoView.kt index ca93349092..9f7b5dc9c6 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVideoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVideoView.kt @@ -431,6 +431,9 @@ fun VideoPreviewImageViewFullScreen(preview: ImageBitmap, onClick: () -> Unit, o @Composable expect fun LocalWindowWidth(): Dp +@Composable +expect fun LocalWindowHeight(): Dp + @Composable private fun progressIndicator() { CircularProgressIndicator( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt index 3db9b55c5b..5096992c29 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt @@ -56,8 +56,8 @@ fun ChatItemView( imageProvider: (() -> ImageGalleryProvider)? = null, useLinkPreviews: Boolean, linkMode: SimplexLinkMode, - revealed: MutableState, - range: IntRange?, + revealed: State, + range: State, selectedChatItems: MutableState?>, fillMaxWidth: Boolean = true, selectChatItem: () -> Unit, @@ -79,6 +79,7 @@ fun ChatItemView( findModelMember: (String) -> GroupMember?, setReaction: (ChatInfo, ChatItem, Boolean, MsgReaction) -> Unit, showItemDetails: (ChatInfo, ChatItem) -> Unit, + reveal: (Boolean) -> Unit, developerTools: Boolean, showViaProxy: Boolean, showTimestamp: Boolean, @@ -91,7 +92,7 @@ fun ChatItemView( val showMenu = remember { mutableStateOf(false) } val fullDeleteAllowed = remember(cInfo) { cInfo.featureEnabled(ChatFeature.FullDelete) } val onLinkLongClick = { _: String -> showMenu.value = true } - val live = composeState.value.liveMessage != null + val live = remember { derivedStateOf { composeState.value.liveMessage != null } }.value Box( modifier = if (fillMaxWidth) Modifier.fillMaxWidth() else Modifier, @@ -275,7 +276,7 @@ fun ChatItemView( } ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) if (revealed.value) { - HideItemAction(revealed, showMenu) + HideItemAction(revealed, showMenu, reveal) } if (cItem.meta.itemDeleted == null && cItem.file != null && cItem.file.cancelAction != null && !cItem.localNote) { CancelFileItemAction(cItem.file.fileId, showMenu, cancelFile = cancelFile, cancelAction = cItem.file.cancelAction) @@ -296,11 +297,11 @@ fun ChatItemView( cItem.meta.itemDeleted != null -> { DefaultDropdownMenu(showMenu) { if (revealed.value) { - HideItemAction(revealed, showMenu) + HideItemAction(revealed, showMenu, reveal) } else if (!cItem.isDeletedContent) { - RevealItemAction(revealed, showMenu) - } else if (range != null) { - ExpandItemAction(revealed, showMenu) + RevealItemAction(revealed, showMenu, reveal) + } else if (range.value != null) { + ExpandItemAction(revealed, showMenu, reveal) } ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) @@ -320,12 +321,12 @@ fun ChatItemView( } } } - cItem.mergeCategory != null && ((range?.count() ?: 0) > 1 || revealed.value) -> { + cItem.mergeCategory != null && ((range.value?.count() ?: 0) > 1 || revealed.value) -> { DefaultDropdownMenu(showMenu) { if (revealed.value) { - ShrinkItemAction(revealed, showMenu) + ShrinkItemAction(revealed, showMenu, reveal) } else { - ExpandItemAction(revealed, showMenu) + ExpandItemAction(revealed, showMenu, reveal) } DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) if (cItem.canBeDeletedForSelf) { @@ -350,7 +351,7 @@ fun ChatItemView( fun MarkedDeletedItemDropdownMenu() { DefaultDropdownMenu(showMenu) { if (!cItem.isDeletedContent) { - RevealItemAction(revealed, showMenu) + RevealItemAction(revealed, showMenu, reveal) } ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) @@ -623,7 +624,7 @@ fun ItemInfoAction( @Composable fun DeleteItemAction( cItem: ChatItem, - revealed: MutableState, + revealed: State, showMenu: MutableState, questionText: String, deleteMessage: (Long, CIDeleteMode) -> Unit, @@ -700,48 +701,48 @@ fun SelectItemAction( } @Composable -private fun RevealItemAction(revealed: MutableState, showMenu: MutableState) { +private fun RevealItemAction(revealed: State, showMenu: MutableState, reveal: (Boolean) -> Unit) { ItemAction( stringResource(MR.strings.reveal_verb), painterResource(MR.images.ic_visibility), onClick = { - revealed.value = true + reveal(true) showMenu.value = false } ) } @Composable -private fun HideItemAction(revealed: MutableState, showMenu: MutableState) { +private fun HideItemAction(revealed: State, showMenu: MutableState, reveal: (Boolean) -> Unit) { ItemAction( stringResource(MR.strings.hide_verb), painterResource(MR.images.ic_visibility_off), onClick = { - revealed.value = false + reveal(false) showMenu.value = false } ) } @Composable -private fun ExpandItemAction(revealed: MutableState, showMenu: MutableState) { +private fun ExpandItemAction(revealed: State, showMenu: MutableState, reveal: (Boolean) -> Unit) { ItemAction( stringResource(MR.strings.expand_verb), painterResource(MR.images.ic_expand_all), onClick = { - revealed.value = true + reveal(true) showMenu.value = false }, ) } @Composable -private fun ShrinkItemAction(revealed: MutableState, showMenu: MutableState) { +private fun ShrinkItemAction(revealed: State, showMenu: MutableState, reveal: (Boolean) -> Unit) { ItemAction( stringResource(MR.strings.hide_verb), painterResource(MR.images.ic_collapse_all), onClick = { - revealed.value = false + reveal(false) showMenu.value = false }, ) @@ -1063,7 +1064,7 @@ fun PreviewChatItemView( linkMode = SimplexLinkMode.DESCRIPTION, composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) }, revealed = remember { mutableStateOf(false) }, - range = 0..1, + range = remember { mutableStateOf(0..1) }, selectedChatItems = remember { mutableStateOf(setOf()) }, selectChatItem = {}, deleteMessage = { _, _ -> }, @@ -1084,6 +1085,7 @@ fun PreviewChatItemView( findModelMember = { null }, setReaction = { _, _, _, _ -> }, showItemDetails = { _, _ -> }, + reveal = {}, developerTools = false, showViaProxy = false, showTimestamp = true, @@ -1104,7 +1106,7 @@ fun PreviewChatItemViewDeletedContent() { linkMode = SimplexLinkMode.DESCRIPTION, composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) }, revealed = remember { mutableStateOf(false) }, - range = 0..1, + range = remember { mutableStateOf(0..1) }, selectedChatItems = remember { mutableStateOf(setOf()) }, selectChatItem = {}, deleteMessage = { _, _ -> }, @@ -1125,6 +1127,7 @@ fun PreviewChatItemViewDeletedContent() { findModelMember = { null }, setReaction = { _, _, _, _ -> }, showItemDetails = { _, _ -> }, + reveal = {}, developerTools = false, showViaProxy = false, preview = true, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt index f0480a5c50..cfaa3eced5 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt @@ -129,7 +129,14 @@ fun FramedItemView( .fillMaxWidth() .combinedClickable( onLongClick = { showMenu.value = true }, - onClick = { scrollToItem(qi.itemId?: return@combinedClickable) } + onClick = { + val itemId = qi.itemId + if (itemId != null) { + scrollToItem(itemId) + } else { + showQuotedItemDoesNotExistAlert() + } + } ) .onRightClick { showMenu.value = true } ) { @@ -465,6 +472,13 @@ fun CenteredRowLayout( } } +fun showQuotedItemDoesNotExistAlert() { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.message_deleted_or_not_received_error_title), + text = generalGetString(MR.strings.message_deleted_or_not_received_error_desc) + ) +} + /* class EditedProvider: PreviewParameterProvider { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/MarkedDeletedItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/MarkedDeletedItemView.kt index ea71895ce5..d2e19a37d6 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/MarkedDeletedItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/MarkedDeletedItemView.kt @@ -20,7 +20,7 @@ import dev.icerock.moko.resources.compose.stringResource import kotlinx.datetime.Clock @Composable -fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, revealed: MutableState, showViaProxy: Boolean, showTimestamp: Boolean) { +fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, revealed: State, showViaProxy: Boolean, showTimestamp: Boolean) { val sentColor = MaterialTheme.appColors.sentMessage val receivedColor = MaterialTheme.appColors.receivedMessage Surface( @@ -41,7 +41,7 @@ fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, revealed: Mutabl } @Composable -private fun MergedMarkedDeletedText(chatItem: ChatItem, revealed: MutableState) { +private fun MergedMarkedDeletedText(chatItem: ChatItem, revealed: State) { var i = getChatItemIndexOrNull(chatItem) val ciCategory = chatItem.mergeCategory val text = if (!revealed.value && ciCategory != null && i != null) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt index 3c7f1e781f..d071a9d4fd 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt @@ -1,7 +1,6 @@ package chat.simplex.common.views.chatlist import SectionItemView -import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.* @@ -14,6 +13,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.style.TextAlign @@ -33,6 +33,7 @@ import chat.simplex.common.views.newchat.* import chat.simplex.res.MR import kotlinx.coroutines.* import kotlinx.datetime.Clock +import kotlin.math.min @Composable fun ChatListNavLinkView(chat: Chat, nextChatSelected: State) { @@ -71,7 +72,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State) { ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, contactNetworkStatus, disabled, linkMode, inProgress = false, progressByTimeout = false) } }, - click = { scope.launch { directChatAction(chat.remoteHostId, chat.chatInfo.contact, chatModel) } }, + click = { if (chatModel.chatId.value != chat.id) scope.launch { directChatAction(chat.remoteHostId, chat.chatInfo.contact, chatModel) } }, dropdownMenuItems = { tryOrShowError("${chat.id}ChatListNavLinkDropdown", error = {}) { ContactMenuItems(chat, chat.chatInfo.contact, chatModel, showMenu, showMarkRead) @@ -90,7 +91,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State) { ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, null, disabled, linkMode, inProgress.value, progressByTimeout) } }, - click = { if (!inProgress.value) scope.launch { groupChatAction(chat.remoteHostId, chat.chatInfo.groupInfo, chatModel, inProgress) } }, + click = { if (!inProgress.value && chatModel.chatId.value != chat.id) scope.launch { groupChatAction(chat.remoteHostId, chat.chatInfo.groupInfo, chatModel, inProgress) } }, dropdownMenuItems = { tryOrShowError("${chat.id}ChatListNavLinkDropdown", error = {}) { GroupMenuItems(chat, chat.chatInfo.groupInfo, chatModel, showMenu, inProgress, showMarkRead) @@ -108,7 +109,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State) { ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, null, disabled, linkMode, inProgress = false, progressByTimeout = false) } }, - click = { scope.launch { noteFolderChatAction(chat.remoteHostId, chat.chatInfo.noteFolder) } }, + click = { if (chatModel.chatId.value != chat.id) scope.launch { noteFolderChatAction(chat.remoteHostId, chat.chatInfo.noteFolder) } }, dropdownMenuItems = { tryOrShowError("${chat.id}ChatListNavLinkDropdown", error = {}) { NoteFolderMenuItems(chat, showMenu, showMarkRead) @@ -187,7 +188,7 @@ fun ErrorChatListItem() { suspend fun directChatAction(rhId: Long?, contact: Contact, chatModel: ChatModel) { when { contact.activeConn == null && contact.profile.contactLink != null && contact.active -> askCurrentOrIncognitoProfileConnectContactViaAddress(chatModel, rhId, contact, close = null, openChat = true) - else -> openChat(rhId, ChatInfo.Direct(contact), chatModel) + else -> openChat(rhId, ChatInfo.Direct(contact)) } } @@ -195,54 +196,31 @@ suspend fun groupChatAction(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatMo when (groupInfo.membership.memberStatus) { GroupMemberStatus.MemInvited -> acceptGroupInvitationAlertDialog(rhId, groupInfo, chatModel, inProgress) GroupMemberStatus.MemAccepted -> groupInvitationAcceptedAlert(rhId) - else -> openChat(rhId, ChatInfo.Group(groupInfo), chatModel) + else -> openChat(rhId, ChatInfo.Group(groupInfo)) } } -suspend fun noteFolderChatAction(rhId: Long?, noteFolder: NoteFolder) { - openChat(rhId, ChatInfo.Local(noteFolder), chatModel) -} +suspend fun noteFolderChatAction(rhId: Long?, noteFolder: NoteFolder) = openChat(rhId, ChatInfo.Local(noteFolder)) -suspend fun openDirectChat(rhId: Long?, contactId: Long, chatModel: ChatModel) = coroutineScope { - val chat = chatModel.controller.apiGetChat(rhId, ChatType.Direct, contactId) - if (chat != null && isActive) { - openLoadedChat(chat, chatModel) - } -} +suspend fun openDirectChat(rhId: Long?, contactId: Long) = openChat(rhId, ChatType.Direct, contactId) -suspend fun openGroupChat(rhId: Long?, groupId: Long, chatModel: ChatModel) = coroutineScope { - val chat = chatModel.controller.apiGetChat(rhId, ChatType.Group, groupId) - if (chat != null && isActive) { - openLoadedChat(chat, chatModel) - } -} +suspend fun openGroupChat(rhId: Long?, groupId: Long) = openChat(rhId, ChatType.Group, groupId) -suspend fun openChat(rhId: Long?, chatInfo: ChatInfo, chatModel: ChatModel) = coroutineScope { - val chat = chatModel.controller.apiGetChat(rhId, chatInfo.chatType, chatInfo.apiId) - if (chat != null && isActive) { - openLoadedChat(chat, chatModel) - } -} +suspend fun openChat(rhId: Long?, chatInfo: ChatInfo) = openChat(rhId, chatInfo.chatType, chatInfo.apiId) -fun openLoadedChat(chat: Chat, chatModel: ChatModel) { +private suspend fun openChat(rhId: Long?, chatType: ChatType, apiId: Long) = + apiLoadMessages(rhId, chatType, apiId, ChatPagination.Initial(ChatPagination.INITIAL_COUNT), chatModel.chatState) + +fun openLoadedChat(chat: Chat) { chatModel.chatItemStatuses.clear() chatModel.chatItems.replaceAll(chat.chatItems) chatModel.chatId.value = chat.chatInfo.id + chatModel.chatState.clear() } -suspend fun apiLoadPrevMessages(ch: Chat, chatModel: ChatModel, beforeChatItemId: Long, search: String) { - val chatInfo = ch.chatInfo - val pagination = ChatPagination.Before(beforeChatItemId, ChatPagination.PRELOAD_COUNT) - val chat = chatModel.controller.apiGetChat(ch.remoteHostId, chatInfo.chatType, chatInfo.apiId, pagination, search) ?: return - if (chatModel.chatId.value != chat.id) return - chatModel.chatItems.addAll(0, chat.chatItems) -} - -suspend fun apiFindMessages(ch: Chat, chatModel: ChatModel, search: String) { - val chatInfo = ch.chatInfo - val chat = chatModel.controller.apiGetChat(ch.remoteHostId, chatInfo.chatType, chatInfo.apiId, search = search) ?: return - if (chatModel.chatId.value != chat.id) return - chatModel.chatItems.replaceAll(chat.chatItems) +suspend fun apiFindMessages(ch: Chat, search: String) { + chatModel.chatItems.clearAndNotify() + apiLoadMessages(ch.remoteHostId, ch.chatInfo.chatType, ch.chatInfo.apiId, pagination = ChatPagination.Last(ChatPagination.INITIAL_COUNT), chatModel.chatState, search = search) } suspend fun setGroupMembers(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel) { @@ -724,7 +702,7 @@ fun askCurrentOrIncognitoProfileConnectContactViaAddress( close?.invoke() val ok = connectContactViaAddress(chatModel, rhId, contact.contactId, incognito = false) if (ok && openChat) { - openDirectChat(rhId, contact.contactId, chatModel) + openDirectChat(rhId, contact.contactId) } } }) { @@ -736,7 +714,7 @@ fun askCurrentOrIncognitoProfileConnectContactViaAddress( close?.invoke() val ok = connectContactViaAddress(chatModel, rhId, contact.contactId, incognito = true) if (ok && openChat) { - openDirectChat(rhId, contact.contactId, chatModel) + openDirectChat(rhId, contact.contactId) } } }) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt index d63e47bcdd..036768c6e7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt @@ -231,7 +231,7 @@ fun ChatPreviewView( fun chatItemContentPreview(chat: Chat, ci: ChatItem?) { val mc = ci?.content?.msgContent val provider by remember(chat.id, ci?.id, ci?.file?.fileStatus) { - mutableStateOf({ providerForGallery(0, chat.chatItems, ci?.id ?: 0) {} }) + mutableStateOf({ providerForGallery(chat.chatItems, ci?.id ?: 0) {} }) } val uriHandler = LocalUriHandler.current when (mc) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactListNavView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactListNavView.kt index 711fb1377d..0af8e7ca38 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactListNavView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactListNavView.kt @@ -21,7 +21,7 @@ fun onRequestAccepted(chat: Chat) { if (chatInfo is ChatInfo.Direct) { ModalManager.start.closeModals() if (chatInfo.contact.sndReady) { - openLoadedChat(chat, chatModel) + openLoadedChat(chat) } } } @@ -54,13 +54,13 @@ fun ContactListNavLinkView(chat: Chat, nextChatSelected: State, showDel when (contactType) { ContactType.RECENT -> { withApi { - openChat(rhId, chat.chatInfo, chatModel) + openChat(rhId, chat.chatInfo) ModalManager.start.closeModals() } } ContactType.CHAT_DELETED -> { withApi { - openChat(rhId, chat.chatInfo, chatModel) + openChat(rhId, chat.chatInfo) ModalManager.start.closeModals() } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt index d36bd255e3..3dd7e673c4 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt @@ -500,7 +500,7 @@ fun deleteChatDatabaseFilesAndState() { // Clear sensitive data on screen just in case ModalManager will fail to prevent hiding its modals while database encrypts itself chatModel.chatId.value = null - chatModel.chatItems.clear() + chatModel.chatItems.clearAndNotify() withLongRunningApi { withChats { chats.clear() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt index e1d3d6541a..6cecbe4979 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt @@ -44,7 +44,7 @@ fun AddGroupView(chatModel: ChatModel, rh: RemoteHostInfo?, close: () -> Unit, c if (groupInfo != null) { withChats { updateGroup(rhId = rhId, groupInfo) - chatModel.chatItems.clear() + chatModel.chatItems.clearAndNotify() chatModel.chatItemStatuses.clear() chatModel.chatId.value = groupInfo.id } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt index 6c18e47df3..7cd272c109 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt @@ -409,7 +409,7 @@ fun openKnownContact(chatModel: ChatModel, rhId: Long?, close: (() -> Unit)?, co val c = chatModel.getContactChat(contact.contactId) if (c != null) { close?.invoke() - openDirectChat(rhId, contact.contactId, chatModel) + openDirectChat(rhId, contact.contactId) } } } @@ -490,7 +490,7 @@ fun openKnownGroup(chatModel: ChatModel, rhId: Long?, close: (() -> Unit)?, grou val g = chatModel.getGroupChat(groupInfo.groupId) if (g != null) { close?.invoke() - openGroupChat(rhId, groupInfo.groupId, chatModel) + openGroupChat(rhId, groupInfo.groupId) } } } diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 673100bd8d..7236b22563 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -276,6 +276,8 @@ Message delivery error Message delivery warning Most likely this contact has deleted the connection with you. + No message + This message was deleted or not received yet. Error: %1$s diff --git a/apps/multiplatform/common/src/commonTest/kotlin/chat/simplex/app/ChatItemsMergerTest.kt b/apps/multiplatform/common/src/commonTest/kotlin/chat/simplex/app/ChatItemsMergerTest.kt new file mode 100644 index 0000000000..18b17b25a9 --- /dev/null +++ b/apps/multiplatform/common/src/commonTest/kotlin/chat/simplex/app/ChatItemsMergerTest.kt @@ -0,0 +1,158 @@ +package chat.simplex.app + +import androidx.compose.runtime.mutableStateOf +import chat.simplex.common.model.* +import chat.simplex.common.views.chat.* +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.datetime.Clock +import kotlin.test.Test +import kotlin.test.assertEquals + +class ChatItemsMergerTest { + + @Test + fun testRecalculateSplitPositions() { + val oldItems = listOf(ChatItem.getSampleData(0), ChatItem.getSampleData(123L), ChatItem.getSampleData(124L), ChatItem.getSampleData(125L)) + + val splits1 = MutableStateFlow(listOf(123L)) + val chatState1 = ActiveChatState(splits = splits1) + val removed1 = listOf(oldItems[1]) + val newItems1 = oldItems - removed1 + val recalc1 = recalculateChatStatePositions(chatState1) + recalc1.removed(removed1.map { Triple(it.id, oldItems.indexOf(removed1[0]), it.isRcvNew) }, newItems1) + assertEquals(1, splits1.value.size) + assertEquals(124L, splits1.value.first()) + + val splits2 = MutableStateFlow(listOf(123L)) + val chatState2 = ActiveChatState(splits = splits2) + val removed2 = listOf(oldItems[1], oldItems[2]) + val newItems2 = oldItems - removed2 + val recalc2 = recalculateChatStatePositions(chatState2) + recalc2.removed(removed2.mapIndexed { index, it -> Triple(it.id, oldItems.indexOf(removed2[index]), it.isRcvNew) }, newItems2) + assertEquals(1, splits2.value.size) + assertEquals(125L, splits2.value.first()) + + val splits3 = MutableStateFlow(listOf(123L)) + val chatState3 = ActiveChatState(splits = splits3) + val removed3 = listOf(oldItems[1], oldItems[2], oldItems[3]) + val newItems3 = oldItems - removed3 + val recalc3 = recalculateChatStatePositions(chatState3) + recalc3.removed(removed3.mapIndexed { index, it -> Triple(it.id, oldItems.indexOf(removed3[index]), it.isRcvNew) }, newItems3) + assertEquals(0, splits3.value.size) + + val splits4 = MutableStateFlow(listOf(123L)) + val chatState4 = ActiveChatState(splits = splits4) + val recalc4 = recalculateChatStatePositions(chatState4) + recalc4.cleared() + assertEquals(0, splits4.value.size) + } + + @Test + fun testItemsMerging() { + val items = listOf( + ChatItem(CIDirection.DirectRcv(), CIMeta.getSample(100L, Clock.System.now(), text = ""), CIContent.SndGroupFeature(GroupFeature.Voice, GroupPreference(GroupFeatureEnabled.ON), memberRole_ = null), reactions = emptyList()), + ChatItem(CIDirection.DirectRcv(), CIMeta.getSample(99L, Clock.System.now(), text = ""), CIContent.SndGroupFeature(GroupFeature.FullDelete, GroupPreference(GroupFeatureEnabled.ON), memberRole_ = null), reactions = emptyList()), + ChatItem(CIDirection.DirectRcv(), CIMeta.getSample(98L, Clock.System.now(), text = "", itemDeleted = CIDeleted.Deleted(null)), CIContent.RcvDeleted(CIDeleteMode.cidmBroadcast), reactions = emptyList()), + ChatItem(CIDirection.DirectRcv(), CIMeta.getSample(97L, Clock.System.now(), text = "", itemDeleted = CIDeleted.Deleted(null)), CIContent.RcvDeleted(CIDeleteMode.cidmBroadcast), reactions = emptyList()), + ChatItem(CIDirection.DirectRcv(), CIMeta.getSample(96L, Clock.System.now(), text = ""), CIContent.RcvMsgContent(MsgContent.MCText("")), reactions = emptyList()), + ChatItem(CIDirection.DirectRcv(), CIMeta.getSample(95L, Clock.System.now(), text = ""), CIContent.RcvMsgContent(MsgContent.MCText("")), reactions = emptyList()), + ChatItem(CIDirection.DirectRcv(), CIMeta.getSample(94L, Clock.System.now(), text = ""), CIContent.RcvMsgContent(MsgContent.MCText("")), reactions = emptyList()), + ) + + val unreadCount = mutableStateOf(0) + val chatState = ActiveChatState() + val merged1 = MergedItems.create(items, unreadCount, emptySet(), chatState) + assertEquals( + listOf( + listOf(0, false, + listOf( + listOf(0, 100, CIMergeCategory.ChatFeature), + listOf(1, 99, CIMergeCategory.ChatFeature) + ) + ), + listOf(2, false, + listOf( + listOf(0, 98, CIMergeCategory.RcvItemDeleted), + listOf(1, 97, CIMergeCategory.RcvItemDeleted) + ) + ), + listOf(4, true, + listOf( + listOf(0, 96, null), + ) + ), + listOf(5, true, + listOf( + listOf(0, 95, null), + ) + ), + listOf(6, true, + listOf( + listOf(0, 94, null) + ) + ) + ).toList().toString(), + merged1.items.map { + listOf( + it.startIndexInReversedItems, + if (it is MergedItem.Grouped) it.revealed else true, + when (it) { + is MergedItem.Grouped -> it.items.mapIndexed { index, listItem -> + listOf(index, listItem.item.id, listItem.item.mergeCategory) + } + is MergedItem.Single -> listOf(listOf(0, it.item.item.id, it.item.item.mergeCategory)) + } + ) + }.toString() + ) + + val merged2 = MergedItems.create(items, unreadCount, setOf(98L, 97L), chatState) + assertEquals( + listOf( + listOf(0, false, + listOf( + listOf(0, 100, CIMergeCategory.ChatFeature), + listOf(1, 99, CIMergeCategory.ChatFeature) + ) + ), + listOf(2, true, + listOf( + listOf(0, 98, CIMergeCategory.RcvItemDeleted), + ) + ), + listOf(3, true, + listOf( + listOf(0, 97, CIMergeCategory.RcvItemDeleted) + ) + ), + listOf(4, true, + listOf( + listOf(0, 96, null), + ) + ), + listOf(5, true, + listOf( + listOf(0, 95, null), + ) + ), + listOf(6, true, + listOf( + listOf(0, 94, null) + ) + ) + ).toList().toString(), + merged2.items.map { + listOf( + it.startIndexInReversedItems, + if (it is MergedItem.Grouped) it.revealed else true, + when (it) { + is MergedItem.Grouped -> it.items.mapIndexed { index, listItem -> + listOf(index, listItem.item.id, listItem.item.mergeCategory) + } + is MergedItem.Single -> listOf(listOf(0, it.item.item.id, it.item.item.mergeCategory)) + } + ) + }.toString() + ) + } +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt index 25d85a6b7d..2702862e47 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt @@ -56,7 +56,7 @@ fun showApp() { } else { // The last possible cause that can be closed chatModel.chatId.value = null - chatModel.chatItems.clear() + chatModel.chatItems.clearAndNotify() } chatModel.activeCall.value?.let { withBGApi { diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/CIVideoView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/CIVideoView.desktop.kt index 2c063b5888..bdfbf6863f 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/CIVideoView.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/CIVideoView.desktop.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalWindowInfo import androidx.compose.ui.unit.Dp import chat.simplex.common.platform.* import chat.simplex.common.simplexWindowState @@ -37,3 +38,6 @@ actual fun LocalWindowWidth(): Dp = with(LocalDensity.current) { simplexWindowState.windowState.size.width } } + +@Composable +actual fun LocalWindowHeight(): Dp = with(LocalDensity.current) { LocalWindowInfo.current.containerSize.height.toDp() } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.desktop.kt index a1df7091d6..3fa78bbbb5 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.desktop.kt @@ -45,7 +45,7 @@ private fun ActiveCallInteractiveAreaOneHand(call: Call, showMenu: MutableState< val chat = chatModel.getChat(call.contact.id) if (chat != null) { withBGApi { - openChat(chat.remoteHostId, chat.chatInfo, chatModel) + openChat(chat.remoteHostId, chat.chatInfo) } } }, @@ -110,7 +110,7 @@ private fun ActiveCallInteractiveAreaNonOneHand(call: Call, showMenu: MutableSta val chat = chatModel.getChat(call.contact.id) if (chat != null) { withBGApi { - openChat(chat.remoteHostId, chat.chatInfo, chatModel) + openChat(chat.remoteHostId, chat.chatInfo) } } }, From 522f99aadd20d6fe376b97678af85a74d76e5e43 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Wed, 20 Nov 2024 22:39:13 +0000 Subject: [PATCH 042/167] directory service: notify admins about group registration events (#5223) --- .../src/Directory/Service.hs | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/apps/simplex-directory-service/src/Directory/Service.hs b/apps/simplex-directory-service/src/Directory/Service.hs index 2c18d4df27..afcdb233e8 100644 --- a/apps/simplex-directory-service/src/Directory/Service.hs +++ b/apps/simplex-directory-service/src/Directory/Service.hs @@ -105,8 +105,11 @@ directoryService st DirectoryOpts {adminUsers, superUsers, serviceName, searchRe SDRSuperUser -> deSuperUserCommand ct ciId cmd DELogChatResponse r -> logInfo r where + withAdminUsers action = void . forkIO $ do + forM_ superUsers $ \KnownContact {contactId} -> action contactId + forM_ adminUsers $ \KnownContact {contactId} -> action contactId withSuperUsers action = void . forkIO $ forM_ superUsers $ \KnownContact {contactId} -> action contactId - notifySuperUsers s = withSuperUsers $ \contactId -> sendMessage' cc contactId s + notifyAdminUsers s = withAdminUsers $ \contactId -> sendMessage' cc contactId s notifyOwner GroupReg {dbContactId} = sendMessage' cc dbContactId ctId `isOwner` GroupReg {dbContactId} = ctId == dbContactId withGroupReg GroupInfo {groupId, localDisplayName} err action = do @@ -288,16 +291,16 @@ directoryService st DirectoryOpts {adminUsers, superUsers, serviceName, searchRe notifyOwner gr $ "The group profile is updated " <> userGroupRef <> ", but no link is added to the welcome message.\n\nThe group will remain hidden from the directory until the group link is added and the group is re-approved." GPServiceLinkRemoved -> do notifyOwner gr $ "The group link for " <> userGroupRef <> " is removed from the welcome message.\n\nThe group is hidden from the directory until the group link is added and the group is re-approved." - notifySuperUsers $ "The group link is removed from " <> groupRef <> ", de-listed." + notifyAdminUsers $ "The group link is removed from " <> groupRef <> ", de-listed." GPServiceLinkAdded -> do setGroupStatus st gr $ GRSPendingApproval n' notifyOwner gr $ "The group link is added to " <> userGroupRef <> "!\nIt is hidden from the directory until approved." - notifySuperUsers $ "The group link is added to " <> groupRef <> "." + notifyAdminUsers $ "The group link is added to " <> groupRef <> "." checkRolesSendToApprove gr n' GPHasServiceLink -> do setGroupStatus st gr $ GRSPendingApproval n' notifyOwner gr $ "The group " <> userGroupRef <> " is updated!\nIt is hidden from the directory until approved." - notifySuperUsers $ "The group " <> groupRef <> " is updated." + notifyAdminUsers $ "The group " <> groupRef <> " is updated." checkRolesSendToApprove gr n' GPServiceLinkError -> logError $ "Error: no group link for " <> groupRef <> " pending approval." groupProfileUpdate = profileUpdate <$> sendChatCmd cc (APIGetGroupLink groupId) @@ -329,7 +332,7 @@ directoryService st DirectoryOpts {adminUsers, superUsers, serviceName, searchRe maybe ("The group ID " <> tshow dbGroupId <> " submitted: ") (\c -> localDisplayName' c <> " submitted the group ID " <> tshow dbGroupId <> ": ") ct_ <> ("\n" <> groupInfoText p <> "\n" <> membersStr <> "\nTo approve send:") msg = maybe (MCText text) (\image -> MCImage {text, image}) image' - withSuperUsers $ \cId -> do + withAdminUsers $ \cId -> do sendComposedMessage' cc cId Nothing msg sendMessage' cc cId $ "/approve " <> tshow dbGroupId <> ":" <> viewName displayName <> " " <> tshow gaId @@ -344,14 +347,14 @@ directoryService st DirectoryOpts {adminUsers, superUsers, serviceName, searchRe GRSSuspendedBadRoles -> when (rStatus == GRSOk) $ do setGroupStatus st gr GRSActive notifyOwner gr $ uCtRole <> ".\n\nThe group is listed in the directory again." - notifySuperUsers $ "The group " <> groupRef <> " is listed " <> suCtRole + notifyAdminUsers $ "The group " <> groupRef <> " is listed " <> suCtRole GRSPendingApproval gaId -> when (rStatus == GRSOk) $ do sendToApprove g gr gaId notifyOwner gr $ uCtRole <> ".\n\nThe group is submitted for approval." GRSActive -> when (rStatus /= GRSOk) $ do setGroupStatus st gr GRSSuspendedBadRoles notifyOwner gr $ uCtRole <> ".\n\nThe group is no longer listed in the directory." - notifySuperUsers $ "The group " <> groupRef <> " is de-listed " <> suCtRole + notifyAdminUsers $ "The group " <> groupRef <> " is de-listed " <> suCtRole _ -> pure () where rStatus = groupRolesStatus contactRole serviceRole @@ -370,7 +373,7 @@ directoryService st DirectoryOpts {adminUsers, superUsers, serviceName, searchRe whenContactIsOwner gr $ do setGroupStatus st gr GRSActive notifyOwner gr $ uSrvRole <> ".\n\nThe group is listed in the directory again." - notifySuperUsers $ "The group " <> groupRef <> " is listed " <> suSrvRole + notifyAdminUsers $ "The group " <> groupRef <> " is listed " <> suSrvRole GRSPendingApproval gaId -> when (serviceRole == GRAdmin) $ whenContactIsOwner gr $ do sendToApprove g gr gaId @@ -378,7 +381,7 @@ directoryService st DirectoryOpts {adminUsers, superUsers, serviceName, searchRe GRSActive -> when (serviceRole /= GRAdmin) $ do setGroupStatus st gr GRSSuspendedBadRoles notifyOwner gr $ uSrvRole <> ".\n\nThe group is no longer listed in the directory." - notifySuperUsers $ "The group " <> groupRef <> " is de-listed " <> suSrvRole + notifyAdminUsers $ "The group " <> groupRef <> " is de-listed " <> suSrvRole _ -> pure () where groupRef = groupReference g @@ -395,7 +398,7 @@ directoryService st DirectoryOpts {adminUsers, superUsers, serviceName, searchRe when (ctId `isOwner` gr) $ do setGroupStatus st gr GRSRemoved notifyOwner gr $ "You are removed from the group " <> userGroupReference gr g <> ".\n\nThe group is no longer listed in the directory." - notifySuperUsers $ "The group " <> groupReference g <> " is de-listed (group owner is removed)." + notifyAdminUsers $ "The group " <> groupReference g <> " is de-listed (group owner is removed)." deContactLeftGroup :: ContactId -> GroupInfo -> IO () deContactLeftGroup ctId g = do @@ -404,7 +407,7 @@ directoryService st DirectoryOpts {adminUsers, superUsers, serviceName, searchRe when (ctId `isOwner` gr) $ do setGroupStatus st gr GRSRemoved notifyOwner gr $ "You left the group " <> userGroupReference gr g <> ".\n\nThe group is no longer listed in the directory." - notifySuperUsers $ "The group " <> groupReference g <> " is de-listed (group owner left)." + notifyAdminUsers $ "The group " <> groupReference g <> " is de-listed (group owner left)." deServiceRemovedFromGroup :: GroupInfo -> IO () deServiceRemovedFromGroup g = do @@ -412,7 +415,7 @@ directoryService st DirectoryOpts {adminUsers, superUsers, serviceName, searchRe withGroupReg g "service removed" $ \gr -> do setGroupStatus st gr GRSRemoved notifyOwner gr $ serviceName <> " is removed from the group " <> userGroupReference gr g <> ".\n\nThe group is no longer listed in the directory." - notifySuperUsers $ "The group " <> groupReference g <> " is de-listed (directory service is removed)." + notifyAdminUsers $ "The group " <> groupReference g <> " is de-listed (directory service is removed)." deUserCommand :: ServiceState -> Contact -> ChatItemId -> DirectoryCmd 'DRUser -> IO () deUserCommand env@ServiceState {searchRequests} ct ciId = \case From 61d7df89069ad9b4725d4b0f47c546fddc163b77 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Thu, 21 Nov 2024 16:54:35 +0000 Subject: [PATCH 043/167] ui: always use private routing by default --- apps/ios/SimpleXChat/APITypes.swift | 2 +- .../commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt | 2 +- src/Simplex/Chat/Options.hs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 8014600d47..51aa9108a1 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -1722,7 +1722,7 @@ public struct NetCfg: Codable, Equatable { public var hostMode: HostMode = .publicHost public var requiredHostMode = true public var sessionMode = TransportSessionMode.user - public var smpProxyMode: SMPProxyMode = .unknown + public var smpProxyMode: SMPProxyMode = .always public var smpProxyFallback: SMPProxyFallback = .allowProtected public var smpWebPort = false public var tcpConnectTimeout: Int // microseconds diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index 7f7f8a6e58..580a663945 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -3683,7 +3683,7 @@ data class NetCfg( val hostMode: HostMode = HostMode.OnionViaSocks, val requiredHostMode: Boolean = false, val sessionMode: TransportSessionMode = TransportSessionMode.default, - val smpProxyMode: SMPProxyMode = SMPProxyMode.Unknown, + val smpProxyMode: SMPProxyMode = SMPProxyMode.Always, val smpProxyFallback: SMPProxyFallback = SMPProxyFallback.AllowProtected, val smpWebPort: Boolean = false, val tcpConnectTimeout: Long, // microseconds diff --git a/src/Simplex/Chat/Options.hs b/src/Simplex/Chat/Options.hs index 16ffe6e28f..f398831194 100644 --- a/src/Simplex/Chat/Options.hs +++ b/src/Simplex/Chat/Options.hs @@ -236,7 +236,7 @@ coreChatOptsP appDir defaultDbFileName = do ) yesToUpMigrations <- switch - ( long "--yes-migrate" + ( long "yes-migrate" <> short 'y' <> help "Automatically confirm \"up\" database migrations" ) From 78b3b12ec1cd02e440c954add8d320f2f6b954ad Mon Sep 17 00:00:00 2001 From: Evgeny Date: Thu, 21 Nov 2024 17:02:55 +0000 Subject: [PATCH 044/167] ios: button to open conditions and changes (#5225) --- .../Onboarding/ChooseServerOperators.swift | 3 ++ .../NetworkAndServers/NetworkAndServers.swift | 12 ++++--- .../NetworkAndServers/OperatorView.swift | 35 ++++++++++++++++--- 3 files changed, 40 insertions(+), 10 deletions(-) diff --git a/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift b/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift index 471d27ea50..19d67bc62c 100644 --- a/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift +++ b/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift @@ -9,6 +9,8 @@ import SwiftUI import SimpleXChat +let conditionsURL = URL(string: "https://github.com/simplex-chat/simplex-chat/blob/stable/PRIVACY.md")! + struct OnboardingButtonStyle: ButtonStyle { @EnvironmentObject var theme: AppTheme var isDisabled: Bool = false @@ -313,6 +315,7 @@ struct ChooseServerOperators: View { reviewConditionsView() .navigationTitle("Conditions of use") .navigationBarTitleDisplayMode(.large) + .toolbar { ToolbarItem(placement: .navigationBarTrailing, content: conditionsLinkButton) } .modifier(ThemedBackground(grouped: true)) } diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift index 8b07c9a519..9b03b79353 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift @@ -238,11 +238,13 @@ struct UsageConditionsView: View { var body: some View { VStack(alignment: .leading, spacing: 20) { - Text("Conditions of use") - .font(.largeTitle) - .bold() - .padding(.top) - .padding(.top) + HStack { + Text("Conditions of use").font(.largeTitle).bold() + Spacer() + conditionsLinkButton() + } + .padding(.top) + .padding(.top) switch ChatModel.shared.conditions.conditionsAction { diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift index 6cebfdcde6..83152a001f 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift @@ -464,11 +464,13 @@ struct SingleOperatorUsageConditionsView: View { } private func viewHeader() -> some View { - Text("Use servers of \(userServers[operatorIndex].operator_.tradeName)") - .font(.largeTitle) - .bold() - .padding(.top) - .padding(.top) + HStack { + Text("Use \(userServers[operatorIndex].operator_.tradeName)").font(.largeTitle).bold() + Spacer() + conditionsLinkButton() + } + .padding(.top) + .padding(.top) } @ViewBuilder private func conditionsAppliedToOtherOperatorsText() -> some View { @@ -545,10 +547,33 @@ struct SingleOperatorUsageConditionsView: View { .padding(.bottom) .navigationTitle("Conditions of use") .navigationBarTitleDisplayMode(.large) + .toolbar { ToolbarItem(placement: .navigationBarTrailing, content: conditionsLinkButton) } .modifier(ThemedBackground(grouped: true)) } } +func conditionsLinkButton() -> some View { + let commit = ChatModel.shared.conditions.currentConditions.conditionsCommit + let mdUrl = URL(string: "https://github.com/simplex-chat/simplex-chat/blob/\(commit)/PRIVACY.md") ?? conditionsURL + return Menu { + Link(destination: mdUrl) { + Label("Open conditions", systemImage: "doc") + } + if let commitUrl = URL(string: "https://github.com/simplex-chat/simplex-chat/commit/\(commit)") { + Link(destination: commitUrl) { + Label("Open changes", systemImage: "ellipsis") + } + } + } label: { + Image(systemName: "arrow.up.right.circle") + .resizable() + .scaledToFit() + .frame(width: 20) + .padding(2) + .contentShape(Circle()) + } +} + #Preview { OperatorView( currUserServers: Binding.constant([UserOperatorServers.sampleData1, UserOperatorServers.sampleDataNilOperator]), From 1083a0727a4750ede208ee8fe0cdc4acd1deee4d Mon Sep 17 00:00:00 2001 From: sh <37271604+shumvgolove@users.noreply.github.com> Date: Fri, 22 Nov 2024 08:31:58 +0000 Subject: [PATCH 045/167] flatpak: update metainfo (#5146) --- .../flatpak/chat.simplex.simplex.metainfo.xml | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/scripts/flatpak/chat.simplex.simplex.metainfo.xml b/scripts/flatpak/chat.simplex.simplex.metainfo.xml index c3c7954836..bc90e4e041 100644 --- a/scripts/flatpak/chat.simplex.simplex.metainfo.xml +++ b/scripts/flatpak/chat.simplex.simplex.metainfo.xml @@ -38,6 +38,34 @@ + + https://simplex.chat/blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.html + +

New in v6.1 - v6.1.1:

+
    +
  • Misc fixes
  • +
+

New in v6.1:

+

Better security:

+
    +
  • SimpleX protocols reviewed by Trail of Bits.
  • +
  • security improvements (don't worry, there is nothing critical there).
  • +
+

Better calls:

+
    +
  • you can switch audio and video during the call.
  • +
  • share the screen from desktop app.
  • +
+

Better user experience:

+
    +
  • switch chat profile for 1-time invitations.
  • +
  • customizable message shape.
  • +
  • better message dates.
  • +
  • forward up to 20 messages at once.
  • +
  • delete or moderate up to 200 messages.
  • +
+ + https://simplex.chat/blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.html From bab63d8f27118c3f3d59b979305c6ee5732e0065 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Fri, 22 Nov 2024 13:23:33 +0400 Subject: [PATCH 046/167] ios: fix repeatedly showing updated conditions --- apps/ios/Shared/ContentView.swift | 13 +++++++++++++ apps/ios/Shared/Views/Onboarding/WhatsNewView.swift | 10 ---------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/apps/ios/Shared/ContentView.swift b/apps/ios/Shared/ContentView.swift index c5a7a6f20b..652258415e 100644 --- a/apps/ios/Shared/ContentView.swift +++ b/apps/ios/Shared/ContentView.swift @@ -295,12 +295,16 @@ struct ContentView: View { case let .whatsNew(updatedConditions): WhatsNewView(updatedConditions: updatedConditions) .modifier(ThemedBackground()) + .if(updatedConditions) { v in + v.task { await setConditionsNotified_() } + } case .updatedConditions: UsageConditionsView( currUserServers: Binding.constant([]), userServers: Binding.constant([]) ) .modifier(ThemedBackground(grouped: true)) + .task { await setConditionsNotified_() } } } if chatModel.setDeliveryReceipts { @@ -313,6 +317,15 @@ struct ContentView: View { .onContinueUserActivity("INStartVideoCallIntent", perform: processUserActivity) } + private func setConditionsNotified_() async { + do { + let conditionsId = ChatModel.shared.conditions.currentConditions.conditionsId + try await setConditionsNotified(conditionsId: conditionsId) + } catch let error { + logger.error("setConditionsNotified error: \(responseError(error))") + } + } + private func processUserActivity(_ activity: NSUserActivity) { let intent = activity.interaction?.intent if let intent = intent as? INStartCallIntent { diff --git a/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift b/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift index 4208c4a068..c1c2cb8383 100644 --- a/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift +++ b/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift @@ -598,16 +598,6 @@ struct WhatsNewView: View { var body: some View { whatsNewView() - .task { - if updatedConditions { - do { - let conditionsId = ChatModel.shared.conditions.currentConditions.conditionsId - try await setConditionsNotified(conditionsId: conditionsId) - } catch let error { - logger.error("WhatsNewView setConditionsNotified error: \(responseError(error))") - } - } - } .sheet(item: $sheetItem) { item in switch item { case .showConditions: From 49d1b26bba44bf417b56bf6a0345bac98e1827ce Mon Sep 17 00:00:00 2001 From: Evgeny Date: Fri, 22 Nov 2024 10:38:00 +0000 Subject: [PATCH 047/167] core: tests for operators api, CLI command to update operators (#5226) --- src/Simplex/Chat.hs | 25 ++++++++++++++++++ src/Simplex/Chat/Controller.hs | 1 + src/Simplex/Chat/Operators.hs | 8 ++++++ src/Simplex/Chat/View.hs | 21 ++++++++++----- tests/ChatClient.hs | 10 +++++++ tests/ChatTests/Direct.hs | 48 ++++++++++++++++++++++++++++------ 6 files changed, 98 insertions(+), 15 deletions(-) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 0daf9fa394..5906da57de 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -1640,6 +1640,16 @@ processChatCommand' vr = \case xftpSrvs <- getProtocolServers db SPXFTP user uss <- groupByOperator (ops, smpSrvs, xftpSrvs) pure $ (aUserId user,) $ useServers as opDomains uss + SetServerOperators operatorsRoles -> do + ops <- serverOperators <$> withFastStore getServerOperators + ops' <- mapM (updateOp ops) operatorsRoles + processChatCommand $ APISetServerOperators ops' + where + updateOp :: [ServerOperator] -> ServerOperatorRoles -> CM ServerOperator + updateOp ops r = + case find (\ServerOperator {operatorId = DBEntityId opId} -> operatorId' r == opId) ops of + Just op -> pure op {enabled = enabled' r, smpRoles = smpRoles' r, xftpRoles = xftpRoles' r} + Nothing -> throwError $ ChatErrorStore $ SEOperatorNotFound $ operatorId' r APIGetUserServers userId -> withUserId userId $ \user -> withFastStore $ \db -> do CRUserServers user <$> (liftIO . groupByOperator =<< getUserServers db user) APISetUserServers userId userServers -> withUserId userId $ \user -> do @@ -8308,6 +8318,7 @@ chatCommandP = "/xftp" $> GetUserProtoServers (AProtocolType SPXFTP), "/_operators" $> APIGetServerOperators, "/_operators " *> (APISetServerOperators <$> jsonP), + "/operators " *> (SetServerOperators . L.fromList <$> operatorRolesP `A.sepBy1` A.char ','), "/_servers " *> (APIGetUserServers <$> A.decimal), "/_servers " *> (APISetUserServers <$> A.decimal <* A.space <*> jsonP), "/_validate_servers " *> (APIValidateServers <$> A.decimal <* A.space <*> jsonP), @@ -8637,6 +8648,20 @@ chatCommandP = optional ("yes" *> A.space) *> (TMEEnableSetTTL <$> timedTTLP) <|> ("yes" $> TMEEnableKeepTTL) <|> ("no" $> TMEDisableKeepTTL) + operatorRolesP = do + operatorId' <- A.decimal + enabled' <- A.char ':' *> onOffP + smpRoles' <- (":smp=" *> srvRolesP) <|> pure allRoles + xftpRoles' <- (":xftp=" *> srvRolesP) <|> pure allRoles + pure ServerOperatorRoles {operatorId', enabled', smpRoles', xftpRoles'} + srvRolesP = srvRoles <$?> A.takeTill (\c -> c == ':' || c == ',') + where + srvRoles = \case + "off" -> Right $ ServerRoles False False + "proxy" -> Right ServerRoles {storage = False, proxy = True} + "storage" -> Right ServerRoles {storage = True, proxy = False} + "on" -> Right allRoles + _ -> Left "bad ServerRoles" netCfgP = do socksProxy <- "socks=" *> ("off" $> Nothing <|> "on" $> Just defaultSocksProxyWithAuth <|> Just <$> strP) socksMode <- " socks-mode=" *> strP <|> pure SMAlways diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index e44ea2ac18..23aa632478 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -358,6 +358,7 @@ data ChatCommand | TestProtoServer AProtoServerWithAuth | APIGetServerOperators | APISetServerOperators (NonEmpty ServerOperator) + | SetServerOperators (NonEmpty ServerOperatorRoles) | APIGetUserServers UserId | APISetUserServers UserId (NonEmpty UpdatedUserOperatorServers) | APIValidateServers UserId [UpdatedUserOperatorServers] -- response is CRUserServersValidation diff --git a/src/Simplex/Chat/Operators.hs b/src/Simplex/Chat/Operators.hs index ebe1da8176..e14e95211a 100644 --- a/src/Simplex/Chat/Operators.hs +++ b/src/Simplex/Chat/Operators.hs @@ -192,6 +192,14 @@ data ServerOperator' s = ServerOperator } deriving (Show) +data ServerOperatorRoles = ServerOperatorRoles + { operatorId' :: Int64, + enabled' :: Bool, + smpRoles' :: ServerRoles, + xftpRoles' :: ServerRoles + } + deriving (Show) + operatorRoles :: UserProtocol p => SProtocolType p -> ServerOperator -> ServerRoles operatorRoles p op = case p of SPSMP -> smpRoles op diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index e4c0fd5606..f9ec3f936c 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -101,7 +101,7 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe CRServerOperatorConditions (ServerOperatorConditions ops _ ca) -> viewServerOperators ops ca CRUserServers u uss -> ttyUser u $ concatMap viewUserServers uss <> (if testView then [] else serversUserHelp) CRUserServersValidation {} -> [] - CRUsageConditions {} -> [] + CRUsageConditions current _ accepted_ -> viewUsageConditions current accepted_ CRChatItemTTL u ttl -> ttyUser u $ viewChatItemTTL ttl CRNetworkConfig cfg -> viewNetworkConfig cfg CRContactInfo u ct cStats customUserProfile -> ttyUser u $ viewContactInfo ct cStats customUserProfile @@ -1280,8 +1280,8 @@ viewOperator op@ServerOperator {tradeName, legalName, serverDomains, conditionsA <> tradeName <> maybe "" parens legalName <> (", domains: " <> T.intercalate ", " serverDomains) + <> (", servers: " <> viewOpEnabled op) <> (", conditions: " <> viewOpConditions conditionsAcceptance) - <> (", " <> viewOpEnabled op) shortViewOperator :: ServerOperator -> Text shortViewOperator ServerOperator {operatorId = DBEntityId opId, tradeName, enabled} = @@ -1289,10 +1289,10 @@ shortViewOperator ServerOperator {operatorId = DBEntityId opId, tradeName, enabl viewOpIdTag :: ServerOperator' s -> Text viewOpIdTag ServerOperator {operatorId, operatorTag} = case operatorId of - DBEntityId i -> tshow i <> " - " <> tag + DBEntityId i -> tshow i <> tag DBNewEntity -> tag where - tag = maybe "" textEncode operatorTag <> ". " + tag = maybe "" (parens . textEncode) operatorTag <> ". " viewOpConditions :: ConditionsAcceptance -> Text viewOpConditions = \case @@ -1306,7 +1306,7 @@ viewOpEnabled ServerOperator {enabled, smpRoles, xftpRoles} | not enabled = "disabled" | no smpRoles && no xftpRoles = "disabled (servers known)" | both smpRoles && both xftpRoles = "enabled" - | otherwise = "SMP " <> viewRoles smpRoles <> ", XFTP" <> viewRoles xftpRoles + | otherwise = "SMP " <> viewRoles smpRoles <> ", XFTP " <> viewRoles xftpRoles where no rs = not $ storage rs || proxy rs both rs = storage rs && proxy rs @@ -1319,13 +1319,20 @@ viewOpEnabled ServerOperator {enabled, smpRoles, xftpRoles} viewConditionsAction :: UsageConditionsAction -> [StyledString] viewConditionsAction = \case UCAReview {operators, deadline, showNotice} | showNotice -> case deadline of - Just ts -> [plain $ "New conditions will be accepted at " <> tshow ts <> " for " <> ops] - Nothing -> [plain $ "New conditions have to be accepted for " <> ops] + Just ts -> [plain $ "The new conditions will be accepted for " <> ops <> " at " <> tshow ts] + Nothing -> [plain $ "The new conditions have to be accepted for " <> ops] where ops = T.intercalate ", " $ map legalName_ operators legalName_ ServerOperator {tradeName, legalName} = fromMaybe tradeName legalName _ -> [] +viewUsageConditions :: UsageConditions -> Maybe UsageConditions -> [StyledString] +viewUsageConditions current accepted_ = + [plain $ "Current conditions: " <> viewConds current <> maybe "" (\ac -> ", accepted conditions: " <> viewConds ac) accepted_] + where + viewConds UsageConditions {conditionsId, conditionsCommit, notifiedAt} = + tshow conditionsId <> maybe "" (const " (notified)") notifiedAt <> ". " <> conditionsCommit + viewChatItemTTL :: Maybe Int64 -> [StyledString] viewChatItemTTL = \case Nothing -> ["old messages are not being deleted"] diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index 7bf7804472..8b7e8fcd32 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -376,6 +376,16 @@ userName :: TestCC -> IO [Char] userName (TestCC ChatController {currentUser} _ _ _ _ _) = maybe "no current user" (\User {localDisplayName} -> T.unpack localDisplayName) <$> readTVarIO currentUser +testChat :: HasCallStack => Profile -> (HasCallStack => TestCC -> IO ()) -> FilePath -> IO () +testChat = testChatCfgOpts testCfg testOpts + +testChatCfgOpts :: HasCallStack => ChatConfig -> ChatOpts -> Profile -> (HasCallStack => TestCC -> IO ()) -> FilePath -> IO () +testChatCfgOpts cfg opts p test = testChatN cfg opts [p] test_ + where + test_ :: HasCallStack => [TestCC] -> IO () + test_ [tc] = test tc + test_ _ = error "expected 1 chat client" + testChat2 :: HasCallStack => Profile -> Profile -> (HasCallStack => TestCC -> TestCC -> IO ()) -> FilePath -> IO () testChat2 = testChatCfgOpts2 testCfg testOpts diff --git a/tests/ChatTests/Direct.hs b/tests/ChatTests/Direct.hs index 6bbf72171e..d305055d94 100644 --- a/tests/ChatTests/Direct.hs +++ b/tests/ChatTests/Direct.hs @@ -85,6 +85,8 @@ chatDirectTests = do describe "XFTP servers" $ do it "get and set XFTP servers" testGetSetXFTPServers it "test XFTP server connection" testTestXFTPServer + describe "operators and usage conditions" $ do + it "get and enable operators, accept conditions" testOperators describe "async connection handshake" $ do describe "connect when initiating client goes offline" $ do it "curr" $ testAsyncInitiatingOffline testCfg testCfg @@ -1140,8 +1142,8 @@ testSendMultiManyBatches = testGetSetSMPServers :: HasCallStack => FilePath -> IO () testGetSetSMPServers = - testChat2 aliceProfile bobProfile $ - \alice _ -> do + testChat aliceProfile $ + \alice -> do alice ##> "/_servers 1" alice <## "Your servers" alice <## " SMP servers" @@ -1168,8 +1170,8 @@ testGetSetSMPServers = testTestSMPServerConnection :: HasCallStack => FilePath -> IO () testTestSMPServerConnection = - testChat2 aliceProfile bobProfile $ - \alice _ -> do + testChat aliceProfile $ + \alice -> do alice ##> "/smp test smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7001" alice <## "SMP server test passed" -- to test with password: @@ -1183,8 +1185,8 @@ testTestSMPServerConnection = testGetSetXFTPServers :: HasCallStack => FilePath -> IO () testGetSetXFTPServers = - testChat2 aliceProfile bobProfile $ - \alice _ -> withXFTPServer $ do + testChat aliceProfile $ + \alice -> withXFTPServer $ do alice ##> "/_servers 1" alice <## "Your servers" alice <## " SMP servers" @@ -1210,8 +1212,8 @@ testGetSetXFTPServers = testTestXFTPServer :: HasCallStack => FilePath -> IO () testTestXFTPServer = - testChat2 aliceProfile bobProfile $ - \alice _ -> withXFTPServer $ do + testChat aliceProfile $ + \alice -> withXFTPServer $ do alice ##> "/xftp test xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7002" alice <## "XFTP server test passed" -- to test with password: @@ -1223,6 +1225,36 @@ testTestXFTPServer = alice <## "XFTP server test failed at Connect, error: BROKER {brokerAddress = \"xftp://LcJU@localhost:7002\", brokerErr = NETWORK}" alice <## "Possibly, certificate fingerprint in XFTP server address is incorrect" +testOperators :: HasCallStack => FilePath -> IO () +testOperators = + testChatCfgOpts testCfg opts' aliceProfile $ + \alice -> do + -- initial load + alice ##> "/_conditions" + alice <##. "Current conditions: 2." + alice ##> "/_operators" + alice <##. "1 (simplex). SimpleX Chat (SimpleX Chat Ltd), domains: simplex.im, servers: enabled, conditions: required (" + alice <## "2 (flux). Flux (InFlux Technologies Limited), domains: simplexonflux.com, servers: disabled, conditions: required" + alice <##. "The new conditions will be accepted for SimpleX Chat Ltd at " + -- set conditions notified + alice ##> "/_conditions_notified 2" + alice <## "ok" + alice ##> "/_operators" + alice <##. "1 (simplex). SimpleX Chat (SimpleX Chat Ltd), domains: simplex.im, servers: enabled, conditions: required (" + alice <## "2 (flux). Flux (InFlux Technologies Limited), domains: simplexonflux.com, servers: disabled, conditions: required" + alice ##> "/_conditions" + alice <##. "Current conditions: 2 (notified)." + -- accept conditions + alice ##> "/_accept_conditions 2 1,2" + alice <##. "1 (simplex). SimpleX Chat (SimpleX Chat Ltd), domains: simplex.im, servers: enabled, conditions: accepted (" + alice <##. "2 (flux). Flux (InFlux Technologies Limited), domains: simplexonflux.com, servers: disabled, conditions: accepted (" + -- update operators + alice ##> "/operators 2:on:smp=proxy" + alice <##. "1 (simplex). SimpleX Chat (SimpleX Chat Ltd), domains: simplex.im, servers: enabled, conditions: accepted (" + alice <##. "2 (flux). Flux (InFlux Technologies Limited), domains: simplexonflux.com, servers: SMP enabled proxy, XFTP enabled, conditions: accepted (" + where + opts' = testOpts {coreOptions = testCoreOpts {smpServers = [], xftpServers = []}} + testAsyncInitiatingOffline :: HasCallStack => ChatConfig -> ChatConfig -> FilePath -> IO () testAsyncInitiatingOffline aliceCfg bobCfg tmp = do inv <- withNewTestChatCfg tmp aliceCfg "alice" aliceProfile $ \alice -> do From ea9ee987cfb3e89618b8edc6d6e0a15da8e70c7f Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Fri, 22 Nov 2024 19:59:39 +0700 Subject: [PATCH 048/167] android, desktop: better message info screen (#5227) - changed tabBar style to leading icon - made tabBar the same size as AppBars - made background color as theme background --- .../common/views/chat/ChatItemInfoView.kt | 48 +++++++++++-------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt index 8b4f7f8ec9..d6744a0a0d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt @@ -23,6 +23,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.platform.* import chat.simplex.common.views.helpers.* import chat.simplex.common.ui.theme.* @@ -477,28 +478,33 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools selection.value = CIInfoTab.Delivery(ciInfo.memberDeliveryStatuses) } } - TabRow( - selectedTabIndex = availableTabs.indexOfFirst { it::class == selection.value::class }, - backgroundColor = Color.Transparent, - contentColor = MaterialTheme.colors.primary, - ) { - availableTabs.forEach { ciInfoTab -> - Tab( - selected = selection.value::class == ciInfoTab::class, - onClick = { - selection.value = ciInfoTab - }, - text = { Text(tabTitle(ciInfoTab), fontSize = 13.sp) }, - icon = { - Icon( - painterResource(tabIcon(ciInfoTab)), - tabTitle(ciInfoTab) - ) - }, - selectedContentColor = MaterialTheme.colors.primary, - unselectedContentColor = MaterialTheme.colors.secondary, - ) + val oneHandUI = remember { appPrefs.oneHandUI.state } + Box(Modifier.offset(x = 0.dp, y = if (oneHandUI.value) -AppBarHeight * fontSizeSqrtMultiplier else 0.dp)) { + TabRow( + selectedTabIndex = availableTabs.indexOfFirst { it::class == selection.value::class }, + Modifier.height(AppBarHeight * fontSizeSqrtMultiplier), + backgroundColor = MaterialTheme.colors.background, + contentColor = MaterialTheme.colors.primary, + ) { + availableTabs.forEach { ciInfoTab -> + LeadingIconTab( + selected = selection.value::class == ciInfoTab::class, + onClick = { + selection.value = ciInfoTab + }, + text = { Text(tabTitle(ciInfoTab), fontSize = 13.sp) }, + icon = { + Icon( + painterResource(tabIcon(ciInfoTab)), + tabTitle(ciInfoTab) + ) + }, + selectedContentColor = MaterialTheme.colors.primary, + unselectedContentColor = MaterialTheme.colors.secondary, + ) + } } + Divider() } } } else { From 396fa7f988bb12460dfe3f9217b8f0886d601029 Mon Sep 17 00:00:00 2001 From: Diogo Date: Fri, 22 Nov 2024 14:42:07 +0000 Subject: [PATCH 049/167] desktop, android: server operators (#5212) * api and types * whats new view * new package and movements * move network and servers to new package * network and servers view * wip * api update * build * conditions modal in settings * network and servers fns * save server fixes * more servers * move protocol servers view * message servers with validation * added message servers * use for files * fix error by server type * list xftp servers * android: add server view (#5221) * android add server wip * test servers button * fix save of custom servers * remove unused code * edit and view servers * fix * allow to enable untested * show all test errors in the end * android: custom servers view (#5224) * cleanup * validation footers * operator enabled validation * var -> val * reuse onboarding button * AppBarTitle without alpha * remove non scrollable title * change in AppBarTitle * changes in AppBar * bold strings + bordered text view * ChooseServerOperators * fix * new server view wip * fix * scan * rename * fix roles toggle texts * UsageConditionsView * aligned texts * more texts * replace hard coded logos with object ref * use snapshot state to recalculate errors * align views; fix accept * remove extra snapshots * fix ts * fix whatsnew * stage * animation on onboarding * refactor and fix * remember * fix start chat alert * show notice in chat list * refactor * fix validation * open conditions * whats new view updates * icon for navigation improvements * remove debug * simplify * fix * handle click when have unsaved changes * fix * Revert "fix" This reverts commit d49c3736415a9fe08464237e041c2d2b6fc665d3. * Revert "handle click when have unsaved changes" This reverts commit 39ca03f9c086b87b5b6571f93443ba16f2870d24. * fixed close of modals in whats new view * grouping * android: conditions view paddings (#5228) * revert padding * refresh operators on save * fixed modals in different views for desktop * ios: fix enabling operator model update * fix modals --------- Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Co-authored-by: Avently <7953703+avently@users.noreply.github.com> --- .../NetworkAndServers/NetworkAndServers.swift | 2 + .../ScanProtocolServer.android.kt | 6 +- .../kotlin/chat/simplex/common/App.kt | 9 +- .../chat/simplex/common/model/ChatModel.kt | 9 + .../chat/simplex/common/model/SimpleXAPI.kt | 557 ++++++++++++-- .../chat/simplex/common/platform/Core.kt | 12 +- .../chat/simplex/common/views/WelcomeView.kt | 4 +- .../simplex/common/views/chat/ChatView.kt | 2 +- .../common/views/chatlist/ChatListView.kt | 26 +- .../views/chatlist/ServersSummaryView.kt | 19 +- .../common/views/helpers/AppBarTitle.kt | 19 +- .../common/views/helpers/CollapsingAppBar.kt | 1 + .../common/views/helpers/DefaultTopAppBar.kt | 3 +- .../common/views/migration/MigrateToDevice.kt | 1 + .../views/onboarding/ChooseServerOperators.kt | 354 +++++++++ .../common/views/onboarding/HowItWorks.kt | 4 +- .../common/views/onboarding/OnboardingView.kt | 1 + .../views/onboarding/SetNotificationsMode.kt | 8 +- .../onboarding/SetupDatabasePassphrase.kt | 3 +- .../common/views/onboarding/SimpleXInfo.kt | 5 +- .../common/views/onboarding/WhatsNewView.kt | 261 ++++--- .../common/views/remote/ConnectMobileView.kt | 1 + .../views/usersettings/ProtocolServersView.kt | 383 ---------- .../common/views/usersettings/SettingsView.kt | 7 +- .../AdvancedNetworkSettings.kt | 3 +- .../NetworkAndServers.kt | 540 ++++++++++++-- .../networkAndServers/NewServerView.kt | 144 ++++ .../networkAndServers/OperatorView.kt | 701 ++++++++++++++++++ .../ProtocolServerView.kt | 169 +++-- .../networkAndServers/ProtocolServersView.kt | 407 ++++++++++ .../ScanProtocolServer.kt | 14 +- .../commonMain/resources/MR/base/strings.xml | 84 +++ .../resources/MR/images/flux_logo@4x.png | Bin 0 -> 34876 bytes .../MR/images/flux_logo_light@4x.png | Bin 0 -> 33847 bytes .../MR/images/flux_logo_symbol@4x.png | Bin 0 -> 17248 bytes .../resources/MR/images/ic_outbound.svg | 1 + .../ScanProtocolServer.desktop.kt | 9 - .../ScanProtocolServer.desktop.kt | 9 + 38 files changed, 3032 insertions(+), 746 deletions(-) rename apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/{ => networkAndServers}/ScanProtocolServer.android.kt (69%) create mode 100644 apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt delete mode 100644 apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ProtocolServersView.kt rename apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/{ => networkAndServers}/AdvancedNetworkSettings.kt (99%) rename apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/{ => networkAndServers}/NetworkAndServers.kt (52%) create mode 100644 apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NewServerView.kt create mode 100644 apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt rename apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/{ => networkAndServers}/ProtocolServerView.kt (51%) create mode 100644 apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServersView.kt rename apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/{ => networkAndServers}/ScanProtocolServer.kt (62%) create mode 100644 apps/multiplatform/common/src/commonMain/resources/MR/images/flux_logo@4x.png create mode 100644 apps/multiplatform/common/src/commonMain/resources/MR/images/flux_logo_light@4x.png create mode 100644 apps/multiplatform/common/src/commonMain/resources/MR/images/flux_logo_symbol@4x.png create mode 100644 apps/multiplatform/common/src/commonMain/resources/MR/images/ic_outbound.svg delete mode 100644 apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/ScanProtocolServer.desktop.kt create mode 100644 apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ScanProtocolServer.desktop.kt diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift index 9b03b79353..8b6421b502 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift @@ -395,7 +395,9 @@ func saveServers(_ currUserServers: Binding<[UserOperatorServers]>, _ userServer // Get updated servers to learn new server ids (otherwise it messes up delete of newly added and saved servers) do { let updatedServers = try await getUserServers() + let updatedOperators = try await getServerOperators() await MainActor.run { + ChatModel.shared.conditions = updatedOperators currUserServers.wrappedValue = updatedServers userServers.wrappedValue = updatedServers } diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/ScanProtocolServer.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ScanProtocolServer.android.kt similarity index 69% rename from apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/ScanProtocolServer.android.kt rename to apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ScanProtocolServer.android.kt index af5a27be11..8b5def7451 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/ScanProtocolServer.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ScanProtocolServer.android.kt @@ -1,13 +1,13 @@ -package chat.simplex.common.views.usersettings +package chat.simplex.common.views.usersettings.networkAndServers import android.Manifest import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import chat.simplex.common.model.ServerCfg +import chat.simplex.common.model.UserServer import com.google.accompanist.permissions.rememberPermissionState @Composable -actual fun ScanProtocolServer(rhId: Long?, onNext: (ServerCfg) -> Unit) { +actual fun ScanProtocolServer(rhId: Long?, onNext: (UserServer) -> Unit) { val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA) LaunchedEffect(Unit) { cameraPermissionState.launchPermissionRequest() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt index 7af1d574ad..b1ce003812 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt @@ -15,7 +15,6 @@ import androidx.compose.ui.draw.* import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.* -import androidx.compose.ui.graphics.drawscope.clipRect import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.dp @@ -42,7 +41,6 @@ import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import kotlin.math.absoluteValue @Composable fun AppScreen() { @@ -194,6 +192,13 @@ fun MainScreen() { OnboardingStage.Step2_CreateProfile -> CreateFirstProfile(chatModel) {} OnboardingStage.LinkAMobile -> LinkAMobile() OnboardingStage.Step2_5_SetupDatabasePassphrase -> SetupDatabasePassphrase(chatModel) + OnboardingStage.Step3_ChooseServerOperators -> { + val modalData = remember { ModalData() } + modalData.ChooseServerOperators(true) + if (appPlatform.isDesktop) { + ModalManager.fullscreen.showInView() + } + } // Ensure backwards compatibility with old onboarding stage for address creation, otherwise notification setup would be skipped OnboardingStage.Step3_CreateSimpleXAddress -> SetNotificationsMode(chatModel) OnboardingStage.Step4_SetNotificationsMode -> SetNotificationsMode(chatModel) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index ef777f151f..e501ed5a91 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -145,6 +145,8 @@ object ChatModel { val clipboardHasText = mutableStateOf(false) val networkInfo = mutableStateOf(UserNetworkInfo(networkType = UserNetworkType.OTHER, online = true)) + val conditions = mutableStateOf(ServerOperatorConditionsDetail.empty) + val updatingProgress = mutableStateOf(null as Float?) var updatingRequest: Closeable? = null @@ -2567,6 +2569,13 @@ fun localTimestamp(t: Instant): String { return ts.toJavaLocalDateTime().format(dateFormatter) } +fun localDate(t: Instant): String { + val tz = TimeZone.currentSystemDefault() + val ts: LocalDateTime = t.toLocalDateTime(tz) + val dateFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM) + return ts.toJavaLocalDateTime().format(dateFormatter) +} + @Serializable sealed class CIStatus { @Serializable @SerialName("sndNew") class SndNew: CIStatus() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index 580a663945..0cab7ce8e9 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -26,10 +26,12 @@ import chat.simplex.common.views.chat.item.showQuotedItemDoesNotExistAlert import chat.simplex.common.views.migration.MigrationFileLinkData import chat.simplex.common.views.onboarding.OnboardingStage import chat.simplex.common.views.usersettings.* +import chat.simplex.common.views.usersettings.networkAndServers.serverHostname import com.charleskorn.kaml.Yaml import com.charleskorn.kaml.YamlConfiguration import chat.simplex.res.MR import com.russhwolf.settings.Settings +import dev.icerock.moko.resources.ImageResource import dev.icerock.moko.resources.compose.stringResource import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel @@ -963,36 +965,6 @@ object ChatController { return null } - suspend fun getUserProtoServers(rh: Long?, serverProtocol: ServerProtocol): UserProtocolServers? { - val userId = kotlin.runCatching { currentUserId("getUserProtoServers") }.getOrElse { return null } - val r = sendCmd(rh, CC.APIGetUserProtoServers(userId, serverProtocol)) - return if (r is CR.UserProtoServers) { if (rh == null) r.servers else r.servers.copy(protoServers = r.servers.protoServers.map { it.copy(remoteHostId = rh) }) } - else { - Log.e(TAG, "getUserProtoServers bad response: ${r.responseType} ${r.details}") - AlertManager.shared.showAlertMsg( - generalGetString(if (serverProtocol == ServerProtocol.SMP) MR.strings.error_loading_smp_servers else MR.strings.error_loading_xftp_servers), - "${r.responseType}: ${r.details}" - ) - null - } - } - - suspend fun setUserProtoServers(rh: Long?, serverProtocol: ServerProtocol, servers: List): Boolean { - val userId = kotlin.runCatching { currentUserId("setUserProtoServers") }.getOrElse { return false } - val r = sendCmd(rh, CC.APISetUserProtoServers(userId, serverProtocol, servers)) - return when (r) { - is CR.CmdOk -> true - else -> { - Log.e(TAG, "setUserProtoServers bad response: ${r.responseType} ${r.details}") - AlertManager.shared.showAlertMsg( - generalGetString(if (serverProtocol == ServerProtocol.SMP) MR.strings.error_saving_smp_servers else MR.strings.error_saving_xftp_servers), - generalGetString(if (serverProtocol == ServerProtocol.SMP) MR.strings.ensure_smp_server_address_are_correct_format_and_unique else MR.strings.ensure_xftp_server_address_are_correct_format_and_unique) - ) - false - } - } - } - suspend fun testProtoServer(rh: Long?, server: String): ProtocolTestFailure? { val userId = currentUserId("testProtoServer") val r = sendCmd(rh, CC.APITestProtoServer(userId, server)) @@ -1005,6 +977,106 @@ object ChatController { } } + suspend fun getServerOperators(rh: Long?): ServerOperatorConditionsDetail? { + val r = sendCmd(rh, CC.ApiGetServerOperators()) + + return when (r) { + is CR.ServerOperatorConditions -> r.conditions + else -> { + Log.e(TAG, "getServerOperators bad response: ${r.responseType} ${r.details}") + null + } + } + } + + suspend fun setServerOperators(rh: Long?, operators: List): ServerOperatorConditionsDetail? { + val r = sendCmd(rh, CC.ApiSetServerOperators(operators)) + return when (r) { + is CR.ServerOperatorConditions -> r.conditions + else -> { + Log.e(TAG, "setServerOperators bad response: ${r.responseType} ${r.details}") + null + } + } + } + + suspend fun getUserServers(rh: Long?): List? { + val userId = currentUserId("getUserServers") + val r = sendCmd(rh, CC.ApiGetUserServers(userId)) + return when (r) { + is CR.UserServers -> r.userServers + else -> { + Log.e(TAG, "getUserServers bad response: ${r.responseType} ${r.details}") + null + } + } + } + + suspend fun setUserServers(rh: Long?, userServers: List): Boolean { + val userId = currentUserId("setUserServers") + val r = sendCmd(rh, CC.ApiSetUserServers(userId, userServers)) + return when (r) { + is CR.CmdOk -> true + else -> { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.failed_to_save_servers), + "${r.responseType}: ${r.details}" + ) + Log.e(TAG, "setUserServers bad response: ${r.responseType} ${r.details}") + false + } + } + } + + suspend fun validateServers(rh: Long?, userServers: List): List? { + val userId = currentUserId("validateServers") + val r = sendCmd(rh, CC.ApiValidateServers(userId, userServers)) + return when (r) { + is CR.UserServersValidation -> r.serverErrors + else -> { + Log.e(TAG, "validateServers bad response: ${r.responseType} ${r.details}") + null + } + } + } + + suspend fun getUsageConditions(rh: Long?): Triple? { + val r = sendCmd(rh, CC.ApiGetUsageConditions()) + return when (r) { + is CR.UsageConditions -> Triple(r.usageConditions, r.conditionsText, r.acceptedConditions) + else -> { + Log.e(TAG, "getUsageConditions bad response: ${r.responseType} ${r.details}") + null + } + } + } + + suspend fun setConditionsNotified(rh: Long?, conditionsId: Long): Boolean { + val r = sendCmd(rh, CC.ApiSetConditionsNotified(conditionsId)) + return when (r) { + is CR.CmdOk -> true + else -> { + Log.e(TAG, "setConditionsNotified bad response: ${r.responseType} ${r.details}") + false + } + } + } + + suspend fun acceptConditions(rh: Long?, conditionsId: Long, operatorIds: List): ServerOperatorConditionsDetail? { + val r = sendCmd(rh, CC.ApiAcceptConditions(conditionsId, operatorIds)) + return when (r) { + is CR.ServerOperatorConditions -> r.conditions + else -> { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.error_accepting_operator_conditions), + "${r.responseType}: ${r.details}" + ) + Log.e(TAG, "acceptConditions bad response: ${r.responseType} ${r.details}") + null + } + } + } + suspend fun getChatItemTTL(rh: Long?): ChatItemTTL { val userId = currentUserId("getChatItemTTL") val r = sendCmd(rh, CC.APIGetChatItemTTL(userId)) @@ -3037,9 +3109,15 @@ sealed class CC { class APIGetGroupLink(val groupId: Long): CC() class APICreateMemberContact(val groupId: Long, val groupMemberId: Long): CC() class APISendMemberContactInvitation(val contactId: Long, val mc: MsgContent): CC() - class APIGetUserProtoServers(val userId: Long, val serverProtocol: ServerProtocol): CC() - class APISetUserProtoServers(val userId: Long, val serverProtocol: ServerProtocol, val servers: List): CC() class APITestProtoServer(val userId: Long, val server: String): CC() + class ApiGetServerOperators(): CC() + class ApiSetServerOperators(val operators: List): CC() + class ApiGetUserServers(val userId: Long): CC() + class ApiSetUserServers(val userId: Long, val userServers: List): CC() + class ApiValidateServers(val userId: Long, val userServers: List): CC() + class ApiGetUsageConditions(): CC() + class ApiSetConditionsNotified(val conditionsId: Long): CC() + class ApiAcceptConditions(val conditionsId: Long, val operatorIds: List): CC() class APISetChatItemTTL(val userId: Long, val seconds: Long?): CC() class APIGetChatItemTTL(val userId: Long): CC() class APISetNetworkConfig(val networkConfig: NetCfg): CC() @@ -3197,9 +3275,15 @@ sealed class CC { is APIGetGroupLink -> "/_get link #$groupId" is APICreateMemberContact -> "/_create member contact #$groupId $groupMemberId" is APISendMemberContactInvitation -> "/_invite member contact @$contactId ${mc.cmdString}" - is APIGetUserProtoServers -> "/_servers $userId ${serverProtocol.name.lowercase()}" - is APISetUserProtoServers -> "/_servers $userId ${serverProtocol.name.lowercase()} ${protoServersStr(servers)}" is APITestProtoServer -> "/_server test $userId $server" + is ApiGetServerOperators -> "/_operators" + is ApiSetServerOperators -> "/_operators ${json.encodeToString(operators)}" + is ApiGetUserServers -> "/_servers $userId" + is ApiSetUserServers -> "/_servers $userId ${json.encodeToString(userServers)}" + is ApiValidateServers -> "/_validate_servers $userId ${json.encodeToString(userServers)}" + is ApiGetUsageConditions -> "/_conditions" + is ApiSetConditionsNotified -> "/_conditions_notified ${conditionsId}" + is ApiAcceptConditions -> "/_accept_conditions ${conditionsId} ${operatorIds.joinToString(",")}" is APISetChatItemTTL -> "/_ttl $userId ${chatItemTTLStr(seconds)}" is APIGetChatItemTTL -> "/_ttl $userId" is APISetNetworkConfig -> "/_network ${json.encodeToString(networkConfig)}" @@ -3342,9 +3426,15 @@ sealed class CC { is APIGetGroupLink -> "apiGetGroupLink" is APICreateMemberContact -> "apiCreateMemberContact" is APISendMemberContactInvitation -> "apiSendMemberContactInvitation" - is APIGetUserProtoServers -> "apiGetUserProtoServers" - is APISetUserProtoServers -> "apiSetUserProtoServers" is APITestProtoServer -> "testProtoServer" + is ApiGetServerOperators -> "apiGetServerOperators" + is ApiSetServerOperators -> "apiSetServerOperators" + is ApiGetUserServers -> "apiGetUserServers" + is ApiSetUserServers -> "apiSetUserServers" + is ApiValidateServers -> "apiValidateServers" + is ApiGetUsageConditions -> "apiGetUsageConditions" + is ApiSetConditionsNotified -> "apiSetConditionsNotified" + is ApiAcceptConditions -> "apiAcceptConditions" is APISetChatItemTTL -> "apiSetChatItemTTL" is APIGetChatItemTTL -> "apiGetChatItemTTL" is APISetNetworkConfig -> "apiSetNetworkConfig" @@ -3459,8 +3549,6 @@ sealed class CC { companion object { fun chatRef(chatType: ChatType, id: Long) = "${chatType.type}${id}" - - fun protoServersStr(servers: List) = json.encodeToString(ProtoServersConfig(servers)) } } @@ -3510,24 +3598,350 @@ enum class ServerProtocol { } @Serializable -data class ProtoServersConfig( - val servers: List +enum class OperatorTag { + @SerialName("simplex") SimpleX, + @SerialName("flux") Flux, + @SerialName("xyz") XYZ, + @SerialName("demo") Demo +} + +data class ServerOperatorInfo( + val description: List, + val website: String, + val logo: ImageResource, + val largeLogo: ImageResource, + val logoDarkMode: ImageResource, + val largeLogoDarkMode: ImageResource +) +val operatorsInfo: Map = mapOf( + OperatorTag.SimpleX to ServerOperatorInfo( + description = listOf( + "SimpleX Chat is the first communication network that has no user profile IDs of any kind, not even random numbers or keys that identify the users.", + "SimpleX Chat Ltd develops the communication software for SimpleX network." + ), + website = "https://simplex.chat", + logo = MR.images.decentralized, + largeLogo = MR.images.logo, + logoDarkMode = MR.images.decentralized_light, + largeLogoDarkMode = MR.images.logo_light + ), + OperatorTag.Flux to ServerOperatorInfo( + description = listOf( + "Flux is the largest decentralized cloud infrastructure, leveraging a global network of user-operated computational nodes.", + "Flux offers a powerful, scalable, and affordable platform designed to support individuals, businesses, and cutting-edge technologies like AI. With high uptime and worldwide distribution, Flux ensures reliable, accessible cloud computing for all." + ), + website = "https://runonflux.com", + logo = MR.images.flux_logo_symbol, + largeLogo = MR.images.flux_logo, + logoDarkMode = MR.images.flux_logo_symbol, + largeLogoDarkMode = MR.images.flux_logo_light + ), + OperatorTag.XYZ to ServerOperatorInfo( + description = listOf("XYZ servers"), + website = "XYZ website", + logo = MR.images.shield, + largeLogo = MR.images.logo, + logoDarkMode = MR.images.shield, + largeLogoDarkMode = MR.images.logo_light + ), + OperatorTag.Demo to ServerOperatorInfo( + description = listOf("Demo operator"), + website = "Demo website", + logo = MR.images.decentralized, + largeLogo = MR.images.logo, + logoDarkMode = MR.images.decentralized_light, + largeLogoDarkMode = MR.images.logo_light + ) ) @Serializable -data class UserProtocolServers( - val serverProtocol: ServerProtocol, - val protoServers: List, - val presetServers: List, +data class UsageConditionsDetail( + val conditionsId: Long, + val conditionsCommit: String, + val notifiedAt: Instant?, + val createdAt: Instant +) { + companion object { + val sampleData = UsageConditionsDetail( + conditionsId = 1, + conditionsCommit = "11a44dc1fd461a93079f897048b46998db55da5c", + notifiedAt = null, + createdAt = Clock.System.now() + ) + } +} + +@Serializable +sealed class UsageConditionsAction { + @Serializable @SerialName("review") data class Review(val operators: List, val deadline: Instant?, val showNotice: Boolean) : UsageConditionsAction() + @Serializable @SerialName("accepted") data class Accepted(val operators: List) : UsageConditionsAction() + + val shouldShowNotice: Boolean + get() = when (this) { + is Review -> showNotice + else -> false + } +} + +@Serializable +data class ServerOperatorConditionsDetail( + val serverOperators: List, + val currentConditions: UsageConditionsDetail, + val conditionsAction: UsageConditionsAction? +) { + companion object { + val empty = ServerOperatorConditionsDetail( + serverOperators = emptyList(), + currentConditions = UsageConditionsDetail(conditionsId = 0, conditionsCommit = "empty", notifiedAt = null, createdAt = Clock.System.now()), + conditionsAction = null + ) + } +} + +@Serializable() +sealed class ConditionsAcceptance { + @Serializable @SerialName("accepted") data class Accepted(val acceptedAt: Instant?) : ConditionsAcceptance() + @Serializable @SerialName("required") data class Required(val deadline: Instant?) : ConditionsAcceptance() + + val conditionsAccepted: Boolean + get() = when (this) { + is Accepted -> true + is Required -> false + } + + val usageAllowed: Boolean + get() = when (this) { + is Accepted -> true + is Required -> this.deadline != null + } +} + +@Serializable +data class ServerOperator( + val operatorId: Long, + val operatorTag: OperatorTag?, + val tradeName: String, + val legalName: String?, + val serverDomains: List, + val conditionsAcceptance: ConditionsAcceptance, + val enabled: Boolean, + val smpRoles: ServerRoles, + val xftpRoles: ServerRoles, +) { + companion object { + val dummyOperatorInfo = ServerOperatorInfo( + description = listOf("Default"), + website = "Default", + logo = MR.images.decentralized, + largeLogo = MR.images.logo, + logoDarkMode = MR.images.decentralized_light, + largeLogoDarkMode = MR.images.logo_light + ) + + val sampleData1 = ServerOperator( + operatorId = 1, + operatorTag = OperatorTag.SimpleX, + tradeName = "SimpleX Chat", + legalName = "SimpleX Chat Ltd", + serverDomains = listOf("simplex.im"), + conditionsAcceptance = ConditionsAcceptance.Accepted(acceptedAt = null), + enabled = true, + smpRoles = ServerRoles(storage = true, proxy = true), + xftpRoles = ServerRoles(storage = true, proxy = true) + ) + + val sampleData2 = ServerOperator( + operatorId = 2, + operatorTag = OperatorTag.XYZ, + tradeName = "XYZ", + legalName = null, + serverDomains = listOf("xyz.com"), + conditionsAcceptance = ConditionsAcceptance.Required(deadline = null), + enabled = false, + smpRoles = ServerRoles(storage = false, proxy = true), + xftpRoles = ServerRoles(storage = false, proxy = true) + ) + + val sampleData3 = ServerOperator( + operatorId = 3, + operatorTag = OperatorTag.Demo, + tradeName = "Demo", + legalName = null, + serverDomains = listOf("demo.com"), + conditionsAcceptance = ConditionsAcceptance.Required(deadline = null), + enabled = false, + smpRoles = ServerRoles(storage = true, proxy = false), + xftpRoles = ServerRoles(storage = true, proxy = false) + ) + } + + val id: Long + get() = operatorId + + override fun equals(other: Any?): Boolean { + if (other !is ServerOperator) return false + return other.operatorId == this.operatorId && + other.operatorTag == this.operatorTag && + other.tradeName == this.tradeName && + other.legalName == this.legalName && + other.serverDomains == this.serverDomains && + other.conditionsAcceptance == this.conditionsAcceptance && + other.enabled == this.enabled && + other.smpRoles == this.smpRoles && + other.xftpRoles == this.xftpRoles + } + + override fun hashCode(): Int { + var result = operatorId.hashCode() + result = 31 * result + (operatorTag?.hashCode() ?: 0) + result = 31 * result + tradeName.hashCode() + result = 31 * result + (legalName?.hashCode() ?: 0) + result = 31 * result + serverDomains.hashCode() + result = 31 * result + conditionsAcceptance.hashCode() + result = 31 * result + enabled.hashCode() + result = 31 * result + smpRoles.hashCode() + result = 31 * result + xftpRoles.hashCode() + return result + } + + val legalName_: String + get() = legalName ?: tradeName + + val info: ServerOperatorInfo get() { + return if (this.operatorTag != null) { + operatorsInfo[this.operatorTag] ?: dummyOperatorInfo + } else { + dummyOperatorInfo + } + } + + val logo: ImageResource + @Composable + get() { + return if (isInDarkTheme()) info.logoDarkMode else info.logo + } + + val largeLogo: ImageResource + @Composable + get() { + return if (isInDarkTheme()) info.largeLogoDarkMode else info.largeLogo + } +} + +@Serializable +data class ServerRoles( + val storage: Boolean, + val proxy: Boolean ) @Serializable -data class ServerCfg( +data class UserOperatorServers( + val operator: ServerOperator?, + val smpServers: List, + val xftpServers: List +) { + val id: String + get() = operator?.operatorId?.toString() ?: "nil operator" + + val operator_: ServerOperator + get() = operator ?: ServerOperator( + operatorId = 0, + operatorTag = null, + tradeName = "", + legalName = null, + serverDomains = emptyList(), + conditionsAcceptance = ConditionsAcceptance.Accepted(null), + enabled = false, + smpRoles = ServerRoles(storage = true, proxy = true), + xftpRoles = ServerRoles(storage = true, proxy = true) + ) + + companion object { + val sampleData1 = UserOperatorServers( + operator = ServerOperator.sampleData1, + smpServers = listOf(UserServer.sampleData.preset), + xftpServers = listOf(UserServer.sampleData.xftpPreset) + ) + + val sampleDataNilOperator = UserOperatorServers( + operator = null, + smpServers = listOf(UserServer.sampleData.preset), + xftpServers = listOf(UserServer.sampleData.xftpPreset) + ) + } +} + +@Serializable +sealed class UserServersError { + @Serializable @SerialName("noServers") data class NoServers(val protocol: ServerProtocol, val user: UserRef?): UserServersError() + @Serializable @SerialName("storageMissing") data class StorageMissing(val protocol: ServerProtocol, val user: UserRef?): UserServersError() + @Serializable @SerialName("proxyMissing") data class ProxyMissing(val protocol: ServerProtocol, val user: UserRef?): UserServersError() + @Serializable @SerialName("duplicateServer") data class DuplicateServer(val protocol: ServerProtocol, val duplicateServer: String, val duplicateHost: String): UserServersError() + + val globalError: String? + get() = when (this.protocol_) { + ServerProtocol.SMP -> globalSMPError + ServerProtocol.XFTP -> globalXFTPError + } + + private val protocol_: ServerProtocol + get() = when (this) { + is NoServers -> this.protocol + is StorageMissing -> this.protocol + is ProxyMissing -> this.protocol + is DuplicateServer -> this.protocol + } + + val globalSMPError: String? + get() = if (this.protocol_ == ServerProtocol.SMP) { + when (this) { + is NoServers -> this.user?.let { "${userStr(it)} ${generalGetString(MR.strings.no_message_servers_configured)}" } + ?: generalGetString(MR.strings.no_message_servers_configured) + + is StorageMissing -> this.user?.let { "${userStr(it)} ${generalGetString(MR.strings.no_message_servers_configured_for_receiving)}" } + ?: generalGetString(MR.strings.no_message_servers_configured_for_receiving) + + is ProxyMissing -> this.user?.let { "${userStr(it)} ${generalGetString(MR.strings.no_message_servers_configured_for_private_routing)}" } + ?: generalGetString(MR.strings.no_message_servers_configured_for_private_routing) + + else -> null + } + } else { + null + } + + val globalXFTPError: String? + get() = if (this.protocol_ == ServerProtocol.XFTP) { + when (this) { + is NoServers -> this.user?.let { "${userStr(it)} ${generalGetString(MR.strings.no_media_servers_configured)}" } + ?: generalGetString(MR.strings.no_media_servers_configured) + + is StorageMissing -> this.user?.let { "${userStr(it)} ${generalGetString(MR.strings.no_media_servers_configured_for_sending)}" } + ?: generalGetString(MR.strings.no_media_servers_configured_for_sending) + + is ProxyMissing -> this.user?.let { "${userStr(it)} ${generalGetString(MR.strings.no_media_servers_configured_for_private_routing)}" } + ?: generalGetString(MR.strings.no_media_servers_configured_for_private_routing) + + else -> null + } + } else { + null + } + + private fun userStr(user: UserRef): String { + return String.format(generalGetString(MR.strings.for_chat_profile), user.localDisplayName) + } +} + +@Serializable +data class UserServer( val remoteHostId: Long?, + val serverId: Long?, val server: String, val preset: Boolean, val tested: Boolean? = null, - val enabled: Boolean + val enabled: Boolean, + val deleted: Boolean ) { @Transient private val createdAt: Date = Date() @@ -3541,35 +3955,51 @@ data class ServerCfg( get() = server.isBlank() companion object { - val empty = ServerCfg(remoteHostId = null, server = "", preset = false, tested = null, enabled = false) + val empty = UserServer(remoteHostId = null, serverId = null, server = "", preset = false, tested = null, enabled = false, deleted = false) class SampleData( - val preset: ServerCfg, - val custom: ServerCfg, - val untested: ServerCfg + val preset: UserServer, + val custom: UserServer, + val untested: UserServer, + val xftpPreset: UserServer ) val sampleData = SampleData( - preset = ServerCfg( + preset = UserServer( remoteHostId = null, + serverId = 1, server = "smp://abcd@smp8.simplex.im", preset = true, tested = true, - enabled = true + enabled = true, + deleted = false ), - custom = ServerCfg( + custom = UserServer( remoteHostId = null, + serverId = 2, server = "smp://abcd@smp9.simplex.im", preset = false, tested = false, - enabled = false + enabled = false, + deleted = false ), - untested = ServerCfg( + untested = UserServer( remoteHostId = null, + serverId = 3, server = "smp://abcd@smp10.simplex.im", preset = false, tested = null, - enabled = true + enabled = true, + deleted = false + ), + xftpPreset = UserServer( + remoteHostId = null, + serverId = 4, + server = "xftp://abcd@xftp8.simplex.im", + preset = true, + tested = true, + enabled = true, + deleted = false ) ) } @@ -4928,8 +5358,11 @@ sealed class CR { @Serializable @SerialName("apiChats") class ApiChats(val user: UserRef, val chats: List): CR() @Serializable @SerialName("apiChat") class ApiChat(val user: UserRef, val chat: Chat, val navInfo: NavigationInfo = NavigationInfo()): CR() @Serializable @SerialName("chatItemInfo") class ApiChatItemInfo(val user: UserRef, val chatItem: AChatItem, val chatItemInfo: ChatItemInfo): CR() - @Serializable @SerialName("userProtoServers") class UserProtoServers(val user: UserRef, val servers: UserProtocolServers): CR() @Serializable @SerialName("serverTestResult") class ServerTestResult(val user: UserRef, val testServer: String, val testFailure: ProtocolTestFailure? = null): CR() + @Serializable @SerialName("serverOperatorConditions") class ServerOperatorConditions(val conditions: ServerOperatorConditionsDetail): CR() + @Serializable @SerialName("userServers") class UserServers(val user: UserRef, val userServers: List): CR() + @Serializable @SerialName("userServersValidation") class UserServersValidation(val user: UserRef, val serverErrors: List): CR() + @Serializable @SerialName("usageConditions") class UsageConditions(val usageConditions: UsageConditionsDetail, val conditionsText: String?, val acceptedConditions: UsageConditionsDetail?): CR() @Serializable @SerialName("chatItemTTL") class ChatItemTTL(val user: UserRef, val chatItemTTL: Long? = null): 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() @@ -5108,8 +5541,11 @@ sealed class CR { is ApiChats -> "apiChats" is ApiChat -> "apiChat" is ApiChatItemInfo -> "chatItemInfo" - is UserProtoServers -> "userProtoServers" is ServerTestResult -> "serverTestResult" + is ServerOperatorConditions -> "serverOperatorConditions" + is UserServers -> "userServers" + is UserServersValidation -> "userServersValidation" + is UsageConditions -> "usageConditions" is ChatItemTTL -> "chatItemTTL" is NetworkConfig -> "networkConfig" is ContactInfo -> "contactInfo" @@ -5278,8 +5714,11 @@ sealed class CR { is ApiChats -> withUser(user, json.encodeToString(chats)) is ApiChat -> withUser(user, "chat: ${json.encodeToString(chat)}\nnavInfo: ${navInfo}") is ApiChatItemInfo -> withUser(user, "chatItem: ${json.encodeToString(chatItem)}\n${json.encodeToString(chatItemInfo)}") - is UserProtoServers -> withUser(user, "servers: ${json.encodeToString(servers)}") is ServerTestResult -> withUser(user, "server: $testServer\nresult: ${json.encodeToString(testFailure)}") + is ServerOperatorConditions -> "conditions: ${json.encodeToString(conditions)}" + is UserServers -> withUser(user, "userServers: ${json.encodeToString(userServers)}") + is UserServersValidation -> withUser(user, "serverErrors: ${json.encodeToString(serverErrors)}") + is UsageConditions -> "usageConditions: ${json.encodeToString(usageConditions)}\nnacceptedConditions: ${json.encodeToString(acceptedConditions)}" is ChatItemTTL -> withUser(user, json.encodeToString(chatItemTTL)) is NetworkConfig -> json.encodeToString(networkConfig) is ContactInfo -> withUser(user, "contact: ${json.encodeToString(contact)}\nconnectionStats: ${json.encodeToString(connectionStats_)}") diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt index 79132b5eb1..08ca72c6bd 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt @@ -118,6 +118,7 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat if (appPreferences.encryptionStartedAt.get() != null) appPreferences.encryptionStartedAt.set(null) val user = chatController.apiGetActiveUser(null) chatModel.currentUser.value = user + chatModel.conditions.value = chatController.getServerOperators(null) ?: ServerOperatorConditionsDetail.empty if (user == null) { chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true) chatModel.currentUser.value = null @@ -137,13 +138,12 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat } } else if (startChat().await()) { val savedOnboardingStage = appPreferences.onboardingStage.get() - val next = if (appPlatform.isAndroid) { - OnboardingStage.Step4_SetNotificationsMode - } else { - OnboardingStage.OnboardingComplete - } val newStage = if (listOf(OnboardingStage.Step1_SimpleXInfo, OnboardingStage.Step2_CreateProfile).contains(savedOnboardingStage) && chatModel.users.size == 1) { - next + if (appPlatform.isAndroid) { + OnboardingStage.Step4_SetNotificationsMode + } else { + OnboardingStage.OnboardingComplete + } } else { savedOnboardingStage } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt index 17658d23e8..15d38c5490 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt @@ -165,7 +165,7 @@ fun createProfileInNoProfileSetup(displayName: String, close: () -> Unit) { if (!chatModel.connectedToRemote()) { chatModel.localUserCreated.value = true } - controller.appPrefs.onboardingStage.set(OnboardingStage.Step4_SetNotificationsMode) + controller.appPrefs.onboardingStage.set(OnboardingStage.Step3_ChooseServerOperators) controller.startChat(user) controller.switchUIRemoteHost(null) close() @@ -204,7 +204,7 @@ fun createProfileOnboarding(chatModel: ChatModel, displayName: String, close: () onboardingStage.set(if (appPlatform.isDesktop && chatModel.controller.appPrefs.initialRandomDBPassphrase.get() && !chatModel.desktopOnboardingRandomPassword.value) { OnboardingStage.Step2_5_SetupDatabasePassphrase } else { - OnboardingStage.Step4_SetNotificationsMode + OnboardingStage.Step3_ChooseServerOperators }) } else { // the next two lines are only needed for failure case when because of the database error the app gets stuck on on-boarding screen, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index 8dd3e42440..e9e590ee95 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -1249,7 +1249,7 @@ fun BoxScope.ChatItemsList( } else { null } - val showAvatar = if (merged is MergedItem.Grouped) shouldShowAvatar(item, listItem.nextItem) else true + val showAvatar = shouldShowAvatar(item, listItem.nextItem) val isRevealed = remember { derivedStateOf { revealedItems.value.contains(item.id) } } val itemSeparation: ItemSeparation val prevItemSeparationLargeGap: Boolean diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt index 9661a305cc..20bb65ec7d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt @@ -25,19 +25,21 @@ import androidx.compose.ui.unit.* import chat.simplex.common.AppLock import chat.simplex.common.model.* import chat.simplex.common.model.ChatController.appPrefs +import chat.simplex.common.model.ChatController.setConditionsNotified import chat.simplex.common.model.ChatController.stopRemoteHostAndReloadHosts import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* -import chat.simplex.common.views.onboarding.WhatsNewView -import chat.simplex.common.views.onboarding.shouldShowWhatsNew import chat.simplex.common.platform.* import chat.simplex.common.views.call.Call import chat.simplex.common.views.chat.item.CIFileViewScope import chat.simplex.common.views.chat.topPaddingToContent import chat.simplex.common.views.mkValidName import chat.simplex.common.views.newchat.* +import chat.simplex.common.views.onboarding.* import chat.simplex.common.views.showInvalidNameAlert import chat.simplex.common.views.usersettings.* +import chat.simplex.common.views.usersettings.networkAndServers.ConditionsLinkButton +import chat.simplex.common.views.usersettings.networkAndServers.UsageConditionsView import chat.simplex.res.MR import kotlinx.coroutines.* import kotlinx.coroutines.flow.MutableStateFlow @@ -115,10 +117,26 @@ fun ToggleChatListCard() { @Composable fun ChatListView(chatModel: ChatModel, userPickerState: MutableStateFlow, setPerformLA: (Boolean) -> Unit, stopped: Boolean) { val oneHandUI = remember { appPrefs.oneHandUI.state } + val rhId = chatModel.remoteHostId() + LaunchedEffect(Unit) { - if (shouldShowWhatsNew(chatModel)) { + val showWhatsNew = shouldShowWhatsNew(chatModel) + val showUpdatedConditions = chatModel.conditions.value.conditionsAction?.shouldShowNotice ?: false + if (showWhatsNew) { delay(1000L) - ModalManager.center.showCustomModal { close -> WhatsNewView(close = close) } + ModalManager.center.showCustomModal { close -> WhatsNewView(close = close, updatedConditions = showUpdatedConditions) } + } else if (showUpdatedConditions) { + ModalManager.center.showModalCloseable(endButtons = { ConditionsLinkButton() }) { close -> + LaunchedEffect(Unit) { + val conditionsId = chatModel.conditions.value.currentConditions.conditionsId + try { + setConditionsNotified(rh = rhId, conditionsId = conditionsId) + } catch (e: Exception) { + Log.d(TAG, "UsageConditionsView setConditionsNotified error: ${e.message}") + } + } + UsageConditionsView(userServers = mutableStateOf(emptyList()), currUserServers = mutableStateOf(emptyList()), close = close, rhId = rhId) + } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ServersSummaryView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ServersSummaryView.kt index 4e3ee2340c..acbc72ff48 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ServersSummaryView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ServersSummaryView.kt @@ -48,7 +48,6 @@ import chat.simplex.common.model.localTimestamp import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* -import chat.simplex.common.views.usersettings.ProtocolServersView import chat.simplex.common.views.usersettings.SettingsPreferenceItem import chat.simplex.res.MR import dev.icerock.moko.resources.compose.painterResource @@ -540,15 +539,8 @@ fun XFTPServerSummaryLayout(summary: XFTPServerSummary, statsStartedAt: Instant, ) ) } - if (summary.known == true) { - SectionItemView(click = { - ModalManager.start.showCustomModal { close -> ProtocolServersView(chatModel, rhId = rh?.remoteHostId, ServerProtocol.XFTP, close) } - }) { - Text(generalGetString(MR.strings.open_server_settings_button)) - } - if (summary.stats != null || summary.sessions != null) { - SectionDividerSpaced() - } + if (summary.stats != null || summary.sessions != null) { + SectionDividerSpaced() } if (summary.stats != null) { @@ -579,12 +571,7 @@ fun SMPServerSummaryLayout(summary: SMPServerSummary, statsStartedAt: Instant, r ) ) } - if (summary.known == true) { - SectionItemView(click = { - ModalManager.start.showCustomModal { close -> ProtocolServersView(chatModel, rhId = rh?.remoteHostId, ServerProtocol.SMP, close) } - }) { - Text(generalGetString(MR.strings.open_server_settings_button)) - } + if (summary.stats != null || summary.subs != null || summary.sessions != null) { SectionDividerSpaced() } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AppBarTitle.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AppBarTitle.kt index 195ec020e5..afb557cc78 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AppBarTitle.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AppBarTitle.kt @@ -17,11 +17,21 @@ import dev.icerock.moko.resources.compose.painterResource import kotlin.math.absoluteValue @Composable -fun AppBarTitle(title: String, hostDevice: Pair? = null, withPadding: Boolean = true, bottomPadding: Dp = DEFAULT_PADDING * 1.5f + 8.dp) { +fun AppBarTitle( + title: String, + hostDevice: Pair? = null, + withPadding: Boolean = true, + bottomPadding: Dp = DEFAULT_PADDING * 1.5f + 8.dp, + enableAlphaChanges: Boolean = true +) { val handler = LocalAppBarHandler.current - val connection = handler?.connection + val connection = if (enableAlphaChanges) handler?.connection else null LaunchedEffect(title) { - handler?.title?.value = title + if (enableAlphaChanges) { + handler?.title?.value = title + } else { + handler?.connection?.scrollTrackingEnabled = false + } } val theme = CurrentColors.collectAsState() val titleColor = MaterialTheme.appColors.title @@ -54,7 +64,8 @@ fun AppBarTitle(title: String, hostDevice: Pair? = null, withPad } private fun bottomTitleAlpha(connection: CollapsingAppBarNestedScrollConnection?) = - if ((connection?.appBarOffset ?: 0f).absoluteValue < AppBarHandler.appBarMaxHeightPx / 3) 1f + if (connection?.scrollTrackingEnabled == false) 1f + else if ((connection?.appBarOffset ?: 0f).absoluteValue < AppBarHandler.appBarMaxHeightPx / 3) 1f else ((AppBarHandler.appBarMaxHeightPx) + (connection?.appBarOffset ?: 0f) / 1.5f).coerceAtLeast(0f) / AppBarHandler.appBarMaxHeightPx @Composable diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CollapsingAppBar.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CollapsingAppBar.kt index 50942169b3..ad6611b9d9 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CollapsingAppBar.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CollapsingAppBar.kt @@ -84,6 +84,7 @@ class AppBarHandler( } class CollapsingAppBarNestedScrollConnection(): NestedScrollConnection { + var scrollTrackingEnabled = true var appBarOffset: Float by mutableFloatStateOf(0f) override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultTopAppBar.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultTopAppBar.kt index cf0c5f7e96..4bf20d2128 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultTopAppBar.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultTopAppBar.kt @@ -258,7 +258,8 @@ private fun AppBarCenterAligned( } private fun topTitleAlpha(text: Boolean, connection: CollapsingAppBarNestedScrollConnection, alpha: Float = appPrefs.inAppBarsAlpha.get()) = - if (connection.appBarOffset.absoluteValue < AppBarHandler.appBarMaxHeightPx / 3) 0f + if (!connection.scrollTrackingEnabled) 0f + else if (connection.appBarOffset.absoluteValue < AppBarHandler.appBarMaxHeightPx / 3) 0f else ((-connection.appBarOffset * 1.5f) / (AppBarHandler.appBarMaxHeightPx)).coerceIn(0f, if (text) 1f else alpha) val AppBarHeight = 56.dp diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateToDevice.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateToDevice.kt index 90f8593c4a..28ec77de70 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateToDevice.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateToDevice.kt @@ -26,6 +26,7 @@ import chat.simplex.common.views.helpers.DatabaseUtils.ksDatabasePassword import chat.simplex.common.views.newchat.QRCodeScanner import chat.simplex.common.views.onboarding.OnboardingStage import chat.simplex.common.views.usersettings.* +import chat.simplex.common.views.usersettings.networkAndServers.OnionRelatedLayout import chat.simplex.res.MR import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt new file mode 100644 index 0000000000..8b383e0146 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt @@ -0,0 +1,354 @@ +package chat.simplex.common.views.onboarding + +import SectionBottomSpacer +import SectionTextFooter +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.* +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import chat.simplex.common.model.ChatController.appPrefs +import chat.simplex.common.model.ServerOperator +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.usersettings.networkAndServers.* +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource + +@Composable +fun ModalData.ChooseServerOperators( + onboarding: Boolean, + close: (() -> Unit) = { ModalManager.fullscreen.closeModals() }, + modalManager: ModalManager = ModalManager.fullscreen +) { + LaunchedEffect(Unit) { + prepareChatBeforeFinishingOnboarding() + } + + CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) { + ModalView({}, showClose = false, endButtons = { + IconButton({ modalManager.showModal { ChooseServerOperatorsInfoView() } }) { + Icon(painterResource(MR.images.ic_info), null, Modifier.size(28.dp), tint = MaterialTheme.colors.primary) + } + }) { + val serverOperators = remember { derivedStateOf { chatModel.conditions.value.serverOperators } } + val selectedOperatorIds = remember { stateGetOrPut("selectedOperatorIds") { serverOperators.value.filter { it.enabled }.map { it.operatorId }.toSet() } } + val selectedOperators = remember { derivedStateOf { serverOperators.value.filter { selectedOperatorIds.value.contains(it.operatorId) } } } + + ColumnWithScrollBar( + Modifier + .themedBackground(bgLayerSize = LocalAppBarHandler.current?.backgroundGraphicsLayerSize, bgLayer = LocalAppBarHandler.current?.backgroundGraphicsLayer), + maxIntrinsicSize = true + ) { + Box(Modifier.align(Alignment.CenterHorizontally)) { + AppBarTitle(stringResource(MR.strings.onboarding_choose_server_operators)) + } + Column(( + if (appPlatform.isDesktop) Modifier.width(600.dp).align(Alignment.CenterHorizontally) else Modifier) + .padding(horizontal = DEFAULT_PADDING) + ) { + Text(stringResource(MR.strings.onboarding_select_network_operators_to_use)) + Spacer(Modifier.height(DEFAULT_PADDING)) + } + Spacer(Modifier.weight(1f)) + Column(( + if (appPlatform.isDesktop) Modifier.width(600.dp).align(Alignment.CenterHorizontally) else Modifier) + .fillMaxWidth() + .padding(horizontal = DEFAULT_PADDING) + ) { + serverOperators.value.forEachIndexed { index, srvOperator -> + OperatorCheckView(srvOperator, selectedOperatorIds) + if (index != serverOperators.value.lastIndex) { + Spacer(Modifier.height(DEFAULT_PADDING)) + } + } + Spacer(Modifier.height(DEFAULT_PADDING_HALF)) + + SectionTextFooter(annotatedStringResource(MR.strings.onboarding_network_operators_configure_via_settings), textAlign = TextAlign.Center) + } + Spacer(Modifier.weight(1f)) + + val reviewForOperators = selectedOperators.value.filter { !it.conditionsAcceptance.conditionsAccepted } + val canReviewLater = reviewForOperators.all { it.conditionsAcceptance.usageAllowed } + val currEnabledOperatorIds = serverOperators.value.filter { it.enabled }.map { it.operatorId }.toSet() + + Column(Modifier.widthIn(max = if (appPlatform.isAndroid) 450.dp else 1000.dp).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) { + val enabled = selectedOperatorIds.value.isNotEmpty() + when { + reviewForOperators.isNotEmpty() -> ReviewConditionsButton(enabled, onboarding, selectedOperators, selectedOperatorIds, modalManager) + selectedOperatorIds.value != currEnabledOperatorIds && enabled -> SetOperatorsButton(true, onboarding, serverOperators, selectedOperatorIds, close) + else -> ContinueButton(enabled, onboarding, close) + } + if (onboarding && reviewForOperators.isEmpty()) { + TextButtonBelowOnboardingButton(stringResource(MR.strings.operator_conditions_of_use)) { + modalManager.showModalCloseable(endButtons = { ConditionsLinkButton() }) { close -> + UsageConditionsView( + currUserServers = remember { mutableStateOf(emptyList()) }, + userServers = remember { mutableStateOf(emptyList()) }, + close = close, + rhId = null + ) + } + } + } else if (onboarding || reviewForOperators.isEmpty()) { + // Reserve space + TextButtonBelowOnboardingButton("", null) + } + if (!onboarding && reviewForOperators.isNotEmpty()) { + ReviewLaterButton(canReviewLater, close) + SectionTextFooter( + annotatedStringResource(MR.strings.onboarding_network_operators_conditions_will_be_accepted) + + AnnotatedString(" ") + + annotatedStringResource(MR.strings.onboarding_network_operators_conditions_you_can_configure), + textAlign = TextAlign.Center + ) + SectionBottomSpacer() + } + } + } + } + } +} + +@Composable +private fun OperatorCheckView(serverOperator: ServerOperator, selectedOperatorIds: MutableState>) { + val checked = selectedOperatorIds.value.contains(serverOperator.operatorId) + TextButton({ + if (checked) { + selectedOperatorIds.value -= serverOperator.operatorId + } else { + selectedOperatorIds.value += serverOperator.operatorId + } + }, + border = BorderStroke(1.dp, color = if (checked) MaterialTheme.colors.primary else MaterialTheme.colors.secondary.copy(alpha = 0.5f)), + shape = RoundedCornerShape(18.dp) + ) { + Row(Modifier.padding(DEFAULT_PADDING_HALF), verticalAlignment = Alignment.CenterVertically) { + Image(painterResource(serverOperator.largeLogo), null, Modifier.height(48.dp)) + Spacer(Modifier.width(DEFAULT_PADDING_HALF).weight(1f)) + CircleCheckbox(checked) + } + } +} + +@Composable +private fun CircleCheckbox(checked: Boolean) { + if (checked) { + Box(contentAlignment = Alignment.Center) { + Icon( + painterResource(MR.images.ic_circle_filled), + null, + Modifier.size(26.dp), + tint = MaterialTheme.colors.primary + ) + Icon( + painterResource(MR.images.ic_check_filled), + null, + Modifier.size(20.dp), tint = MaterialTheme.colors.background + ) + } + } else { + Icon( + painterResource(MR.images.ic_circle), + null, + Modifier.size(26.dp), + tint = MaterialTheme.colors.secondary.copy(alpha = 0.5f) + ) + } +} + +@Composable +private fun ReviewConditionsButton( + enabled: Boolean, + onboarding: Boolean, + selectedOperators: State>, + selectedOperatorIds: State>, + modalManager: ModalManager +) { + OnboardingActionButton( + modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_PADDING * 2).fillMaxWidth() else Modifier, + labelId = MR.strings.operator_review_conditions, + onboarding = null, + enabled = enabled, + onclick = { + modalManager.showModalCloseable(endButtons = { ConditionsLinkButton() }) { close -> + ReviewConditionsView(onboarding, selectedOperators, selectedOperatorIds, close) + } + } + ) +} + +@Composable +private fun SetOperatorsButton(enabled: Boolean, onboarding: Boolean, serverOperators: State>, selectedOperatorIds: State>, close: () -> Unit) { + OnboardingActionButton( + modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_PADDING * 2).fillMaxWidth() else Modifier, + labelId = MR.strings.onboarding_network_operators_update, + onboarding = null, + enabled = enabled, + onclick = { + withBGApi { + val enabledOperators = enabledOperators(serverOperators.value, selectedOperatorIds.value) + if (enabledOperators != null) { + val r = chatController.setServerOperators(rh = chatModel.remoteHostId(), operators = enabledOperators) + if (r != null) { + chatModel.conditions.value = r + } + continueToNextStep(onboarding, close) + } + } + } + ) +} + +@Composable +private fun ContinueButton(enabled: Boolean, onboarding: Boolean, close: () -> Unit) { + OnboardingActionButton( + modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_PADDING * 2).fillMaxWidth() else Modifier, + labelId = MR.strings.onboarding_network_operators_continue, + onboarding = null, + enabled = enabled, + onclick = { + continueToNextStep(onboarding, close) + } + ) +} + +@Composable +private fun ReviewLaterButton(enabled: Boolean, close: () -> Unit) { + TextButtonBelowOnboardingButton( + stringResource(MR.strings.onboarding_network_operators_review_later), + onClick = if (!enabled) null else {{ continueToNextStep(false, close) }} + ) +} + +@Composable +private fun ReviewConditionsView( + onboarding: Boolean, + selectedOperators: State>, + selectedOperatorIds: State>, + close: () -> Unit +) { + // remembering both since we don't want to reload the view after the user accepts conditions + val operatorsWithConditionsAccepted = remember { chatModel.conditions.value.serverOperators.filter { it.conditionsAcceptance.conditionsAccepted } } + val acceptForOperators = remember { selectedOperators.value.filter { !it.conditionsAcceptance.conditionsAccepted } } + ColumnWithScrollBar(modifier = Modifier.fillMaxSize().padding(horizontal = DEFAULT_PADDING)) { + AppBarTitle(stringResource(MR.strings.operator_conditions_of_use), withPadding = false, enableAlphaChanges = false) + if (operatorsWithConditionsAccepted.isNotEmpty()) { + ReadableText(MR.strings.operator_conditions_accepted_for_some, args = operatorsWithConditionsAccepted.joinToString(", ") { it.legalName_ }) + ReadableText(MR.strings.operator_same_conditions_will_apply_to_operators, args = acceptForOperators.joinToString(", ") { it.legalName_ }) + } else { + ReadableText(MR.strings.operator_conditions_will_be_accepted_for_some, args = acceptForOperators.joinToString(", ") { it.legalName_ }) + } + Column(modifier = Modifier.weight(1f).padding(top = DEFAULT_PADDING_HALF)) { + ConditionsTextView(chatModel.remoteHostId()) + } + Column(Modifier.padding(top = DEFAULT_PADDING).widthIn(max = if (appPlatform.isAndroid) 450.dp else 1000.dp).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) { + AcceptConditionsButton(onboarding, selectedOperators, selectedOperatorIds, close) + // Reserve space + TextButtonBelowOnboardingButton("", null) + } + } +} + +@Composable +private fun AcceptConditionsButton( + onboarding: Boolean, + selectedOperators: State>, + selectedOperatorIds: State>, + close: () -> Unit +) { + fun continueOnAccept() { + if (appPlatform.isDesktop || !onboarding) { + if (onboarding) { close() } + continueToNextStep(onboarding, close) + } else { + continueToSetNotificationsAfterAccept() + } + } + OnboardingActionButton( + modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_PADDING * 2).fillMaxWidth() else Modifier, + labelId = MR.strings.accept_conditions, + onboarding = null, + onclick = { + withBGApi { + val conditionsId = chatModel.conditions.value.currentConditions.conditionsId + val acceptForOperators = selectedOperators.value.filter { !it.conditionsAcceptance.conditionsAccepted } + val operatorIds = acceptForOperators.map { it.operatorId } + val r = chatController.acceptConditions(chatModel.remoteHostId(), conditionsId = conditionsId, operatorIds = operatorIds) + if (r != null) { + chatModel.conditions.value = r + val enabledOperators = enabledOperators(r.serverOperators, selectedOperatorIds.value) + if (enabledOperators != null) { + val r2 = chatController.setServerOperators(rh = chatModel.remoteHostId(), operators = enabledOperators) + if (r2 != null) { + chatModel.conditions.value = r2 + continueOnAccept() + } + } else { + continueOnAccept() + } + } + } + } + ) +} + +private fun continueToNextStep(onboarding: Boolean, close: () -> Unit) { + if (onboarding) { + appPrefs.onboardingStage.set(if (appPlatform.isAndroid) OnboardingStage.Step4_SetNotificationsMode else OnboardingStage.OnboardingComplete) + } else { + close() + } +} + +private fun continueToSetNotificationsAfterAccept() { + appPrefs.onboardingStage.set(OnboardingStage.Step4_SetNotificationsMode) + ModalManager.fullscreen.showModalCloseable(showClose = false) { SetNotificationsMode(chatModel) } +} + +private fun enabledOperators(operators: List, selectedOperatorIds: Set): List? { + val ops = ArrayList(operators) + if (ops.isNotEmpty()) { + for (i in ops.indices) { + val op = ops[i] + ops[i] = op.copy(enabled = selectedOperatorIds.contains(op.operatorId)) + } + val haveSMPStorage = ops.any { it.enabled && it.smpRoles.storage } + val haveSMPProxy = ops.any { it.enabled && it.smpRoles.proxy } + val haveXFTPStorage = ops.any { it.enabled && it.xftpRoles.storage } + val haveXFTPProxy = ops.any { it.enabled && it.xftpRoles.proxy } + val firstEnabledIndex = ops.indexOfFirst { it.enabled } + if (haveSMPStorage && haveSMPProxy && haveXFTPStorage && haveXFTPProxy) { + return ops + } else if (firstEnabledIndex != -1) { + var op = ops[firstEnabledIndex] + if (!haveSMPStorage) op = op.copy(smpRoles = op.smpRoles.copy(storage = true)) + if (!haveSMPProxy) op = op.copy(smpRoles = op.smpRoles.copy(proxy = true)) + if (!haveXFTPStorage) op = op.copy(xftpRoles = op.xftpRoles.copy(storage = true)) + if (!haveXFTPProxy) op = op.copy(xftpRoles = op.xftpRoles.copy(proxy = true)) + ops[firstEnabledIndex] = op + return ops + } else { // Shouldn't happen - view doesn't let to proceed if no operators are enabled + return null + } + } else { + return null + } +} + +@Composable +private fun ChooseServerOperatorsInfoView() { + ColumnWithScrollBar(Modifier.padding(horizontal = DEFAULT_PADDING)) { + AppBarTitle(stringResource(MR.strings.onboarding_network_operators), withPadding = false) + ReadableText(stringResource(MR.strings.onboarding_network_operators_app_will_use_different_operators)) + ReadableText(stringResource(MR.strings.onboarding_network_operators_app_will_use_for_routing)) + SectionBottomSpacer() + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/HowItWorks.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/HowItWorks.kt index 98e8ec971d..34b6209ffe 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/HowItWorks.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/HowItWorks.kt @@ -48,8 +48,8 @@ fun HowItWorks(user: User?, onboardingStage: SharedPreference? } @Composable -fun ReadableText(stringResId: StringResource, textAlign: TextAlign = TextAlign.Start, padding: PaddingValues = PaddingValues(bottom = 12.dp), style: TextStyle = LocalTextStyle.current) { - Text(annotatedStringResource(stringResId), modifier = Modifier.padding(padding), textAlign = textAlign, lineHeight = 22.sp, style = style) +fun ReadableText(stringResId: StringResource, textAlign: TextAlign = TextAlign.Start, padding: PaddingValues = PaddingValues(bottom = 12.dp), style: TextStyle = LocalTextStyle.current, args: Any? = null) { + Text(annotatedStringResource(stringResId, args), modifier = Modifier.padding(padding), textAlign = textAlign, lineHeight = 22.sp, style = style) } @Composable diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/OnboardingView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/OnboardingView.kt index d4c63248e5..510df13c3d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/OnboardingView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/OnboardingView.kt @@ -5,6 +5,7 @@ enum class OnboardingStage { Step2_CreateProfile, LinkAMobile, Step2_5_SetupDatabasePassphrase, + Step3_ChooseServerOperators, Step3_CreateSimpleXAddress, Step4_SetNotificationsMode, OnboardingComplete diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetNotificationsMode.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetNotificationsMode.kt index d6d5753b6c..49c91813dc 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetNotificationsMode.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetNotificationsMode.kt @@ -16,8 +16,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.model.ChatModel import chat.simplex.common.model.NotificationsMode -import chat.simplex.common.platform.ColumnWithScrollBar -import chat.simplex.common.platform.appPlatform +import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.common.views.usersettings.changeNotificationsMode @@ -26,7 +25,7 @@ import chat.simplex.res.MR @Composable fun SetNotificationsMode(m: ChatModel) { LaunchedEffect(Unit) { - prepareChatBeforeNotificationsSetup(m) + prepareChatBeforeFinishingOnboarding() } CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) { @@ -57,6 +56,7 @@ fun SetNotificationsMode(m: ChatModel) { onboarding = OnboardingStage.OnboardingComplete, onclick = { changeNotificationsMode(currentMode.value, m) + ModalManager.fullscreen.closeModals() } ) // Reserve space @@ -99,7 +99,7 @@ fun SelectableCard(currentValue: State, newValue: T, title: String, descr Spacer(Modifier.height(14.dp)) } -private fun prepareChatBeforeNotificationsSetup(chatModel: ChatModel) { +fun prepareChatBeforeFinishingOnboarding() { // No visible users but may have hidden. In this case chat should be started anyway because it's stopped on this stage with hidden users if (chatModel.users.any { u -> !u.user.hidden }) return withBGApi { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetupDatabasePassphrase.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetupDatabasePassphrase.kt index 4ad2675e83..f20cb38dad 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetupDatabasePassphrase.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetupDatabasePassphrase.kt @@ -17,7 +17,6 @@ import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import chat.simplex.common.model.* -import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.database.* @@ -36,7 +35,7 @@ fun SetupDatabasePassphrase(m: ChatModel) { val confirmNewKey = rememberSaveable { mutableStateOf("") } fun nextStep() { if (appPlatform.isAndroid || chatModel.currentUser.value != null) { - m.controller.appPrefs.onboardingStage.set(OnboardingStage.Step4_SetNotificationsMode) + m.controller.appPrefs.onboardingStage.set(OnboardingStage.Step3_ChooseServerOperators) } else { m.controller.appPrefs.onboardingStage.set(OnboardingStage.LinkAMobile) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt index e43404cb07..b133ae27d4 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt @@ -164,14 +164,15 @@ fun OnboardingActionButton( @Composable fun TextButtonBelowOnboardingButton(text: String, onClick: (() -> Unit)?) { val state = getKeyboardState() + val enabled = onClick != null val topPadding by animateDpAsState(if (appPlatform.isAndroid && state.value == KeyboardState.Opened) 0.dp else DEFAULT_PADDING) val bottomPadding by animateDpAsState(if (appPlatform.isAndroid && state.value == KeyboardState.Opened) 0.dp else DEFAULT_PADDING * 2) if ((appPlatform.isAndroid && state.value == KeyboardState.Closed) || topPadding > 0.dp) { - TextButton({ onClick?.invoke() }, Modifier.padding(top = topPadding, bottom = bottomPadding).clip(CircleShape), enabled = onClick != null) { + TextButton({ onClick?.invoke() }, Modifier.padding(top = topPadding, bottom = bottomPadding).clip(CircleShape), enabled = enabled) { Text( text, Modifier.padding(start = DEFAULT_PADDING_HALF, end = DEFAULT_PADDING_HALF, bottom = 5.dp), - color = MaterialTheme.colors.primary, + color = if (enabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, fontWeight = FontWeight.Medium, textAlign = TextAlign.Center ) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt index bdbef3b654..6cf945bcba 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt @@ -8,7 +8,6 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.platform.LocalUriHandler import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource @@ -17,17 +16,32 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import chat.simplex.common.model.ChatModel +import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.setConditionsNotified +import chat.simplex.common.model.ServerOperator.Companion.dummyOperatorInfo import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.usersettings.networkAndServers.UsageConditionsView import chat.simplex.res.MR import dev.icerock.moko.resources.ImageResource import dev.icerock.moko.resources.StringResource @Composable -fun WhatsNewView(viaSettings: Boolean = false, close: () -> Unit) { +fun ModalData.WhatsNewView(updatedConditions: Boolean = false, viaSettings: Boolean = false, close: () -> Unit) { val currentVersion = remember { mutableStateOf(versionDescriptions.lastIndex) } + val rhId = chatModel.remoteHostId() + + if (updatedConditions) { + LaunchedEffect(Unit) { + val conditionsId = chatModel.conditions.value.currentConditions.conditionsId + try { + setConditionsNotified(rh = rhId, conditionsId = conditionsId) + } catch (e: Exception) { + Log.d(TAG, "WhatsNewView setConditionsNotified error: ${e.message}") + } + } + } @Composable fun featureDescription(icon: ImageResource?, titleId: StringResource, descrId: StringResource?, link: String?, subfeatures: List>) { @@ -124,9 +138,18 @@ fun WhatsNewView(viaSettings: Boolean = false, close: () -> Unit) { ) { AppBarTitle(String.format(generalGetString(MR.strings.new_in_version), v.version), withPadding = false, bottomPadding = DEFAULT_PADDING) + val modalManager = if (viaSettings) ModalManager.start else ModalManager.center + v.features.forEach { feature -> - if (feature.show) { - featureDescription(feature.icon, feature.titleId, feature.descrId, feature.link, feature.subfeatures) + when (feature) { + is VersionFeature.FeatureDescription -> { + if (feature.show) { + featureDescription(feature.icon, feature.titleId, feature.descrId, feature.link, feature.subfeatures) + } + } + is VersionFeature.FeatureView -> { + feature.view(modalManager) + } } } @@ -134,6 +157,18 @@ fun WhatsNewView(viaSettings: Boolean = false, close: () -> Unit) { ReadMoreButton(v.post) } + if (updatedConditions) { + Text( + stringResource(MR.strings.view_updated_conditions), + color = MaterialTheme.colors.primary, + modifier = Modifier.clickable { + modalManager.showModalCloseable { + close -> UsageConditionsView(userServers = mutableStateOf(emptyList()), currUserServers = mutableStateOf(emptyList()), close = close, rhId = rhId) + } + } + ) + } + if (!viaSettings) { Spacer(Modifier.fillMaxHeight().weight(1f)) Box( @@ -141,7 +176,9 @@ fun WhatsNewView(viaSettings: Boolean = false, close: () -> Unit) { ) { Text( generalGetString(MR.strings.ok), - modifier = Modifier.clickable(onClick = close), + modifier = Modifier.clickable(onClick = { + close() + }), style = MaterialTheme.typography.h3, color = MaterialTheme.colors.primary ) @@ -166,18 +203,26 @@ fun ReadMoreButton(url: String) { } } -private data class FeatureDescription( - val icon: ImageResource?, - val titleId: StringResource, - val descrId: StringResource?, - var subfeatures: List> = listOf(), - val link: String? = null, - val show: Boolean = true -) +private sealed class VersionFeature { + class FeatureDescription( + val icon: ImageResource?, + val titleId: StringResource, + val descrId: StringResource?, + var subfeatures: List> = listOf(), + val link: String? = null, + val show: Boolean = true + ): VersionFeature() + + class FeatureView( + val icon: ImageResource?, + val titleId: StringResource, + val view: @Composable (modalManager: ModalManager) -> Unit + ): VersionFeature() +} private data class VersionDescription( val version: String, - val features: List, + val features: List, val post: String? = null, ) @@ -186,18 +231,18 @@ private val versionDescriptions: List = listOf( version = "v4.2", post = "https://simplex.chat/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html", features = listOf( - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_verified_user, titleId = MR.strings.v4_2_security_assessment, descrId = MR.strings.v4_2_security_assessment_desc, link = "https://simplex.chat/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html" ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_group, titleId = MR.strings.v4_2_group_links, descrId = MR.strings.v4_2_group_links_desc ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_check, titleId = MR.strings.v4_2_auto_accept_contact_requests, descrId = MR.strings.v4_2_auto_accept_contact_requests_desc @@ -208,22 +253,22 @@ private val versionDescriptions: List = listOf( version = "v4.3", post = "https://simplex.chat/blog/20221206-simplex-chat-v4.3-voice-messages.html", features = listOf( - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_mic, titleId = MR.strings.v4_3_voice_messages, descrId = MR.strings.v4_3_voice_messages_desc ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_delete_forever, titleId = MR.strings.v4_3_irreversible_message_deletion, descrId = MR.strings.v4_3_irreversible_message_deletion_desc ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_wifi_tethering, titleId = MR.strings.v4_3_improved_server_configuration, descrId = MR.strings.v4_3_improved_server_configuration_desc ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_visibility_off, titleId = MR.strings.v4_3_improved_privacy_and_security, descrId = MR.strings.v4_3_improved_privacy_and_security_desc @@ -234,22 +279,22 @@ private val versionDescriptions: List = listOf( version = "v4.4", post = "https://simplex.chat/blog/20230103-simplex-chat-v4.4-disappearing-messages.html", features = listOf( - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_timer, titleId = MR.strings.v4_4_disappearing_messages, descrId = MR.strings.v4_4_disappearing_messages_desc ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_pending, titleId = MR.strings.v4_4_live_messages, descrId = MR.strings.v4_4_live_messages_desc ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_verified_user, titleId = MR.strings.v4_4_verify_connection_security, descrId = MR.strings.v4_4_verify_connection_security_desc ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_translate, titleId = MR.strings.v4_4_french_interface, descrId = MR.strings.v4_4_french_interface_descr @@ -260,33 +305,33 @@ private val versionDescriptions: List = listOf( version = "v4.5", post = "https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html", features = listOf( - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_manage_accounts, titleId = MR.strings.v4_5_multiple_chat_profiles, descrId = MR.strings.v4_5_multiple_chat_profiles_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_edit_note, titleId = MR.strings.v4_5_message_draft, descrId = MR.strings.v4_5_message_draft_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_safety_divider, titleId = MR.strings.v4_5_transport_isolation, descrId = MR.strings.v4_5_transport_isolation_descr, link = "https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation" ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_task, titleId = MR.strings.v4_5_private_filenames, descrId = MR.strings.v4_5_private_filenames_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_battery_2_bar, titleId = MR.strings.v4_5_reduced_battery_usage, descrId = MR.strings.v4_5_reduced_battery_usage_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_translate, titleId = MR.strings.v4_5_italian_interface, descrId = MR.strings.v4_5_italian_interface_descr, @@ -297,32 +342,32 @@ private val versionDescriptions: List = listOf( version = "v4.6", post = "https://simplex.chat/blog/20230328-simplex-chat-v4-6-hidden-profiles.html", features = listOf( - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_lock, titleId = MR.strings.v4_6_hidden_chat_profiles, descrId = MR.strings.v4_6_hidden_chat_profiles_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_flag, titleId = MR.strings.v4_6_group_moderation, descrId = MR.strings.v4_6_group_moderation_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_maps_ugc, titleId = MR.strings.v4_6_group_welcome_message, descrId = MR.strings.v4_6_group_welcome_message_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_call, titleId = MR.strings.v4_6_audio_video_calls, descrId = MR.strings.v4_6_audio_video_calls_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_battery_3_bar, titleId = MR.strings.v4_6_reduced_battery_usage, descrId = MR.strings.v4_6_reduced_battery_usage_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_translate, titleId = MR.strings.v4_6_chinese_spanish_interface, descrId = MR.strings.v4_6_chinese_spanish_interface_descr, @@ -333,17 +378,17 @@ private val versionDescriptions: List = listOf( version = "v5.0", post = "https://simplex.chat/blog/20230422-simplex-chat-vision-funding-v5-videos-files-passcode.html", features = listOf( - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_upload_file, titleId = MR.strings.v5_0_large_files_support, descrId = MR.strings.v5_0_large_files_support_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_lock, titleId = MR.strings.v5_0_app_passcode, descrId = MR.strings.v5_0_app_passcode_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_translate, titleId = MR.strings.v5_0_polish_interface, descrId = MR.strings.v5_0_polish_interface_descr, @@ -354,27 +399,27 @@ private val versionDescriptions: List = listOf( version = "v5.1", post = "https://simplex.chat/blog/20230523-simplex-chat-v5-1-message-reactions-self-destruct-passcode.html", features = listOf( - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_add_reaction, titleId = MR.strings.v5_1_message_reactions, descrId = MR.strings.v5_1_message_reactions_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_chat, titleId = MR.strings.v5_1_better_messages, descrId = MR.strings.v5_1_better_messages_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_light_mode, titleId = MR.strings.v5_1_custom_themes, descrId = MR.strings.v5_1_custom_themes_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_lock, titleId = MR.strings.v5_1_self_destruct_passcode, descrId = MR.strings.v5_1_self_destruct_passcode_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_translate, titleId = MR.strings.v5_1_japanese_portuguese_interface, descrId = MR.strings.whats_new_thanks_to_users_contribute_weblate, @@ -385,27 +430,27 @@ private val versionDescriptions: List = listOf( version = "v5.2", post = "https://simplex.chat/blog/20230722-simplex-chat-v5-2-message-delivery-receipts.html", features = listOf( - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_check, titleId = MR.strings.v5_2_message_delivery_receipts, descrId = MR.strings.v5_2_message_delivery_receipts_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_star, titleId = MR.strings.v5_2_favourites_filter, descrId = MR.strings.v5_2_favourites_filter_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_sync_problem, titleId = MR.strings.v5_2_fix_encryption, descrId = MR.strings.v5_2_fix_encryption_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_timer, titleId = MR.strings.v5_2_disappear_one_message, descrId = MR.strings.v5_2_disappear_one_message_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_redeem, titleId = MR.strings.v5_2_more_things, descrId = MR.strings.v5_2_more_things_descr @@ -416,29 +461,29 @@ private val versionDescriptions: List = listOf( version = "v5.3", post = "https://simplex.chat/blog/20230925-simplex-chat-v5-3-desktop-app-local-file-encryption-directory-service.html", features = listOf( - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_desktop, titleId = MR.strings.v5_3_new_desktop_app, descrId = MR.strings.v5_3_new_desktop_app_descr, link = "https://simplex.chat/downloads/" ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_lock, titleId = MR.strings.v5_3_encrypt_local_files, descrId = MR.strings.v5_3_encrypt_local_files_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_search, titleId = MR.strings.v5_3_discover_join_groups, descrId = MR.strings.v5_3_discover_join_groups_descr, link = "simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion" ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_theater_comedy, titleId = MR.strings.v5_3_simpler_incognito_mode, descrId = MR.strings.v5_3_simpler_incognito_mode_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_translate, titleId = MR.strings.v5_3_new_interface_languages, descrId = MR.strings.v5_3_new_interface_languages_descr, @@ -449,27 +494,27 @@ private val versionDescriptions: List = listOf( version = "v5.4", post = "https://simplex.chat/blog/20231125-simplex-chat-v5-4-link-mobile-desktop-quantum-resistant-better-groups.html", features = listOf( - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_desktop, titleId = MR.strings.v5_4_link_mobile_desktop, descrId = MR.strings.v5_4_link_mobile_desktop_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_group, titleId = MR.strings.v5_4_better_groups, descrId = MR.strings.v5_4_better_groups_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_theater_comedy, titleId = MR.strings.v5_4_incognito_groups, descrId = MR.strings.v5_4_incognito_groups_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_back_hand, titleId = MR.strings.v5_4_block_group_members, descrId = MR.strings.v5_4_block_group_members_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_redeem, titleId = MR.strings.v5_2_more_things, descrId = MR.strings.v5_4_more_things_descr @@ -480,28 +525,28 @@ private val versionDescriptions: List = listOf( version = "v5.5", post = "https://simplex.chat/blog/20240124-simplex-chat-infrastructure-costs-v5-5-simplex-ux-private-notes-group-history.html", features = listOf( - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_folder_pen, titleId = MR.strings.v5_5_private_notes, descrId = MR.strings.v5_5_private_notes_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_link, titleId = MR.strings.v5_5_simpler_connect_ui, descrId = MR.strings.v5_5_simpler_connect_ui_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_forum, titleId = MR.strings.v5_5_join_group_conversation, descrId = MR.strings.v5_5_join_group_conversation_descr, link = "simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion" ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_battery_3_bar, titleId = MR.strings.v5_5_message_delivery, descrId = MR.strings.v5_5_message_delivery_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_translate, titleId = MR.strings.v5_5_new_interface_languages, descrId = MR.strings.whats_new_thanks_to_users_contribute_weblate, @@ -512,22 +557,22 @@ private val versionDescriptions: List = listOf( version = "v5.6", post = "https://simplex.chat/blog/20240323-simplex-network-privacy-non-profit-v5-6-quantum-resistant-e2e-encryption-simple-migration.html", features = listOf( - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_vpn_key_filled, titleId = MR.strings.v5_6_quantum_resistant_encryption, descrId = MR.strings.v5_6_quantum_resistant_encryption_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_ios_share, titleId = MR.strings.v5_6_app_data_migration, descrId = MR.strings.v5_6_app_data_migration_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_call, titleId = MR.strings.v5_6_picture_in_picture_calls, descrId = MR.strings.v5_6_picture_in_picture_calls_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_back_hand, titleId = MR.strings.v5_6_safer_groups, descrId = MR.strings.v5_6_safer_groups_descr @@ -538,32 +583,32 @@ private val versionDescriptions: List = listOf( version = "v5.7", post = "https://simplex.chat/blog/20240426-simplex-legally-binding-transparency-v5-7-better-user-experience.html", features = listOf( - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_vpn_key_filled, titleId = MR.strings.v5_6_quantum_resistant_encryption, descrId = MR.strings.v5_7_quantum_resistant_encryption_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_forward, titleId = MR.strings.v5_7_forward, descrId = MR.strings.v5_7_forward_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_music_note, titleId = MR.strings.v5_7_call_sounds, descrId = MR.strings.v5_7_call_sounds_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_account_box, titleId = MR.strings.v5_7_shape_profile_images, descrId = MR.strings.v5_7_shape_profile_images_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_wifi_tethering, titleId = MR.strings.v5_7_network, descrId = MR.strings.v5_7_network_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_translate, titleId = MR.strings.v5_7_new_interface_languages, descrId = MR.strings.whats_new_thanks_to_users_contribute_weblate, @@ -574,27 +619,27 @@ private val versionDescriptions: List = listOf( version = "v5.8", post = "https://simplex.chat/blog/20240604-simplex-chat-v5.8-private-message-routing-chat-themes.html", features = listOf( - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_settings_ethernet, titleId = MR.strings.v5_8_private_routing, descrId = MR.strings.v5_8_private_routing_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_palette, titleId = MR.strings.v5_8_chat_themes, descrId = MR.strings.v5_8_chat_themes_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_security, titleId = MR.strings.v5_8_safe_files, descrId = MR.strings.v5_8_safe_files_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_battery_3_bar, titleId = MR.strings.v5_8_message_delivery, descrId = MR.strings.v5_8_message_delivery_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_translate, titleId = MR.strings.v5_8_persian_ui, descrId = MR.strings.whats_new_thanks_to_users_contribute_weblate @@ -605,7 +650,7 @@ private val versionDescriptions: List = listOf( version = "v6.0", post = "https://simplex.chat/blog/20240814-simplex-chat-vision-funding-v6-private-routing-new-user-experience.html", features = listOf( - FeatureDescription( + VersionFeature.FeatureDescription( icon = null, titleId = MR.strings.v6_0_new_chat_experience, descrId = null, @@ -616,7 +661,7 @@ private val versionDescriptions: List = listOf( MR.images.ic_match_case to MR.strings.v6_0_increase_font_size ) ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = null, titleId = MR.strings.v6_0_new_media_options, descrId = null, @@ -625,23 +670,23 @@ private val versionDescriptions: List = listOf( MR.images.ic_blur_on to MR.strings.v6_0_privacy_blur, ) ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_toast, titleId = MR.strings.v6_0_reachable_chat_toolbar, descrId = MR.strings.v6_0_reachable_chat_toolbar_descr, show = appPlatform.isAndroid ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_settings_ethernet, titleId = MR.strings.v5_8_private_routing, descrId = MR.strings.v6_0_private_routing_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_wifi_tethering, titleId = MR.strings.v6_0_connection_servers_status, descrId = MR.strings.v6_0_connection_servers_status_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_upgrade, titleId = MR.strings.v6_0_upgrade_app, descrId = MR.strings.v6_0_upgrade_app_descr, @@ -653,18 +698,18 @@ private val versionDescriptions: List = listOf( version = "v6.1", post = "https://simplex.chat/blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.html", features = listOf( - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_verified_user, titleId = MR.strings.v6_1_better_security, descrId = MR.strings.v6_1_better_security_descr, link = "https://simplex.chat/blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.html" ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = MR.images.ic_videocam, titleId = MR.strings.v6_1_better_calls, descrId = MR.strings.v6_1_better_calls_descr ), - FeatureDescription( + VersionFeature.FeatureDescription( icon = null, titleId = MR.strings.v6_1_better_user_experience, descrId = null, @@ -678,6 +723,39 @@ private val versionDescriptions: List = listOf( ), ), ), + VersionDescription( + version = "v6.2 (beta.1)", + post = "https://simplex.chat/blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.html", + features = listOf( + VersionFeature.FeatureView( + icon = null, + titleId = MR.strings.v6_2_network_decentralization, + view = { modalManager -> + Column { + val src = (operatorsInfo[OperatorTag.Flux] ?: dummyOperatorInfo).largeLogo + Image(painterResource(src), null, modifier = Modifier.height(48.dp)) + Text(stringResource(MR.strings.v6_2_network_decentralization_descr), modifier = Modifier.padding(top = 8.dp)) + Row { + Text( + stringResource(MR.strings.v6_2_network_decentralization_enable_flux), + color = MaterialTheme.colors.primary, + modifier = Modifier.clickable { + modalManager.showModalCloseable { close -> ChooseServerOperators(onboarding = false, close, modalManager) } + } + ) + Text(" ") + Text(stringResource(MR.strings.v6_2_network_decentralization_enable_flux_reason)) + } + } + } + ), + VersionFeature.FeatureDescription( + icon = MR.images.ic_chat, + titleId = MR.strings.v6_2_improved_chat_navigation, + descrId = MR.strings.v6_2_improved_chat_navigation_descr + ), + ), + ) ) private val lastVersion = versionDescriptions.last().version @@ -700,7 +778,8 @@ fun shouldShowWhatsNew(m: ChatModel): Boolean { @Composable fun PreviewWhatsNewView() { SimpleXTheme { - WhatsNewView( + val data = remember { ModalData() } + data.WhatsNewView( viaSettings = true, close = {} ) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt index e727b94781..1d01ab11ff 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt @@ -35,6 +35,7 @@ import chat.simplex.common.views.chatlist.* import chat.simplex.common.views.helpers.* import chat.simplex.common.views.newchat.QRCode import chat.simplex.common.views.usersettings.* +import chat.simplex.common.views.usersettings.networkAndServers.validPort import chat.simplex.res.MR import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ProtocolServersView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ProtocolServersView.kt deleted file mode 100644 index f5e3cda2c7..0000000000 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ProtocolServersView.kt +++ /dev/null @@ -1,383 +0,0 @@ -package chat.simplex.common.views.usersettings - -import SectionBottomSpacer -import SectionDividerSpaced -import SectionItemView -import SectionTextFooter -import SectionView -import androidx.compose.foundation.* -import androidx.compose.foundation.layout.* -import androidx.compose.material.* -import androidx.compose.runtime.* -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalUriHandler -import dev.icerock.moko.resources.compose.painterResource -import dev.icerock.moko.resources.compose.stringResource -import androidx.compose.ui.text.* -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import chat.simplex.common.model.ServerAddress.Companion.parseServerAddress -import chat.simplex.common.views.helpers.* -import chat.simplex.common.model.* -import chat.simplex.common.platform.ColumnWithScrollBar -import chat.simplex.common.platform.appPlatform -import chat.simplex.res.MR - -@Composable -fun ModalData.ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: ServerProtocol, close: () -> Unit) { - var presetServers by remember(rhId) { mutableStateOf(emptyList()) } - var servers by remember { stateGetOrPut("servers") { emptyList() } } - var serversAlreadyLoaded by remember { stateGetOrPut("serversAlreadyLoaded") { false } } - val currServers = remember(rhId) { mutableStateOf(servers) } - val testing = rememberSaveable(rhId) { mutableStateOf(false) } - val serversUnchanged = remember(servers) { derivedStateOf { servers == currServers.value || testing.value } } - val allServersDisabled = remember { derivedStateOf { servers.none { it.enabled } } } - val saveDisabled = remember(servers) { - derivedStateOf { - servers.isEmpty() || - servers == currServers.value || - testing.value || - servers.none { srv -> - val address = parseServerAddress(srv.server) - address != null && uniqueAddress(srv, address, servers) - } || - allServersDisabled.value - } - } - - KeyChangeEffect(rhId) { - servers = emptyList() - serversAlreadyLoaded = false - } - - LaunchedEffect(rhId) { - withApi { - val res = m.controller.getUserProtoServers(rhId, serverProtocol) - if (res != null) { - currServers.value = res.protoServers - presetServers = res.presetServers - if (servers.isEmpty() && !serversAlreadyLoaded) { - servers = currServers.value - serversAlreadyLoaded = true - } - } - } - } - val testServersJob = CancellableOnGoneJob() - fun showServer(server: ServerCfg) { - ModalManager.start.showModalCloseable(true) { close -> - var old by remember { mutableStateOf(server) } - val index = servers.indexOf(old) - ProtocolServerView( - m, - old, - serverProtocol, - onUpdate = { updated -> - val newServers = ArrayList(servers) - newServers.removeAt(index) - newServers.add(index, updated) - old = updated - servers = newServers - }, - onDelete = { - val newServers = ArrayList(servers) - newServers.removeAt(index) - servers = newServers - close() - }) - } - } - ModalView( - close = { - if (saveDisabled.value) close() - else showUnsavedChangesAlert({ saveServers(rhId, serverProtocol, currServers, servers, m, close) }, close) - }, - ) { - ProtocolServersLayout( - serverProtocol, - testing = testing.value, - servers = servers, - serversUnchanged = serversUnchanged.value, - saveDisabled = saveDisabled.value, - allServersDisabled = allServersDisabled.value, - m.currentUser.value, - addServer = { - AlertManager.shared.showAlertDialogButtonsColumn( - title = generalGetString(MR.strings.smp_servers_add), - buttons = { - Column { - SectionItemView({ - AlertManager.shared.hideAlert() - servers = servers + ServerCfg.empty - // No saving until something will be changed on the next screen to prevent blank servers on the list - showServer(servers.last()) - }) { - Text(stringResource(MR.strings.smp_servers_enter_manually), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) - } - if (appPlatform.isAndroid) { - SectionItemView({ - AlertManager.shared.hideAlert() - ModalManager.start.showModalCloseable { close -> - ScanProtocolServer(rhId) { - close() - servers = servers + it - } - } - } - ) { - Text(stringResource(MR.strings.smp_servers_scan_qr), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) - } - } - val hasAllPresets = hasAllPresets(presetServers, servers, m) - if (!hasAllPresets) { - SectionItemView({ - AlertManager.shared.hideAlert() - servers = (servers + addAllPresets(rhId, presetServers, servers, m)).sortedByDescending { it.preset } - }) { - Text(stringResource(MR.strings.smp_servers_preset_add), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) - } - } - } - } - ) - }, - testServers = { - testServersJob.value = withLongRunningApi { - testServers(testing, servers, m) { - servers = it - } - } - }, - resetServers = { - servers = currServers.value - }, - saveSMPServers = { - saveServers(rhId, serverProtocol, currServers, servers, m) - }, - showServer = ::showServer, - ) - - if (testing.value) { - Box( - Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator( - Modifier - .padding(horizontal = 2.dp) - .size(30.dp), - color = MaterialTheme.colors.secondary, - strokeWidth = 2.5.dp - ) - } - } - } -} - -@Composable -private fun ProtocolServersLayout( - serverProtocol: ServerProtocol, - testing: Boolean, - servers: List, - serversUnchanged: Boolean, - saveDisabled: Boolean, - allServersDisabled: Boolean, - currentUser: User?, - addServer: () -> Unit, - testServers: () -> Unit, - resetServers: () -> Unit, - saveSMPServers: () -> Unit, - showServer: (ServerCfg) -> Unit, -) { - ColumnWithScrollBar { - AppBarTitle(stringResource(if (serverProtocol == ServerProtocol.SMP) MR.strings.your_SMP_servers else MR.strings.your_XFTP_servers)) - - val configuredServers = servers.filter { it.preset || it.enabled } - val otherServers = servers.filter { !(it.preset || it.enabled) } - - if (configuredServers.isNotEmpty()) { - SectionView(stringResource(if (serverProtocol == ServerProtocol.SMP) MR.strings.smp_servers_configured else MR.strings.xftp_servers_configured).uppercase()) { - for (srv in configuredServers) { - SectionItemView({ showServer(srv) }, disabled = testing) { - ProtocolServerView(serverProtocol, srv, servers, testing) - } - } - } - SectionTextFooter( - remember(currentUser?.displayName) { - buildAnnotatedString { - append(generalGetString(MR.strings.smp_servers_per_user) + " ") - withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { - append(currentUser?.displayName ?: "") - } - append(".") - } - } - ) - SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false) - } - - if (otherServers.isNotEmpty()) { - SectionView(stringResource(if (serverProtocol == ServerProtocol.SMP) MR.strings.smp_servers_other else MR.strings.xftp_servers_other).uppercase()) { - for (srv in otherServers.filter { !(it.preset || it.enabled) }) { - SectionItemView({ showServer(srv) }, disabled = testing) { - ProtocolServerView(serverProtocol, srv, servers, testing) - } - } - } - } - - SectionView { - SettingsActionItem( - painterResource(MR.images.ic_add), - stringResource(MR.strings.smp_servers_add), - addServer, - disabled = testing, - textColor = if (testing) MaterialTheme.colors.secondary else MaterialTheme.colors.primary, - iconColor = if (testing) MaterialTheme.colors.secondary else MaterialTheme.colors.primary - ) - SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = false) - } - - SectionView { - SectionItemView(resetServers, disabled = serversUnchanged) { - Text(stringResource(MR.strings.reset_verb), color = if (!serversUnchanged) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary) - } - val testServersDisabled = testing || allServersDisabled - SectionItemView(testServers, disabled = testServersDisabled) { - Text(stringResource(MR.strings.smp_servers_test_servers), color = if (!testServersDisabled) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary) - } - SectionItemView(saveSMPServers, disabled = saveDisabled) { - Text(stringResource(MR.strings.smp_servers_save), color = if (!saveDisabled) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary) - } - } - SectionDividerSpaced(maxBottomPadding = false) - SectionView { - HowToButton() - } - SectionBottomSpacer() - } -} - -@Composable -private fun ProtocolServerView(serverProtocol: ServerProtocol, srv: ServerCfg, servers: List, disabled: Boolean) { - val address = parseServerAddress(srv.server) - when { - address == null || !address.valid || address.serverProtocol != serverProtocol || !uniqueAddress(srv, address, servers) -> InvalidServer() - !srv.enabled -> Icon(painterResource(MR.images.ic_do_not_disturb_on), null, tint = MaterialTheme.colors.secondary) - else -> ShowTestStatus(srv) - } - Spacer(Modifier.padding(horizontal = 4.dp)) - val text = address?.hostnames?.firstOrNull() ?: srv.server - if (srv.enabled) { - Text(text, color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.onBackground, maxLines = 1) - } else { - Text(text, maxLines = 1, color = MaterialTheme.colors.secondary) - } -} - -@Composable -private fun HowToButton() { - val uriHandler = LocalUriHandler.current - SettingsActionItem( - painterResource(MR.images.ic_open_in_new), - stringResource(MR.strings.how_to_use_your_servers), - { uriHandler.openUriCatching("https://simplex.chat/docs/server.html") }, - textColor = MaterialTheme.colors.primary, - iconColor = MaterialTheme.colors.primary - ) -} - -@Composable -fun InvalidServer() { - Icon(painterResource(MR.images.ic_error), null, tint = MaterialTheme.colors.error) -} - -private fun uniqueAddress(s: ServerCfg, address: ServerAddress, servers: List): Boolean = servers.all { srv -> - address.hostnames.all { host -> - srv.id == s.id || !srv.server.contains(host) - } -} - -private fun hasAllPresets(presetServers: List, servers: List, m: ChatModel): Boolean = - presetServers.all { hasPreset(it, servers) } ?: true - -private fun addAllPresets(rhId: Long?, presetServers: List, servers: List, m: ChatModel): List { - val toAdd = ArrayList() - for (srv in presetServers) { - if (!hasPreset(srv, servers)) { - toAdd.add(srv) - } - } - return toAdd -} - -private fun hasPreset(srv: ServerCfg, servers: List): Boolean = - servers.any { it.server == srv.server } - -private suspend fun testServers(testing: MutableState, servers: List, m: ChatModel, onUpdated: (List) -> Unit) { - val resetStatus = resetTestStatus(servers) - onUpdated(resetStatus) - testing.value = true - val fs = runServersTest(resetStatus, m) { onUpdated(it) } - testing.value = false - if (fs.isNotEmpty()) { - val msg = fs.map { it.key + ": " + it.value.localizedDescription }.joinToString("\n") - AlertManager.shared.showAlertMsg( - title = generalGetString(MR.strings.smp_servers_test_failed), - text = generalGetString(MR.strings.smp_servers_test_some_failed) + "\n" + msg - ) - } -} - -private fun resetTestStatus(servers: List): List { - val copy = ArrayList(servers) - for ((index, server) in servers.withIndex()) { - if (server.enabled) { - copy.removeAt(index) - copy.add(index, server.copy(tested = null)) - } - } - return copy -} - -private suspend fun runServersTest(servers: List, m: ChatModel, onUpdated: (List) -> Unit): Map { - val fs: MutableMap = mutableMapOf() - val updatedServers = ArrayList(servers) - for ((index, server) in servers.withIndex()) { - if (server.enabled) { - interruptIfCancelled() - val (updatedServer, f) = testServerConnection(server, m) - updatedServers.removeAt(index) - updatedServers.add(index, updatedServer) - // toList() is important. Otherwise, Compose will not redraw the screen after first update - onUpdated(updatedServers.toList()) - if (f != null) { - fs[serverHostname(updatedServer.server)] = f - } - } - } - return fs -} - -private fun saveServers(rhId: Long?, protocol: ServerProtocol, currServers: MutableState>, servers: List, m: ChatModel, afterSave: () -> Unit = {}) { - withBGApi { - if (m.controller.setUserProtoServers(rhId, protocol, servers)) { - currServers.value = servers - } - afterSave() - } -} - -private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) { - AlertManager.shared.showAlertDialogStacked( - title = generalGetString(MR.strings.smp_save_servers_question), - confirmText = generalGetString(MR.strings.save_verb), - dismissText = generalGetString(MR.strings.exit_without_saving), - onConfirm = save, - onDismiss = revert, - ) -} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt index 78c5e3b212..f3d22e0cdf 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt @@ -25,14 +25,13 @@ import chat.simplex.common.model.* import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* -import chat.simplex.common.views.CreateProfile import chat.simplex.common.views.database.DatabaseView import chat.simplex.common.views.helpers.* import chat.simplex.common.views.migration.MigrateFromDeviceView import chat.simplex.common.views.onboarding.SimpleXInfo import chat.simplex.common.views.onboarding.WhatsNewView +import chat.simplex.common.views.usersettings.networkAndServers.NetworkAndServersView import chat.simplex.res.MR -import kotlinx.coroutines.* @Composable fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, close: () -> Unit) { @@ -102,7 +101,7 @@ fun SettingsLayout( SectionView(stringResource(MR.strings.settings_section_title_settings)) { SettingsActionItem(painterResource(if (notificationsMode.value == NotificationsMode.OFF) MR.images.ic_bolt_off else MR.images.ic_bolt), stringResource(MR.strings.notifications), showSettingsModal { NotificationsSettingsView(it) }, disabled = stopped) - SettingsActionItem(painterResource(MR.images.ic_wifi_tethering), stringResource(MR.strings.network_and_servers), showSettingsModal { NetworkAndServersView() }, disabled = stopped) + SettingsActionItem(painterResource(MR.images.ic_wifi_tethering), stringResource(MR.strings.network_and_servers), showCustomModal { _, close -> NetworkAndServersView(close) }, disabled = stopped) SettingsActionItem(painterResource(MR.images.ic_videocam), stringResource(MR.strings.settings_audio_video_calls), showSettingsModal { CallSettingsView(it, showModal) }, disabled = stopped) SettingsActionItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.privacy_and_security), showSettingsModal { PrivacySettingsView(it, showSettingsModal, setPerformLA) }, disabled = stopped) SettingsActionItem(painterResource(MR.images.ic_light_mode), stringResource(MR.strings.appearance_settings), showSettingsModal { AppearanceView(it) }) @@ -118,7 +117,7 @@ fun SettingsLayout( SectionView(stringResource(MR.strings.settings_section_title_help)) { SettingsActionItem(painterResource(MR.images.ic_help), stringResource(MR.strings.how_to_use_simplex_chat), showModal { HelpView(userDisplayName ?: "") }, disabled = stopped) - SettingsActionItem(painterResource(MR.images.ic_add), stringResource(MR.strings.whats_new), showCustomModal { _, close -> WhatsNewView(viaSettings = true, close) }, disabled = stopped) + SettingsActionItem(painterResource(MR.images.ic_add), stringResource(MR.strings.whats_new), showCustomModal { _, close -> WhatsNewView(viaSettings = true, close = close) }, disabled = stopped) SettingsActionItem(painterResource(MR.images.ic_info), stringResource(MR.strings.about_simplex_chat), showModal { SimpleXInfo(it, onboarding = false) }) if (!chatModel.desktopNoUserNoRemote) { SettingsActionItem(painterResource(MR.images.ic_tag), stringResource(MR.strings.chat_with_the_founder), { uriHandler.openVerifiedSimplexUri(simplexTeamUri) }, textColor = MaterialTheme.colors.primary, disabled = stopped) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/AdvancedNetworkSettings.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/AdvancedNetworkSettings.kt similarity index 99% rename from apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/AdvancedNetworkSettings.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/AdvancedNetworkSettings.kt index 5757b5d1f4..838cac0172 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/AdvancedNetworkSettings.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/AdvancedNetworkSettings.kt @@ -1,4 +1,4 @@ -package chat.simplex.common.views.usersettings +package chat.simplex.common.views.usersettings.networkAndServers import SectionBottomSpacer import SectionDividerSpaced @@ -26,6 +26,7 @@ import chat.simplex.common.views.helpers.* import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.platform.ColumnWithScrollBar import chat.simplex.common.platform.chatModel +import chat.simplex.common.views.usersettings.SettingsPreferenceItem import chat.simplex.res.MR import java.text.DecimalFormat diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NetworkAndServers.kt similarity index 52% rename from apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NetworkAndServers.kt index 2c4870b121..ef5b82a5d9 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NetworkAndServers.kt @@ -1,6 +1,7 @@ -package chat.simplex.common.views.usersettings +package chat.simplex.common.views.usersettings.networkAndServers import SectionBottomSpacer +import SectionCustomFooter import SectionDividerSpaced import SectionItemView import SectionItemWithValue @@ -20,119 +21,245 @@ import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.* import androidx.compose.ui.text.input.* import androidx.compose.desktop.ui.tooling.preview.Preview -import androidx.compose.ui.graphics.Color +import androidx.compose.foundation.Image +import androidx.compose.ui.graphics.* import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.* import chat.simplex.common.model.* import chat.simplex.common.model.ChatController.appPrefs +import chat.simplex.common.model.ChatController.getServerOperators +import chat.simplex.common.model.ChatController.getUserServers +import chat.simplex.common.model.ChatController.setUserServers import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.onboarding.OnboardingActionButton +import chat.simplex.common.views.onboarding.ReadableText +import chat.simplex.common.views.usersettings.* import chat.simplex.res.MR +import kotlinx.coroutines.launch @Composable -fun NetworkAndServersView() { +fun ModalData.NetworkAndServersView(close: () -> Unit) { val currentRemoteHost by remember { chatModel.currentRemoteHost } // It's not a state, just a one-time value. Shouldn't be used in any state-related situations val netCfg = remember { chatModel.controller.getNetCfg() } val networkUseSocksProxy: MutableState = remember { mutableStateOf(netCfg.useSocksProxy) } + val currUserServers = remember { stateGetOrPut("currUserServers") { emptyList() } } + val userServers = remember { stateGetOrPut("userServers") { emptyList() } } + val serverErrors = remember { stateGetOrPut("serverErrors") { emptyList() } } + val scope = rememberCoroutineScope() val proxyPort = remember { derivedStateOf { appPrefs.networkProxy.state.value.port } } - NetworkAndServersLayout( - currentRemoteHost = currentRemoteHost, - networkUseSocksProxy = networkUseSocksProxy, - onionHosts = remember { mutableStateOf(netCfg.onionHosts) }, - toggleSocksProxy = { enable -> - val def = NetCfg.defaults - val proxyDef = NetCfg.proxyDefaults - if (enable) { - AlertManager.shared.showAlertDialog( - title = generalGetString(MR.strings.network_enable_socks), - text = generalGetString(MR.strings.network_enable_socks_info).format(proxyPort.value), - confirmText = generalGetString(MR.strings.confirm_verb), - onConfirm = { - withBGApi { - var conf = controller.getNetCfg().withProxy(controller.appPrefs.networkProxy.get()) - if (conf.tcpConnectTimeout == def.tcpConnectTimeout) { - conf = conf.copy(tcpConnectTimeout = proxyDef.tcpConnectTimeout) - } - if (conf.tcpTimeout == def.tcpTimeout) { - conf = conf.copy(tcpTimeout = proxyDef.tcpTimeout) - } - if (conf.tcpTimeoutPerKb == def.tcpTimeoutPerKb) { - conf = conf.copy(tcpTimeoutPerKb = proxyDef.tcpTimeoutPerKb) - } - if (conf.rcvConcurrency == def.rcvConcurrency) { - conf = conf.copy(rcvConcurrency = proxyDef.rcvConcurrency) - } - chatModel.controller.apiSetNetworkConfig(conf) - chatModel.controller.setNetCfg(conf) - networkUseSocksProxy.value = true - } - } - ) + ModalView( + close = { + if (!serversCanBeSaved(currUserServers.value, userServers.value, serverErrors.value)) { + close() } else { - AlertManager.shared.showAlertDialog( - title = generalGetString(MR.strings.network_disable_socks), - text = generalGetString(MR.strings.network_disable_socks_info), - confirmText = generalGetString(MR.strings.confirm_verb), - onConfirm = { - withBGApi { - var conf = controller.getNetCfg().copy(socksProxy = null) - if (conf.tcpConnectTimeout == proxyDef.tcpConnectTimeout) { - conf = conf.copy(tcpConnectTimeout = def.tcpConnectTimeout) - } - if (conf.tcpTimeout == proxyDef.tcpTimeout) { - conf = conf.copy(tcpTimeout = def.tcpTimeout) - } - if (conf.tcpTimeoutPerKb == proxyDef.tcpTimeoutPerKb) { - conf = conf.copy(tcpTimeoutPerKb = def.tcpTimeoutPerKb) - } - if (conf.rcvConcurrency == proxyDef.rcvConcurrency) { - conf = conf.copy(rcvConcurrency = def.rcvConcurrency) - } - chatModel.controller.apiSetNetworkConfig(conf) - chatModel.controller.setNetCfg(conf) - networkUseSocksProxy.value = false - } - } + showUnsavedChangesAlert( + { scope.launch { saveServers(currentRemoteHost?.remoteHostId, currUserServers, userServers) }}, + close ) } } - ) + ) { + NetworkAndServersLayout( + currentRemoteHost = currentRemoteHost, + networkUseSocksProxy = networkUseSocksProxy, + onionHosts = remember { mutableStateOf(netCfg.onionHosts) }, + currUserServers = currUserServers, + userServers = userServers, + serverErrors = serverErrors, + toggleSocksProxy = { enable -> + val def = NetCfg.defaults + val proxyDef = NetCfg.proxyDefaults + if (enable) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.network_enable_socks), + text = generalGetString(MR.strings.network_enable_socks_info).format(proxyPort.value), + confirmText = generalGetString(MR.strings.confirm_verb), + onConfirm = { + withBGApi { + var conf = controller.getNetCfg().withProxy(controller.appPrefs.networkProxy.get()) + if (conf.tcpConnectTimeout == def.tcpConnectTimeout) { + conf = conf.copy(tcpConnectTimeout = proxyDef.tcpConnectTimeout) + } + if (conf.tcpTimeout == def.tcpTimeout) { + conf = conf.copy(tcpTimeout = proxyDef.tcpTimeout) + } + if (conf.tcpTimeoutPerKb == def.tcpTimeoutPerKb) { + conf = conf.copy(tcpTimeoutPerKb = proxyDef.tcpTimeoutPerKb) + } + if (conf.rcvConcurrency == def.rcvConcurrency) { + conf = conf.copy(rcvConcurrency = proxyDef.rcvConcurrency) + } + chatModel.controller.apiSetNetworkConfig(conf) + chatModel.controller.setNetCfg(conf) + networkUseSocksProxy.value = true + } + } + ) + } else { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.network_disable_socks), + text = generalGetString(MR.strings.network_disable_socks_info), + confirmText = generalGetString(MR.strings.confirm_verb), + onConfirm = { + withBGApi { + var conf = controller.getNetCfg().copy(socksProxy = null) + if (conf.tcpConnectTimeout == proxyDef.tcpConnectTimeout) { + conf = conf.copy(tcpConnectTimeout = def.tcpConnectTimeout) + } + if (conf.tcpTimeout == proxyDef.tcpTimeout) { + conf = conf.copy(tcpTimeout = def.tcpTimeout) + } + if (conf.tcpTimeoutPerKb == proxyDef.tcpTimeoutPerKb) { + conf = conf.copy(tcpTimeoutPerKb = def.tcpTimeoutPerKb) + } + if (conf.rcvConcurrency == proxyDef.rcvConcurrency) { + conf = conf.copy(rcvConcurrency = def.rcvConcurrency) + } + chatModel.controller.apiSetNetworkConfig(conf) + chatModel.controller.setNetCfg(conf) + networkUseSocksProxy.value = false + } + } + ) + } + } + ) + } } @Composable fun NetworkAndServersLayout( currentRemoteHost: RemoteHostInfo?, networkUseSocksProxy: MutableState, onionHosts: MutableState, + currUserServers: MutableState>, + serverErrors: MutableState>, + userServers: MutableState>, toggleSocksProxy: (Boolean) -> Unit, ) { val m = chatModel + val conditionsAction = remember { m.conditions.value.conditionsAction } + val anyOperatorEnabled = remember { derivedStateOf { userServers.value.any { it.operator?.enabled == true } } } + val scope = rememberCoroutineScope() + + LaunchedEffect(Unit) { + if (currUserServers.value.isNotEmpty() || userServers.value.isNotEmpty()) { + return@LaunchedEffect + } + try { + val servers = getUserServers(rh = currentRemoteHost?.remoteHostId) + if (servers != null) { + currUserServers.value = servers + userServers.value = servers + } + } catch (e: Exception) { + Log.e(TAG, e.stackTraceToString()) + } + } + + @Composable + fun ConditionsButton(conditionsAction: UsageConditionsAction, rhId: Long?) { + SectionItemView( + click = { ModalManager.start.showModalCloseable(endButtons = { ConditionsLinkButton() }) { close -> UsageConditionsView(currUserServers, userServers, close, rhId) } }, + ) { + Text( + stringResource(if (conditionsAction is UsageConditionsAction.Review) MR.strings.operator_review_conditions else MR.strings.operator_conditions_accepted), + color = MaterialTheme.colors.primary + ) + } + } + ColumnWithScrollBar { - val showModal = { it: @Composable ModalData.() -> Unit -> ModalManager.start.showModal(content = it) } - val showCustomModal = { it: @Composable (close: () -> Unit) -> Unit -> ModalManager.start.showCustomModal { close -> it(close) }} + val showModal = { it: @Composable ModalData.() -> Unit -> ModalManager.start.showModal(content = it) } + val showCustomModal = { it: @Composable (close: () -> Unit) -> Unit -> ModalManager.start.showCustomModal { close -> it(close) } } AppBarTitle(stringResource(MR.strings.network_and_servers)) + // TODO: Review this and socks. if (!chatModel.desktopNoUserNoRemote) { - SectionView(generalGetString(MR.strings.settings_section_title_messages)) { - SettingsActionItem(painterResource(MR.images.ic_dns), stringResource(MR.strings.message_servers), { ModalManager.start.showCustomModal { close -> ProtocolServersView(m, m.remoteHostId, ServerProtocol.SMP, close) } }) + SectionView(generalGetString(MR.strings.network_preset_servers_title).uppercase()) { + userServers.value.forEachIndexed { index, srv -> + srv.operator?.let { ServerOperatorRow(index, it, currUserServers, userServers, serverErrors, currentRemoteHost?.remoteHostId) } + } + } + if (conditionsAction != null && anyOperatorEnabled.value) { + ConditionsButton(conditionsAction, rhId = currentRemoteHost?.remoteHostId) + } + val footerText = if (conditionsAction is UsageConditionsAction.Review && conditionsAction.deadline != null && anyOperatorEnabled.value) { + String.format(generalGetString(MR.strings.operator_conditions_will_be_accepted_on), localDate(conditionsAction.deadline)) + } else null - SettingsActionItem(painterResource(MR.images.ic_dns), stringResource(MR.strings.media_and_file_servers), { ModalManager.start.showCustomModal { close -> ProtocolServersView(m, m.remoteHostId, ServerProtocol.XFTP, close) } }) + if (footerText != null) { + SectionTextFooter(footerText) + } + SectionDividerSpaced() + } - if (currentRemoteHost == null) { - UseSocksProxySwitch(networkUseSocksProxy, toggleSocksProxy) - SettingsActionItem(painterResource(MR.images.ic_settings_ethernet), stringResource(MR.strings.network_socks_proxy_settings), { showCustomModal { SocksProxySettings(networkUseSocksProxy.value, appPrefs.networkProxy, onionHosts, sessionMode = appPrefs.networkSessionMode.get(), false, it) }}) - SettingsActionItem(painterResource(MR.images.ic_cable), stringResource(MR.strings.network_settings), { ModalManager.start.showCustomModal { AdvancedNetworkSettingsView(showModal, it) } }) - if (networkUseSocksProxy.value) { - SectionTextFooter(annotatedStringResource(MR.strings.socks_proxy_setting_limitations)) - SectionDividerSpaced(maxTopPadding = true) - } else { - SectionDividerSpaced() + SectionView(generalGetString(MR.strings.settings_section_title_messages)) { + val nullOperatorIndex = userServers.value.indexOfFirst { it.operator == null } + + if (nullOperatorIndex != -1) { + SectionItemView({ + ModalManager.start.showModal { + YourServersView( + userServers = userServers, + serverErrors = serverErrors, + operatorIndex = nullOperatorIndex, + rhId = currentRemoteHost?.remoteHostId + ) + } + }) { + Icon( + painterResource(MR.images.ic_dns), + stringResource(MR.strings.your_servers), + tint = MaterialTheme.colors.secondary + ) + TextIconSpaced() + Text(stringResource(MR.strings.your_servers), color = MaterialTheme.colors.onBackground) + + if (currUserServers.value.getOrNull(nullOperatorIndex) != userServers.value.getOrNull(nullOperatorIndex)) { + Spacer(Modifier.weight(1f)) + UnsavedChangesIndicator() } } } + + if (currentRemoteHost == null) { + UseSocksProxySwitch(networkUseSocksProxy, toggleSocksProxy) + SettingsActionItem(painterResource(MR.images.ic_settings_ethernet), stringResource(MR.strings.network_socks_proxy_settings), { showCustomModal { SocksProxySettings(networkUseSocksProxy.value, appPrefs.networkProxy, onionHosts, sessionMode = appPrefs.networkSessionMode.get(), false, it) } }) + SettingsActionItem(painterResource(MR.images.ic_cable), stringResource(MR.strings.network_settings), { ModalManager.start.showCustomModal { AdvancedNetworkSettingsView(showModal, it) } }) + if (networkUseSocksProxy.value) { + SectionTextFooter(annotatedStringResource(MR.strings.socks_proxy_setting_limitations)) + SectionDividerSpaced(maxTopPadding = true) + } else { + SectionDividerSpaced(maxBottomPadding = false) + } + } } + val saveDisabled = !serversCanBeSaved(currUserServers.value, userServers.value, serverErrors.value) + + SectionItemView( + { scope.launch { saveServers(rhId = currentRemoteHost?.remoteHostId, currUserServers, userServers) } }, + disabled = saveDisabled, + ) { + Text(stringResource(MR.strings.smp_servers_save), color = if (!saveDisabled) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary) + } + val serversErr = globalServersError(serverErrors.value) + if (serversErr != null) { + SectionCustomFooter { + ServersErrorFooter(serversErr) + } + } else if (serverErrors.value.isNotEmpty()) { + SectionCustomFooter { + ServersErrorFooter(generalGetString(MR.strings.errors_in_servers_configuration)) + } + } + + SectionDividerSpaced() SectionView(generalGetString(MR.strings.settings_section_title_calls)) { SettingsActionItem(painterResource(MR.images.ic_electrical_services), stringResource(MR.strings.webrtc_ice_servers), { ModalManager.start.showModal { RTCServersView(m) } }) @@ -504,6 +631,165 @@ fun showWrongProxyConfigAlert() { ) } +@Composable() +private fun ServerOperatorRow( + index: Int, + operator: ServerOperator, + currUserServers: MutableState>, + userServers: MutableState>, + serverErrors: MutableState>, + rhId: Long? +) { + SectionItemView( + { + ModalManager.start.showModalCloseable { close -> + OperatorView( + currUserServers, + userServers, + serverErrors, + index, + rhId + ) + } + } + ) { + Image( + painterResource(operator.logo), + operator.tradeName, + modifier = Modifier.size(24.dp), + colorFilter = if (operator.enabled) null else ColorFilter.colorMatrix(ColorMatrix().apply { + setToSaturation(0f) + }) + ) + TextIconSpaced() + Text(operator.tradeName, color = if (operator.enabled) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary) + + if (currUserServers.value.getOrNull(index) != userServers.value.getOrNull(index)) { + Spacer(Modifier.weight(1f)) + UnsavedChangesIndicator() + } + } +} + +@Composable +private fun UnsavedChangesIndicator() { + Icon( + painterResource(MR.images.ic_edit_filled), + stringResource(MR.strings.icon_descr_edited), + tint = MaterialTheme.colors.secondary, + modifier = Modifier.size(16.dp) + ) +} + +@Composable +fun UsageConditionsView( + currUserServers: MutableState>, + userServers: MutableState>, + close: () -> Unit, + rhId: Long? +) { + suspend fun acceptForOperators(rhId: Long?, operatorIds: List, close: () -> Unit) { + try { + val conditionsId = chatModel.conditions.value.currentConditions.conditionsId + val r = chatController.acceptConditions(rhId, conditionsId, operatorIds) ?: return + chatModel.conditions.value = r + updateOperatorsConditionsAcceptance(currUserServers, r.serverOperators) + updateOperatorsConditionsAcceptance(userServers, r.serverOperators) + close() + } catch (ex: Exception) { + Log.e(TAG, ex.stackTraceToString()) + } + } + + @Composable + fun AcceptConditionsButton(operatorIds: List, close: () -> Unit, bottomPadding: Dp = DEFAULT_PADDING * 2) { + val scope = rememberCoroutineScope() + Column(Modifier.fillMaxWidth().padding(bottom = bottomPadding), horizontalAlignment = Alignment.CenterHorizontally) { + OnboardingActionButton( + labelId = MR.strings.accept_conditions, + onboarding = null, + enabled = operatorIds.isNotEmpty(), + onclick = { + scope.launch { + acceptForOperators(rhId, operatorIds, close) + } + } + ) + } + } + + ColumnWithScrollBar(modifier = Modifier.fillMaxSize().padding(horizontal = DEFAULT_PADDING)) { + AppBarTitle(stringResource(MR.strings.operator_conditions_of_use), enableAlphaChanges = false, withPadding = false) + when (val conditionsAction = chatModel.conditions.value.conditionsAction) { + is UsageConditionsAction.Review -> { + if (conditionsAction.operators.isNotEmpty()) { + ReadableText(MR.strings.operators_conditions_will_be_accepted_for, args = conditionsAction.operators.joinToString(", ") { it.legalName_ }) + } + Column(modifier = Modifier.weight(1f).padding(bottom = DEFAULT_PADDING, top = DEFAULT_PADDING_HALF)) { + ConditionsTextView(rhId) + } + AcceptConditionsButton(conditionsAction.operators.map { it.operatorId }, close, if (conditionsAction.deadline != null) DEFAULT_PADDING_HALF else DEFAULT_PADDING * 2) + if (conditionsAction.deadline != null) { + SectionTextFooter( + text = AnnotatedString(String.format(generalGetString(MR.strings.operator_conditions_accepted_for_enabled_operators_on), localDate(conditionsAction.deadline))), + textAlign = TextAlign.Center + ) + Spacer(Modifier.fillMaxWidth().height(DEFAULT_PADDING)) + } + } + + is UsageConditionsAction.Accepted -> { + if (conditionsAction.operators.isNotEmpty()) { + ReadableText(MR.strings.operators_conditions_accepted_for, args = conditionsAction.operators.joinToString(", ") { it.legalName_ }) + } + Column(modifier = Modifier.weight(1f).padding(bottom = DEFAULT_PADDING, top = DEFAULT_PADDING_HALF)) { + ConditionsTextView(rhId) + } + } + + else -> { + Column(modifier = Modifier.weight(1f).padding(bottom = DEFAULT_PADDING, top = DEFAULT_PADDING_HALF)) { + ConditionsTextView(rhId) + } + } + } + } +} + +@Composable +fun ServersErrorFooter(errStr: String) { + Row( + Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painterResource(MR.images.ic_error), + contentDescription = stringResource(MR.strings.server_error), + tint = Color.Red, + modifier = Modifier + .size(19.sp.toDp()) + .offset(x = 2.sp.toDp()) + ) + TextIconSpaced() + Text( + errStr, + color = MaterialTheme.colors.secondary, + lineHeight = 18.sp, + fontSize = 14.sp + ) + } +} + +private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) { + AlertManager.shared.showAlertDialogStacked( + title = generalGetString(MR.strings.smp_save_servers_question), + confirmText = generalGetString(MR.strings.save_verb), + dismissText = generalGetString(MR.strings.exit_without_saving), + onConfirm = save, + onDismiss = revert, + ) +} + fun showUpdateNetworkSettingsDialog( title: String, startsWith: String = "", @@ -521,6 +807,107 @@ fun showUpdateNetworkSettingsDialog( ) } +fun updateOperatorsConditionsAcceptance(usvs: MutableState>, updatedOperators: List) { + val modified = ArrayList(usvs.value) + for (i in modified.indices) { + val updatedOperator = updatedOperators.firstOrNull { it.operatorId == modified[i].operator?.operatorId } ?: continue + modified[i] = modified[i].copy(operator = modified[i].operator?.copy(conditionsAcceptance = updatedOperator.conditionsAcceptance)) + } + usvs.value = modified +} + +suspend fun validateServers_( + rhId: Long?, + userServersToValidate: List, + serverErrors: MutableState> +) { + try { + val errors = chatController.validateServers(rhId, userServersToValidate) ?: return + serverErrors.value = errors + } catch (ex: Exception) { + Log.e(TAG, ex.stackTraceToString()) + } +} + +fun serversCanBeSaved( + currUserServers: List, + userServers: List, + serverErrors: List +): Boolean { + return userServers != currUserServers && serverErrors.isEmpty() +} + +fun globalServersError(serverErrors: List): String? { + for (err in serverErrors) { + if (err.globalError != null) { + return err.globalError + } + } + return null +} + +fun globalSMPServersError(serverErrors: List): String? { + for (err in serverErrors) { + if (err.globalSMPError != null) { + return err.globalSMPError + } + } + return null +} + +fun globalXFTPServersError(serverErrors: List): String? { + for (err in serverErrors) { + if (err.globalXFTPError != null) { + return err.globalXFTPError + } + } + return null +} + +fun findDuplicateHosts(serverErrors: List): Set { + val duplicateHostsList = serverErrors.mapNotNull { err -> + if (err is UserServersError.DuplicateServer) { + err.duplicateHost + } else { + null + } + } + return duplicateHostsList.toSet() +} + +private suspend fun saveServers( + rhId: Long?, + currUserServers: MutableState>, + userServers: MutableState> +) { + val userServersToSave = userServers.value + try { + val set = setUserServers(rhId, userServersToSave) + + if (set) { + // Get updated servers to learn new server ids (otherwise it messes up delete of newly added and saved servers) + val updatedServers = getUserServers(rhId) + // Get updated operators to update model + val updatedOperators = getServerOperators(rhId) + + if (updatedOperators != null) { + chatModel.conditions.value = updatedOperators + } + + if (updatedServers != null ) { + currUserServers.value = updatedServers + userServers.value = updatedServers + } else { + currUserServers.value = userServersToSave + } + } else { + currUserServers.value = userServersToSave + } + } catch (ex: Exception) { + Log.e(TAG, ex.stackTraceToString()) + } +} + @Preview @Composable fun PreviewNetworkAndServersLayout() { @@ -530,6 +917,9 @@ fun PreviewNetworkAndServersLayout() { networkUseSocksProxy = remember { mutableStateOf(true) }, onionHosts = remember { mutableStateOf(OnionHosts.PREFER) }, toggleSocksProxy = {}, + currUserServers = remember { mutableStateOf(emptyList()) }, + userServers = remember { mutableStateOf(emptyList()) }, + serverErrors = remember { mutableStateOf(emptyList()) } ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NewServerView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NewServerView.kt new file mode 100644 index 0000000000..1ec2534ab1 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NewServerView.kt @@ -0,0 +1,144 @@ +package chat.simplex.common.views.usersettings.networkAndServers + +import SectionBottomSpacer +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.* +import dev.icerock.moko.resources.compose.stringResource +import chat.simplex.common.model.* +import chat.simplex.common.model.ServerAddress.Companion.parseServerAddress +import chat.simplex.common.views.helpers.* +import chat.simplex.common.platform.* +import chat.simplex.res.MR +import kotlinx.coroutines.* + +@Composable +fun ModalData.NewServerView( + userServers: MutableState>, + serverErrors: MutableState>, + rhId: Long?, + close: () -> Unit +) { + val testing = remember { mutableStateOf(false) } + val scope = rememberCoroutineScope() + val newServer = remember { mutableStateOf(UserServer.empty) } + + ModalView(close = { + addServer( + scope, + newServer.value, + userServers, + serverErrors, + rhId, + close = close + ) + }) { + Box { + NewServerLayout( + newServer, + testing.value, + testServer = { + testing.value = true + withLongRunningApi { + val res = testServerConnection(newServer.value, chatModel) + if (isActive) { + newServer.value = res.first + testing.value = false + } + } + }, + ) + + if (testing.value) { + DefaultProgressView(null) + } + } + } +} + +@Composable +private fun NewServerLayout( + server: MutableState, + testing: Boolean, + testServer: () -> Unit, +) { + ColumnWithScrollBar { + AppBarTitle(stringResource(MR.strings.smp_servers_new_server)) + CustomServer(server, testing, testServer, onDelete = null) + SectionBottomSpacer() + } +} + +fun serverProtocolAndOperator( + server: UserServer, + userServers: List +): Pair? { + val serverAddress = parseServerAddress(server.server) + return if (serverAddress != null) { + val serverProtocol = serverAddress.serverProtocol + val hostnames = serverAddress.hostnames + val matchingOperator = userServers.mapNotNull { it.operator }.firstOrNull { op -> + op.serverDomains.any { domain -> + hostnames.any { hostname -> + hostname.endsWith(domain) + } + } + } + Pair(serverProtocol, matchingOperator) + } else { + null + } +} + +fun addServer( + scope: CoroutineScope, + server: UserServer, + userServers: MutableState>, + serverErrors: MutableState>, + rhId: Long?, + close: () -> Unit +) { + val result = serverProtocolAndOperator(server, userServers.value) + if (result != null) { + val (serverProtocol, matchingOperator) = result + val operatorIndex = userServers.value.indexOfFirst { it.operator?.operatorId == matchingOperator?.operatorId } + if (operatorIndex != -1) { + // Create a mutable copy of the userServers list + val updatedUserServers = userServers.value.toMutableList() + val operatorServers = updatedUserServers[operatorIndex] + // Create a mutable copy of the smpServers or xftpServers and add the server + when (serverProtocol) { + ServerProtocol.SMP -> { + val updatedSMPServers = operatorServers.smpServers.toMutableList() + updatedSMPServers.add(server) + updatedUserServers[operatorIndex] = operatorServers.copy(smpServers = updatedSMPServers) + } + + ServerProtocol.XFTP -> { + val updatedXFTPServers = operatorServers.xftpServers.toMutableList() + updatedXFTPServers.add(server) + updatedUserServers[operatorIndex] = operatorServers.copy(xftpServers = updatedXFTPServers) + } + } + + userServers.value = updatedUserServers + close() + matchingOperator?.let { op -> + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.operator_server_alert_title), + text = String.format(generalGetString(MR.strings.server_added_to_operator__name), op.tradeName) + ) + } + } else { // Shouldn't happen + close() + AlertManager.shared.showAlertMsg(title = generalGetString(MR.strings.error_adding_server)) + } + } else { + close() + if (server.server.trim().isNotEmpty()) { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.smp_servers_invalid_address), + text = generalGetString(MR.strings.smp_servers_check_address) + ) + } + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt new file mode 100644 index 0000000000..cb02745511 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt @@ -0,0 +1,701 @@ +package chat.simplex.common.views.usersettings.networkAndServers + +import SectionBottomSpacer +import SectionCustomFooter +import SectionDividerSpaced +import SectionItemView +import SectionTextFooter +import SectionView +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.* +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.text.* +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs +import chat.simplex.common.model.ChatController.getUsageConditions +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.chat.item.ItemAction +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.onboarding.* +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import java.net.URI + +@Composable +fun ModalData.OperatorView( + currUserServers: MutableState>, + userServers: MutableState>, + serverErrors: MutableState>, + operatorIndex: Int, + rhId: Long? +) { + val testing = remember { mutableStateOf(false) } + val operator = remember { userServers.value[operatorIndex].operator_ } + val currentUser = remember { chatModel.currentUser }.value + + LaunchedEffect(userServers) { + snapshotFlow { userServers.value } + .collect { updatedServers -> + validateServers_(rhId = rhId, userServersToValidate = updatedServers, serverErrors = serverErrors) + } + } + + Box { + ColumnWithScrollBar(Modifier.alpha(if (testing.value) 0.6f else 1f)) { + AppBarTitle(String.format(stringResource(MR.strings.operator_servers_title), operator.tradeName)) + OperatorViewLayout( + currUserServers, + userServers, + serverErrors, + operatorIndex, + navigateToProtocolView = { serverIndex, server, protocol -> + navigateToProtocolView(userServers, serverErrors, operatorIndex, rhId, serverIndex, server, protocol) + }, + currentUser, + rhId, + testing + ) + } + + if (testing.value) { + DefaultProgressView(null) + } + } +} + +fun navigateToProtocolView( + userServers: MutableState>, + serverErrors: MutableState>, + operatorIndex: Int, + rhId: Long?, + serverIndex: Int, + server: UserServer, + protocol: ServerProtocol +) { + ModalManager.start.showCustomModal { close -> + ProtocolServerView( + m = chatModel, + server = server, + serverProtocol = protocol, + userServers = userServers, + serverErrors = serverErrors, + onDelete = { + if (protocol == ServerProtocol.SMP) { + deleteSMPServer(userServers, operatorIndex, serverIndex) + } else { + deleteXFTPServer(userServers, operatorIndex, serverIndex) + } + close() + }, + onUpdate = { updatedServer -> + userServers.value = userServers.value.toMutableList().apply { + this[operatorIndex] = this[operatorIndex].copy( + smpServers = if (protocol == ServerProtocol.SMP) { + this[operatorIndex].smpServers.toMutableList().apply { + this[serverIndex] = updatedServer + } + } else this[operatorIndex].smpServers, + xftpServers = if (protocol == ServerProtocol.XFTP) { + this[operatorIndex].xftpServers.toMutableList().apply { + this[serverIndex] = updatedServer + } + } else this[operatorIndex].xftpServers + ) + } + }, + close = close, + rhId = rhId + ) + } +} + +@Composable +fun OperatorViewLayout( + currUserServers: MutableState>, + userServers: MutableState>, + serverErrors: MutableState>, + operatorIndex: Int, + navigateToProtocolView: (Int, UserServer, ServerProtocol) -> Unit, + currentUser: User?, + rhId: Long?, + testing: MutableState +) { + val operator by remember { derivedStateOf { userServers.value[operatorIndex].operator_ } } + val scope = rememberCoroutineScope() + val duplicateHosts = findDuplicateHosts(serverErrors.value) + + Column { + SectionView(generalGetString(MR.strings.operator).uppercase()) { + SectionItemView({ ModalManager.start.showModalCloseable { _ -> OperatorInfoView(operator) } }) { + Image(painterResource(operator.largeLogo), null, Modifier.height(48.dp)) + } + UseOperatorToggle( + scope = scope, + currUserServers = currUserServers, + userServers = userServers, + serverErrors = serverErrors, + operatorIndex = operatorIndex, + rhId = rhId + ) + } + val serversErr = globalServersError(serverErrors.value) + if (serversErr != null) { + SectionCustomFooter { + ServersErrorFooter(serversErr) + } + } else { + val footerText = when (val c = operator.conditionsAcceptance) { + is ConditionsAcceptance.Accepted -> if (c.acceptedAt != null) { + String.format(generalGetString(MR.strings.operator_conditions_accepted_on), localDate(c.acceptedAt)) + } else null + is ConditionsAcceptance.Required -> if (operator.enabled && c.deadline != null) { + String.format(generalGetString(MR.strings.operator_conditions_will_be_accepted_on), localDate(c.deadline)) + } else null + } + if (footerText != null) { + SectionTextFooter(footerText) + } + } + + if (operator.enabled) { + if (userServers.value[operatorIndex].smpServers.any { !it.deleted }) { + SectionDividerSpaced() + SectionView(generalGetString(MR.strings.operator_use_for_messages).uppercase()) { + SectionItemView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) { + Text( + stringResource(MR.strings.operator_use_for_messages_receiving), + Modifier.padding(end = 24.dp), + color = Color.Unspecified + ) + Spacer(Modifier.fillMaxWidth().weight(1f)) + DefaultSwitch( + checked = userServers.value[operatorIndex].operator_.smpRoles.storage, + onCheckedChange = { enabled -> + userServers.value = userServers.value.toMutableList().apply { + this[operatorIndex] = this[operatorIndex].copy( + operator = this[operatorIndex].operator?.copy( + smpRoles = this[operatorIndex].operator?.smpRoles?.copy(storage = enabled) ?: ServerRoles(storage = enabled, proxy = false) + ) + ) + } + } + ) + } + SectionItemView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) { + Text( + stringResource(MR.strings.operator_use_for_messages_private_routing), + Modifier.padding(end = 24.dp), + color = Color.Unspecified + ) + Spacer(Modifier.fillMaxWidth().weight(1f)) + DefaultSwitch( + checked = userServers.value[operatorIndex].operator_.smpRoles.proxy, + onCheckedChange = { enabled -> + userServers.value = userServers.value.toMutableList().apply { + this[operatorIndex] = this[operatorIndex].copy( + operator = this[operatorIndex].operator?.copy( + smpRoles = this[operatorIndex].operator?.smpRoles?.copy(proxy = enabled) ?: ServerRoles(storage = false, proxy = enabled) + ) + ) + } + } + ) + } + + } + val smpErr = globalSMPServersError(serverErrors.value) + if (smpErr != null) { + SectionCustomFooter { + ServersErrorFooter(smpErr) + } + } + } + + // Preset servers can't be deleted + if (userServers.value[operatorIndex].smpServers.any { it.preset }) { + SectionDividerSpaced() + SectionView(generalGetString(MR.strings.message_servers).uppercase()) { + userServers.value[operatorIndex].smpServers.forEachIndexed { i, server -> + if (!server.preset) return@forEachIndexed + SectionItemView({ navigateToProtocolView(i, server, ServerProtocol.SMP) }) { + ProtocolServerViewLink( + srv = server, + serverProtocol = ServerProtocol.SMP, + duplicateHosts = duplicateHosts + ) + } + } + } + val smpErr = globalSMPServersError(serverErrors.value) + if (smpErr != null) { + SectionCustomFooter { + ServersErrorFooter(smpErr) + } + } else { + SectionTextFooter( + remember(currentUser?.displayName) { + buildAnnotatedString { + append(generalGetString(MR.strings.smp_servers_per_user) + " ") + withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { + append(currentUser?.displayName ?: "") + } + append(".") + } + } + ) + } + } + + if (userServers.value[operatorIndex].smpServers.any { !it.preset && !it.deleted }) { + SectionDividerSpaced() + SectionView(generalGetString(MR.strings.operator_added_message_servers).uppercase()) { + userServers.value[operatorIndex].smpServers.forEachIndexed { i, server -> + if (server.deleted || server.preset) return@forEachIndexed + SectionItemView({ navigateToProtocolView(i, server, ServerProtocol.SMP) }) { + ProtocolServerViewLink( + srv = server, + serverProtocol = ServerProtocol.SMP, + duplicateHosts = duplicateHosts + ) + } + } + } + } + + if (userServers.value[operatorIndex].xftpServers.any { !it.deleted }) { + SectionDividerSpaced() + SectionView(generalGetString(MR.strings.operator_use_for_files).uppercase()) { + SectionItemView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) { + Text( + stringResource(MR.strings.operator_use_for_sending), + Modifier.padding(end = 24.dp), + color = Color.Unspecified + ) + Spacer(Modifier.fillMaxWidth().weight(1f)) + DefaultSwitch( + checked = userServers.value[operatorIndex].operator_.xftpRoles.storage, + onCheckedChange = { enabled -> + userServers.value = userServers.value.toMutableList().apply { + this[operatorIndex] = this[operatorIndex].copy( + operator = this[operatorIndex].operator?.copy( + xftpRoles = this[operatorIndex].operator?.xftpRoles?.copy(storage = enabled) ?: ServerRoles(storage = enabled, proxy = false) + ) + ) + } + } + ) + } + } + val xftpErr = globalXFTPServersError(serverErrors.value) + if (xftpErr != null) { + SectionCustomFooter { + ServersErrorFooter(xftpErr) + } + } + } + + // Preset servers can't be deleted + if (userServers.value[operatorIndex].xftpServers.any { it.preset }) { + SectionDividerSpaced() + SectionView(generalGetString(MR.strings.media_and_file_servers).uppercase()) { + userServers.value[operatorIndex].xftpServers.forEachIndexed { i, server -> + if (!server.preset) return@forEachIndexed + SectionItemView({ navigateToProtocolView(i, server, ServerProtocol.XFTP) }) { + ProtocolServerViewLink( + srv = server, + serverProtocol = ServerProtocol.XFTP, + duplicateHosts = duplicateHosts + ) + } + } + } + val xftpErr = globalXFTPServersError(serverErrors.value) + if (xftpErr != null) { + SectionCustomFooter { + ServersErrorFooter(xftpErr) + } + } else { + SectionTextFooter( + remember(currentUser?.displayName) { + buildAnnotatedString { + append(generalGetString(MR.strings.xftp_servers_per_user) + " ") + withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { + append(currentUser?.displayName ?: "") + } + append(".") + } + } + ) + } + } + + if (userServers.value[operatorIndex].xftpServers.any { !it.preset && !it.deleted}) { + SectionDividerSpaced() + SectionView(generalGetString(MR.strings.operator_added_xftp_servers).uppercase()) { + userServers.value[operatorIndex].xftpServers.forEachIndexed { i, server -> + if (server.deleted || server.preset) return@forEachIndexed + SectionItemView({ navigateToProtocolView(i, server, ServerProtocol.XFTP) }) { + ProtocolServerViewLink( + srv = server, + serverProtocol = ServerProtocol.XFTP, + duplicateHosts = duplicateHosts + ) + } + } + } + } + + SectionDividerSpaced() + SectionView { + TestServersButton( + testing = testing, + smpServers = userServers.value[operatorIndex].smpServers, + xftpServers = userServers.value[operatorIndex].xftpServers, + ) { p, l -> + when (p) { + ServerProtocol.XFTP -> userServers.value = userServers.value.toMutableList().apply { + this[operatorIndex] = this[operatorIndex].copy( + xftpServers = l + ) + } + + ServerProtocol.SMP -> userServers.value = userServers.value.toMutableList().apply { + this[operatorIndex] = this[operatorIndex].copy( + smpServers = l + ) + } + } + } + } + + SectionBottomSpacer() + } + } +} + +@Composable +private fun OperatorInfoView(serverOperator: ServerOperator) { + ColumnWithScrollBar { + AppBarTitle(stringResource(MR.strings.operator_info_title)) + + SectionView { + SectionItemView { + Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { + Image(painterResource(serverOperator.largeLogo), null, Modifier.height(48.dp)) + if (serverOperator.legalName != null) { + Text(serverOperator.legalName) + } + } + } + } + + SectionDividerSpaced(maxBottomPadding = false) + + SectionView { + SectionItemView { + Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { + serverOperator.info.description.forEach { d -> + Text(d) + } + } + } + } + + SectionDividerSpaced() + + SectionView(generalGetString(MR.strings.operator_website).uppercase()) { + SectionItemView { + val website = serverOperator.info.website + val uriHandler = LocalUriHandler.current + Text(website, color = MaterialTheme.colors.primary, modifier = Modifier.clickable { uriHandler.openUriCatching(website) }) + } + } + } +} + +@Composable +private fun UseOperatorToggle( + scope: CoroutineScope, + currUserServers: MutableState>, + userServers: MutableState>, + serverErrors: MutableState>, + operatorIndex: Int, + rhId: Long? +) { + SectionItemView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) { + Text( + stringResource(MR.strings.operator_use_operator_toggle_description), + Modifier.padding(end = 24.dp), + color = Color.Unspecified + ) + Spacer(Modifier.fillMaxWidth().weight(1f)) + DefaultSwitch( + checked = userServers.value[operatorIndex].operator?.enabled ?: false, + onCheckedChange = { enabled -> + val operator = userServers.value[operatorIndex].operator + if (enabled) { + when (val conditionsAcceptance = operator?.conditionsAcceptance) { + is ConditionsAcceptance.Accepted -> { + changeOperatorEnabled(userServers, operatorIndex, true) + } + + is ConditionsAcceptance.Required -> { + if (conditionsAcceptance.deadline == null) { + ModalManager.start.showModalCloseable(endButtons = { ConditionsLinkButton() }) { close -> + SingleOperatorUsageConditionsView( + currUserServers = currUserServers, + userServers = userServers, + serverErrors = serverErrors, + operatorIndex = operatorIndex, + rhId = rhId, + close = close + ) + } + } else { + changeOperatorEnabled(userServers, operatorIndex, true) + } + } + + else -> {} + } + } else { + changeOperatorEnabled(userServers, operatorIndex, false) + } + }, + ) + } +} + +@Composable +private fun SingleOperatorUsageConditionsView( + currUserServers: MutableState>, + userServers: MutableState>, + serverErrors: MutableState>, + operatorIndex: Int, + rhId: Long?, + close: () -> Unit +) { + val operatorsWithConditionsAccepted = remember { chatModel.conditions.value.serverOperators.filter { it.conditionsAcceptance.conditionsAccepted } } + val operator = remember { userServers.value[operatorIndex].operator_ } + val scope = rememberCoroutineScope() + + suspend fun acceptForOperators(rhId: Long?, operatorIds: List, operatorIndexToEnable: Int, close: () -> Unit) { + try { + val conditionsId = chatModel.conditions.value.currentConditions.conditionsId + val r = chatController.acceptConditions(rhId, conditionsId, operatorIds) ?: return + + chatModel.conditions.value = r + updateOperatorsConditionsAcceptance(currUserServers, r.serverOperators) + updateOperatorsConditionsAcceptance(userServers, r.serverOperators) + changeOperatorEnabled(userServers, operatorIndex, true) + close() + } catch (ex: Exception) { + Log.e(TAG, ex.stackTraceToString()) + } + } + + @Composable + fun AcceptConditionsButton(close: () -> Unit) { + // Opened operator or Other enabled operators with conditions not accepted + val operatorIds = chatModel.conditions.value.serverOperators + .filter { it.operatorId == operator.id || (it.enabled && !it.conditionsAcceptance.conditionsAccepted) } + .map { it.operatorId } + + Column(Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING * 2), horizontalAlignment = Alignment.CenterHorizontally) { + OnboardingActionButton( + labelId = MR.strings.accept_conditions, + onboarding = null, + enabled = operatorIds.isNotEmpty(), + onclick = { + scope.launch { + acceptForOperators(rhId, operatorIds, operatorIndex, close) + } + } + ) + } + } + + @Composable + fun UsageConditionsDestinationView(close: () -> Unit) { + ColumnWithScrollBar(modifier = Modifier.fillMaxSize()) { + AppBarTitle(stringResource(MR.strings.operator_conditions_of_use), enableAlphaChanges = false) + Column(modifier = Modifier.weight(1f).padding(end = DEFAULT_PADDING, start = DEFAULT_PADDING, bottom = DEFAULT_PADDING, top = DEFAULT_PADDING_HALF)) { + ConditionsTextView(rhId) + } + } + } + + @Composable + fun UsageConditionsNavLinkButton() { + Text( + stringResource(MR.strings.view_conditions), + color = MaterialTheme.colors.primary, + modifier = Modifier.padding(top = DEFAULT_PADDING_HALF).clickable { + ModalManager.start.showModalCloseable(endButtons = { ConditionsLinkButton() }) { close -> + UsageConditionsDestinationView(close) + } + } + ) + } + + ColumnWithScrollBar(modifier = Modifier.fillMaxSize().padding(horizontal = DEFAULT_PADDING)) { + AppBarTitle(String.format(stringResource(MR.strings.use_servers_of_operator_x), operator.tradeName), enableAlphaChanges = false, withPadding = false) + if (operator.conditionsAcceptance is ConditionsAcceptance.Accepted) { + // In current UI implementation this branch doesn't get shown - as conditions can't be opened from inside operator once accepted + Column(modifier = Modifier.weight(1f).padding(end = DEFAULT_PADDING, start = DEFAULT_PADDING, bottom = DEFAULT_PADDING)) { + ConditionsTextView(rhId) + } + } else if (operatorsWithConditionsAccepted.isNotEmpty()) { + ReadableText( + MR.strings.operator_conditions_accepted_for_some, + args = operatorsWithConditionsAccepted.joinToString(", ") { it.legalName_ } + ) + ReadableText( + MR.strings.operator_same_conditions_will_be_applied, + args = operator.legalName_ + ) + ConditionsAppliedToOtherOperatorsText(userServers = userServers.value, operatorIndex = operatorIndex) + + UsageConditionsNavLinkButton() + Spacer(Modifier.fillMaxWidth().weight(1f)) + AcceptConditionsButton(close) + } else { + ReadableText( + MR.strings.operator_in_order_to_use_accept_conditions, + args = operator.legalName_ + ) + ConditionsAppliedToOtherOperatorsText(userServers = userServers.value, operatorIndex = operatorIndex) + Column(modifier = Modifier.weight(1f).padding(end = DEFAULT_PADDING, start = DEFAULT_PADDING, bottom = DEFAULT_PADDING)) { + ConditionsTextView(rhId) + } + AcceptConditionsButton(close) + } + } +} + +@Composable +fun ConditionsTextView( + rhId: Long? +) { + val conditionsData = remember { mutableStateOf?>(null) } + val failedToLoad = remember { mutableStateOf(false) } + val defaultConditionsLink = "https://github.com/simplex-chat/simplex-chat/blob/stable/PRIVACY.md" + val scope = rememberCoroutineScope() + + LaunchedEffect(Unit) { + scope.launch { + try { + val conditions = getUsageConditions(rh = rhId) + + if (conditions != null) { + conditionsData.value = conditions + } else { + failedToLoad.value = true + } + } catch (ex: Exception) { + failedToLoad.value = true + } + } + } + val conditions = conditionsData.value + + if (conditions != null) { + val (usageConditions, conditionsText, _) = conditions + + if (conditionsText != null) { + val scrollState = rememberScrollState() + Box( + modifier = Modifier + .fillMaxSize() + .border(border = BorderStroke(1.dp, CurrentColors.value.colors.secondary.copy(alpha = 0.6f)), shape = RoundedCornerShape(12.dp)) + .verticalScroll(scrollState) + .padding(8.dp) + ) { + Text( + text = conditionsText.trimIndent(), + modifier = Modifier.padding(8.dp) + ) + } + } else { + val conditionsLink = "https://github.com/simplex-chat/simplex-chat/blob/${usageConditions.conditionsCommit}/PRIVACY.md" + ConditionsLinkView(conditionsLink) + } + } else if (failedToLoad.value) { + ConditionsLinkView(defaultConditionsLink) + } else { + DefaultProgressView(null) + } +} + +@Composable +private fun ConditionsLinkView(conditionsLink: String) { + SectionItemView { + val uriHandler = LocalUriHandler.current + Text(stringResource(MR.strings.operator_conditions_failed_to_load), color = MaterialTheme.colors.onBackground) + Text(conditionsLink, color = MaterialTheme.colors.primary, modifier = Modifier.clickable { uriHandler.openUriCatching(conditionsLink) }) + } +} + +@Composable +private fun ConditionsAppliedToOtherOperatorsText(userServers: List, operatorIndex: Int) { + val otherOperatorsToApply = remember { + derivedStateOf { + chatModel.conditions.value.serverOperators.filter { + it.enabled && + !it.conditionsAcceptance.conditionsAccepted && + it.operatorId != userServers[operatorIndex].operator_.operatorId + } + } + } + + if (otherOperatorsToApply.value.isNotEmpty()) { + ReadableText(MR.strings.operator_conditions_will_be_applied) + } +} + +@Composable +fun ConditionsLinkButton() { + val showMenu = remember { mutableStateOf(false) } + val uriHandler = LocalUriHandler.current + val oneHandUI = remember { appPrefs.oneHandUI.state } + Column { + DefaultDropdownMenu(showMenu, offset = if (oneHandUI.value) DpOffset(0.dp, -AppBarHeight * fontSizeSqrtMultiplier * 3) else DpOffset.Zero) { + val commit = chatModel.conditions.value.currentConditions.conditionsCommit + ItemAction(stringResource(MR.strings.operator_open_conditions), painterResource(MR.images.ic_draft), onClick = { + val mdUrl = "https://github.com/simplex-chat/simplex-chat/blob/$commit/PRIVACY.md" + uriHandler.openUriCatching(mdUrl) + showMenu.value = false + }) + ItemAction(stringResource(MR.strings.operator_open_changes), painterResource(MR.images.ic_more_horiz), onClick = { + val commitUrl = "https://github.com/simplex-chat/simplex-chat/commit/$commit" + uriHandler.openUriCatching(commitUrl) + showMenu.value = false + }) + } + IconButton({ showMenu.value = true }) { + Icon(painterResource(MR.images.ic_outbound), null, tint = MaterialTheme.colors.primary) + } + } +} + +private fun changeOperatorEnabled(userServers: MutableState>, operatorIndex: Int, enabled: Boolean) { + userServers.value = userServers.value.toMutableList().apply { + this[operatorIndex] = this[operatorIndex].copy( + operator = this[operatorIndex].operator?.copy(enabled = enabled) + ) + } +} \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ProtocolServerView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServerView.kt similarity index 51% rename from apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ProtocolServerView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServerView.kt index be566e6c5a..bebc96a28c 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ProtocolServerView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServerView.kt @@ -1,16 +1,14 @@ -package chat.simplex.common.views.usersettings +package chat.simplex.common.views.usersettings.networkAndServers import SectionBottomSpacer import SectionDividerSpaced import SectionItemView import SectionItemViewSpaceBetween import SectionView -import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.* import androidx.compose.runtime.* -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import dev.icerock.moko.resources.compose.painterResource @@ -26,62 +24,103 @@ import chat.simplex.common.views.helpers.* import chat.simplex.common.views.newchat.QRCode import chat.simplex.common.model.ChatModel import chat.simplex.common.platform.* +import chat.simplex.common.views.usersettings.PreferenceToggle import chat.simplex.res.MR import kotlinx.coroutines.* import kotlinx.coroutines.flow.distinctUntilChanged @Composable -fun ProtocolServerView(m: ChatModel, server: ServerCfg, serverProtocol: ServerProtocol, onUpdate: (ServerCfg) -> Unit, onDelete: () -> Unit) { - var testing by remember { mutableStateOf(false) } - ProtocolServerLayout( - testing, - server, - serverProtocol, - testServer = { - testing = true - withLongRunningApi { - val res = testServerConnection(server, m) - if (isActive) { - onUpdate(res.first) - testing = false +fun ProtocolServerView( + m: ChatModel, + server: UserServer, + serverProtocol: ServerProtocol, + userServers: MutableState>, + serverErrors: MutableState>, + onDelete: () -> Unit, + onUpdate: (UserServer) -> Unit, + close: () -> Unit, + rhId: Long? +) { + val testing = remember { mutableStateOf(false) } + val scope = rememberCoroutineScope() + val draftServer = remember { mutableStateOf(server) } + + ModalView( + close = { + scope.launch { + val draftResult = serverProtocolAndOperator(draftServer.value, userServers.value) + val savedResult = serverProtocolAndOperator(server, userServers.value) + + if (draftResult != null && savedResult != null) { + val (serverToEditProtocol, serverToEditOperator) = draftResult + val (svProtocol, serverOperator) = savedResult + + if (serverToEditProtocol != svProtocol) { + close() + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.error_updating_server_title), + text = generalGetString(MR.strings.error_server_protocol_changed) + ) + } else if (serverToEditOperator != serverOperator) { + close() + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.error_updating_server_title), + text = generalGetString(MR.strings.error_server_operator_changed) + ) + } else { + onUpdate(draftServer.value) + close() + } + } else { + close() + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.smp_servers_invalid_address), + text = generalGetString(MR.strings.smp_servers_check_address) + ) } } - }, - onUpdate, - onDelete - ) - if (testing) { - Box( - Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator( - Modifier - .padding(horizontal = 2.dp) - .size(30.dp), - color = MaterialTheme.colors.secondary, - strokeWidth = 2.5.dp + } + ) { + Box { + ProtocolServerLayout( + draftServer, + serverProtocol, + testing.value, + testServer = { + testing.value = true + withLongRunningApi { + val res = testServerConnection(draftServer.value, m) + if (isActive) { + draftServer.value = res.first + testing.value = false + } + } + }, + onDelete ) + + if (testing.value) { + DefaultProgressView(null) + } } } } @Composable private fun ProtocolServerLayout( - testing: Boolean, - server: ServerCfg, + server: MutableState, serverProtocol: ServerProtocol, + testing: Boolean, testServer: () -> Unit, - onUpdate: (ServerCfg) -> Unit, onDelete: () -> Unit, ) { ColumnWithScrollBar { - AppBarTitle(stringResource(if (server.preset) MR.strings.smp_servers_preset_server else MR.strings.smp_servers_your_server)) + AppBarTitle(stringResource(if (serverProtocol == ServerProtocol.XFTP) MR.strings.xftp_server else MR.strings.smp_server)) - if (server.preset) { - PresetServer(testing, server, testServer, onUpdate, onDelete) + if (server.value.preset) { + PresetServer(server, testing, testServer) } else { - CustomServer(testing, server, serverProtocol, testServer, onUpdate, onDelete) + CustomServer(server, testing, testServer, onDelete) } SectionBottomSpacer() } @@ -89,16 +128,14 @@ private fun ProtocolServerLayout( @Composable private fun PresetServer( + server: MutableState, testing: Boolean, - server: ServerCfg, - testServer: () -> Unit, - onUpdate: (ServerCfg) -> Unit, - onDelete: () -> Unit, + testServer: () -> Unit ) { SectionView(stringResource(MR.strings.smp_servers_preset_address).uppercase()) { SelectionContainer { Text( - server.server, + server.value.server, Modifier.padding(start = DEFAULT_PADDING, top = 5.dp, end = DEFAULT_PADDING, bottom = 10.dp), style = TextStyle( fontFamily = FontFamily.Monospace, fontSize = 16.sp, @@ -108,23 +145,21 @@ private fun PresetServer( } } SectionDividerSpaced() - UseServerSection(true, testing, server, testServer, onUpdate, onDelete) + UseServerSection(server, true, testing, testServer) } @Composable -private fun CustomServer( +fun CustomServer( + server: MutableState, testing: Boolean, - server: ServerCfg, - serverProtocol: ServerProtocol, testServer: () -> Unit, - onUpdate: (ServerCfg) -> Unit, - onDelete: () -> Unit, + onDelete: (() -> Unit)?, ) { - val serverAddress = remember { mutableStateOf(server.server) } + val serverAddress = remember { mutableStateOf(server.value.server) } val valid = remember { derivedStateOf { with(parseServerAddress(serverAddress.value)) { - this?.valid == true && this.serverProtocol == serverProtocol + this?.valid == true } } } @@ -142,13 +177,14 @@ private fun CustomServer( snapshotFlow { serverAddress.value } .distinctUntilChanged() .collect { - testedPreviously[server.server] = server.tested - onUpdate(server.copy(server = it, tested = testedPreviously[serverAddress.value])) + testedPreviously[server.value.server] = server.value.tested + server.value = server.value.copy(server = it, tested = testedPreviously[serverAddress.value]) } } } SectionDividerSpaced(maxTopPadding = true) - UseServerSection(valid.value, testing, server, testServer, onUpdate, onDelete) + + UseServerSection(server, valid.value, testing, testServer, onDelete) if (valid.value) { SectionDividerSpaced() @@ -160,43 +196,44 @@ private fun CustomServer( @Composable private fun UseServerSection( + server: MutableState, valid: Boolean, testing: Boolean, - server: ServerCfg, testServer: () -> Unit, - onUpdate: (ServerCfg) -> Unit, - onDelete: () -> Unit, + onDelete: (() -> Unit)? = null, ) { SectionView(stringResource(MR.strings.smp_servers_use_server).uppercase()) { SectionItemViewSpaceBetween(testServer, disabled = !valid || testing) { Text(stringResource(MR.strings.smp_servers_test_server), color = if (valid && !testing) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary) - ShowTestStatus(server) + ShowTestStatus(server.value) } - val enabled = rememberUpdatedState(server.enabled) + val enabled = rememberUpdatedState(server.value.enabled) PreferenceToggle( stringResource(MR.strings.smp_servers_use_server_for_new_conn), - disabled = server.tested != true && !server.preset, + disabled = testing, checked = enabled.value ) { - onUpdate(server.copy(enabled = it)) + server.value = server.value.copy(enabled = it) } - - SectionItemView(onDelete, disabled = testing) { - Text(stringResource(MR.strings.smp_servers_delete_server), color = if (testing) MaterialTheme.colors.secondary else MaterialTheme.colors.error) + + if (onDelete != null) { + SectionItemView(onDelete, disabled = testing) { + Text(stringResource(MR.strings.smp_servers_delete_server), color = if (testing) MaterialTheme.colors.secondary else MaterialTheme.colors.error) + } } } } @Composable -fun ShowTestStatus(server: ServerCfg, modifier: Modifier = Modifier) = +fun ShowTestStatus(server: UserServer, modifier: Modifier = Modifier) = when (server.tested) { true -> Icon(painterResource(MR.images.ic_check), null, modifier, tint = SimplexGreen) false -> Icon(painterResource(MR.images.ic_close), null, modifier, tint = MaterialTheme.colors.error) else -> Icon(painterResource(MR.images.ic_check), null, modifier, tint = Color.Transparent) } -suspend fun testServerConnection(server: ServerCfg, m: ChatModel): Pair = +suspend fun testServerConnection(server: UserServer, m: ChatModel): Pair = try { val r = m.controller.testProtoServer(server.remoteHostId, server.server) server.copy(tested = r == null) to r diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServersView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServersView.kt new file mode 100644 index 0000000000..63bf8b1dc4 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServersView.kt @@ -0,0 +1,407 @@ +package chat.simplex.common.views.usersettings.networkAndServers + +import SectionBottomSpacer +import SectionCustomFooter +import SectionDividerSpaced +import SectionItemView +import SectionTextFooter +import SectionView +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalUriHandler +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource +import androidx.compose.ui.text.* +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import chat.simplex.common.model.ServerAddress.Companion.parseServerAddress +import chat.simplex.common.views.helpers.* +import chat.simplex.common.model.* +import chat.simplex.common.platform.* +import chat.simplex.common.views.usersettings.SettingsActionItem +import chat.simplex.res.MR +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +@Composable +fun ModalData.YourServersView( + userServers: MutableState>, + serverErrors: MutableState>, + operatorIndex: Int, + rhId: Long? +) { + val testing = remember { mutableStateOf(false) } + val currentUser = remember { chatModel.currentUser }.value + val scope = rememberCoroutineScope() + + LaunchedEffect(userServers) { + snapshotFlow { userServers.value } + .collect { updatedServers -> + validateServers_(rhId = rhId, userServersToValidate = updatedServers, serverErrors = serverErrors) + } + } + + Box { + ColumnWithScrollBar { + AppBarTitle(stringResource(MR.strings.your_servers)) + YourServersViewLayout( + scope, + userServers, + serverErrors, + operatorIndex, + navigateToProtocolView = { serverIndex, server, protocol -> + navigateToProtocolView(userServers, serverErrors, operatorIndex, rhId, serverIndex, server, protocol) + }, + currentUser, + rhId, + testing + ) + } + + if (testing.value) { + DefaultProgressView(null) + } + } +} + +@Composable +fun YourServersViewLayout( + scope: CoroutineScope, + userServers: MutableState>, + serverErrors: MutableState>, + operatorIndex: Int, + navigateToProtocolView: (Int, UserServer, ServerProtocol) -> Unit, + currentUser: User?, + rhId: Long?, + testing: MutableState +) { + val duplicateHosts = findDuplicateHosts(serverErrors.value) + + Column { + if (userServers.value[operatorIndex].smpServers.any { !it.deleted }) { + SectionView(generalGetString(MR.strings.message_servers).uppercase()) { + userServers.value[operatorIndex].smpServers.forEachIndexed { i, server -> + if (server.deleted) return@forEachIndexed + SectionItemView({ navigateToProtocolView(i, server, ServerProtocol.SMP) }) { + ProtocolServerViewLink( + srv = server, + serverProtocol = ServerProtocol.SMP, + duplicateHosts = duplicateHosts + ) + } + } + } + val smpErr = globalSMPServersError(serverErrors.value) + if (smpErr != null) { + SectionCustomFooter { + ServersErrorFooter(smpErr) + } + } else { + SectionTextFooter( + remember(currentUser?.displayName) { + buildAnnotatedString { + append(generalGetString(MR.strings.smp_servers_per_user) + " ") + withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { + append(currentUser?.displayName ?: "") + } + append(".") + } + } + ) + } + } + + if (userServers.value[operatorIndex].xftpServers.any { !it.deleted }) { + SectionDividerSpaced() + SectionView(generalGetString(MR.strings.media_and_file_servers).uppercase()) { + userServers.value[operatorIndex].xftpServers.forEachIndexed { i, server -> + if (server.deleted) return@forEachIndexed + SectionItemView({ navigateToProtocolView(i, server, ServerProtocol.XFTP) }) { + ProtocolServerViewLink( + srv = server, + serverProtocol = ServerProtocol.XFTP, + duplicateHosts = duplicateHosts + ) + } + } + } + val xftpErr = globalXFTPServersError(serverErrors.value) + if (xftpErr != null) { + SectionCustomFooter { + ServersErrorFooter(xftpErr) + } + } else { + SectionTextFooter( + remember(currentUser?.displayName) { + buildAnnotatedString { + append(generalGetString(MR.strings.xftp_servers_per_user) + " ") + withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { + append(currentUser?.displayName ?: "") + } + append(".") + } + } + ) + } + } + + if ( + userServers.value[operatorIndex].smpServers.any { !it.deleted } || + userServers.value[operatorIndex].xftpServers.any { !it.deleted } + ) { + SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = false) + } + + SectionView { + SettingsActionItem( + painterResource(MR.images.ic_add), + stringResource(MR.strings.smp_servers_add), + click = { showAddServerDialog(scope, userServers, serverErrors, rhId) }, + disabled = testing.value, + textColor = if (testing.value) MaterialTheme.colors.secondary else MaterialTheme.colors.primary, + iconColor = if (testing.value) MaterialTheme.colors.secondary else MaterialTheme.colors.primary + ) + } + val serversErr = globalServersError(serverErrors.value) + if (serversErr != null) { + SectionCustomFooter { + ServersErrorFooter(serversErr) + } + } + SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = false) + + SectionView { + TestServersButton( + testing = testing, + smpServers = userServers.value[operatorIndex].smpServers, + xftpServers = userServers.value[operatorIndex].xftpServers, + ) { p, l -> + when (p) { + ServerProtocol.XFTP -> userServers.value = userServers.value.toMutableList().apply { + this[operatorIndex] = this[operatorIndex].copy( + xftpServers = l + ) + } + + ServerProtocol.SMP -> userServers.value = userServers.value.toMutableList().apply { + this[operatorIndex] = this[operatorIndex].copy( + smpServers = l + ) + } + } + } + + HowToButton() + } + SectionBottomSpacer() + } +} + +@Composable +fun TestServersButton( + smpServers: List, + xftpServers: List, + testing: MutableState, + onUpdate: (ServerProtocol, List) -> Unit +) { + val scope = rememberCoroutineScope() + val disabled = derivedStateOf { (smpServers.none { it.enabled } && xftpServers.none { it.enabled }) || testing.value } + + SectionItemView( + { + scope.launch { + testServers(testing, smpServers, xftpServers, chatModel, onUpdate) + } + }, + disabled = disabled.value + ) { + Text(stringResource(MR.strings.smp_servers_test_servers), color = if (!disabled.value) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary) + } +} + +fun showAddServerDialog( + scope: CoroutineScope, + userServers: MutableState>, + serverErrors: MutableState>, + rhId: Long? +) { + AlertManager.shared.showAlertDialogButtonsColumn( + title = generalGetString(MR.strings.smp_servers_add), + buttons = { + Column { + SectionItemView({ + AlertManager.shared.hideAlert() + ModalManager.start.showCustomModal { close -> + NewServerView(userServers, serverErrors, rhId, close) + } + }) { + Text(stringResource(MR.strings.smp_servers_enter_manually), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + if (appPlatform.isAndroid) { + SectionItemView({ + AlertManager.shared.hideAlert() + ModalManager.start.showModalCloseable { close -> + ScanProtocolServer(rhId) { server -> + addServer( + scope, + server, + userServers, + serverErrors, + rhId, + close = close + ) + } + } + } + ) { + Text(stringResource(MR.strings.smp_servers_scan_qr), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + } + } + } + ) +} + +@Composable +fun ProtocolServerViewLink(serverProtocol: ServerProtocol, srv: UserServer, duplicateHosts: Set) { + val address = parseServerAddress(srv.server) + when { + address == null || !address.valid || address.serverProtocol != serverProtocol || address.hostnames.any { it in duplicateHosts } -> InvalidServer() + !srv.enabled -> Icon(painterResource(MR.images.ic_do_not_disturb_on), null, tint = MaterialTheme.colors.secondary) + else -> ShowTestStatus(srv) + } + Spacer(Modifier.padding(horizontal = 4.dp)) + val text = address?.hostnames?.firstOrNull() ?: srv.server + if (srv.enabled) { + Text(text, color = MaterialTheme.colors.onBackground, maxLines = 1) + } else { + Text(text, maxLines = 1, color = MaterialTheme.colors.secondary) + } +} + +@Composable +private fun HowToButton() { + val uriHandler = LocalUriHandler.current + SettingsActionItem( + painterResource(MR.images.ic_open_in_new), + stringResource(MR.strings.how_to_use_your_servers), + { uriHandler.openUriCatching("https://simplex.chat/docs/server.html") }, + textColor = MaterialTheme.colors.primary, + iconColor = MaterialTheme.colors.primary + ) +} + +@Composable +fun InvalidServer() { + Icon(painterResource(MR.images.ic_error), null, tint = MaterialTheme.colors.error) +} + +private suspend fun testServers( + testing: MutableState, + smpServers: List, + xftpServers: List, + m: ChatModel, + onUpdate: (ServerProtocol, List) -> Unit +) { + val smpResetStatus = resetTestStatus(smpServers) + onUpdate(ServerProtocol.SMP, smpResetStatus) + val xftpResetStatus = resetTestStatus(xftpServers) + onUpdate(ServerProtocol.XFTP, xftpResetStatus) + testing.value = true + val smpFailures = runServersTest(smpResetStatus, m) { onUpdate(ServerProtocol.SMP, it) } + val xftpFailures = runServersTest(xftpResetStatus, m) { onUpdate(ServerProtocol.XFTP, it) } + testing.value = false + val fs = smpFailures + xftpFailures + if (fs.isNotEmpty()) { + val msg = fs.map { it.key + ": " + it.value.localizedDescription }.joinToString("\n") + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.smp_servers_test_failed), + text = generalGetString(MR.strings.smp_servers_test_some_failed) + "\n" + msg + ) + } +} + +private fun resetTestStatus(servers: List): List { + val copy = ArrayList(servers) + for ((index, server) in servers.withIndex()) { + if (server.enabled) { + copy.removeAt(index) + copy.add(index, server.copy(tested = null)) + } + } + return copy +} + +private suspend fun runServersTest(servers: List, m: ChatModel, onUpdated: (List) -> Unit): Map { + val fs: MutableMap = mutableMapOf() + val updatedServers = ArrayList(servers) + for ((index, server) in servers.withIndex()) { + if (server.enabled) { + interruptIfCancelled() + val (updatedServer, f) = testServerConnection(server, m) + updatedServers.removeAt(index) + updatedServers.add(index, updatedServer) + // toList() is important. Otherwise, Compose will not redraw the screen after first update + onUpdated(updatedServers.toList()) + if (f != null) { + fs[serverHostname(updatedServer.server)] = f + } + } + } + return fs +} + +fun deleteXFTPServer( + userServers: MutableState>, + operatorServersIndex: Int, + serverIndex: Int +) { + val serverIsSaved = userServers.value[operatorServersIndex].xftpServers[serverIndex].serverId != null + + if (serverIsSaved) { + userServers.value = userServers.value.toMutableList().apply { + this[operatorServersIndex] = this[operatorServersIndex].copy( + xftpServers = this[operatorServersIndex].xftpServers.toMutableList().apply { + this[serverIndex] = this[serverIndex].copy(deleted = true) + } + ) + } + } else { + userServers.value = userServers.value.toMutableList().apply { + this[operatorServersIndex] = this[operatorServersIndex].copy( + xftpServers = this[operatorServersIndex].xftpServers.toMutableList().apply { + this.removeAt(serverIndex) + } + ) + } + } +} + +fun deleteSMPServer( + userServers: MutableState>, + operatorServersIndex: Int, + serverIndex: Int +) { + val serverIsSaved = userServers.value[operatorServersIndex].smpServers[serverIndex].serverId != null + + if (serverIsSaved) { + userServers.value = userServers.value.toMutableList().apply { + this[operatorServersIndex] = this[operatorServersIndex].copy( + smpServers = this[operatorServersIndex].smpServers.toMutableList().apply { + this[serverIndex] = this[serverIndex].copy(deleted = true) + } + ) + } + } else { + userServers.value = userServers.value.toMutableList().apply { + this[operatorServersIndex] = this[operatorServersIndex].copy( + smpServers = this[operatorServersIndex].smpServers.toMutableList().apply { + this.removeAt(serverIndex) + } + ) + } + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ScanProtocolServer.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ScanProtocolServer.kt similarity index 62% rename from apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ScanProtocolServer.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ScanProtocolServer.kt index 966f44cac7..56f16d4eb1 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ScanProtocolServer.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ScanProtocolServer.kt @@ -1,29 +1,25 @@ -package chat.simplex.common.views.usersettings +package chat.simplex.common.views.usersettings.networkAndServers -import androidx.compose.foundation.layout.* import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier import dev.icerock.moko.resources.compose.stringResource -import androidx.compose.ui.unit.dp import chat.simplex.common.model.ServerAddress.Companion.parseServerAddress -import chat.simplex.common.model.ServerCfg +import chat.simplex.common.model.UserServer import chat.simplex.common.platform.ColumnWithScrollBar -import chat.simplex.common.ui.theme.DEFAULT_PADDING import chat.simplex.common.views.helpers.* import chat.simplex.common.views.newchat.QRCodeScanner import chat.simplex.res.MR @Composable -expect fun ScanProtocolServer(rhId: Long?, onNext: (ServerCfg) -> Unit) +expect fun ScanProtocolServer(rhId: Long?, onNext: (UserServer) -> Unit) @Composable -fun ScanProtocolServerLayout(rhId: Long?, onNext: (ServerCfg) -> Unit) { +fun ScanProtocolServerLayout(rhId: Long?, onNext: (UserServer) -> Unit) { ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.smp_servers_scan_qr)) QRCodeScanner { text -> val res = parseServerAddress(text) if (res != null) { - onNext(ServerCfg(remoteHostId = rhId, text, false, null, false)) + onNext(UserServer(remoteHostId = rhId, null, text, false, null, false, false)) } else { AlertManager.shared.showAlertMsg( title = generalGetString(MR.strings.smp_servers_invalid_address), diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 7236b22563..3c1ede1d23 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -109,6 +109,16 @@ Invalid display name! This display name is invalid. Please choose another name. Error switching profile! + Error saving servers + No message servers. + No servers to receive messages. + No servers for private message routing. + No media & file servers. + No servers to send files. + No servers to receive files. + For chat profile %s: + Errors in servers configuration. + Error accepting conditions Connection timeout @@ -750,6 +760,7 @@ Some servers failed the test: Scan server QR code Enter server manually + New server Preset server Your server Your server address @@ -1038,6 +1049,19 @@ Random passphrase is stored in settings as plaintext.\nYou can change it later. Use random passphrase + + Choose operators + Network operators + When more than one network operator is enabled, the app will use the servers of different operators for each conversation. + For example, if you receive messages via SimpleX Chat server, the app will use one of Flux servers for private routing. + Select network operators to use. + You can configure servers via settings. + Conditions will be accepted for enabled operators after 30 days. + You can configure operators in Network & servers settings. + Review later + Update + Continue + Incoming video call Incoming audio call @@ -1667,6 +1691,59 @@ Save group profile Error saving group profile + + Preset servers + Review conditions + Accepted conditions + Conditions will be automatically accepted for enabled operators on: %s. + Your servers + %s
.]]>
+ %s.]]> + + + Operator + %s servers + Network operator + Website + Conditions accepted on: %s. + Conditions will be accepted on: %s. + Operator + Use servers + Use %s + Current conditions text couldn\'t be loaded, you can review conditions via this link: + %s.]]> + %s.]]> + %s.]]> + %s.]]> + %s.]]> + %s.]]> + %s.]]> + %s.]]> + View conditions + Accept conditions + Conditions of use + %s, accept conditions of use.]]> + Use for messages + To receive + For private routing + Added message servers + Use for files + To send + The servers for new files of your current chat profile + Added media & file servers + Open conditions + Open changes + + + Error updating server + Server protocol changed. + Server operator changed. + + + Operator server + Server added to operator %s. + Error adding server + TCP connection Reset to defaults @@ -2059,6 +2136,13 @@ Better message dates. Forward up to 20 messages at once. Delete or moderate up to 200 messages. + Network decentralization + The second preset operator in the app! + Enable flux + for better metadata privacy + Improved chat navigation + - Open chat on the first unread message.\n- Jump to quoted messages. + View updated conditions seconds diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/flux_logo@4x.png b/apps/multiplatform/common/src/commonMain/resources/MR/images/flux_logo@4x.png new file mode 100644 index 0000000000000000000000000000000000000000..87f1373d750bece6fbbd37fcdb4b115deaaeffbe GIT binary patch literal 34876 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

PL9g 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 F{C_Q?zn%a9 literal 0 HcmV?d00001 diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/flux_logo_light@4x.png b/apps/multiplatform/common/src/commonMain/resources/MR/images/flux_logo_light@4x.png new file mode 100644 index 0000000000000000000000000000000000000000..e1d6dda4fedd57195f0ee425a0a28a579f16b266 GIT binary patch literal 33847 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�>Y9E5G~yEbTaSm_@QckJ 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 E0Eh^ \ No newline at end of file diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/ScanProtocolServer.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/ScanProtocolServer.desktop.kt deleted file mode 100644 index 2d436dbbf0..0000000000 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/ScanProtocolServer.desktop.kt +++ /dev/null @@ -1,9 +0,0 @@ -package chat.simplex.common.views.usersettings - -import androidx.compose.runtime.Composable -import chat.simplex.common.model.ServerCfg - -@Composable -actual fun ScanProtocolServer(rhId: Long?, onNext: (ServerCfg) -> Unit) { - ScanProtocolServerLayout(rhId, onNext) -} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ScanProtocolServer.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ScanProtocolServer.desktop.kt new file mode 100644 index 0000000000..7d6f305a83 --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ScanProtocolServer.desktop.kt @@ -0,0 +1,9 @@ +package chat.simplex.common.views.usersettings.networkAndServers + +import androidx.compose.runtime.Composable +import chat.simplex.common.model.UserServer + +@Composable +actual fun ScanProtocolServer(rhId: Long?, onNext: (UserServer) -> Unit) { + ScanProtocolServerLayout(rhId, onNext) +} From b5170684adf312b914d95a403099f92e228b8ea7 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Fri, 22 Nov 2024 19:21:21 +0400 Subject: [PATCH 050/167] android: fix single operator conditions paddings --- .../views/usersettings/networkAndServers/OperatorView.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt index cb02745511..df6122024f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt @@ -556,7 +556,7 @@ private fun SingleOperatorUsageConditionsView( AppBarTitle(String.format(stringResource(MR.strings.use_servers_of_operator_x), operator.tradeName), enableAlphaChanges = false, withPadding = false) if (operator.conditionsAcceptance is ConditionsAcceptance.Accepted) { // In current UI implementation this branch doesn't get shown - as conditions can't be opened from inside operator once accepted - Column(modifier = Modifier.weight(1f).padding(end = DEFAULT_PADDING, start = DEFAULT_PADDING, bottom = DEFAULT_PADDING)) { + Column(modifier = Modifier.weight(1f).padding(top = DEFAULT_PADDING_HALF, bottom = DEFAULT_PADDING)) { ConditionsTextView(rhId) } } else if (operatorsWithConditionsAccepted.isNotEmpty()) { @@ -579,7 +579,7 @@ private fun SingleOperatorUsageConditionsView( args = operator.legalName_ ) ConditionsAppliedToOtherOperatorsText(userServers = userServers.value, operatorIndex = operatorIndex) - Column(modifier = Modifier.weight(1f).padding(end = DEFAULT_PADDING, start = DEFAULT_PADDING, bottom = DEFAULT_PADDING)) { + Column(modifier = Modifier.weight(1f).padding(top = DEFAULT_PADDING_HALF, bottom = DEFAULT_PADDING)) { ConditionsTextView(rhId) } AcceptConditionsButton(close) From 2adfa0c18b877457c2f68b295ef88e292035a338 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Fri, 22 Nov 2024 19:33:49 +0400 Subject: [PATCH 051/167] android: information icon right of operator logo --- .../views/usersettings/networkAndServers/OperatorView.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt index df6122024f..121b6535c3 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt @@ -141,7 +141,14 @@ fun OperatorViewLayout( Column { SectionView(generalGetString(MR.strings.operator).uppercase()) { SectionItemView({ ModalManager.start.showModalCloseable { _ -> OperatorInfoView(operator) } }) { - Image(painterResource(operator.largeLogo), null, Modifier.height(48.dp)) + Row( + Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Image(painterResource(operator.largeLogo), null, Modifier.height(48.dp)) + Spacer(Modifier.fillMaxWidth().weight(1f)) + Icon(painterResource(MR.images.ic_info), null, Modifier.size(28.dp), tint = MaterialTheme.colors.primary) + } } UseOperatorToggle( scope = scope, From bff2d7d3b6230d138e14f85e2381a66c5f2fd484 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Fri, 22 Nov 2024 22:34:43 +0700 Subject: [PATCH 052/167] android, desktop: highlight quoted messaged on click to scroll to it (#5229) --- .../simplex/common/views/chat/ChatView.kt | 25 ++++++++++++++++--- .../common/views/chat/item/ChatItemView.kt | 16 +++++++++++- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index 8dd3e42440..2aee98acba 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -980,8 +980,9 @@ fun BoxScope.ChatItemsList( } val chatInfoUpdated = rememberUpdatedState(chatInfo) + val highlightedItems = remember { mutableStateOf(setOf()) } val scope = rememberCoroutineScope() - val scrollToItem: (Long) -> Unit = remember { scrollToItem(loadingMoreItems, chatInfoUpdated, maxHeight, scope, reversedChatItems, mergedItems, listState, loadMessages) } + val scrollToItem: (Long) -> Unit = remember { scrollToItem(loadingMoreItems, highlightedItems, chatInfoUpdated, maxHeight, scope, reversedChatItems, mergedItems, listState, loadMessages) } LoadLastItems(loadingMoreItems, remoteHostId, chatInfo) SmallScrollOnNewMessage(listState, chatModel.chatItems) @@ -1031,7 +1032,17 @@ fun BoxScope.ChatItemsList( tryOrShowError("${cItem.id}ChatItem", error = { CIBrokenComposableView(if (cItem.chatDir.sent) Alignment.CenterEnd else Alignment.CenterStart) }) { - ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, range = range, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, reveal = reveal, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp) + val highlighted = remember { derivedStateOf { highlightedItems.value.contains(cItem.id) } } + LaunchedEffect(Unit) { + snapshotFlow { highlighted.value } + .distinctUntilChanged() + .filter { it } + .collect { + delay(500) + highlightedItems.value = setOf() + } + } + ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, highlighted = highlighted, range = range, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, reveal = reveal, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp) } } @@ -1810,6 +1821,7 @@ private fun lastFullyVisibleIemInListState(topPaddingToContentPx: State, de private fun scrollToItem( loadingMoreItems: MutableState, + highlightedItems: MutableState>, chatInfo: State, maxHeight: State, scope: CoroutineScope, @@ -1840,8 +1852,13 @@ private fun scrollToItem( index = mergedItems.value.indexInParentItems[itemId] ?: -1 } if (index != -1) { - withContext(scope.coroutineContext) { - listState.value.animateScrollToItem(min(reversedChatItems.value.lastIndex, index + 1), -maxHeight.value) + if (listState.value.layoutInfo.visibleItemsInfo.any { it.index == index && it.offset + it.size <= maxHeight.value }) { + highlightedItems.value = setOf(itemId) + } else { + withContext(scope.coroutineContext) { + listState.value.animateScrollToItem(min(reversedChatItems.value.lastIndex, index + 1), -maxHeight.value) + highlightedItems.value = setOf(itemId) + } } } } finally { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt index 5096992c29..f0c85736af 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt @@ -2,6 +2,8 @@ package chat.simplex.common.views.chat.item import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.* +import androidx.compose.foundation.interaction.HoverInteraction +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.* import androidx.compose.material.* @@ -57,6 +59,7 @@ fun ChatItemView( useLinkPreviews: Boolean, linkMode: SimplexLinkMode, revealed: State, + highlighted: State, range: State, selectedChatItems: MutableState?>, fillMaxWidth: Boolean = true, @@ -135,10 +138,19 @@ fun ChatItemView( } Column(horizontalAlignment = if (cItem.chatDir.sent) Alignment.End else Alignment.Start) { + val interactionSource = remember { MutableInteractionSource() } + val enterInteraction = remember { HoverInteraction.Enter() } + KeyChangeEffect(highlighted.value) { + if (highlighted.value) { + interactionSource.emit(enterInteraction) + } else { + interactionSource.emit(HoverInteraction.Exit(enterInteraction)) + } + } Column( Modifier .clipChatItem(cItem, itemSeparation.largeGap, revealed.value) - .combinedClickable(onLongClick = { showMenu.value = true }, onClick = onClick) + .combinedClickable(onLongClick = { showMenu.value = true }, onClick = onClick, interactionSource = interactionSource, indication = LocalIndication.current) .onRightClick { showMenu.value = true }, ) { @Composable @@ -1064,6 +1076,7 @@ fun PreviewChatItemView( linkMode = SimplexLinkMode.DESCRIPTION, composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) }, revealed = remember { mutableStateOf(false) }, + highlighted = remember { mutableStateOf(false) }, range = remember { mutableStateOf(0..1) }, selectedChatItems = remember { mutableStateOf(setOf()) }, selectChatItem = {}, @@ -1106,6 +1119,7 @@ fun PreviewChatItemViewDeletedContent() { linkMode = SimplexLinkMode.DESCRIPTION, composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) }, revealed = remember { mutableStateOf(false) }, + highlighted = remember { mutableStateOf(false) }, range = remember { mutableStateOf(0..1) }, selectedChatItems = remember { mutableStateOf(setOf()) }, selectChatItem = {}, From e47b16f3b4b876bc4ab88ba954fb3de1d633876a Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Fri, 22 Nov 2024 19:40:41 +0400 Subject: [PATCH 053/167] android: improve layout of operator logo --- .../views/usersettings/networkAndServers/OperatorView.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt index 121b6535c3..6f90836e89 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt @@ -147,11 +147,12 @@ fun OperatorViewLayout( ) { Image(painterResource(operator.largeLogo), null, Modifier.height(48.dp)) Spacer(Modifier.fillMaxWidth().weight(1f)) - Icon(painterResource(MR.images.ic_info), null, Modifier.size(28.dp), tint = MaterialTheme.colors.primary) + Box(Modifier.padding(horizontal = 2.dp)) { + Icon(painterResource(MR.images.ic_info), null, Modifier.size(28.dp), tint = MaterialTheme.colors.primary) + } } } UseOperatorToggle( - scope = scope, currUserServers = currUserServers, userServers = userServers, serverErrors = serverErrors, @@ -436,7 +437,6 @@ private fun OperatorInfoView(serverOperator: ServerOperator) { @Composable private fun UseOperatorToggle( - scope: CoroutineScope, currUserServers: MutableState>, userServers: MutableState>, serverErrors: MutableState>, From a6f5ba541b88f4479dd62761b5ebff40c6556e9f Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Fri, 22 Nov 2024 16:43:10 +0000 Subject: [PATCH 054/167] android, desktop: smaller info icon, corrections --- .../kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt | 2 +- .../common/views/usersettings/networkAndServers/OperatorView.kt | 2 +- .../common/src/commonMain/resources/MR/base/strings.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt index 6cf945bcba..e20a56c407 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt @@ -724,7 +724,7 @@ private val versionDescriptions: List = listOf( ), ), VersionDescription( - version = "v6.2 (beta.1)", + version = "v6.2-beta.1", post = "https://simplex.chat/blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.html", features = listOf( VersionFeature.FeatureView( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt index 6f90836e89..c61a9f5ef7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt @@ -148,7 +148,7 @@ fun OperatorViewLayout( Image(painterResource(operator.largeLogo), null, Modifier.height(48.dp)) Spacer(Modifier.fillMaxWidth().weight(1f)) Box(Modifier.padding(horizontal = 2.dp)) { - Icon(painterResource(MR.images.ic_info), null, Modifier.size(28.dp), tint = MaterialTheme.colors.primary) + Icon(painterResource(MR.images.ic_info), null, Modifier.size(24.dp), tint = MaterialTheme.colors.primaryVariant) } } } diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 3c1ede1d23..8b379d8212 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -2139,7 +2139,7 @@ Network decentralization The second preset operator in the app! Enable flux - for better metadata privacy + for better metadata privacy. Improved chat navigation - Open chat on the first unread message.\n- Jump to quoted messages. View updated conditions From 76aedb4a15f8ab3dd62b47146ad6e85744369868 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Fri, 22 Nov 2024 17:21:05 +0000 Subject: [PATCH 055/167] core: update simplexmq --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cabal.project b/cabal.project index cfa9099517..793fc18952 100644 --- a/cabal.project +++ b/cabal.project @@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 5e6fa7fb94ca54e3d6d68aa83e403f1182197081 + tag: 97104988a307bd27b8bf5da7ed67455f3531d7ae source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index a92a2b5ca5..e3985379d0 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."5e6fa7fb94ca54e3d6d68aa83e403f1182197081" = "1jwrk60nw9h8f5zxbcj57ybqdnfmchq65c07xybifcycid0016l3"; + "https://github.com/simplex-chat/simplexmq.git"."97104988a307bd27b8bf5da7ed67455f3531d7ae" = "1xhk8cg4338d0cfjhdm2460p6nbvxfra80qnab2607nvy8wpddvl"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; From 9b71702ac8c07d985c1fe83b2c3e56e64944dc0f Mon Sep 17 00:00:00 2001 From: Evgeny Date: Fri, 22 Nov 2024 18:19:49 +0000 Subject: [PATCH 056/167] ios: move onboarding action cards, paddings (#5231) --- .../Shared/Views/ChatList/ChatListView.swift | 26 ++++++++++--------- .../Shared/Views/ChatList/OneHandUICard.swift | 1 - .../Onboarding/AddressCreationCard.swift | 1 - 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift index 6da17fb312..b18e9295b9 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift @@ -304,18 +304,6 @@ struct ChatListView: View { .padding(.top, oneHandUI ? 8 : 0) .id("searchBar") } - if !oneHandUICardShown { - OneHandUICard() - .scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center) - .listRowSeparator(.hidden) - .listRowBackground(Color.clear) - } - if !addressCreationCardShown { - AddressCreationCard() - .scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center) - .listRowSeparator(.hidden) - .listRowBackground(Color.clear) - } if #available(iOS 16.0, *) { ForEach(cs, id: \.viewId) { chat in ChatListNavLink(chat: chat) @@ -341,6 +329,20 @@ struct ChatListView: View { .disabled(chatModel.chatRunning != true || chatModel.deletedChats.contains(chat.chatInfo.id)) } } + if !oneHandUICardShown { + OneHandUICard() + .padding(.vertical, 6) + .scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center) + .listRowSeparator(.hidden) + .listRowBackground(Color.clear) + } + if !addressCreationCardShown { + AddressCreationCard() + .padding(.vertical, 6) + .scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center) + .listRowSeparator(.hidden) + .listRowBackground(Color.clear) + } } .listStyle(.plain) .onChange(of: chatModel.chatId) { currentChatId in diff --git a/apps/ios/Shared/Views/ChatList/OneHandUICard.swift b/apps/ios/Shared/Views/ChatList/OneHandUICard.swift index 636d165114..059f24cc82 100644 --- a/apps/ios/Shared/Views/ChatList/OneHandUICard.swift +++ b/apps/ios/Shared/Views/ChatList/OneHandUICard.swift @@ -32,7 +32,6 @@ struct OneHandUICard: View { .background(theme.appColors.sentMessage) .cornerRadius(12) .frame(height: dynamicSize(userFont).rowHeight) - .padding(.vertical, 12) .alert(isPresented: $showOneHandUIAlert) { Alert( title: Text("Reachable chat toolbar"), diff --git a/apps/ios/Shared/Views/Onboarding/AddressCreationCard.swift b/apps/ios/Shared/Views/Onboarding/AddressCreationCard.swift index e9a8fedaf9..eae64e4465 100644 --- a/apps/ios/Shared/Views/Onboarding/AddressCreationCard.swift +++ b/apps/ios/Shared/Views/Onboarding/AddressCreationCard.swift @@ -65,7 +65,6 @@ struct AddressCreationCard: View { .background(theme.appColors.sentMessage) .cornerRadius(12) .frame(height: dynamicSize(userFont).rowHeight) - .padding(.vertical, 12) .alert(isPresented: $showAddressCreationAlert) { Alert( title: Text("SimpleX address"), From 4f640c96d14e5d5005d8ba4694953e4c413c9ee9 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Fri, 22 Nov 2024 18:38:49 +0000 Subject: [PATCH 057/167] build: use openssl 3.0 (#5183) * build: use openssl 3.0 * docs * mac script --- .github/workflows/build.yml | 16 ++++++++-------- docs/CLI.md | 2 +- docs/CONTRIBUTING.md | 4 ++-- docs/lang/cs/CLI.md | 2 +- docs/lang/cs/CONTRIBUTING.md | 4 ++-- docs/lang/fr/CLI.md | 2 +- docs/lang/fr/CONTRIBUTING.md | 4 ++-- docs/lang/pl/CLI.md | 2 +- docs/lang/pl/CONTRIBUTING.md | 4 ++-- scripts/cabal.project.local.mac | 8 ++++---- scripts/desktop/build-lib-mac.sh | 14 +++++++------- scripts/desktop/build-lib-windows.sh | 4 ++-- scripts/desktop/prepare-openssl-windows.sh | 6 +++--- 13 files changed, 36 insertions(+), 36 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6ad4f12ef9..21fea3fe8b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -124,12 +124,12 @@ jobs: run: | echo "ignore-project: False" >> cabal.project.local echo "package simplexmq" >> cabal.project.local - echo " extra-include-dirs: /opt/homebrew/opt/openssl@1.1/include" >> cabal.project.local - echo " extra-lib-dirs: /opt/homebrew/opt/openssl@1.1/lib" >> cabal.project.local + echo " extra-include-dirs: /opt/homebrew/opt/openssl@3.0/include" >> cabal.project.local + echo " extra-lib-dirs: /opt/homebrew/opt/openssl@3.0/lib" >> cabal.project.local echo "" >> cabal.project.local echo "package direct-sqlcipher" >> cabal.project.local - echo " extra-include-dirs: /opt/homebrew/opt/openssl@1.1/include" >> cabal.project.local - echo " extra-lib-dirs: /opt/homebrew/opt/openssl@1.1/lib" >> cabal.project.local + echo " extra-include-dirs: /opt/homebrew/opt/openssl@3.0/include" >> cabal.project.local + echo " extra-lib-dirs: /opt/homebrew/opt/openssl@3.0/lib" >> cabal.project.local echo " flags: +openssl" >> cabal.project.local - name: Unix prepare cabal.project.local for Mac @@ -138,12 +138,12 @@ jobs: run: | echo "ignore-project: False" >> cabal.project.local echo "package simplexmq" >> cabal.project.local - echo " extra-include-dirs: /usr/local/opt/openssl@1.1/include" >> cabal.project.local - echo " extra-lib-dirs: /usr/local/opt/openssl@1.1/lib" >> cabal.project.local + echo " extra-include-dirs: /usr/local/opt/openssl@3.0/include" >> cabal.project.local + echo " extra-lib-dirs: /usr/local/opt/openssl@3.0/lib" >> cabal.project.local echo "" >> cabal.project.local echo "package direct-sqlcipher" >> cabal.project.local - echo " extra-include-dirs: /usr/local/opt/openssl@1.1/include" >> cabal.project.local - echo " extra-lib-dirs: /usr/local/opt/openssl@1.1/lib" >> cabal.project.local + echo " extra-include-dirs: /usr/local/opt/openssl@3.0/include" >> cabal.project.local + echo " extra-lib-dirs: /usr/local/opt/openssl@3.0/lib" >> cabal.project.local echo " flags: +openssl" >> cabal.project.local - name: Install AppImage dependencies diff --git a/docs/CLI.md b/docs/CLI.md index abc09b0e7c..6f56cf6cd3 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -134,7 +134,7 @@ cp scripts/cabal.project.local.linux cabal.project.local On Mac: ``` -brew install openssl@1.1 +brew install openssl@3.0 cp scripts/cabal.project.local.mac cabal.project.local ``` diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 493496fd3d..e7ce63ea54 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -21,9 +21,9 @@ cp scripts/cabal.project.local.mac cabal.project.local MacOS comes with LibreSSL as default, OpenSSL must be installed to compile SimpleX from source. -OpenSSL can be installed with `brew install openssl@1.1` +OpenSSL can be installed with `brew install openssl@3.0` -You will have to add `/opt/homebrew/opt/openssl@1.1/bin` to your PATH in order to have things working properly +You will have to add `/opt/homebrew/opt/openssl@3.0/bin` to your PATH in order to have things working properly ## Project branches diff --git a/docs/lang/cs/CLI.md b/docs/lang/cs/CLI.md index 338e48e57e..2477f6ea2f 100644 --- a/docs/lang/cs/CLI.md +++ b/docs/lang/cs/CLI.md @@ -117,7 +117,7 @@ git checkout stable apt-get update && apt-get install -y build-essential libgmp3-dev zlib1g-dev cp scripts/cabal.project.local.linux cabal.project.local # nebo na MacOS: -# brew install openssl@1.1 +# brew install openssl@3.0 # cp scripts/cabal.project.local.mac cabal.project.local # možná budete muset změnit cabal.project.local tak, aby ukazoval na skutečné umístění openssl cabal update diff --git a/docs/lang/cs/CONTRIBUTING.md b/docs/lang/cs/CONTRIBUTING.md index 17574bed4a..226b4d9343 100644 --- a/docs/lang/cs/CONTRIBUTING.md +++ b/docs/lang/cs/CONTRIBUTING.md @@ -20,6 +20,6 @@ cp scripts/cabal.project.local.mac cabal.project.local Systém MacOS je standardně dodáván s LibreSSL, pro kompilaci SimpleX ze zdrojových kódů je nutné nainstalovat OpenSSL. -OpenSSL lze nainstalovat pomocí `brew install openssl@1.1`. +OpenSSL lze nainstalovat pomocí `brew install openssl@3.0`. -Aby vše fungovalo správně, musíte do své cesty PATH přidat `/opt/homebrew/opt/openssl@1.1/bin`. +Aby vše fungovalo správně, musíte do své cesty PATH přidat `/opt/homebrew/opt/openssl@3.0/bin`. diff --git a/docs/lang/fr/CLI.md b/docs/lang/fr/CLI.md index e5093f20c0..58b84a0919 100644 --- a/docs/lang/fr/CLI.md +++ b/docs/lang/fr/CLI.md @@ -119,7 +119,7 @@ git checkout stable apt-get update && apt-get install -y build-essential libgmp3-dev zlib1g-dev cp scripts/cabal.project.local.linux cabal.project.local # ou sur MacOS: -# brew install openssl@1.1 +# brew install openssl@3.0 # cp scripts/cabal.project.local.mac cabal.project.local # vous devrez peut-être modifier cabal.project.local pour indiquer l'emplacement réel d'openssl cabal update diff --git a/docs/lang/fr/CONTRIBUTING.md b/docs/lang/fr/CONTRIBUTING.md index ea6dcb5ca3..1b83fc24ce 100644 --- a/docs/lang/fr/CONTRIBUTING.md +++ b/docs/lang/fr/CONTRIBUTING.md @@ -20,6 +20,6 @@ cp scripts/cabal.project.local.mac cabal.project.local LibreSSL est fourni par défaut sur MacOS, OpenSSL doit être installé pour compiler SimpleX à partir de la source. -OpenSSL peut être installé avec `brew install openssl@1.1` +OpenSSL peut être installé avec `brew install openssl@3.0` -Vous devez ajouter `/opt/homebrew/opt/openssl@1.1/bin` à votre PATH pour que tout fonctionne correctement. +Vous devez ajouter `/opt/homebrew/opt/openssl@3.0/bin` à votre PATH pour que tout fonctionne correctement. diff --git a/docs/lang/pl/CLI.md b/docs/lang/pl/CLI.md index 0a72b163bb..bc64b04415 100644 --- a/docs/lang/pl/CLI.md +++ b/docs/lang/pl/CLI.md @@ -133,7 +133,7 @@ cp scripts/cabal.project.local.linux cabal.project.local Na Macu: ``` -brew install openssl@1.1 +brew install openssl@3.0 cp scripts/cabal.project.local.mac cabal.project.local ``` diff --git a/docs/lang/pl/CONTRIBUTING.md b/docs/lang/pl/CONTRIBUTING.md index 4f62217479..5205e3c5a6 100644 --- a/docs/lang/pl/CONTRIBUTING.md +++ b/docs/lang/pl/CONTRIBUTING.md @@ -21,9 +21,9 @@ cp scripts/cabal.project.local.mac cabal.project.local MacOS ma domyślnie zainstalowany LibreSSL, OpenSSL musi być zainstalowany, aby skompilować SimpleX z kodu źródłowego. -OpenSSL można zainstalować za pomocą `brew install openssl@1.1` +OpenSSL można zainstalować za pomocą `brew install openssl@3.0` -Będziesz musiał dodać `/opt/homebrew/opt/openssl@1.1/bin` do swojego PATH, aby wszystko działało poprawnie +Będziesz musiał dodać `/opt/homebrew/opt/openssl@3.0/bin` do swojego PATH, aby wszystko działało poprawnie ## Branche projektu diff --git a/scripts/cabal.project.local.mac b/scripts/cabal.project.local.mac index dd62f1a391..6b4ff718b6 100644 --- a/scripts/cabal.project.local.mac +++ b/scripts/cabal.project.local.mac @@ -3,12 +3,12 @@ ignore-project: False -- amend to point to the actual openssl location package simplexmq - extra-include-dirs: /opt/homebrew/opt/openssl@1.1/include - extra-lib-dirs: /opt/homebrew/opt/openssl@1.1/lib + extra-include-dirs: /opt/homebrew/opt/openssl@3.0/include + extra-lib-dirs: /opt/homebrew/opt/openssl@3.0/lib package direct-sqlcipher - extra-include-dirs: /opt/homebrew/opt/openssl@1.1/include - extra-lib-dirs: /opt/homebrew/opt/openssl@1.1/lib + extra-include-dirs: /opt/homebrew/opt/openssl@3.0/include + extra-lib-dirs: /opt/homebrew/opt/openssl@3.0/lib flags: +openssl test-show-details: direct diff --git a/scripts/desktop/build-lib-mac.sh b/scripts/desktop/build-lib-mac.sh index 9d7d5031a0..070257ea5f 100755 --- a/scripts/desktop/build-lib-mac.sh +++ b/scripts/desktop/build-lib-mac.sh @@ -100,25 +100,25 @@ cp $BUILD_DIR/build/libHSsimplex-chat-*-inplace-ghc*.$LIB_EXT apps/multiplatform cd apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ LIBCRYPTO_PATH=$(otool -l libHSdrct-*.$LIB_EXT | grep libcrypto | cut -d' ' -f11) -install_name_tool -change $LIBCRYPTO_PATH @rpath/libcrypto.1.1.$LIB_EXT libHSdrct-*.$LIB_EXT -cp $LIBCRYPTO_PATH libcrypto.1.1.$LIB_EXT -chmod 755 libcrypto.1.1.$LIB_EXT -install_name_tool -id "libcrypto.1.1.$LIB_EXT" libcrypto.1.1.$LIB_EXT +install_name_tool -change $LIBCRYPTO_PATH @rpath/libcrypto.3.0.$LIB_EXT libHSdrct-*.$LIB_EXT +cp $LIBCRYPTO_PATH libcrypto.3.0.$LIB_EXT +chmod 755 libcrypto.3.0.$LIB_EXT +install_name_tool -id "libcrypto.3.0.$LIB_EXT" libcrypto.3.0.$LIB_EXT install_name_tool -id "libffi.8.$LIB_EXT" libffi.$LIB_EXT LIBCRYPTO_PATH=$(otool -l $LIB | grep libcrypto | cut -d' ' -f11) if [ -n "$LIBCRYPTO_PATH" ]; then - install_name_tool -change $LIBCRYPTO_PATH @rpath/libcrypto.1.1.$LIB_EXT $LIB + install_name_tool -change $LIBCRYPTO_PATH @rpath/libcrypto.3.0.$LIB_EXT $LIB fi LIBCRYPTO_PATH=$(otool -l libHSsmplxmq*.$LIB_EXT | grep libcrypto | cut -d' ' -f11) if [ -n "$LIBCRYPTO_PATH" ]; then - install_name_tool -change $LIBCRYPTO_PATH @rpath/libcrypto.1.1.$LIB_EXT libHSsmplxmq*.$LIB_EXT + install_name_tool -change $LIBCRYPTO_PATH @rpath/libcrypto.3.0.$LIB_EXT libHSsmplxmq*.$LIB_EXT fi LIBCRYPTO_PATH=$(otool -l libHSsqlcphr-*.$LIB_EXT | grep libcrypto | cut -d' ' -f11) if [ -n "$LIBCRYPTO_PATH" ]; then - install_name_tool -change $LIBCRYPTO_PATH @rpath/libcrypto.1.1.$LIB_EXT libHSsqlcphr-*.$LIB_EXT + install_name_tool -change $LIBCRYPTO_PATH @rpath/libcrypto.3.0.$LIB_EXT libHSsqlcphr-*.$LIB_EXT fi for lib in $(find . -type f -name "*.$LIB_EXT"); do diff --git a/scripts/desktop/build-lib-windows.sh b/scripts/desktop/build-lib-windows.sh index 0e96a42e86..d27d71c08f 100755 --- a/scripts/desktop/build-lib-windows.sh +++ b/scripts/desktop/build-lib-windows.sh @@ -36,7 +36,7 @@ mkdir dist-newstyle 2>/dev/null || true scripts/desktop/prepare-openssl-windows.sh -openssl_windows_style_path=$(echo `pwd`/dist-newstyle/openssl-1.1.1w | sed 's#/\([a-zA-Z]\)#\1:#' | sed 's#/#\\#g') +openssl_windows_style_path=$(echo `pwd`/dist-newstyle/openssl-3.0.15 | sed 's#/\([a-zA-Z]\)#\1:#' | sed 's#/#\\#g') rm -rf $BUILD_DIR 2>/dev/null || true # Existence of this directory produces build error: cabal's bug rm -rf dist-newstyle/src/direct-sq* 2>/dev/null || true @@ -57,7 +57,7 @@ rm -rf apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ rm -rf apps/multiplatform/desktop/build/cmake mkdir -p apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ -cp dist-newstyle/openssl-1.1.1w/libcrypto-1_1-x64.dll apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ +cp dist-newstyle/openssl-3.0.15/libcrypto-1_1-x64.dll apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ cp libsimplex.dll apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ scripts/desktop/prepare-vlc-windows.sh diff --git a/scripts/desktop/prepare-openssl-windows.sh b/scripts/desktop/prepare-openssl-windows.sh index d65d4b8e31..c579b8ef90 100644 --- a/scripts/desktop/prepare-openssl-windows.sh +++ b/scripts/desktop/prepare-openssl-windows.sh @@ -9,12 +9,12 @@ root_dir="$(dirname "$(dirname "$(readlink "$0")")")" cd $root_dir -if [ ! -f dist-newstyle/openssl-1.1.1w/libcrypto-1_1-x64.dll ]; then +if [ ! -f dist-newstyle/openssl-3.0.15/libcrypto-1_1-x64.dll ]; then mkdir dist-newstyle 2>/dev/null || true cd dist-newstyle - curl --tlsv1.2 https://www.openssl.org/source/openssl-1.1.1w.tar.gz -L -o openssl.tar.gz + curl --tlsv1.2 https://www.openssl.org/source/openssl-3.0.15.tar.gz -L -o openssl.tar.gz $WINDIR\\System32\\tar.exe -xvzf openssl.tar.gz - cd openssl-1.1.1w + cd openssl-3.0.15 ./Configure mingw64 make cd ../../ From bda84b08a1dcc62143c826d59ef371eebf76d014 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Sat, 23 Nov 2024 18:41:48 +0700 Subject: [PATCH 058/167] ci: fix mac & Windows build (#5232) * core: 6.2.0.1 (simplexmq 6.2.0.4) * action: fix mac build * fix Windows * version * revert version change --------- Co-authored-by: Evgeny Poberezkin --- .github/workflows/build.yml | 6 +++--- scripts/desktop/build-lib-windows.sh | 4 ++-- scripts/desktop/prepare-openssl-windows.sh | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 21fea3fe8b..c7ac10b9cf 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -150,9 +150,9 @@ jobs: if: startsWith(github.ref, 'refs/tags/v') && matrix.asset_name && matrix.os == 'ubuntu-20.04' run: sudo apt install -y desktop-file-utils - - name: Install pkg-config for Mac + - name: Install openssl for Mac if: matrix.os == 'macos-latest' || matrix.os == 'macos-13' - run: brew install pkg-config + run: brew install openssl@3.0 - name: Unix prepare cabal.project.local for Ubuntu if: matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-22.04' @@ -334,7 +334,7 @@ jobs: run: | export PATH=$PATH:/c/ghcup/bin:$(echo /c/tools/ghc-*/bin || echo) scripts/desktop/prepare-openssl-windows.sh - openssl_windows_style_path=$(echo `pwd`/dist-newstyle/openssl-1.1.1w | sed 's#/\([a-zA-Z]\)#\1:#' | sed 's#/#\\#g') + openssl_windows_style_path=$(echo `pwd`/dist-newstyle/openssl-3.0.15 | sed 's#/\([a-zA-Z]\)#\1:#' | sed 's#/#\\#g') rm cabal.project.local 2>/dev/null || true echo "ignore-project: False" >> cabal.project.local echo "package direct-sqlcipher" >> cabal.project.local diff --git a/scripts/desktop/build-lib-windows.sh b/scripts/desktop/build-lib-windows.sh index d27d71c08f..cbb886ccb3 100755 --- a/scripts/desktop/build-lib-windows.sh +++ b/scripts/desktop/build-lib-windows.sh @@ -47,7 +47,7 @@ echo " flags: +openssl" >> cabal.project.local echo " extra-include-dirs: $openssl_windows_style_path\include" >> cabal.project.local echo " extra-lib-dirs: $openssl_windows_style_path" >> cabal.project.local echo "package simplex-chat" >> cabal.project.local -echo " ghc-options: -shared -threaded -optl-L$openssl_windows_style_path -optl-lcrypto-1_1-x64 -o libsimplex.dll libsimplex.dll.def" >> cabal.project.local +echo " ghc-options: -shared -threaded -optl-L$openssl_windows_style_path -optl-lcrypto-3-x64 -o libsimplex.dll libsimplex.dll.def" >> cabal.project.local # Very important! Without it the build fails on linking step since the linker can't find exported symbols. # It looks like GHC bug because with such random path the build ends successfully sed -i "s/ld.lld.exe/abracadabra.exe/" `ghc --print-libdir`/settings @@ -57,7 +57,7 @@ rm -rf apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ rm -rf apps/multiplatform/desktop/build/cmake mkdir -p apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ -cp dist-newstyle/openssl-3.0.15/libcrypto-1_1-x64.dll apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ +cp dist-newstyle/openssl-3.0.15/libcrypto-3-x64.dll apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ cp libsimplex.dll apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ scripts/desktop/prepare-vlc-windows.sh diff --git a/scripts/desktop/prepare-openssl-windows.sh b/scripts/desktop/prepare-openssl-windows.sh index c579b8ef90..ce50ee0a74 100644 --- a/scripts/desktop/prepare-openssl-windows.sh +++ b/scripts/desktop/prepare-openssl-windows.sh @@ -9,7 +9,7 @@ root_dir="$(dirname "$(dirname "$(readlink "$0")")")" cd $root_dir -if [ ! -f dist-newstyle/openssl-3.0.15/libcrypto-1_1-x64.dll ]; then +if [ ! -f dist-newstyle/openssl-3.0.15/libcrypto-3-x64.dll ]; then mkdir dist-newstyle 2>/dev/null || true cd dist-newstyle curl --tlsv1.2 https://www.openssl.org/source/openssl-3.0.15.tar.gz -L -o openssl.tar.gz From 7bcb514baf79e215b97be4734faadd42ef7f6df5 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sat, 23 Nov 2024 11:43:52 +0000 Subject: [PATCH 059/167] core: 6.2.0.1 (simplexmq: 6.2.0.4) --- cabal.project | 2 +- package.yaml | 2 +- scripts/nix/sha256map.nix | 2 +- simplex-chat.cabal | 2 +- src/Simplex/Chat/Remote.hs | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cabal.project b/cabal.project index 793fc18952..2246cfeb1d 100644 --- a/cabal.project +++ b/cabal.project @@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 97104988a307bd27b8bf5da7ed67455f3531d7ae + tag: 601620bdde612ebdd33da2637d99b15ff32170c9 source-repository-package type: git diff --git a/package.yaml b/package.yaml index bbfc624bf9..8e45db71e2 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: simplex-chat -version: 6.2.0.0 +version: 6.2.0.1 #synopsis: #description: homepage: https://github.com/simplex-chat/simplex-chat#readme diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index e3985379d0..72d2ddd59b 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."97104988a307bd27b8bf5da7ed67455f3531d7ae" = "1xhk8cg4338d0cfjhdm2460p6nbvxfra80qnab2607nvy8wpddvl"; + "https://github.com/simplex-chat/simplexmq.git"."601620bdde612ebdd33da2637d99b15ff32170c9" = "0lgiphb9sf5i29d378pah24mhf7m8df75jk6asvw8ns527g4amj1"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 9ff84d28d7..1a65b87d0b 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplex-chat -version: 6.2.0.0 +version: 6.2.0.1 category: Web, System, Services, Cryptography homepage: https://github.com/simplex-chat/simplex-chat#readme author: simplex.chat diff --git a/src/Simplex/Chat/Remote.hs b/src/Simplex/Chat/Remote.hs index 320a35815d..f818c8ea3a 100644 --- a/src/Simplex/Chat/Remote.hs +++ b/src/Simplex/Chat/Remote.hs @@ -73,11 +73,11 @@ import UnliftIO.Directory (copyFile, createDirectoryIfMissing, doesDirectoryExis -- when acting as host minRemoteCtrlVersion :: AppVersion -minRemoteCtrlVersion = AppVersion [6, 2, 0, 0] +minRemoteCtrlVersion = AppVersion [6, 2, 0, 1] -- when acting as controller minRemoteHostVersion :: AppVersion -minRemoteHostVersion = AppVersion [6, 2, 0, 0] +minRemoteHostVersion = AppVersion [6, 2, 0, 1] currentAppVersion :: AppVersion currentAppVersion = AppVersion SC.version From 30a24df9c03d327c8e36bfa15ac38baeb7158379 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Sat, 23 Nov 2024 14:37:44 +0000 Subject: [PATCH 060/167] ui: update whats new link (#5234) * ui: update whats new link * fix file name --- .../Views/Onboarding/WhatsNewView.swift | 8 ++--- .../common/views/onboarding/WhatsNewView.kt | 2 +- ...vacy-and-decentralization-for-all-users.md | 30 ++++++++++++++++++ blog/images/simplexonflux.png | Bin 0 -> 64801 bytes 4 files changed, 35 insertions(+), 5 deletions(-) create mode 100644 blog/20241125-servers-operated-by-flux-true-privacy-and-decentralization-for-all-users.md create mode 100644 blog/images/simplexonflux.png diff --git a/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift b/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift index c1c2cb8383..0a3ef05029 100644 --- a/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift +++ b/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift @@ -521,7 +521,7 @@ private let versionDescriptions: [VersionDescription] = [ ), VersionDescription( version: "v6.2 (beta.1)", - post: URL(string: "https://simplex.chat/blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.html"), + post: URL(string: "https://simplex.chat/blog/20241125-servers-operated-by-flux-true-privacy-and-decentralization-for-all-users.html"), features: [ .view(FeatureView( icon: nil, @@ -529,9 +529,9 @@ private let versionDescriptions: [VersionDescription] = [ view: { NewOperatorsView() } )), .feature(Description( - icon: "text.quote", - title: "Improved chat navigation", - description: "- Open chat on the first unread message.\n- Jump to quoted messages." + icon: "bolt", + title: "More reliable notifications", + description: "They are delivered even when Apple drops them." )), ] ) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt index e20a56c407..8eb89931b3 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt @@ -725,7 +725,7 @@ private val versionDescriptions: List = listOf( ), VersionDescription( version = "v6.2-beta.1", - post = "https://simplex.chat/blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.html", + post = "https://simplex.chat/blog/20241125-servers-operated-by-flux-true-privacy-and-decentralization-for-all-users.html", features = listOf( VersionFeature.FeatureView( icon = null, diff --git a/blog/20241125-servers-operated-by-flux-true-privacy-and-decentralization-for-all-users.md b/blog/20241125-servers-operated-by-flux-true-privacy-and-decentralization-for-all-users.md new file mode 100644 index 0000000000..cb5db41c88 --- /dev/null +++ b/blog/20241125-servers-operated-by-flux-true-privacy-and-decentralization-for-all-users.md @@ -0,0 +1,30 @@ +--- +layout: layouts/article.html +title: "Servers operated by Flux - true privacy and decentralization for all users" +date: 2024-11-25 +# previewBody: blog_previews/20241125.html +image: images/simplexonflux.png +imageWide: true +draft: true +permalink: "/blog/20241125-servers-operated-by-flux-true-privacy-and-decentralization-for-all-users.html" +--- + +# Servers operated by Flux - true privacy and decentralization for all users + +**Will be published:** Nov 25, 2024 + +- [Welcome, Flux](#welcome-flux--the-new-servers-in-v62-beta1) - the new servers in v6.2-beta.1! +- What's the problem? +- Several operators improve connection privacy. +- SimpleX decentralization compared with Matrix, Session and Tor. +- What is next? + +## Welcome, Flux – the new servers in v6.2-beta.1! + + + +[Flux](https://runonflux.com) is a decentralized cloud infrastructure that consists of user-operated nodes. + +With v6.2 release all SimpleX Chat users can use pre-configured Flux servers to improve metadata privacy and decentralization. + +Come back on Monday November 25th to learn why it is important and how having several operators improves metadata privacy. diff --git a/blog/images/simplexonflux.png b/blog/images/simplexonflux.png new file mode 100644 index 0000000000000000000000000000000000000000..dad3f480f640513e8a92e6a51319be5fccb160e2 GIT binary patch literal 64801 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 literal 0 HcmV?d00001 From 909edac64fb9099c5abe5d0e8d8c736dfc80eceb Mon Sep 17 00:00:00 2001 From: Diogo Date: Sat, 23 Nov 2024 14:42:25 +0000 Subject: [PATCH 061/167] desktop: unsaved changes popup for network and servers when clicking middle lane (#5230) * Revert "Revert "handle click when have unsaved changes"" This reverts commit ba53cc63c6a860ca8802fe726c51bf172410a2e5. * fix in children view * unsaved changes for network and children * don't close all modals when pressing back * explicit param --------- Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> --- .../kotlin/chat/simplex/common/App.kt | 6 ++- .../chat/simplex/common/model/ChatModel.kt | 3 ++ .../networkAndServers/NetworkAndServers.kt | 39 +++++++++++++------ 3 files changed, 34 insertions(+), 14 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt index b1ce003812..fc17c49c7e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt @@ -431,8 +431,10 @@ fun DesktopScreen(userPickerState: MutableStateFlow) { .fillMaxSize() .padding(start = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier) .clickable(interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = { - ModalManager.start.closeModals() - userPickerState.value = AnimatedViewState.HIDING + if (chatModel.centerPanelBackgroundClickHandler == null || chatModel.centerPanelBackgroundClickHandler?.invoke() == false) { + ModalManager.start.closeModals() + userPickerState.value = AnimatedViewState.HIDING + } }) ) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index e501ed5a91..ca03d0ce72 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -167,6 +167,9 @@ object ChatModel { val processedCriticalError: ProcessedErrors = ProcessedErrors(60_000) val processedInternalError: ProcessedErrors = ProcessedErrors(20_000) + // return true if you handled the click + var centerPanelBackgroundClickHandler: (() -> Boolean)? = null + fun getUser(userId: Long): User? = if (currentUser.value?.userId == userId) { currentUser.value } else { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NetworkAndServers.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NetworkAndServers.kt index ef5b82a5d9..6b1dcec5d8 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NetworkAndServers.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NetworkAndServers.kt @@ -39,10 +39,10 @@ import chat.simplex.common.views.onboarding.OnboardingActionButton import chat.simplex.common.views.onboarding.ReadableText import chat.simplex.common.views.usersettings.* import chat.simplex.res.MR -import kotlinx.coroutines.launch +import kotlinx.coroutines.* @Composable -fun ModalData.NetworkAndServersView(close: () -> Unit) { +fun ModalData.NetworkAndServersView(closeNetworkAndServers: () -> Unit) { val currentRemoteHost by remember { chatModel.currentRemoteHost } // It's not a state, just a one-time value. Shouldn't be used in any state-related situations val netCfg = remember { chatModel.controller.getNetCfg() } @@ -50,21 +50,36 @@ fun ModalData.NetworkAndServersView(close: () -> Unit) { val currUserServers = remember { stateGetOrPut("currUserServers") { emptyList() } } val userServers = remember { stateGetOrPut("userServers") { emptyList() } } val serverErrors = remember { stateGetOrPut("serverErrors") { emptyList() } } - val scope = rememberCoroutineScope() val proxyPort = remember { derivedStateOf { appPrefs.networkProxy.state.value.port } } - ModalView( - close = { - if (!serversCanBeSaved(currUserServers.value, userServers.value, serverErrors.value)) { + fun onClose(close: () -> Unit): Boolean = if (!serversCanBeSaved(currUserServers.value, userServers.value, serverErrors.value)) { + chatModel.centerPanelBackgroundClickHandler = null + close() + false + } else { + showUnsavedChangesAlert( + { + CoroutineScope(Dispatchers.Default).launch { + saveServers(currentRemoteHost?.remoteHostId, currUserServers, userServers) + chatModel.centerPanelBackgroundClickHandler = null + close() + } + }, + { + chatModel.centerPanelBackgroundClickHandler = null close() - } else { - showUnsavedChangesAlert( - { scope.launch { saveServers(currentRemoteHost?.remoteHostId, currUserServers, userServers) }}, - close - ) } + ) + true + } + + LaunchedEffect(Unit) { + // Enables unsaved changes alert on this view and all children views. + chatModel.centerPanelBackgroundClickHandler = { + onClose(close = { ModalManager.start.closeModals() }) } - ) { + } + ModalView(close = { onClose(closeNetworkAndServers) }) { NetworkAndServersLayout( currentRemoteHost = currentRemoteHost, networkUseSocksProxy = networkUseSocksProxy, From 6581e27524ed2f2ca5b76f6b7214fbf707bdeef8 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sat, 23 Nov 2024 17:42:24 +0000 Subject: [PATCH 062/167] 6.2-beta.1: ios 247, android 252, desktop 76 --- .../Views/Onboarding/WhatsNewView.swift | 2 +- apps/ios/SimpleX.xcodeproj/project.pbxproj | 36 +++++++++---------- apps/multiplatform/gradle.properties | 8 ++--- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift b/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift index 0a3ef05029..92b2820681 100644 --- a/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift +++ b/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift @@ -531,7 +531,7 @@ private let versionDescriptions: [VersionDescription] = [ .feature(Description( icon: "bolt", title: "More reliable notifications", - description: "They are delivered even when Apple drops them." + description: "Delivered even when Apple drops them." )), ] ) diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 8dc195e17f..4f03ced132 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -149,11 +149,11 @@ 6419EC562AB8BC8B004A607A /* ContextInvitingContactMemberView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6419EC552AB8BC8B004A607A /* ContextInvitingContactMemberView.swift */; }; 6419EC582AB97507004A607A /* CIMemberCreatedContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6419EC572AB97507004A607A /* CIMemberCreatedContactView.swift */; }; 642BA82D2CE50495005E9412 /* NewServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 642BA82C2CE50495005E9412 /* NewServerView.swift */; }; - 642BA8332CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.0-1FOg7s6V4oE9PrQV6sHPkH.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 642BA82E2CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.0-1FOg7s6V4oE9PrQV6sHPkH.a */; }; + 642BA8332CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.1-3FFlorLJSLlCbWWiG2Vp14.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 642BA82E2CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.1-3FFlorLJSLlCbWWiG2Vp14.a */; }; 642BA8342CEB3D4B005E9412 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 642BA82F2CEB3D4B005E9412 /* libffi.a */; }; 642BA8352CEB3D4B005E9412 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 642BA8302CEB3D4B005E9412 /* libgmp.a */; }; 642BA8362CEB3D4B005E9412 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 642BA8312CEB3D4B005E9412 /* libgmpxx.a */; }; - 642BA8372CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.0-1FOg7s6V4oE9PrQV6sHPkH-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 642BA8322CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.0-1FOg7s6V4oE9PrQV6sHPkH-ghc9.6.3.a */; }; + 642BA8372CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.1-3FFlorLJSLlCbWWiG2Vp14-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 642BA8322CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.1-3FFlorLJSLlCbWWiG2Vp14-ghc9.6.3.a */; }; 6432857C2925443C00FBE5C8 /* GroupPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */; }; 643B3B4E2CCFD6400083A2CF /* OperatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 643B3B4D2CCFD6400083A2CF /* OperatorView.swift */; }; 6440CA00288857A10062C672 /* CIEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440C9FF288857A10062C672 /* CIEventView.swift */; }; @@ -496,11 +496,11 @@ 6419EC552AB8BC8B004A607A /* ContextInvitingContactMemberView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextInvitingContactMemberView.swift; sourceTree = ""; }; 6419EC572AB97507004A607A /* CIMemberCreatedContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIMemberCreatedContactView.swift; sourceTree = ""; }; 642BA82C2CE50495005E9412 /* NewServerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewServerView.swift; sourceTree = ""; }; - 642BA82E2CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.0-1FOg7s6V4oE9PrQV6sHPkH.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.2.0.0-1FOg7s6V4oE9PrQV6sHPkH.a"; sourceTree = ""; }; + 642BA82E2CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.1-3FFlorLJSLlCbWWiG2Vp14.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.2.0.1-3FFlorLJSLlCbWWiG2Vp14.a"; sourceTree = ""; }; 642BA82F2CEB3D4B005E9412 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; 642BA8302CEB3D4B005E9412 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; 642BA8312CEB3D4B005E9412 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; - 642BA8322CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.0-1FOg7s6V4oE9PrQV6sHPkH-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.2.0.0-1FOg7s6V4oE9PrQV6sHPkH-ghc9.6.3.a"; sourceTree = ""; }; + 642BA8322CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.1-3FFlorLJSLlCbWWiG2Vp14-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.2.0.1-3FFlorLJSLlCbWWiG2Vp14-ghc9.6.3.a"; sourceTree = ""; }; 6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupPreferencesView.swift; sourceTree = ""; }; 643B3B4D2CCFD6400083A2CF /* OperatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperatorView.swift; sourceTree = ""; }; 6440C9FF288857A10062C672 /* CIEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIEventView.swift; sourceTree = ""; }; @@ -670,8 +670,8 @@ CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */, 642BA8342CEB3D4B005E9412 /* libffi.a in Frameworks */, 642BA8352CEB3D4B005E9412 /* libgmp.a in Frameworks */, - 642BA8372CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.0-1FOg7s6V4oE9PrQV6sHPkH-ghc9.6.3.a in Frameworks */, - 642BA8332CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.0-1FOg7s6V4oE9PrQV6sHPkH.a in Frameworks */, + 642BA8372CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.1-3FFlorLJSLlCbWWiG2Vp14-ghc9.6.3.a in Frameworks */, + 642BA8332CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.1-3FFlorLJSLlCbWWiG2Vp14.a in Frameworks */, 642BA8362CEB3D4B005E9412 /* libgmpxx.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -752,8 +752,8 @@ 642BA82F2CEB3D4B005E9412 /* libffi.a */, 642BA8302CEB3D4B005E9412 /* libgmp.a */, 642BA8312CEB3D4B005E9412 /* libgmpxx.a */, - 642BA8322CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.0-1FOg7s6V4oE9PrQV6sHPkH-ghc9.6.3.a */, - 642BA82E2CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.0-1FOg7s6V4oE9PrQV6sHPkH.a */, + 642BA8322CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.1-3FFlorLJSLlCbWWiG2Vp14-ghc9.6.3.a */, + 642BA82E2CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.1-3FFlorLJSLlCbWWiG2Vp14.a */, ); path = Libraries; sourceTree = ""; @@ -1927,7 +1927,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 246; + CURRENT_PROJECT_VERSION = 247; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -1976,7 +1976,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 246; + CURRENT_PROJECT_VERSION = 247; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -2017,7 +2017,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 246; + CURRENT_PROJECT_VERSION = 247; DEVELOPMENT_TEAM = 5NN7GUYB6T; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; @@ -2037,7 +2037,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 246; + CURRENT_PROJECT_VERSION = 247; DEVELOPMENT_TEAM = 5NN7GUYB6T; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; @@ -2062,7 +2062,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 246; + CURRENT_PROJECT_VERSION = 247; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GCC_OPTIMIZATION_LEVEL = s; @@ -2099,7 +2099,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 246; + CURRENT_PROJECT_VERSION = 247; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_CODE_COVERAGE = NO; @@ -2136,7 +2136,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 246; + CURRENT_PROJECT_VERSION = 247; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -2187,7 +2187,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 246; + CURRENT_PROJECT_VERSION = 247; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -2238,7 +2238,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 246; + CURRENT_PROJECT_VERSION = 247; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -2272,7 +2272,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 246; + CURRENT_PROJECT_VERSION = 247; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; diff --git a/apps/multiplatform/gradle.properties b/apps/multiplatform/gradle.properties index e105e61dde..b2d7875074 100644 --- a/apps/multiplatform/gradle.properties +++ b/apps/multiplatform/gradle.properties @@ -24,11 +24,11 @@ android.nonTransitiveRClass=true kotlin.mpp.androidSourceSetLayoutVersion=2 kotlin.jvm.target=11 -android.version_name=6.2-beta.0 -android.version_code=251 +android.version_name=6.2-beta.1 +android.version_code=252 -desktop.version_name=6.2-beta.0 -desktop.version_code=75 +desktop.version_name=6.2-beta.1 +desktop.version_code=76 kotlin.version=1.9.23 gradle.plugin.version=8.2.0 From d40d690f86035eaf4a2f0c405c4585cf71d26ff5 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Sun, 24 Nov 2024 15:27:58 +0700 Subject: [PATCH 063/167] desktop (Windows): fix linking with openssl 3 (#5238) --- .../desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt b/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt index f0679c0fa1..0e8a452e08 100644 --- a/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt +++ b/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt @@ -77,7 +77,7 @@ private fun initHaskell() { private fun windowsLoadRequiredLibs(libsTmpDir: File, vlcDir: File) { val mainLibs = arrayOf( - "libcrypto-1_1-x64.dll", + "libcrypto-3-x64.dll", "libsimplex.dll", "libapp-lib.dll" ) From 97b472fd9c43dfdf0f765d18f33794a96458e909 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Mon, 25 Nov 2024 09:24:12 +0000 Subject: [PATCH 064/167] blog: operators (#5240) * blog: network operators (draft) * update * update * ui: update whats new link * fix file name * update * update * update * update --- README.md | 2 + ...simplex-chat-v4.4-disappearing-messages.md | 2 +- ...04-simplex-chat-v4-5-user-chat-profiles.md | 2 +- ...20230301-simplex-file-transfer-protocol.md | 2 +- ...30328-simplex-chat-v4-6-hidden-profiles.md | 2 +- ...vision-funding-v5-videos-files-passcode.md | 2 +- ...essage-reactions-self-destruct-passcode.md | 2 +- ...lex-chat-v5-2-message-delivery-receipts.md | 2 +- ...local-file-encryption-directory-service.md | 2 +- ...desktop-quantum-resistant-better-groups.md | 2 +- ...-simplex-ux-private-notes-group-history.md | 2 +- ...istance-signal-double-ratchet-algorithm.md | 2 +- ...sistant-e2e-encryption-simple-migration.md | 2 +- ...ransparency-v5-7-better-user-experience.md | 2 +- ...5.8-private-message-routing-chat-themes.md | 2 +- ...-v6-private-routing-new-user-experience.md | 2 +- ...ity-review-better-calls-user-experience.md | 2 +- ...vacy-and-decentralization-for-all-users.md | 142 ++++++++++++++++-- blog/README.md | 10 ++ blog/images/20241125-operators-1.png | Bin 0 -> 146040 bytes blog/images/20241125-operators-2.png | Bin 0 -> 631873 bytes blog/images/20241125-operators-3.png | Bin 0 -> 78223 bytes .../src/_includes/blog_previews/20241125.html | 9 ++ website/src/css/blog.css | 56 +++++++ 24 files changed, 222 insertions(+), 29 deletions(-) create mode 100644 blog/images/20241125-operators-1.png create mode 100644 blog/images/20241125-operators-2.png create mode 100644 blog/images/20241125-operators-3.png create mode 100644 website/src/_includes/blog_previews/20241125.html diff --git a/README.md b/README.md index ff4ae8c657..52f753a5ab 100644 --- a/README.md +++ b/README.md @@ -233,6 +233,8 @@ You can use SimpleX with your own servers and still communicate with people usin Recent and important updates: +[Nov 25, 2025. Servers operated by Flux - true privacy and decentralization for all users](./20241125-servers-operated-by-flux-true-privacy-and-decentralization-for-all-users.md) + [Oct 14, 2024. SimpleX network: security review of protocols design by Trail of Bits, v6.1 released with better calls and user experience.](./blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.md) [Aug 14, 2024. SimpleX network: the investment from Jack Dorsey and Asymmetric, v6.0 released with the new user experience and private message routing](./blog/20240814-simplex-chat-vision-funding-v6-private-routing-new-user-experience.md) diff --git a/blog/20230103-simplex-chat-v4.4-disappearing-messages.md b/blog/20230103-simplex-chat-v4.4-disappearing-messages.md index 7c00df3228..ab9010535f 100644 --- a/blog/20230103-simplex-chat-v4.4-disappearing-messages.md +++ b/blog/20230103-simplex-chat-v4.4-disappearing-messages.md @@ -71,7 +71,7 @@ Some links to answer the most common questions: [What are the risks to have identifiers assigned to the users](./20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md#why-having-users-identifiers-is-bad-for-the-users). -[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-technical-details-and-limitations). +[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-and-security-technical-details-and-limitations). [How SimpleX is different from Session, Matrix, Signal, etc.](https://github.com/simplex-chat/simplex-chat/blob/stable/README.md#frequently-asked-questions). diff --git a/blog/20230204-simplex-chat-v4-5-user-chat-profiles.md b/blog/20230204-simplex-chat-v4-5-user-chat-profiles.md index 18817a18b6..0f20747ae8 100644 --- a/blog/20230204-simplex-chat-v4-5-user-chat-profiles.md +++ b/blog/20230204-simplex-chat-v4-5-user-chat-profiles.md @@ -97,7 +97,7 @@ Some links to answer the most common questions: [What are the risks to have identifiers assigned to the users](./20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md#why-having-users-identifiers-is-bad-for-the-users). -[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-technical-details-and-limitations). +[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-and-security-technical-details-and-limitations). [How SimpleX is different from Session, Matrix, Signal, etc.](https://github.com/simplex-chat/simplex-chat/blob/stable/README.md#frequently-asked-questions). diff --git a/blog/20230301-simplex-file-transfer-protocol.md b/blog/20230301-simplex-file-transfer-protocol.md index 0008dd6b9b..9219b8122c 100644 --- a/blog/20230301-simplex-file-transfer-protocol.md +++ b/blog/20230301-simplex-file-transfer-protocol.md @@ -139,7 +139,7 @@ Some links to answer the most common questions: [What are the risks to have identifiers assigned to the users](./20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md#why-having-users-identifiers-is-bad-for-the-users). -[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-technical-details-and-limitations). +[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-and-security-technical-details-and-limitations). [How SimpleX is different from Session, Matrix, Signal, etc.](https://github.com/simplex-chat/simplex-chat/blob/stable/README.md#frequently-asked-questions). diff --git a/blog/20230328-simplex-chat-v4-6-hidden-profiles.md b/blog/20230328-simplex-chat-v4-6-hidden-profiles.md index 4fe282b081..c369eb5792 100644 --- a/blog/20230328-simplex-chat-v4-6-hidden-profiles.md +++ b/blog/20230328-simplex-chat-v4-6-hidden-profiles.md @@ -104,7 +104,7 @@ Some links to answer the most common questions: [What are the risks to have identifiers assigned to the users](https://simplex.chat/#why-ids-bad-for-privacy). -[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-technical-details-and-limitations). +[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-and-security-technical-details-and-limitations). [How SimpleX is different from Session, Matrix, Signal, etc.](https://github.com/simplex-chat/simplex-chat/blob/stable/README.md#frequently-asked-questions). diff --git a/blog/20230422-simplex-chat-vision-funding-v5-videos-files-passcode.md b/blog/20230422-simplex-chat-vision-funding-v5-videos-files-passcode.md index 690292d14c..eb1288059a 100644 --- a/blog/20230422-simplex-chat-vision-funding-v5-videos-files-passcode.md +++ b/blog/20230422-simplex-chat-vision-funding-v5-videos-files-passcode.md @@ -108,7 +108,7 @@ Some links to answer the most common questions: [What are the risks to have identifiers assigned to the users](https://simplex.chat/#why-ids-bad-for-privacy). -[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-technical-details-and-limitations). +[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-and-security-technical-details-and-limitations). [How SimpleX is different from Session, Matrix, Signal, etc.](https://github.com/simplex-chat/simplex-chat/blob/stable/README.md#frequently-asked-questions). diff --git a/blog/20230523-simplex-chat-v5-1-message-reactions-self-destruct-passcode.md b/blog/20230523-simplex-chat-v5-1-message-reactions-self-destruct-passcode.md index 0cdbe2831f..0128a64b21 100644 --- a/blog/20230523-simplex-chat-v5-1-message-reactions-self-destruct-passcode.md +++ b/blog/20230523-simplex-chat-v5-1-message-reactions-self-destruct-passcode.md @@ -102,7 +102,7 @@ Some links to answer the most common questions: [What are the risks to have identifiers assigned to the users](https://simplex.chat/#why-ids-bad-for-privacy). -[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-technical-details-and-limitations). +[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-and-security-technical-details-and-limitations). [How SimpleX is different from Session, Matrix, Signal, etc.](https://github.com/simplex-chat/simplex-chat/blob/stable/README.md#frequently-asked-questions). diff --git a/blog/20230722-simplex-chat-v5-2-message-delivery-receipts.md b/blog/20230722-simplex-chat-v5-2-message-delivery-receipts.md index 759587821b..5818049858 100644 --- a/blog/20230722-simplex-chat-v5-2-message-delivery-receipts.md +++ b/blog/20230722-simplex-chat-v5-2-message-delivery-receipts.md @@ -160,7 +160,7 @@ Some links to answer the most common questions: [What are the risks to have identifiers assigned to the users](https://simplex.chat/#why-ids-bad-for-privacy). -[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-technical-details-and-limitations). +[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-and-security-technical-details-and-limitations). [How SimpleX is different from Session, Matrix, Signal, etc.](https://github.com/simplex-chat/simplex-chat/blob/stable/README.md#frequently-asked-questions). diff --git a/blog/20230925-simplex-chat-v5-3-desktop-app-local-file-encryption-directory-service.md b/blog/20230925-simplex-chat-v5-3-desktop-app-local-file-encryption-directory-service.md index 0222c25d77..3c3fb7b515 100644 --- a/blog/20230925-simplex-chat-v5-3-desktop-app-local-file-encryption-directory-service.md +++ b/blog/20230925-simplex-chat-v5-3-desktop-app-local-file-encryption-directory-service.md @@ -108,7 +108,7 @@ Some links to answer the most common questions: [What are the risks to have identifiers assigned to the users](https://simplex.chat/#why-ids-bad-for-privacy). -[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-technical-details-and-limitations). +[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-and-security-technical-details-and-limitations). [How SimpleX is different from Session, Matrix, Signal, etc.](https://github.com/simplex-chat/simplex-chat/blob/stable/README.md#frequently-asked-questions). diff --git a/blog/20231125-simplex-chat-v5-4-link-mobile-desktop-quantum-resistant-better-groups.md b/blog/20231125-simplex-chat-v5-4-link-mobile-desktop-quantum-resistant-better-groups.md index 7f50446bfa..539d719af4 100644 --- a/blog/20231125-simplex-chat-v5-4-link-mobile-desktop-quantum-resistant-better-groups.md +++ b/blog/20231125-simplex-chat-v5-4-link-mobile-desktop-quantum-resistant-better-groups.md @@ -133,7 +133,7 @@ Some links to answer the most common questions: [What are the risks to have identifiers assigned to the users](./20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md#why-having-users-identifiers-is-bad-for-the-users). -[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-technical-details-and-limitations). +[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-and-security-technical-details-and-limitations). [How SimpleX is different from Session, Matrix, Signal, etc.](https://github.com/simplex-chat/simplex-chat/blob/stable/README.md#frequently-asked-questions). diff --git a/blog/20240124-simplex-chat-infrastructure-costs-v5-5-simplex-ux-private-notes-group-history.md b/blog/20240124-simplex-chat-infrastructure-costs-v5-5-simplex-ux-private-notes-group-history.md index 43c502d8c4..f5539106b7 100644 --- a/blog/20240124-simplex-chat-infrastructure-costs-v5-5-simplex-ux-private-notes-group-history.md +++ b/blog/20240124-simplex-chat-infrastructure-costs-v5-5-simplex-ux-private-notes-group-history.md @@ -94,7 +94,7 @@ Some links to answer the most common questions: [What are the risks to have identifiers assigned to the users](./20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md#why-having-users-identifiers-is-bad-for-the-users). -[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-technical-details-and-limitations). +[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-and-security-technical-details-and-limitations). [How SimpleX is different from Session, Matrix, Signal, etc.](https://github.com/simplex-chat/simplex-chat/blob/stable/README.md#frequently-asked-questions). diff --git a/blog/20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md b/blog/20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md index 6d4c8b77a2..13a514c175 100644 --- a/blog/20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md +++ b/blog/20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md @@ -235,7 +235,7 @@ Some links to answer the most common questions: [What are the risks to have identifiers assigned to the users](./20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md#why-having-users-identifiers-is-bad-for-the-users). -[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-technical-details-and-limitations). +[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-and-security-technical-details-and-limitations). [How SimpleX is different from Session, Matrix, Signal, etc.](https://github.com/simplex-chat/simplex-chat/blob/stable/README.md#frequently-asked-questions). diff --git a/blog/20240323-simplex-network-privacy-non-profit-v5-6-quantum-resistant-e2e-encryption-simple-migration.md b/blog/20240323-simplex-network-privacy-non-profit-v5-6-quantum-resistant-e2e-encryption-simple-migration.md index c017b9d1cc..0980eb8896 100644 --- a/blog/20240323-simplex-network-privacy-non-profit-v5-6-quantum-resistant-e2e-encryption-simple-migration.md +++ b/blog/20240323-simplex-network-privacy-non-profit-v5-6-quantum-resistant-e2e-encryption-simple-migration.md @@ -132,7 +132,7 @@ Some links to answer the most common questions: [What are the risks to have identifiers assigned to the users](./20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md#why-having-users-identifiers-is-bad-for-the-users). -[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-technical-details-and-limitations). +[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-and-security-technical-details-and-limitations). [How SimpleX is different from Session, Matrix, Signal, etc.](https://github.com/simplex-chat/simplex-chat/blob/stable/README.md#frequently-asked-questions). diff --git a/blog/20240426-simplex-legally-binding-transparency-v5-7-better-user-experience.md b/blog/20240426-simplex-legally-binding-transparency-v5-7-better-user-experience.md index cb3e5b2d10..225c2637d7 100644 --- a/blog/20240426-simplex-legally-binding-transparency-v5-7-better-user-experience.md +++ b/blog/20240426-simplex-legally-binding-transparency-v5-7-better-user-experience.md @@ -90,7 +90,7 @@ Some links to answer the most common questions: [What are the risks to have identifiers assigned to the users](./20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md#why-having-users-identifiers-is-bad-for-the-users). -[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-technical-details-and-limitations). +[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-and-security-technical-details-and-limitations). [Frequently asked questions](../docs/FAQ.md). diff --git a/blog/20240604-simplex-chat-v5.8-private-message-routing-chat-themes.md b/blog/20240604-simplex-chat-v5.8-private-message-routing-chat-themes.md index 0519e78e7b..e06f7c2084 100644 --- a/blog/20240604-simplex-chat-v5.8-private-message-routing-chat-themes.md +++ b/blog/20240604-simplex-chat-v5.8-private-message-routing-chat-themes.md @@ -144,7 +144,7 @@ Some links to answer the most common questions: [What are the risks to have identifiers assigned to the users](./20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md#why-having-users-identifiers-is-bad-for-the-users). -[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-technical-details-and-limitations). +[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-and-security-technical-details-and-limitations). [Frequently asked questions](../docs/FAQ.md). diff --git a/blog/20240814-simplex-chat-vision-funding-v6-private-routing-new-user-experience.md b/blog/20240814-simplex-chat-vision-funding-v6-private-routing-new-user-experience.md index e81bf5516a..de9e33a87e 100644 --- a/blog/20240814-simplex-chat-vision-funding-v6-private-routing-new-user-experience.md +++ b/blog/20240814-simplex-chat-vision-funding-v6-private-routing-new-user-experience.md @@ -218,7 +218,7 @@ Some links to answer the most common questions: [What are the risks to have identifiers assigned to the users](./20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md#why-having-users-identifiers-is-bad-for-the-users). -[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-technical-details-and-limitations). +[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-and-security-technical-details-and-limitations). [Frequently asked questions](../docs/FAQ.md). diff --git a/blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.md b/blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.md index cefe8560f5..1bede1cc97 100644 --- a/blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.md +++ b/blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.md @@ -165,7 +165,7 @@ Some links to answer the most common questions: [What are the risks to have identifiers assigned to the users](./20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md#why-having-users-identifiers-is-bad-for-the-users). -[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-technical-details-and-limitations). +[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-and-security-technical-details-and-limitations). [Frequently asked questions](../docs/FAQ.md). diff --git a/blog/20241125-servers-operated-by-flux-true-privacy-and-decentralization-for-all-users.md b/blog/20241125-servers-operated-by-flux-true-privacy-and-decentralization-for-all-users.md index cb5db41c88..57c4f69981 100644 --- a/blog/20241125-servers-operated-by-flux-true-privacy-and-decentralization-for-all-users.md +++ b/blog/20241125-servers-operated-by-flux-true-privacy-and-decentralization-for-all-users.md @@ -2,29 +2,145 @@ layout: layouts/article.html title: "Servers operated by Flux - true privacy and decentralization for all users" date: 2024-11-25 -# previewBody: blog_previews/20241125.html +previewBody: blog_previews/20241125.html image: images/simplexonflux.png imageWide: true -draft: true permalink: "/blog/20241125-servers-operated-by-flux-true-privacy-and-decentralization-for-all-users.html" --- -# Servers operated by Flux - true privacy and decentralization for all users +# Servers operated by Flux — true privacy and decentralization for all users -**Will be published:** Nov 25, 2024 +**Published:** Nov 25, 2024 -- [Welcome, Flux](#welcome-flux--the-new-servers-in-v62-beta1) - the new servers in v6.2-beta.1! -- What's the problem? -- Several operators improve connection privacy. -- SimpleX decentralization compared with Matrix, Session and Tor. -- What is next? +- [Welcome, Flux](#welcome-flux--the-new-servers-in-v62-beta1) — the new servers in v6.2-beta.1! +- [What's the problem](#whats-the-problem)? +- [Using two operators improves connection privacy](#using-two-operators-improves-connection-privacy). +- [SimpleX decentralization](#simplex-decentralization-compared-with-matrix-session-and-tor) compared with Matrix, Session and Tor. +- [What's next](#whats-next-for-simplex-network-decentralization) for SimpleX network decentralization? ## Welcome, Flux – the new servers in v6.2-beta.1! - + -[Flux](https://runonflux.com) is a decentralized cloud infrastructure that consists of user-operated nodes. +[Flux](https://runonflux.com) is a decentralized cloud infrastructure that consists of user-operated nodes [1]. With this beta release all SimpleX Chat users can use pre-configured Flux servers to improve metadata privacy and decentralization. -With v6.2 release all SimpleX Chat users can use pre-configured Flux servers to improve metadata privacy and decentralization. +We are very grateful to [Daniel Keller](https://x.com/dak_flux), CEO and co-founder of Flux, for supporting SimpleX network, and betting on our vision of extreme decentralization of communication. Flux investing their infrastructure in our vision is a game changer for us and our users. -Come back on Monday November 25th to learn why it is important and how having several operators improves metadata privacy. +Download new mobile and desktop SimpleX apps from [TestFlight](https://testflight.apple.com/join/DWuT2LQu) (iOS), [Play Store](https://play.google.com/store/apps/details?id=chat.simplex.app), our [F-Droid repo](https://simplex.chat/fdroid/) or [GitHub](https://github.com/simplex-chat/simplex-chat/releases/tag/v6.2.0-beta.1). + +Read on to learn why it is important and how using several operators improves metadata privacy. + +## What's the problem? + +SimpleX network is fully decentralized, without any central component or bootstrap nodes — you could use your own servers from day one. While there is no full list of SimpleX network servers, we see many hundreds of servers in public groups. + +But a large number of SimpleX app users use the servers pre-configured in the app. Even though the app randomly chooses 4 servers in each connection to improve privacy and security, prior to v6.2 for these users the servers were operated by the same company — ourselves. + +Our open-source code that we are [legally bound to use](./20240426-simplex-legally-binding-transparency-v5-7-better-user-experience.md#legally-binding-transparency) doesn't provide any metadata that could be used to learn who connects to whom. But the privacy of users' connections still depends on us honouring our promises and [privacy policy](../PRIVACY.md). Flux servers in the app improve that. + +## Using two operators improves connection privacy + + + +To ensure that the users' metadata from different servers cannot be combined to discover who talks to whom, the servers in each connection have to be operated by different independent organizations. + +Before this version the app was choosing servers randomly. Now, when both SimpleX Chat and Flux servers are enabled it will always choose servers of different operators in each connection to receive messages and for [private message routing](./20240604-simplex-chat-v5.8-private-message-routing-chat-themes.md), increasing metadata privacy for all users. + +Flux servers are configured as opt-in, and the privacy policy and conditions of use that apply to Flux servers are the same as for SimpleX Chat servers, to make it simple for the users. + +To improve connection privacy by using Flux servers all you have to do is to enable Flux once the app offers it, or later, via Network & servers settings, and accept that the same conditions apply. + + + +By default, if both Flux and SimpleX servers are enabled in this version, you will be using SimpleX Chat servers to receive messages, Flux servers to forward messages to SimpleX Chat servers, and the servers of both to forward messages to unknown servers. We will enable Flux to receive messages by default a bit later, or you can change it now via settings. + +Any additional servers you add to app configuration are treated as belonging to another operator, so they will also be used to improve connection privacy, together with pre-configured servers, unless you disable them. + +## SimpleX decentralization compared with Matrix, Session and Tor + +SimpleX network decentralization model is different from other decentralized networks in several important aspects. + +| Communication network | SimpleX | Matrix | Session | Tor-based | +|:-----------------------------|:-------:|:------:|:-------:|:---------:| +| Full decentralization | ✅ | - | - | - | +| No user profile identity | ✅ | - | - | - | +| Connection privacy | ✅ | - | ✅ | ✅ | +| Server operator transparency | ✅ | ✅ | - | - | + +**Full decentralization** + +Fully decentralized networks do not have a central component, bootstrap nodes or any global shared state, like in cryptocurrency/blockchain-based communication networks. The presence of any central component or shared state introduces an attack vector that undermines privacy and security of the network. + +**No user profile identity** + +User profile identities, even if it is only a random number or a long-term key, undermine privacy of users connections, because in some cases they may allow network operators, observers and users to find out who talks to whom. + +Most communication networks rely on fixed user profile identities. It includes Matrix and communication networks with onion routing. + +SimpleX network design avoids the need for profile identities or keys, while still allowing optional long-term addresses for users and groups for convenience. It protects users from being discovered and approached by malicious parties, and many family users chose to use SimpleX with children because of it. + +**Connection privacy** + +SimpleX network has [private message routing](./20240604-simplex-chat-v5.8-private-message-routing-chat-themes.md) (2-hop onion routing) — it prevents server operators from discovering who connects to whom via network traffic metadata. Onion routing used in Tor-based messengers and in Session also hides it. But because neither Tor nor Session users have knowledge about who operates servers, in some cases the clients may connect via the servers controlled by one entity, that may learn the IP addresses of both parties. + +Statistically, if traffic metadata from 2% of onion network servers is available to an attacker, and the client chooses servers randomly, after about 1750 of such choices the probability of choosing attacker's servers as both entry and exit nodes, and connection privacy being compromised becomes over 50% [2]. + +Matrix network does not provide connection privacy, as not only user identity exists, it is tied to a specific server that knows all user connections and a part of user's contacts connections. What is worse, Element — the most widely used Matrix app — offers the servers of only one organization to create an account, resulting in some degree of network centralization. + +**Server operator transparency** + +Operator transparency means that network users know who operates the servers they use. + +You may argue that when the operators are known, the servers data can be requested by the authorities. But such requests, in particular when multiple operators are used by all users, will follow a due legal process, and will not result in compromising the privacy of all users. + +With Tor and Session networks such legal process becomes impossible, and some users may see it as advantage. But nothing prevents the attackers, both criminal and corporate- or state-funded, to compromise the privacy of Tor or Session users by running many servers, or by purchasing traffic metadata from the existing server owners — there are no legal conditions that prohibit server owners of these networks to share or sell traffic data. + +Because of that, we see operator transparency in SimpleX network as a better trade-off for privacy of most users than operator anonymity provided by Session and Tor. You can see privacy of network participants as a zero sum game — for the end users to have it, server operators should be known. + +## What's next for SimpleX network decentralization + +SimpleX network is designed for extreme decentralization — not only users are distributed across network operators, as happens with federated networks, but each conversation will be relying on servers of 4-6 independent operators, and these operators will be regularly and automatically changed in the near future. + +We believe that the only viable commercial model is freemium — a small share of paying users, who have better service quality and additional features, sponsors free users. This model doesn't have downsides of exploitative "provide service, sell data" approaches, that technology monopolies practice, and it also doesn't have problems of cryptocurrency blockchains, that have shared and immutable state, and also have regulatory problems. + +To provide this extreme decentralization with freemium model we will create the system of payments allowing server operators to receive money for infrastructure certificates that will be used with any other participating network operators without compromising privacy of the paying users. You can read about this model [here](https://github.com/simplex-chat/simplex-chat/blob/stable/docs/rfcs/2024-04-26-commercial-model.md). We will be writing more about it as this development progresses. + +## SimpleX network + +Some links to answer the most common questions: + +[How can SimpleX deliver messages without user identifiers](./20220511-simplex-chat-v2-images-files.md#the-first-messaging-platform-without-user-identifiers). + +[What are the risks to have identifiers assigned to the users](./20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md#why-having-users-identifiers-is-bad-for-the-users). + +[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-and-security-technical-details-and-limitations). + +[Frequently asked questions](../docs/FAQ.md). + +Please also see our [website](https://simplex.chat). + +## Please support us with your donations + +Huge *thank you* to everybody who donated to SimpleX Chat! + +Prioritizing users privacy and security, and also raising the investment, would have been impossible without your support and donations. + +Also, funding the work to transition the protocols to non-profit governance model would not have been possible without the donations we received from the users. + +Our pledge to our users is that SimpleX protocols are and will remain open, and in public domain, so anybody can build the future implementations of the clients and the servers. We are building SimpleX platform based on the same principles as email and web, but much more private and secure. + +Your donations help us raise more funds — any amount, even the price of the cup of coffee, makes a big difference for us. + +See [this section](https://github.com/simplex-chat/simplex-chat/#please-support-us-with-your-donations) for the ways to donate. + +Thank you, + +Evgeny + +SimpleX Chat founder + +[1] You can also to self-host your own SimpleX servers on [Flux decentralized cloud](https://home.runonflux.io/apps/marketplace?q=simplex). + +[2] The probability of connection being de-anonymized and the number of random server choices follow this equation: `(1 - s ^ 2) ^ n = 1 - p`, where `s` is the share of attacker-controlled servers in the network, `n` is the number of random choices of entry and exit nodes for the circuit, and `p` is the probability of both entry and exit nodes, and the connection privacy being compromised. Substituting `0.02` (2%) for `s`, `0.5` (50%) for `p`, and solving this equation for `n` we obtain that `1733` random circuits have 50% probability of privacy being compromised. + +Also see [this presentation about Tor](https://ritter.vg/p/tor-v1.6.pdf), specifically the approximate calculations on page 76, and also [Tor project post](https://blog.torproject.org/announcing-vanguards-add-onion-services/) about the changes that made attack on hidden service anonymity harder, but still viable in case the it is used for a long time. diff --git a/blog/README.md b/blog/README.md index c8de7e83f9..97ccffda9a 100644 --- a/blog/README.md +++ b/blog/README.md @@ -1,5 +1,15 @@ # Blog +Nov 25, 2025 [Servers operated by Flux - true privacy and decentralization for all users](./20241125-servers-operated-by-flux-true-privacy-and-decentralization-for-all-users.md) + +- Welcome, Flux - the new servers in v6.2-beta.1! +- What's the problem? +- Using two operators improves connection privacy. +- SimpleX decentralization compared with Matrix, Session and Tor. +- What is next for SimpleX decentralization? + +--- + Oct 14, 2024 [SimpleX network: security review of protocols design by Trail of Bits, v6.1 released with better calls and user experience.](./20241014-simplex-network-v6-1-security-review-better-calls-user-experience.md) New security audit: Trail of Bits reviewed the cryptographic design of protocols used in SimpleX network and apps. diff --git a/blog/images/20241125-operators-1.png b/blog/images/20241125-operators-1.png new file mode 100644 index 0000000000000000000000000000000000000000..a749fa0ed136c6c05b0c6dbcd79a7ed08b2b52d9 GIT binary patch literal 146040 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

2B z%^Tqvr%#sf*0tyopq!j>?Vd|!P+>@(qhu!5MBOL_Jv*sYuhSr{L{iWXP1Iie95sej zPm%jq{~o5wvQy%1370Vx>wZI)x0E;+n%pW)cvHa8yDxj$HgG~adJ$+RT|ZJ2M-^0V~6ycVxrJcC`IliCc56rt*e&MZFQ{F&fC#Z@0;=m#1y zq^kLppY=z?RBNzJz;N1XF8vFkP(G5rmD*Xi(-p=0(Y|U(9QNyWEm5VIP?@Aso)4Lp{T8nm_uy59&Fg4+ZF*0$2L z!*635@}rC@rem8V47VW1I~-Ad=kh53>!xWC{-o$6Sj@HVblNOz@TW856msHKza*ml zJk4Fg#G=go{oH+^IM#vR$7T>HTHw8xqgzrxr?MgDTIuRh-3rGR{iFV>+K6Ko?<)_3 z=Gw8PX%mpQN>lSsRIxJ(CYMgmjrAv_1En&)0us9e@&P*A{MPHv3sz|xMRU7wMlY3T zPYV>O&eu3kNjjfr8v$aR_T;f0WwgEK@$dSx@s{V;ebf&7`$ z=431WZh)vquf~daWcSjsQQ+)IVX%sYcbdE76S-TWOrup|ZRb6tloDNrZ*wcoU6RYW z1jBXQi+NV&NuR z-{*wxd|DcUjyUH_q3G3{)qEh&G67RJZ@>N(t7}I!dvJY}2=oHAYdJXN5mLXtdI4&; zSjc_lJPM`mLCoF#U)MqvQTElYjvYuK7c`e|$#9hr_qtwVX-09S8TgdDmO}K#}85SIUB$pP=7wx;;wPscB+UVlqJ_ z<8wrO7O>A#vZ_epB(~j`{CtPrZ*S71RcfpYSfr_((-g`XFQaVUp}TXE2fvQTC-s9G=2*QJ#`K3-OUy3J?b5Dm z8V6aFk``^rE@+jBercA)epTd=toxGYZ}WTesJ;a_Gu1BaG*_?tQag@hd+9ruHM*^t z)64L!#fhS-J=vNQtCkE_25^)_I7*r8`AbiF3cr$v)ZSqt`vZJNj<9tLfY!Ey#PQbo zx=P13-U&q!BJ1?FRAmCQod6!@GO)ZulyRt};q1?>AC28jKzyrgb+v{2r4xow5#QrvMu zgdR==g@wA(oQXuKjuCaC_hQk5?*!8jAkFYfMeLCh!n@f=$v%C&eTS3lSO41456c;} z6xu%@3`+(H;T*tFdl$zG#-NkvDP*L>$w$Y}zcRT0i=4VB{N=@jEH-1U*?lv|o>sHG zwnM?v5W`hjs6z81>Dz;01(h=|2Z%Q4KI7?Hg&6p|J-j!fgEuRW6ikEbnB<(l@a4eN z^*eLzYza^LEux$V`sXJZ*8SX9w%#Pp*j!;EMc2UzcHixWl0+LJrZX5uZ@d1U?a8*I zrtmeUO9g0qnzYnp)3a0EhIs342OYjk#zEjf$w3aipLFY(e0PX0ozJ;}Fw-aMuCGq> zn&vo#pL-c~DdUP@xGpg?l9}7~1%V~QpV0b{EDX~N8EbH?I019=c}*IZeQ@?&q{62s zjaAiUp&{X8Ej{!4vTSR?8gC4xK~`*kk;|*L+pz)f!+xeHAc{>&fTbAEC9c|1c>`5T76S!7>6dL1xCPq0cj28;JT+URsp4T{SWyeHj{0dT`%4H5S~( z<|Xivp%IHvE})qD0Ym{xv7W_jJ6-Lzy6KoxQtB2yO!h6TWi`YXu9a@uZL(NYcRT?? zMPYjG8|^?3jTfh13IFf97<}#`@vlVB^c9g_R!yhf&k8MqkCm`LG3*~>H%p(;wT@0e z^2*9A-0o9P28_gpJLUz%&qKaMoL^bSI}zcFQK-h}jI2FOH+Afj${%DrEMYLWG=!wM zH(0NXE~R3dBZL<9Ejod-uJYDtEw5g7)u)~h%Fto`e$M`Tc-){_aux^g{Gnz=67YBc@s2k#-+nJ@SB$^pfCSp%&bmrxHtO=TLCUxJD@~ZL zQsH!y?oPvs26@yKl7VBCirVRtlr-5YW-Z~4>Nji*AKiO#XtFco5K9gEBf|RkxqloZ zp8vAYASunN7G0S-b$PmCAQl%R(qY&Yt#4R<3;30K2`xkkRUan4tC>~vcHF$Ef_+Sn z6VpIz`#hW}jFtb^JK;L-OuWZ1fqfyJ@6wqH^YR6{ehh|ZG$Z$Ko?7;O5+}fDLJ~Pl zrj7-=cg>oPu1-zRXSMJD=Br5a-JE1IY2eO)z7zA8GK# zX3}>;nQZ$br$eTUhvXwoM;|X`$|7}p8t(?Mo9Ey?6p)6uqxU>cUHyw6clB%ySG?`+ zOR-72MQHQp(aFa4dIEbR>IbJM;z<7x+Z%aKAXeVea1wGpwG#NZQwuh%u?pV?lBz^0 zGGEEvU*JsJD1WM8TS*bQru1kZRQ3Z&b=Dirnv>Euaac^gNz&5TXmA;c6yayL(mJWr zR*mXEu(^4__nqaX{w&#AY71Nj@C91Et&R!=#xK-G`(x2 z4tSwW*n3p+iB&4a_S;N;YNdh>sS@iEYpRpbz>2ony>~lYj0}UBjqyC}oY`3ZB!n9k zd|;4YuLR20Ta=HPcCUbMc&*_88;9X}cNTRL`y8->aM z-O_5}pLJu-&sU>aoj!!tkJ9{b?GpLOS<)L?llGo=Lfx4qubX#HlX?0!zFpgG0$4?# z&XtZ^<*STIY|(ea>sKfXlB6aHPyqC7E9L}8k=ml>D zK4#FKsaM@xx;@w{r-}94%Xgk}?NQ>CmT8HgAzx5WmxDE$>62_sjH?undx3{c`D66X zogmg5>A`D_g>f>g+%%cUu4ST7e@seY`oTb0Eg9EKmZBCB)P>v z?F>Q4rZ7NlKX1C|wV4{wRT>Zcg@!rM{N=U11c?eJ=AUZM5<0x4-fyVvC-g2_Q2w zm_)1(DV2WZ7mB`cka=w*=U(9YKu-hc>3>B#zkj?IZHA0GE0O8+DbXsVd#H>cL~U5% zTc7q%54x1@)#pdgSvcxDiTo_`J!>Ay&4z40 z<*+KLohbGjy!8&(p8~f>@)f&)NlDmYW)bq`(&dVviC^Y7oAHw?DpZperICThvMA6S z{|&_-9M*sSS7p+N&;`$M`g@QR{4CD#Gs}78FhSWaI@r#06JS}i>*cTYdeP4F4f8Zv z=i>p5Rw8qWv8$tuOQ&b9!xE%Yq>{QhgD}rz&Bh>;t(vi1=P_Z5fAyK($Al&x*F^*z zyWVTIDDaxz0H#POBNRAb7hd3fm{{6;%;DU#bGNqh*>C!j-(n$gUVr6VN3ayBYxTkE zhNy8%DH?aVL)}UDZ}XVpCdMKs6_~(*BGdS%XGW-6i*%{%mv}0j;`gl$u9ezT^ALYt z^HrUuC+3N7b=sg^kf~2DBhF1MU2+lyu7ZAP8 zWY1A|Do07T&*2eHBBrlL>E&Yl?_In9cczSrAN7ne==Dmt>W=c5xz{R*YC+WOvXx-P zwQr3{G53^3sFsSz!xEM$eU;y|&SyQ!e^8vaRpWJ6>4rC_udI8P4Ekr5rH-+z&h`{H zG#iilPOM`a`a8uOX4=Cj?wiC33j^0Nmt);}Oe%X?F$pKVp0T8w`(AINCNhDf@f$Kh z^X7?~h`m0DgkatJO-c1%22Pn9t56`fsz(@}NL*psVA<_46Te0;SPLao(Kj$KI^Ojh zsN~_i$C7|&3g&i?Go3C11RIASzTH7gyFnD(;aJugjHvf1jFLYe_(u{Zh(eZ9ocnB# zzseOa2^%$?h4H|n6!?<8HFvp$+}8Y#9k#L!Ub@4vF~Ei9Dy?j56z+-npQP>I&_JV{ zCeBjXQ=E{MS2*-^NZPidQqy8PTi=r?f@#FlR^`kaQL*`E9c>i|R#63D((UB^%QkG9 zpq!oU%)pW`RVNJ+(P6q&{Dr3tIz$5~#Tl6JG?HrFBks#{v%dlTt`|2) zImk!Z$d7D3SB&dIq*?r)<+x6B8v z%$^;;DnroC<%n69lnyLJg)7r1P58aW!GB0*f{jiab1*Q8Qivw~tx5VnujZG#%W%Z9 z!mpT;jl0mzGLd#F?f{T34fm1@{+ustZ1nXlYbgn}-={jY;`5uj*Ocn!HS?(@_Sp}q zR|EkEH48f7J~2|8Wys~Es}V~eDta+a@99wyMV!Ik?Cw9l<&>~TuiIrm3%H^>1s<5O zsu`UC&W6;|MVw!5sD90+J3qPF?|MVh+n`*@7$C3d=A?Fh`Ym4i}qn+F`l{5t? zSc=4oAYYZfp?aPM@&3~$F)ir%m~qnIqH`3?s|x)117nYJE?+dpM@8eVW!jdUC*j<| z37;(-4UHjn3-WYu!J*SnprQ||`b<|Bx=EEkvxQab)FBWa&F4i~I#mNnQGhtNJoq~( z{bhS(df4SIz2D!uDh^5tj*&FYm+a@iHL+|yIYOfgLv3tcKyU~lyI+VgQ}LBK*^7-t zpFy9lPJJR8vP0U7Y`DbSR>k<8pIDnB_cv`c#`blI(`)_@bACG}|9&hawA8$%k*bC| z^cX`LR(E5RVOsSg?3+YD-g!g7)D=baP47P0n@0y9sd|lf#cNhwkg{mEEO+MlL%QH7 z5Qo0u&RmUA$^OM>kD%Y6_s>UVVXREexvg1z0tP1ESZak1ol!G0Xf2YTbX&Hliu0Za zox&?jIIfou#_QwAVR%nieKZt6R4ZlVF5M(@80Q3$@F)_qtQw*h@!R_6t?0_pOaO9r z47f5Tfkj)nS<`h3Yfb}h*7TJf8wk!A26pYSmx}jz*~BQ_2_!x{fVsh;OM;j|e7dUZ zU>LEpSMB)V3K`&wcg$^kQsK@09C=?WSg{x6N<&Orr`76j$Jzbs&x{yjCC=saR2rnQ zJrSvNoh#C?Xv|{SJv#t2HGW|#f9xDon}4FnO5kL*@BU0!A4mkmCCp!tn9=VmvA+!3 z(cwSnli$*FFpT%DduuZNA|>y#;BV0QKPK?}>CJkASq9?9al!m4hU4nx40cBiv*`fl zc|BV9gs>LG?LF*_dg^H7g1uCYN>vaoon-RS;faDe%nffw(7xsXktWH{`*&FNA8U@l zbp9$r^JW#`R*;v)+pW}f#B)&vs`Wo*TVQSyp^MwN9)7ON))F9%MS1Z`*L!W9FaVk}Q#| z%ZQ88sh2Ly_F15G>0^cS3r)+}$sKobp+x2WW0_^8Sy z3zNHK2GmKl4|>K!*Fc=oWYEl4>-3TVYrOXdAO6$9YkAI?k>&CdJfnU8S7fjF=}50< z;c=UG8Qu1f>ApOKtiTq)rt92^)z~@b;tyY?8e`sj^gWd0Onz&2$ltlHxp$K=@danXhcERBj$aUZFS z>;tnkbh92)3}?N)-G^~4LZY0?1qD8U@d&d+LU+!#01X1tq5(Z7sh#&FX1Ovi|LJ3K z+`+AsXDQaWE}m3WLhYeL6j>Cqr)g>%0)wQUgR}GePjqQ^rSUua?lC9yC$3-t-444m z)+|XHiD)g7VUGdXhS+|2#+H_ruMNw-gNGU|olQjl=^cun<9-&js8!KmVw+xybW610 ziZY${;P+#$uS1adYPo^KOiK>V_P*@0&$2H>_D=Il)82L_Y@ifBhnK;QzhsQz1Gw}b zLoK5lS>JPv6SAlg;2M4(b1k|Mh9LnxqVz9$atrBdB zX{O?;L`9r=(_iw6Nat;1D2&^oq1zhCE8q4W#4Oh~oGscBVhjJPUiN69nA;^Dq4r*##K z&UBuxl30`DIJswS0nF`u^dmDJONB&>MAG1=#JOl~jLO+fW$Ip(tj@|55ueDqmrq&1 zpIG~stLy^!D`!9Gj|&b4LaN`T%rMQ3u=+acnUYOI7>kk<%~)Kr4+L4(?cFoTUkV(& zP}F?Wsz1aLHG2CHSOk%=NC*oFUDJ_WV+^DMq?6>V1oS)EW22Xf7H2zdc{2%v- z5t0BHzf1|j9GOD`t#Wo{FXO8Z^_T02);g691r-Fw7$$tG8p%94Z)2{;X!snxRL%Yt z3I9A8BlUvQg&4|F?iTIH0E=E+)8jVq#Pj;^tNT6w`S)%olrl6CWd@LnshT)*R!*Ok z*E2JCBRXMewQluJO>^l#farC&I1++RD z36vLuq&L=#=}X_MNc@;v$ErGIdwc;C=5GHV8uPD>0N-GK*SNyb?3+_D?96U8(n(IA zW!uYY+ToMttiF#VqS~@w4HNYvIY?{nDYgiz%$sN(37Z$-5dywe9J4%vX|PzPSi2n^ zZl`SRx03u*Eb&3SsIn8j9C{-y1z1wX4b~t6PQzs1W;@08=NRm$V;0~dY6(?wP(AS` z97`0(Cza4!dzG&E9?C+W+nye9W4}SZ+e?Df!U|UZL*rb{W8q}pl674EIIMgX4 z6g!I3uls};sW#VB6)h4RAu8}?U+2+@r+zj7O&3uGoYzR_+edVIcMH4UYOVJ~1 zV-LYd>eBsVr~SSvKG8>M|AZ4Q+BC{}{vqLNdK9=Crm99+PhB z?=*mIpuQ>^lEjSctCC{g-{^Hd>-yxq3K5LGs|dseCL|OS4I#2>gxMOpH633emIE2_ z)|TUJ9Gv*){|RyZITdLXu-SvM9aFh9ZGn~%-%J#!nzLG^sI=IbP>cumwVSTRjbDTY z7OoCtH{xGvd`_PaAfrM*GeHQq<%ZFQeG8)nKBeQL|K|{b!}@N%2M~cJiC>juH^UCf zny^+C(cIcBzqe{I>*_sltCg21O#r*BZAO{O z;A*{zUuOezEn%C62bD=FObTFKGt;xr_Eo|gsJ&L=8llEkiXm+qzBjAx>^+sB?)%5Z zW}bb?d7?Q+mYf;OqbX=$`3*IqZ`*#=0Zrg?;cCuSA4uGi6zj)Qv^O?o`%roG+PXG) zM~Cis{ML|jqxU#l=s1azoqpM$=;ec^aNBT}?=4S6ldl2<4N2Tv?PAJZ) zbAx%Ik4M`Acy)*RDwfS>C+P|;PDk(jX~VzyD~yKcKWn{@luOsJQcEhD3yH9<9Lp6& zKK_yqNKRQ$To0$*S{~oT)PD8RpM`Yhn>_PJfp{xRQNO_mCtO!@PqIj6=^ zWov}4468&zL}~59nj4`@X1Q%!btP~L=S9(56ZI{%WDfBDm`UJe>_3_CO&)?GOi= zlwz1JpYdjKV3)D?o)t#oW(B)Cs`@$RJ{ovER7#jBY`n{L&Im(aQr=x61bpxc3fm}N z$mE~X6CDoKSgi9>I(b)MpQ^no67`HLhwfnwdajxH-P3`ae+T`K<6E+%WH5u8P0Wl7 zRjf;<5mj2vMf#Z0HLs)aE44?>d3upfa!3*6bGQN0_#V9Vl&9`_^lPejY>j$9EoB^x zsl~V_T6YI@ehTisw~wXWo9U(^_*ZZJ|5)C?l%u>3iz=D!JFYx+oKV;72GwNZX44XX z4?uE;Bs+7^QL%QaHA`+d_2~N_rh3f~yd`X7JGvUNL9?0r#D^h8sk$y*{}{v6eG;c9 zcWwtRvDNl@fNP=v&J*Cun*Bz879NY(8V?w)iP95hqZh?S$QwZ|kE(=i*=7&eDSLxn z#kUAC*fEq)7Cg2BP~L3$y&d?0M^*RY3>Ft`t-vGiPaauM~Yyr{GH?1n}%`Q;x! z{zqgMDlXpPG%9{XJXD}g{#Nm>V_t=J<5UPD3so05Qo+C0b`O4!Z=&~7jxW;E$&hXi z$u4V>j$5;HHmqC2df|C@*0ZxWiQev?7Z7|UUZwR!?>H#0YUDd$t!9zlx^~GKkOYa1 zV9wV$-f0=(@1A6;J=6-T#pjfN0B1PH+$g1bu~Sg;V>-Q67m z2@u>}g9dj7mmq_?ySoG(V34=-uJ3+t?!9@|>L0!4&vf^xQ?+YXo!Upccf4KNw^NDD z&8V>N3!^338jTg(nWRt`UBq4M({0tDmT}J?isvUV{@M9V18#ufFhr$a75c$htz`K_ zYaz`-h2YWE!d&k)+F-#xT)}TjC!26ja`o5N@Xx z_snqib;FXtL4?w#eeyqgaYU*yI=RXjhn|r|YJvo@cnyc8&V?@BR}d3HILtp?nA{gXp-0UDG<(nMXeje81Mq5a5*F9o6EL>1|Zys$0l zK8@Ec8}VTZ{Qu+>J~F^%OduSRsEOMRx60EyT;>~Rcpf4wkd+)syeU6?$vmRIk+%}zh^RxJ~2kMeEh?Ct&g;sM7}OG9FyZ8-w%^kMuhv=Qg@~j zo`~wsyf%eb5N&ymq0zk2cd%x0;Ye0x*(KCtp^@~;5M?LxM^e8DjmKB@xr7b{y3G@K zsxZ?h%OtB3_@)Ib-C+8Hf3~>;Aos1^aX~VV6Qf*g8lP}VXd)~mvTP)uI&d{FoLvC1 zNTe0$OtJvHKjRpOIY!+jse6?o|L|2YVgp0y%8?YLM5nPu7wYI>{?GK$*fGM-h#qp! z;Jc#uF5hv}e7Pg<4=8)}dd)8qfl8{HRH7yvU&|rO`J9Tvb|uvC67Sn*7s$^f>)rvP6n$nIAE3E|C<@Ije6TLE{k;c_9B(`KDaZRI8p7 zn$(5_`$C-oKo323{tVhU&I$^4&Ib`$)gJ(T7)w0%NL=w#!>Ns3qH5PegmgU9XFUeRIdS6($QMkc{6B2rKW?+T0M3A$ASC9Ru1M0bv36gE zr6?kFJ!dpD^UyUpEJ~Sb9=&~dUM=iW_kS}sH%kzv9IfUzIn9BMBVnVY^xkW&L4QS^?{KM`4N9DEros%F+L?AZkuOVb}nhOI3(JsKXyxNb`w;e z+Mf}wcqV?%09x*XHwEFdXd!{pkcRnc-j2g=dJN+WPckHP%o6f$W7fDI80;js;`REx zA{~oB5EZOlxECi-IiB9%DyqN=$xkir{s6{x+TDFtU2HS3%89pCRvFNk|JF~(@tz!Z zX~Weo--uSIDPMWaHH>ED`gANBuiYnaVb#Uo#eq|jry_$a@h|?kHmgH#mYMU(-M#U4 zb|-DP`nY64*k3jBhoNEut0f0L0oCBsNe0Fnj|bDu)zTS2Z6ekAhsV|w#n71-Gb{MC zroozlkCa98`46E3V>M`j%d`ykJ3+pFqp!3l1WU7Oc~IOKIZ4Ee`5H3?V2UCXqgG$k zn;*#yW5n@u*NlNzxks&;_Nm0T2&QixXoD}btWD0e1#C)LXT>pwYZiX`{xw0&UPd6E ztD3p@9bO&gB(UR{wb8JEe88ypdQnC&T4a^bHLL&9uA`KYZkoVSNiiv$su_}GqqVK8 zr}vLvB_t6ILy za;qi7t*5M3ZrJ$JAEys~ZfQP7L9nLkruIQPA5|4!8DqUuMCc*9!iOY7a;SwOnps26 zDsIzSXt5~Y#?vsJ3raC~mW*hc)!cITSnRty#p}~#2WegQxx0SA zb!w{Ddni6oD#Z9D$T@9-ywmEmJ90CGeK_kkhP>e8KGq$^&tr^nMf#0jm6#&)lkaZh zJUC3>VrpGfBxT<6U7GN~QS-2pi5sN|+<#v-)Rm5dZyV3g4z~~F+LLhj)#@4{xNh8` zFLY~~G(5FqveLf^ZE<<%K3x)dKKu&N8>G94Okl^&q#ZOP8Z#7eR)EumTH^&PJM|=y zLd2hc`BwLF($Yueqnk?2l}(sntrgcalun#QuipABBueP~X3-T5{rakjWc$boFM3}K zsT)0Aau0M3rDFN|O{bAR60sAPtQls1_*0oK0cf2sq=o`qNbmgf7n6b|b@*j%n$6>bXH=E+brOEAU%3oPej#>7dEb%Tag-@* z(QtLvisgJlxDZPua$HKCRyaBfe@N_8lq4c!Hyc3>XcXJ^eRtJ#Se|se2=aIz;!cc3 z|2C``r=_Md3|$L>;Wa#RG|XQvS!1}lVek0^y}qa{KzL!H&5>Lo-~A}i(w&0bLv{G% za6j&gDIp*s#+*piV9Jo_Q+PsEb7$5!)Mml9tTX9ZI1ss>QSD_aOsy4AtD_v>S zC0?nQAYN&cAaV7F8AM!L&@lM%axHS&v43H3D`?maI3Cs}GNR?&bP2iE z5bLEJdj(hErRv0`DY$?Wav*MEns%KqtvQaj(37-KjL#*GQ6hf`eiiBLz*m`Ep2^1~ zs1l^V!f!QFnh!l`Zg_e)eRAT39yu8gOcfzNf4*bqx2UzyIEfjmNRYYkZ*) zqB53cO-oj?n=afUYU_1C%#7-QoDkjxc}qTiC+Ow#Tx8t)x6VaCzm69`qN3229)%yW`nUyNbII z?Z+Etn&~ro97lP|5tAOvm_VhS!FiM{=G`NgY5}8O-#Y)u@3@9{7rP{7NrbfL9rcpi zl81g2!XEda^P6(C5hDSfLOqQc8IALrh}pr`MdH{C2%nB_`;!cq1&)i%{{ZO57dose zTKQ6Oc2_FX3?%?HY|E91lO#%@Z27cD4|}QbN%~;CX+|;0c>Muu2y`^Z1TX&z%(?eC zFjx>lu(e;vHCV3GYs!jGgNH51O^)u!@g5(EL&wj*&uNlZHc0YAuoZV2nfKsqQ@rh<_PBhw$m<^MyK} z0tYkTusoiCD(QscazZN&iZ6uOO8~3ix{M%aVZ@j!&4&vWVvnr211TC>T&?wq$ z;S}?Htxb2JCNa@IZ}5=p%i{Tdf8#Ma#A#`VOa6eeLy|iymv#~>lVR_BQ6Q|0Ga|V( zvoM%>lP^$zA?>l*w%dn%F-YX+Wl;q>!=&}9vC(ao6}R(KM(GXCtv3jk;bx~O+u+%9 zX%Tthvc9c)Sw@VJS^#1cXYrc|+2ldCG&lcv(s0bxxJ8dQ2pozxhLkV!bxxPeO6j$e zXu7g~#?pVV_@(|F+M}X{8Q-He8L~~7Q*rdV=2`BvSy>9j-}W*oY)n=~v;3paanx2O z5rzv1I!)@fn7E>0ETyy2R!$MhHgjUDhJ1>j_KNtv$2Ukr`HFoFAd=nQzmO4yhq5cr z-Ey+OXwoH?wk_AJM{RcU&@urYfpr%0*Ss_0m$36Jxe9DSlrii5@usZi^_gq**qKe{ zb4?4nfZJ&Piluf%Ob^QoQxD^5iau&lZr4lQ zEv^|Kz8JL=#j9G9vbJpGu>X!Yz%SIS{v@Th`S*(pPl=Ll6^BeOVuY|27?h6k(?1fi zF&&&9C!s8}Qu|dud9alMBfMHRConIv*iWSzou{|YPfPr^A}NT&zd9A#2U@I~^of@| zD%E2gM?2wV%{W}FeU{4p_IbaeAu7(9>yxMe@et&37;B3#oF=Pmo26jJfKxnOZ)igq z?)jI$nc-)I50v)5Ll2-x<6lb9jUnx-_*p6r1xGwYI`$*0`B`o2)+zd}o@2Llx*7{B z-1`S%opVi(_5023F34kU>3o&Y%VAceLfb}*@`XD2w(2`5+n&6V`*xd|YkWPW{x!2oMvR* z$TGJFP@KKN96joEu||6-JTGc)(~!^a)tT7cc0(je(sApGa}{#2Y8gP*1dal8OGRpYPvEb^_Qp7 z_dNKK;W7;$LEaG@zrjHsnay z#kTL7Xl9%hjXds^)F%Jf-nGJ9b7xxn#!u$K=sp=~CzmY6DK^@rIWxYZ?6%$0)kg+N zY?pG30Zu+_IQU*df6HqLW4XymB74WQ+htm0nT{gk^^1B(kZ*DqPcz-Ol^V0r7!Rl2 z0b4g)-bka)>Dk#+n3ER-S6%3SV1QQmipUzXWEd`>@p?|VT40@DjJN;&=+6-b8Vnu8D zVy7{ZZskdKrMGq|+$@sv$xlNgS(uV@DhP%?F0~<@T+GeXsijud1OZskQ-QY4b+3~b z%Jn_LCoHRcnr=1~fKNWfIjol~kj*%8O&LuDhGEeJc(gAXl#Ad1K)Eyjm96i)@Ki>I z_ihMUd|H$>MzVjwyFj$b5c%I|12|sHdl+L-aH^rl*hIm#e9S2E-R^n7E!dd~y#*ne zn?*~=c@)oN_ZO3}Bu*1dw&cnDbu{$**i)iU0b?}0DZ{~sXX(nf2(n@klAoe{`BEdM zOWZTP*B!oiCE&hkEZ$}!X*)l|*C|NybnT4sGtFNda z`izBE6-XlfIIB9%0-)ZAOwe;VeZ-5^h!Z3cK;BpXvTP7%qn4 zo%P$aew#!$)<^@7&VZ98jm#loN*0BoSr$qMDedbUmGd%5+SJ`f)~uJWqed(i=vsPw zH=GxgQbk)q4tjIHmg}nL%wkm?aGMyyeHO>>q`c^vP0Vs@eGvSMGSwnU9td4Al#UQc zUcZj^r&IdrexJf6{sn#s!8ZnWK%1uW%WBh6$*lqJ-!>!+GDl3uDfbFvlCXT0b`0Oc zGvROZHfD)YuFd*w3MUJcPxoV>?^LM;$&^fbehI)(z9K>p7%R8Y@PU$&rI;zz=#v~l z#6$PKw$pIV%3`wpN!f<0RBi=F-u@g@*=dV%2>(#4@yhBdbIoo8$)j`gpi5!nqKy#C zXOf3*yTT^3z%b5B{U5~kxJ?!ENBTIph>VK1Nubh4i{oOg%74I(y^{Lo#_CL3ZV5md zG<-woqG>g0AKHtvMO5*EAAYd;W=c>Y8g%4mt5{Ob+||PRlpLIO`AwkKsZ6`=`Yot` zDl2{LiiM{W)n9&=?m?PV*Q=+)rUf@mHEnCU@2Jehbbu(1PK?FPr^!jRC6)2_yqFYr zvxE8Vnk9KBZJvUjq9seik1-%jl(ioq`tsS@$yr6b#aw|i3p7z{loWc?#J>lhREf3f z>GtR>yQVgaN4{xA@@>jkc`5akp0Glucm=}ya`_BVt=_=fFJdv~kH3K|L{)D-(V;SK zgnZ08qq^~NDQWhBZbF?NdZy?8t@E+)?e}TEnG=d-uN94>LCm3Glh^Lr_J&;DXGN0* z{k8j(CT|SgfVRh5v#&KDNQ0?^n02GOd*3p;u9cNDd0Byz|NfYoBKlX?F6Wyx6&C)x zu{zWT4J49Ghc8-zB!9yflAGk`HkUP6+J;Ys(YAEL>SqrVm8?KYrzYSVA#iX)5Y#V5 zI>UOH;$p4Gs1ubnZY^u(l2e{a9`%T+m9agX)W30}MtyZ%&t-|{>o#G{PsJq0a+;tE z>Jn%X?Lfcq_jR*v)PYxuWd{?Ta+qTaXg|Qu6kD?%1q)6-dt^jf+dPOLlQ!CaMNNqG z7lW3?g%j3<5Hwmr2g);TB^WUH8s+CEn1V7ddVNM?C`_3TEpe~p(Fr@mT*|*;FFYA6 zerVjNTAex!b=46e#Z5R&iQLbvEK4j0D=-q=L;wxOoc(pFkHw&BAxzSD&3K~G<|-v< zO=ZyKetFdGh_~LiP)}%nMZ3b?a^v>HO{W4Ld_7YD8%GrZT6}T9w6;2q)+;TJK`*a% z81Y|&m=!kR;+qEee|hj|5y(sRMw5hl!>)*B?#^&gVmW4W;rd%eD{cJR81a+mu^>iX zVK&p30v+OYxnGGI!ut{wR!AHCUQ=VhE+J(UP!wod&T(fpDm*Oc519rVskaF6A1Uk9 zo_YP?cd$LnP|4IU&wcIC+%N7HZ&b+p)k$0M#$Ul_E>#`9p14ZLxNbyPf4!G<(Qs~f z%a;CB0$O~}(maqllQ_pymgK!MfFJj_Y~C5epPr<>?a*(JU5e}N>P-Sl=$jlVZD`oG zU5rJmQnQ&u8VmL>m?X;gL&OV>n!at1O?jcnFU`4OCz*YYTr{B{(zwFrj_z-=k5h4C zuV0cbwk};O?b8_n}``Kh$+HGYZqMvOCD$ zL@qnTf{NJOk}*&1UTjB2Q~Wi_HmCNlNc!3gs?wy zMZVJRpvlES^i{KK(U`IIYl+A;P@{~df_a!;ja+AMArtc`9o~4XYf&=Ii2IdNGw6Am zu5A(EaZ}FkE>Edu?cTp;U`qU=9R1229xK=&DOhJ^CM`&OJZNl1jNG+@<8LbtgIpWZ zQ9<&QQ93toB5Ao*n@X7`#Um}s%~>3@8M@*M6J=QBvPee)?src)4kaqtQd3wm)&Sy~ zE8JtIZ2Rn(wxTKCvpg=vBMbxo$HU9{Ei$a25t0W~QcN@?tRV)8z$M!mU@rRg$|@nlRW&DrE%u+Kjd0nnTIFb-8<(?R&sBVe1Gx@ zYq;lkdK#Gp8FILuSsw00N;k9(x>!I1xk9#hn_s|X6wocWEq`@Do$LKJH$fX^6Ak*@ zI{eYYYspT_)sWUK*kg)5(hyb(=F*O^I{EUfm_#*lc{B0a!I#zApj4x-=bcr}uqUBj z`H%LT>a(k#Gs_}tM5pe-gf3Hx{Vx4fNgO7c3PQKW8|{Z_8WVWW|Hkov?Z45AJ&Is^X?ktJdpg?CAwvO0ZoW{fxl7<^+s3 zIZxL_53|JB=ldL0XhU>jAqvNdEO4{-#c?;ojhgFlwnAZIQyLW=-SqzF?8$O7cr(^U z`oH+SUa7SLIk*07)QBpVUG9>26XmfpcsJ>3UG0|H_*=(``&@oMQ6d&md$@|LW-cBR z25UB#*tu-zt{Xu2VB--^!{%;brpm$GnZ>ATlzF#2aBtw%H+cVAbqJsCwW^x$@mQsv z_WKS=Ej%0fj&q#bx>9DZx48R_Vsa6$Yh>Z@4Xq#2)I(g*FI)-@N> z*z>$$%|gIBL_!0P{1Epjts@g!-HOFlM-d6ApILpjH0O^}90ILub^ePYBZj9=ZA=WCb*#+_85vu)7P{y|Xzy=pF`({afbL zT_<>I!*~y^ie=tdDetkd#qr0Yx zMYy`iy{e@+6wv>Uw8AgaWhUaTf3c*Tz=im*uu|?=4m>RU`uw&}qrZOw!3n1|LBkE=(y9)#n5rp(sN`G;b=2RfCbmhn*AMew`R_M{$|F2`hDJ}eL%@_+_*@k1wL$B z>(eh7sZ6vOQ!NuQ=dJQuV~+sdn;)qW$B=z~WcXs9FAFv(>Z1}qRv08&p4ijDpr0o^d?1O;dzc7A#M)J8Q@_3>h_Py%r&2nut#z@uJ3A1b$qNF!DDWDtFQ z#mgl<)VMDqh2|mqJOMkF{0Uubc>zsjD6Xj#5l$l6qXihx^FG}Ya%Er-D8&-tVDKU$ zG7wwWHVe?P=h4Z}sWN>HF7a%UiG=Xro-#r;N>)p$oc_9GaS0s*WhGlNI#F_M`)NDa zlv!;nLcD-(LtK;?wkfLZS|QoSUEfGu05r_t9P>wke|?;YDz+TAh!N`9&4zRFlc=$R z546Ye2=TMnjP4T5@xC~9gYn| z!5c6LCZaaWa7jFB_v_@fL_^;xNVARAvTYP6_Wt#*6_^D{4alHOA5JMLOgFqs+T_am z;pBI>U-+B0mfaEdxpU^-&x?z_AIad~hOR!fEjfUR&ig2$`mT=`O@N``j^pBZ{c#FH zbZ*NoYKDsdu4ndMM~U;M2q~#((*$PYW^nLse4{WP&k>hyQ_V?%bh*2uqM|AjnKqw9 z6~pWQ$qICQCI7WHBS|ym4(6Uy4(W&Tn)kj;ndcVN8}cSX=s>$aq2pQD^=B;E8k}H!OA=^O?u<=ft<{>+c z3aSbH^2g}A$b_^MyABs8?>V)fwn4YVnLQN)jP@t;?_RziyMm8U0Qv@?3jw88_~_?bgD-(lHFNQ~%J9@l=1{Cetng`v># zGjPoZme4z)=f^wjW}OVk(ITYw$91y2ds5g=f<4{4jovpA zK_uH0cK(c|ON}7+@#7(pBH2hiG8{>Ofnps`nO@@X^&}rXY%Lp8(fH`Z*h}(WF##Y8lP5HGXtt8XCWvprK7$lSyALvhB(Ny+!>)oh}OOcLu zwG_?pNK`&YC*hBKRtBa7#}drW$+rUdKPSl;WxwGvq7PMJ_LEc1$O2gLHSEz=P-O^v z7ULSK)$=kyXCzvsM74j^@6icon#@txV>{t|X;)}gmh*7~ul)!A2zy}T(4j7bukm&# zjXi(VW5j7|arLqO#T8Ion9O~rTB`n?e*m)VwiAoz>CXSxEgzd64lg+Inhf;^U+9a2OS5!`phwa@8Qt@j^b(b!`AL>(4XBbv zYp55qZP#_xkd;T_OwuW_xrblD!llB1#yDMJx7?N($z3!cc|fhil_?9D6Zl}s7kV6u zv^%jW-p$>1hUNijUS)Jw^iLwjtnDN;xIUV9T=CeoSP(kr?=5Sg9U%b+T_Mt@V@R^C)lVBX?!sA-tPuEWU^w|OuLIHK z6&6f8q3ZTh8AQ!TnL115u~{WTq}6qLVfJmX-Kp?HX4=O{*MctcYm2$G>qEDkLdN9_ z@(cc?I!{|W9g6_oH6Gc1YsLhIVl5aicvyx=xaYxC(Rn_z4;_}40gNzD93Kgw{zU0_hUXVU7exA4 zq~P;bDRC5-jIZZ=H<>l?4v8pU`Q7DxH;qut7C3G1x^Z(GERLr`Ndlb%ABZLhi{@i> z3fQGVOyFrRe9hw}OHMJ2e*Q_FWP2f7ldQ}O#<9ob$KWGYnVGKEN@$LANs_LcZD{;h z<*KDH>t{f&P5gY27o%_WrgXnBQSxYTLT=)1S~CWt;yWUvKttUksybKPTrXxh7PnK( z%VTu~1wAD!}72Rw$cgv)|Op~?9)+wXW!p=CdX$V50B;M8}qem=;I_bm_iS69we z=nBsae1IJxQ}YoUJ|SUvSa15tbz?iE^)$h)^-#y;beKTogN9fP=V7Y&nL;vp350CPO0m`z7%T|z*<^-Xvg`JLdPnM{c9pxr?o1H2iECf__&dE^8AG zRj&^JBN5GRevW#`)p3$Kscy<%=Qg08S|vyNF=DmyWQ3}q|Ghp#suBHda=ey}HZ3HT zCSFs?N{csV=cZg4l)4G3#{(@Ae}h5YorB3V4Et)0NaUovtyQM14MHzwe11UTW-7G9 zgIl}l+={-*<*=|Yn;JaNLhY}L$pauaHe0{@O_IUi0akWio&APP@m}f&;u3T>%dgGk zF!HzmG_!V4mSGnK+ygUJWm@}qwD5;~V!*=VAO=RjQocrsQe7eG{mT01mUqM6JKaA# zL0&j&(ayb-&!^@h=MQ#Eq1t4{ock^b?Iurg+Nh4(d3*z3V{7v%j}CX3(=KTgua55R zAgE-DIjsI|VdmJ%r3Os~lxVVCGw^yrPKkypZvbmZr=pemb2Ej82(ktSE8#RS|9N&W z4u70N;}cnnW6Z}SA(3z|Ux!vuuaHr@LlY~r0rB{2@I9`@jX zYHCTsipn(TX%xfU>5w*QUxjwCkb`UHq{N)k4!?s93l%QU}LH7QDY_Ax+lTE?q#wf*eFwR zQPjed_G!ZIB5f&AQX3O_EtBTCw1_tK_US6zpNkR`+Z}-Ghnb440X2me!aqWYHfD9) zGIW2xYJPjWp12cX+<79XMI4yYO42-#H(Rcg)=v?K*G`XmK--5>;lfd5$(FLKi}oV9 z1!(e7cytwE>=Y1+M1=GLK&Hvk)`WZsIJ24z77NZ}4=9-319d?DG{AwLZJkTBOW_>v}A7D0gQTG_|dl?H@QUucp-ok%WH1@~(Y`-if4`<}=F`+qOTrHUd40aV=ED?rX5OfSa__0NR{tOe$Kh zKb+rozi%Wt-?Z&{h&lb@@dvNqe?nQQpWW7;v6xY#V0N{4?P3vB6ojn23DO^E3G%7x z?56%GrxflYImD6$(omxZ-B~&qw5zOSn%sSkFCkmtI(~1!!FXx0Lj}%1Vgd1bvDBZg zV86#_%96nd8bvwOAQ1{il-6%{HuW!kC+)yi9(!j7%8=IAC-qvHOJvkaWg8L zeG4=WLY}z>kdtLLlMi<#2Uo<%lTyNQ9w*}I_O%1sNqU*e1~j*N57T;79+Yo*BN4B$ z_kBCuH^+{k6xYLl61=~ee4Y*?smdk{JFUsqu%)0Bl}%7Ic&XO^v3rlaf`8|NE6j3R zRag_XjB{EGn^RyC)ray8BNw|BW38@Af9pw@=x&LUSW!SD95E{oF|`Pta(60M<%v0t za!|D1nsZeP?81USGmcADr7b#soCq?)x$x)tQj!{PK;VbHrc)9BX1vR@Ybb?%8+fy6 z6dNw=a*;BTqInwxr$+j}nE^c^m35T6Vcz3&*-cDej`SvN~Xh0-am@ob#;*U=SF7sAB zGgFcKWivLM$63)E1)0rjrRwEynAWPQ^Fr;cRoScRx)aNg_dts+N8YlxvgmH8l@;!i zq>S}C<|;o*cczo9*1r=e2yn&tbQcNR^QL2@<7e+CXSl99D6pE#)Xt;ZU^y1CnooIStyDe6B8-Ij=J*VJl+*TS~Xlj-x@ue07alh0lGHY5w3M_vGQA3WK)f2O}DhLM2F`QsU)rxGKOF zaO-eh(NpJHP;o8 zjX<9>XjD_O7ZKRCD3ZR9DHgCu{jX%;PM64yZWqeb8t#I)qI2mmsVu|Foe(29S5TG5L z&yMT1+x;ee`az2%u}o}{z*^i16NdX2{R;+#DyKz0c>u=kWpN4s}oQg2)81?A?qzEZ+g4AX~OKKLA@G| z8mY#6Tsz5DZolF|XgZWk22f+F+R6?mE(MMO#?*sLQG(a%o%74MhMfZPB#wP#=jpow z;GyE)Wl!NP8tu`C7`y*;@I3C9G`O*90#aKNoxX_;hjUgmoZ5-(0{rjm$0@OF8-&qjx?@24S> zzCpJmLQSC;od}SD21=nA8N5LuWJcB1%eK}=Iqe>-3K~+kx&WQtH}{d$4!ev4D?VtMk*!@?NwXCu^E5PcfYZD@8w+NX*9=R2)Rcimz=>4Gc|&h}^p z6ZS(mTwks7x-__d)C7tMPB`$(S)~p1d}=4U#~RK365RitGp3_?<*Gbfzs(C``@b}e z@WZ~sHlS=-(o9q9HR`oO0l)>F0^rO}AWE2@`A$(`W-NyH8hnUgn?f*&Zv!D9jxDWG zhcH6Xem+3YJ{?T#X-*x5>I&LCIsg-2Joz_ROj3IsYOTH%zUjn}5WIMET+w!C=`oqM zLg|kG`E&csMLnohZgTTBdC1A`D}ipd_e~ec42g?4{}BT?s!y}Sv*j$puJ-7M!|xC3 z5Bl#9>h_YydWU_hc)0y=q@RI^(%iOvo$URtPq=EeTC3~Lkc~FD{_X_~jwe-r-9UY%-)+rG#zU;aS@_`X*}?;Z=ou5B=8K7mP?baC|24)ez=3 zdcxS7Z_hT%lXN{THsB5$0L(%IkUtr~)&(y|LMc{h{v$Z8_012m=S;lMGFg5ple2k) zUom%HS-Pb8@B~Z4m!uj_($c|*O{kVFXnAt;)XuY+loCn#u_98vLdS_Z1Z`kM33GTI z`(YOh6UXyIs$8^!9xXv2mWujoEj-7@E1NH74%P*~yaIU2v4qllPv2ISIPmc8J`01Z z0vwb;Nz|MLyv4=5NZgCbmMHbL3KmyHuE!j{L>Fd%zsFs_#6`PyL8l=Ak}Y2D#YXuY z8J*wFw(IKthVR`ngeK>glL~`C)Y@)?p2VGc@ABg`54?NP{P}D%3womUASq)1bzoNr z!n=HC<4gjGF7rO#Y$AfK_01sFCWFQRb6;&b{H76TTXzZfdxH2er*YfWQ@`;$kIPba z5Kwx}hS{ihkvhBQUix@=(Y*k6XgM#ll_VMTumJt(A zbpgHs+i0tzZv&q1bxoE9`;wD``4$hZ)ocjlOT_So>oQ=`M*oHnsqBY{g9ouDllpZQ z)tpZ$CDZWVlhEwnn-5oP`w=;>o6nSDAauxx$#d{~_p|Ca4l1S(=VwPmE-<*>4d>c;+B1eNHl7MP$60`4F znCCSASD@i%K&|Z@hj%+(Nx~-JV%V->{n_%cZLe0nbqur5zKrL0)D5qe!NntnUVNZp z2x(^2dTnIJc5-^t;(Bur=q*&J8RY)JWPV}4atFxKzD|f_=1tjMdCvQM$!TW@i%{t$ z(h;uk)BVL}PSa$DyYLp$YB^xD{{g5Z*OKRhU9t_4182yZ# zW$liy;~HJ({Nb5t9I0M2cnqcAMcK){VP~a7PF^uTQzuo|>VACjh6(7?88ELZho2)= z=O6xYeyC&~w=(^`v@7Rjl(B<(rI)}F>1eZOH^iDeEBTrq91}i#=BI z%3Dq=m?n!QuI?%u=j2qT$>^IBVO$k9@6V!qFLUp5LyaW^qBfBUxB2z+a90Dz7rg2zpgC-<%ke#ny3Hh24N1flasCybV~Atw@> z)(y{6BHvPW5?z1;exW-OPUXDu5S=9nl#aaH{~dRb_+#%7?LW48hrE<6i4pa6NV0q* zWhSaw>AU;v_81#vY?(qw4I{sn@N5(dG9Kb3M9*j%ieQP!qBF7ara-0S+$Lt0?fK@( zPdZZEOMFbWdDpoyYt@fdxw4u|tVPZe$Aq8m4-4H(*4E&#MaXTk(K31-ufAx7f6P9H zv&nNH+xuxT;AbwRoA854UK83-nz8Q*o*!YYsd-Z&^5{wZsL-30T zI#nn{g&}m44dmb?Sm|E6j3O=I+ilR2W7wm}9v~uJk`meM4QxVPcX7#zR|(UH7_Ps) z-|pC7yr1|Q??qHRwyE3(1qVRlRFC9icvm#zg2ZcJ3qQ;0NvLy!M}5njm)Pz$WZ1pC64_J*QTU!pT{KB2pey2LDU{WuzH!kQWX`AKShlmVkHe zK%cA_D$b&1m89`nhv5{#(6E3hwYE+?<~FRjPDjzhnti1N31D=UiFzKk?g}s3>xIS=}wV5V17|TuXBv2Ul^Wc+Krz^ zrTjKRXf9hEkEnw?eEP_OIW8%p#EpJNz4ryGCou|5PNQe=g$?$ue6H1Eufg(;;XQ2x z;zFr9T;sdGM^#{4p{~>(WV1cc0seF-^!vQPf{P!ARqI~xpA_E3a{;@Pn(ZJ)H`kSp zUP1C7oYWidM_TxBac;JV6qcEPHwLG#>5{X&!iFKSXOm{A_go3c-DH@g9K-ZiM~Wce zI)8#z2CA3aDCeDvten{0caj)HSo~^8C+5$`fpoEsr@hT(9um}KadIT_Y*#|YV_MJ4&lW& zXNloaou!mr^mmAVJb+&<)YpY4y`dzFZA+9})stU@e$5LFIiZN&%VA8_Wap3~GQzZR zm}x^K@R~q!)-1csx_d67AwODoo$+c>_w+iF?y~qN2o=fQ{lv4=%iW!pow$PpA3x|> z`8&jGeJh(%|B$`6Nx_^o6uLt}e-e#uUxRuT1V_YoEp;+s*`& zCZ$exRsoSwc8>f%{8}HW*$oJ>VDNs8WBNVA)^$VQuCU&$ zJaPYtXeOd-Ilew^DJTx#&hd%5?lESaLkGoT-~%58lOzkMk)D)+gKHbyx!aP>#3r!y z{_1&wro@#lD8WTX;q6$CZb+=MOj-yAPJ(EN%YC^==zPHN7XOoZDEi|_9^WYUf664 zkD*^Vcul$QwK1_V%kC`VzNxC*NBHShvhTYe{fAxutM`a6qT^fxsc+v)pl~v1`s>+L zbn8YjZL2GI)3N`39DL}0nSA*fO3H)B0~`4w;uCDF@}0INUToZ zf$Oy82CH(7fd>|>Rw00ZV*dD)|F0QM#0|@RUzcW(y^#5|Tsne{pyCk00qKo=mhE!L}emdjf=*{W00huV258`q_EA3}smu&e9@ zG}RZtf+t9#{H*b8Z@t_;m)-aVH~Og?!oFOeN3a6{WvhRi*iY|BZSvCzDA`5 zAY6+a2f;iSFG7**d;{-)%>qEV?(A8guLKIo^2w^Es6gK1?vb5MNFY{l`WnvOeES&* z;RD?3g(F)>t@!Sh0wfyy1+woma#LhtTP%NVQ(q8SYY{IT4BHh1p$XR2piJ7lxN}x{ zM{tqAWHeI(sz_2;y2=lbh%0=4frNyx{j!tKsWaq!;6abWL*KqY4meb19Mh%gB7W_F zU#pIW+bp|6a$Ye1a|YA_S!uf!S`p;ou>y$ILjE{4@mWNmRLi@%UD#L>Kc@GgqP}OT z>I?%;I255_@U4_`6Uu%J7RN+M_=Zp4=p*TlW-}-(G0<>0OBPoHXaDsd++Gl%}R} zS(D}1IJFJZyCtn01r$}1q+31#G;R2sv4CjeHz`~k9G4ih=eBEB@6*3ADHY1j^SQ0G zm;gXhuTXB`MsWcB!r9l|y=hIFst0*Dk1*s{t$h{aH_pnQ$`+Apuo=AW`1o^jVO+%tD)pWKyCEH_%sg07W>(vo6|U1X`^x2Mx6Q5} zL+xomQ#SPVMNdma)&|g=v6}#GlZs69>(0~fl=duhY#}^}d^hG2nX=1FkW3z9)KRKYFa=IK$hbyktfgu&456S1>xA6e?@o*1|FJK0YW>amv>F z%i59p@4R$>cw|lpuCGWK9@j<;1nmJ+x`l>52y># zwBjsLyW1o&>+kU2Dr>v`Qa10+RS~z>d1e+)q9`a-RE@ncmJ)Y{?uG^&h_dTli?LX;<8*- z?XZKkP-VzaL#mrv`kf!3d)W|#chp%#P;P_!CY4zD07;_GN6(@u|66b*vxMns4uy^I zh<5+|!KdEq#?|Tn)d2j}2MvY3F$KKlI3;Swb zIz|u|x^aSgKNh}ir`wK(#Xin;%`q9^p^Y$5QR==a?54Mc;Q6at0D-5Pv0DRdVG++NLhe4b&ykq_C{O zm0y`zWNfTadzF3_6i z^?xlooqFDJ+fbqWb{x@F7vzE1Lvv!FvR=qPsJ zGHv4HI^u`PHRi{oln=&2I*!}P_MK4NAqOFJ`pP|GKHlrRJYy&<>F&VsD+YhLYsQld z6}%;XDl8Gd*DocOThFX+fDj8GD`#T~R&hL|By#V;aNM(g)YN@X{* z>CV|$8P!QwyEi{v!^%+drQ>s+YCM_M+YV z?AX%h?e&3c;W>gC)o{_~Clzrrf_KO=CSnc*e8Sp-5R9=myt@12FwdtcmYY80Pc&!U zZE}?g&;cnBr;gZ;j{7g_Enqv+RFvSPLLHqK4&4bP{9d80TZH^Ud0@QC9t0fAtl5hR z4biBjx$2Sv*<)kV-Hvu08;)m{U{;OfIUhT4LDy2#2h(6}DCBbd=gL;)S3Q8!qSBtj zR{7#q@N%908OhK_X59PA)*nE(&QDCO$v^4Q2{}QOWhDup@yk+Pjx331eRHy$OPWXj zTdVp%F?XB{jA_aQaT3*<);ZO`IF{(9o{yqW$kmY+9|kGbvdqw4!%|*kuAQm#tUk)` zFD2?uDhS5hw$L2MNvS*}F4z7l4@eC2PwLPqQuE$erc!3uLHDFC{0?8210P7;KVsN= z2%d7ST1xy$)0e+kk@w*G@B;Sfk6w&ofvf@$->JrQ%wD2KdS>9oI4d(%Tw@Q(1d2a6 z{|E-=dj}kf1lEMC&+T%KzJM_1uJ{5O$ffD3s{b4^u?L%LV{3YuK?<{Y-*|9P;~0AK7h+SZBMmuF?Os)NRIPt|NR@UN-wGVk9)!rBh7u|hD; z?(m5kMIK8H3!3&gF!F4gPnq06WWj@(3?6y>KEJA_OWJD_>%D{~?+?Kpi52A!8PGr$ zP#}?~?cp`NrLf(gy&Fhp!bhmI#) z`xIUSI6$=_s=h{+~1?Wn&$G z@c{o}y?mRINLUQNdER6-41G5b-#Qzp_#W(BR#u{|8L3pus?V@bXdj73gNz%kj4Bey zp`cyhJw+l=n6bXR+Pn{7#?gE%xMbS+O-x z;zI>-u|~>iJRV`Et%zy)V5Y{ztG7t$w)JE>{70mgBJ32ZY9T(RIrve%yB+X2)84wL zRtA+MJ@Q1|bK9|iguO{Jl(?-mpSiI6ddB0h-FOCgT#exN^@f(BDU(#&BjoK*DdA#X z#)*nuzuSAk(E?*~XgI&*&vlhrXTdPXAQ*YMxB@a-T!D-%{4>8~5AK9q`-GD4TZU@2 zA0_@FyTUbK9^NW6t)PwY^kzG$-ozX~TAdvoP}}CTF$_9hq|o(QM|3CMgg?i=_DtX_ z-=gI)dHVIC#^ZGg4IErwcs{ zrggQClico=57?ai-hfw1kAMsCTPdE-Ling9Fw!4XhO+~XV5l5V#>gp8nITwWD`}Ek=QLul)LHU zT^X9I?nVR>!qrFB1Cl?R{N*G5Q%HgS@&Vr6!+p!oYa(uE+$TY+>WJ$s{54mzp$5pv ziuc`%F}1p6v1w%Fkrp+VFwtYyP{c^BkAj9%%kkHp2<~!~ryo}lx6^h7%p%qgf<6X8 zMUVxu63HVfitiQgQ9y0m+`#l*!oq5cIg`DgefIr;iTK20m*RtCc=wNM@BcJEnqjCd zJoFfq!W^9`yLd7WF_pX1lJ=!A#y@GiM!QQU$X(@#!iU*2`sP>SDd5RLWI!GX)p2}K zEA#v?aDz{?N^*6X2LU9z*&h8_4*Bar9^Lmb|CsSu%iTAvYA4iw3p_V6o9fL>?_6G7 zU=FcTT4xQUnS1@zpj#r+ea2L-$*!7+F`iDfE1V*+cpr3RkfM+S&Keg{vJvM1Hv)|c zA1Q1uKP^*@6dW@bF!0NcFJ8Z__EdpPe2EU4#rB-PC_v3vzu&wOp<@UTS>B1#;ISV6Wk20*xOpK? zGrY0qF}oG}F&^HPMCx5^Rh-e$Z54OnG{TzyX3KiJe~c)jjTdEf?F3lLK$n31B&w{v zqC?lDiAw!MG!tpk5ix&55^qOTYWS8`f(5+-;UrC}?*zwIL@pU7nh{|dqx*GjunFof zhc0xo3?9A`8)qvnF}4TJ8CACa88mWH(W4QTPKVaTt`x7oDi!~UD4>`S@frMK$TGVB zMswFA%a%;)@psM4c8R0q_MAs-6=A;kmJjNK92&S-P)US~3^AH7Jd&vCZEH9@ITE#c zyj=~v&WEiQ_)Sii+jHtAS@9FA|ItcxSV4~fxcsUa z^E>565~4CN6Yymkvff0XTv zd;g-mtl>SRhNO#s6m*Pp-B~@s7~89DP7(0(G>-xSQX43+%n;;ZvjvnVo6F9~c0f8g z)ZZz6DIxMV63)L>=2%XV(;C-lyI!8&)<<3slFF^o_$=RLEq8du^H*B}J*bfc5JjAk zl1o4F3Aa@t*%c!-)q}L^vM=7kGN)ye22W^HPb`Avd91rIn-E5$5@9%UPO(ZWymV;B!dGTnJ7t0Ax<3kqK`wWJ zQi`hty37x|J2)=zBR}6O&QigjKRvxKN09Tolf4LOw$`X-?^@2_;q@93n>oYKeo1C^ zXUkTK(V-CaQ3a3V)leh#)6~T0k^OgLrNYc*G{X11G_(l5Olm=)p=ku5YD( zmls@PeBHLcAPe3LR6ismi;LG(9rD*Ck5}dcK?RQF2BB0HBk;OnwfTwp0-$w9o3EGD zvE-ROHDG^{dj4&-tfVVppUa*VmI70i3xb73H%~9Ssr5i@R-W_vm}ed{8$Jswc}f%j z!TruS%SFiKax7-yk zs(v-#rZ}g#@$BNj*5!U8VarLMNG6h9dVHWwZi!6C`jwZG$DFfXr`Ppzx&3Bvutz#z zx82YrDmD*cy{#>qy-O>bTjUN2M3aVynzjCl2}PAHMQTWmQzOY$;Y7YyQ#@%Fio77)N5`tzh+n ztABWMLr2XY=*{fwJ?4@}0Bhx=$IJlD*<|SA?Vk>U;x|ctzZfXBdoqAFiL1X<8I)@o zU!)Vxn&ed)O8NSf4~^USEpZmA2Okhsd#-LOa&mm~!DOkHCXv+5<4)QgS)+c$gW}Um z96Nj*L*JOAQZKKu<=fAw# z-vuRE7LbIb{Hc2<-QmEqqHRZn>2MoCY1ih92Pd`y0ouB4!{Fbgy1mvMI2-!Q`Ibyhtx#qc3cx9;vD@ zW=?UMIp98OS!i!rSvy)0J;Jwh;F4^ROF93&=$Y>)Nf;uGT8bb8xY=6ixB=*bf(ks_ z=RLrACnc(mt)KZn{#=+>I_Kn3J*;}Oc#TZ_6U=5^v2~uM+}+&${1rAEe2+qAU$ijl+#s30$aQ(^Ts$@yX-Tn|MiZf0NNd zJra8~3_C@Jwoba~M@YnIal8bza%`25wvm#Q=vZx_H8JuvC-sbhb{?o@FwpUZqm}7r z7KYa2Zzs8#vTV+WcXxHH#Y;q3d(CBS)aiLE#1!aL#A@mW<;bc;t4uIPCfNp}^M&TF z=a_BZ3KyxZCmhY;PfYCQ%DblTOb32{VT9ny-oYFOw~T0xG!C{PgT$R^OO&IVp#{aN zkHiIS9aeI)j_MUJt|fJuql`{lMuUSq%|W6Nha!WOyY}LXuUs$dr&eoipRXvMA}*{~ zc*r_*Lxk@GQ3fiwsWg9lPNnNHSFDOGky(*xv_1*?hCYVeU`4|6``wB7+-Z}riTonh z4+WvBT-_#BW#0HgdRE(uxhjUNz^m+itIk7tDLU2bjfyc`5Om}r9Ld2VMm&8I9|k8- z=wuReXM%VDWuEwD2;`<}lHK*&@{WMgdCH=a)B>eaiH))w|F+5{>$gWg^i}pV2_c#@ zI{Sm)?+R33qYN zS~mZ1q1%(J0b9Ds(xiL3Hd8x;14Wu)n7{{~=?&qCiHmNjt_pgpP(^Ig>sUq!rsI-n z1J8G-^8-6Ye46O!sb)IU4Y_<(_P-Zqd@GOQ+a>JtikY1}Xd9a#Ize4-=_hrmu4lb- z6554qO$~QOQNp_vTP?*O-~@-vIO-(y5;`=Dy4jBwjj-*7u}uMan~@ zHFrz%>qL6$YU4#s>uNJ$m9&_nQnyRC%I7au9P??B^Tx0BJ_+6}WTF+Wyl3`aO6CZo zg5hf8<0;ZIr`+kYkNEjTEnsz3$IH^UkL7f!pi0W~;5_Uxas^eB(;o&jOIE6wf3cUs z)x-Ro0P^1IT+J2j*aUxNCjdSFI9BlH2O8Xi*?U-(?}qSI*{(%-d?N`GEfE z6rhGM47yrtBniQ8?9B43X!=a^db5(%99I zs=6vQ;)-FBp*~sr#BffQ9+0O++JVs(->3qVvxt#N2t)^_smAS-?@8 zFwMfOsy8r)`@Vw;Dz?oqXbEkR^7zOXKa&};3~IZM94?bnBKVBGB4(#tm!j*|tj&WF z8JUo_A0KhXTa2}N(0>z4Q22(*!1&Pw*grJV7zAx&5R@dSm8FL+8WN}D+j+R3XvShR zc+s8_Zt9q><7(H`6ExcZlIYTyZA{xqx=8q8-|ByV`=E)WTCgrs)$Tv(*bFAQq#b7F z$3ry6i(%GaDIu4#dOGu(JBue*WYh%BN^}Z-4bH2mP-RBim1O&867WA~7!AY8DN;}+ z6Lko2v%fIXFlwve7^|LgzPt;DvG(XHlPUGe89f7BQ8EKq^DV$<->r)wD6(gSUmLE1 zpcVU3){sR^Kcocweq7oE5*hMaE=95`MB^B+xDYRbjnmji;%nBFU=FyCcjH%)@_z-^V+5T72ouVpjVZ>R!PA&D4r%M*lisMzxtU1CZ~fvHEb33M*~XB zMBAt7erx!LB$z0DVHl$uOLiP4lU!LdT@+a645>3iEH&@a0^}WMp9Qa9Ht5SP-i-+j zU7Bu%G6$s@{{;f$$O~Ix>HZ$eaBM$q{uE-W=WM<@C+xzOy~}cu3Kkf_S z%J}UQ!)x#Ozzg1~i$L4V1DId|fuy?3(-1zU9s**|Np#nrgUla4ODd#N9)-<_pK$6_ z$_m_gF1D-C9hW2k+sE&UwtM!^5C0eK3sY# z{W0+((+@5B>tbN>O8%kZv$?v5A{0sD9R{P= zvc9CzvF5+CcmMHSw3K>8rV6CPA4@7g*0pC!ij4KO-X8AmY$=w^Q8;#8`y49o=YHxQ zGc#Vh-S?t7T9z1_EhUC@n}8A;`MXGPzGD<95R89tvF8hV(VJ6EccxouL57zXDz zaFwb;egoV3GcQW{oTczsU6hW`i^^N7*lH*Pnx5@PWnC6?SRpy|c)KE&BVK~B@ z{de5PQubQh?z)}^mIIS|!>&YJIY?W`oyii&prAg5$S%s6ZrNxV{&4#?P!t5NCKgYPwn;@7D6Z_)?)isW>w5 zf2lM4pFLlZ(PM9`u38YC;wS?(1o%LDYUR|_lIhl)(#K2?(Nk1Z&9<0mV6SD0*+$c) zqZ=^nSbE9ID`060S9P?a45RF6{law`B!x%cfL^`=>I*|NVmb4#{LG zjIn~~2R<`h`LdTkaF6y6OMF}&H^-e*6u{SDwpW4Ew23y<6nWjdG2|TNMj0bP56^?9XIXoww#2cBe)!J@)t_fejp$ot7FA`vNSkNn zL+xZ(QG-7|rfW>T%4v>iW2=(A+z_w<**a6=VEV+II2~nV$AIe}@aF9#Kb2R>BqVF^ z%3f|%SgLFqM;c0LV-CqaJOp}%Rgsgv>+kw|68n#5Mv9m@D$U8hM%j(QsEOkj4Xhg4 z48;TH%z>h1PKo$|P7_Tw$4+yfFJ~7LehhJzQxk{U^%n=yQ(KK(!&1N!BnIyrACu9z zUl&$d*YB;`%`d>IE#e_(DVXFm#cTS^8l0P6-nPcG{Xc9uh3_d=VZI=!#5AhG2P#d? zY*xC8a0gUTsXsOKG*YrPD($QnN5ot~sDNvtYyOsH9e)^J!)B@4QLZZJ)Qizm^=p+G zWxZKYhaDY!8txlGUYx2d2r}-*8D9XusedttYrMDGBpoGd)QxW+{63dZyU5G#06{hS(aUvW=y19 ze04Yrs6oWaDa}{HB8}8+ava{~YJIsn9pFUUA)0O6k~0=tBDg zZ&5ae8JVe2d!4HHiyogKBT1$_e4~dAeCbH#UE)Fb;pE|K?>~h=>f_13&X6MoMqf<2 z9b;vlpmh-X6zct7m-u%h=8G)U$%)wDUKkmACa?V%<$yaCJ2rBwP^@%>lD&1;?C9fW zVJXsL-WxK)`#n#PRE6-pPkAJ47t%&;@d`!g96FV+8O8!faM|NtN$A@$V>46WND|Tg z??vhU{l~s`SRt1acFn~Lt?k}f!w4%JXC?+72R(^-_jSq>mu3FlkUBlf5_{`ShcnV4 zg9=nQc=6YCOmFqR1yW>8isafhRrjJro(Ts!cA%~lziQuF!sMDZrK&$9;Jg2SyehOF z9-51a0K`*3I-O^Hy#hLZ8>~O+StcApU#D%8$Y$xm8Gp01z~Sz6&IJ!sRG7Q6p&`$j z|3h3{hOAOIt+X(sp~&`&sV}3az;@l;=zWo~rkrl=Z~7ho+1?~q__OA$y&$}3r;lJ@ zwD&o{6YyoUXPqT@lpibhgg8O?UYV4@+usHVMv(}5Q|0kEr?kG_T&BR|=Vl+&L}SS7 zhO&kUhdYVdGA{g5-Se&iJLMDLLpUtG6K^~DgLK*s)=7F4hpIYNs!c4pLu7vGZ;)qy zF6J$KN**z1PxP&%A%jTOkJpdb&P8Y`A+4d~C5-8>Yjw_%UCglSl_7MAPC;%wemw3k zaXNbheKp~b0<=PE?RPh`*~4I2n`^&$nSmeR)VFr+nhSH6K!&hBdk_2O!v9aVA)Tdo zAr?VJ8omaKaQZaTx*plOS1b%ACbtC5f;9S5tS!)^k#x?}Jx!BDC(&}8*JC=_jh9cA z^;brAEN}syp^QqcETQq7pzRO*2l#J`N~jce*(|s;yo`!J%`a{2K#Jpc{(quCXN(XI zsUIF59@t)NxIKzf{1u4?xwUI|T17`Gwnu#zj-1>0c5=GLEkCQBitL?}XwG4Y4qC5v zau~Z6kcJcc`bjps?D1)nW5d{fe_&z6*2)iZ)!j=4T}a<~X@m3TXPi+|hDR_O zdo&VkvySzZ#=(a$WkJ7eDG3=YS?{Tz@*^>yztX4}%FTDro$cXs9OfnfEw&UR=eL(W z$o==I9IDk7Ge?^$+;<ryloNa6FTc;_uhuUDZS!~ zOWq(`*ND5T!zbM{NR}z+Jyqi~mj12O@&6>RLL`4-h7hBiDtY8BGkef8ZN@o6t(Jwne0}jFcuG^b zptsICOO}voK={9J=j1Is3di(vWoo_zf*t3Tsd@NJdHOcwLnw}wxoS)Vj}n5EDJ!Ly zS5dHYK2+XC<{DH4?!lIY*+MF`F5fBn@xjfCnAtDhGC8FHXm6eT?mr*vZ>*SaIfWik z(OzyHwQ}_9#R2Di-9VtSCH8fdOxhI!U3|-1b`2ja$>ODMN&qQ*Jx0`<)E382nxjw# zf_8;a!1)NS`h$B{JQa-Am*9Yj?0#K+F85X>NgA#ocL_f7(&e-N0_FU7V;?&7?`N(* z$nXV|iD}i^O+-)KBen6231d)to`9+iVxEhUnD}b#>O!-iIl!#1p6uP;))wgyR!s9P zpFCj~KPqtyQ20Rv{$7%VD7z*Gq>X{m= zqZghxGY@lYT{_qV!MQ{hY8`qOid!5|S zIVzNdJI%2Jo68kl-aO=_d*7kgiNY4wAzcYfQmMw6q3{aD8_O%n(u*yKy^CW>8 zJALz7_6glSWpxMNW5C1gz z1mwO-F9J&sx#)UwL$|h992c$JCe0j(vlWTYgSpLm-aYP`LWfz*G_W}JVbpKDkVT{>6mBE3toGVSh zyQ2|9!zPpGv?4UZa?KEBs!zfAa%cC zqlZ5aCi4`6;#5_6gCxk^)Sy!auAk>Uj%|3N=qK1R{I6kP{Sbn(t}}2V2lFMvaMEdq zFAB#TC&rVL1@3#&X3l#uRsiQTqZu`(SHbD&D;Y-_2_L_N$B7Ys{;}KU0wehqMqe@} z;Ej;${p!+v+cz$@5A-#3BSy*stu`^rgoIEr!cCfLl!|sjx0!V3yJTQ=5nTrmJpxTq z4-4Du!iyzzs@8M&Ce?JQAhYo`M9>z>h#ev2w-xdlOXC<})Uhw-yKY5Y`L38)e;{b( zx;cp1;53q7ImoEhSeW~~>Q(Jj!{>QZc~#|lip6E~rsz8fV6cntx0~O#o9v66~ymh&cNWf6#SZ(cc1V4;H5{>`N z{bFN;0lkaoL zf5tgjt`ab=a~Ozz=7e0iKf2Wg0Da0xna1 ziA}^EPc*NeK^Bg*%>37JOE*)^aSQRPx^oI$4`p0BPP(2?$ycl%<675kf{&=T;FhM2 z_PIe;8@1=`yTbZS0ji~*Dj&uHo=Bl ztMGklF_DeJ^9r|5_?-#Qe|kZkL!YuT^5;JtR${lG^;>vnkKDbR{6@f(+Q^i|8<*^j zS~lPC9ryzlF<*P=N{od(4vBrLY9Rbt00n)lCl1Uts~Ld_*TvI1`T>^Y`-etaDe8B% zw22d`2lkBEB{%S9!rXGLEL?<0>kR#K&lkyD|A17KGCRY-IAL_CQH@;-Nj7r>b}QcE*;mVmQbB_bJA-q77$UNQ+sxQ+ zULoI6-Kxt@7P@86QngL*7MDVXc7Jj#!grKi>5J>Ezl7aIg3gDFtTj~laM;wRie-h| zF8Cx#K`u$QOBlBOhAQ#wGQcjH%lr(Nq0 z0&)R5%llCnls9&brq|ZN%40W}jYmBk7UAdndH5N#HmOofZIsp%(jqVoj?aC%kHp%^ z(eey8Z*=0W4p$(uX(yDEYB<|@gY|?R;s7;SxW|8Oa0l6ofmvSP7l>Ca13u3XHHJ)9 z!~B+C`I^f`1?fj7pUqep^ycdv3W6qY^bYSAr}87oo7P6s<*%2Y`unkl#B!(qR1+slpIjj5j6^La=IO6@I3n@4wJBrth ztzQ&eBHP$m@=*u(h+|##2AFGolOO3VU5-I1?FOJot4YH#Vs;i6$und!pDG=<9}9DN zT&2Zzyu_lyWk%XF&2-4{IE#*MqEaAHDUA_*wa^zXCsX$;istLmVIrgMf*pzS?ct5k zzuA{56es2U(hE;s!blPKWH;qnt?A?*1TimP6-YkVwAx~xJzlSZn&ku+_5ra3KgR$x zaQNPs8!v|h!wtCcM!_A3b@eF?rkUO%8&%9sY(~m#ij2O+mv`VjY`q0Ij{#}A5PrYec)2O!dt+L&ercLXYn`>ERa3tS^(cQ0yu~(rj zf8MUQliF}Ovq3|(3QN>ZItanzmOLf?+ZN1mfn@qi8j}*|t9SLLeoY9r_$aK!NFOK& z)xJ)tip?N;&3!z@H0`shoxS>2Xx+YnfNNY0E-WN&76?UJR{_5WnIg>7d>6HUvA3nt zD6H1FQEXXO1R$V4*3YgIj_E=luGn;TDTRNqpObaD*qzF%Z#yUq_;A`T=%hPzIOBF& zFnslO;%ApX;sq?QrDJ(Ep`zN$xu=}&DcuMiP3`oyZ)eF7t-0%^*E-$dRn7JROF{)34)gL;+NoGBN#M;1{Axo|) z=t(XeH(_1k5ajIISldT4uqG#!n6Wy!GCeaM0-S8O5)qsbI3JiN+3;-a<}NR!^>3+X z0Isxt5*?lQ#$75N&!p?O0F>~OgIcf4owEJ7k5md1nOZjJf^D57bq^Q7829|lP=ngq z?(X%p9U1yJm+voB>T$JhD|63YhN~H;-n0cI7rk&}_V>`9LthR!?IL*uF(;8ZfG7Qf zN2ePgAdh2_>VVIUgB!no5v!T)8oI{1r$%5%>!@p09JZ?7Q}LD4f2;b$tWT6$J56mp zI{Jg^Yf?o5hRMr@0It~aj&+0U@ze^wUWi!t=Ykya^c!j5YVA}R?l-cAG1HmW+w+w` zeBJGFzUQ1Ezn2ChfQK6LB2=V@=MCnkcyHK^>^#f4)&leZw&$AzMqcAksFQ0*KqGXJ zCYubkYrj6yJhG**-8g@#zkNq+&BQMYnX3%m&2tw0v|(CwoCbAOGiAK@T1w^?n{x+NOYfhQYn~k;c#(uH4m-{+C!Mgw?En_1 z>=3EMVY-ej`th(4zHh!+k9YLU&FPe8;5VKLK|w(rucizzzmzv$-A&`icDh3nWUMbe zjyJ>TP&Z}K0yw*S>>*FYSg(K3`5d=D=J$gdzw=9FE>s?d?dHSr#U0~tbtYC zoCE6hGm*e+vETt8I$Qe_$iC8Fmrh%=bAhwBZiJv!Dy9OEYckGTf{f7mz=-*C?9lP{SlSgv_6G-uLXf6o_4=cHKP#GXEV3n ze0iNJe+*l^aOHNt>rmGV@V?x_2M41DIp^lmYzjB%?n1EDWn80suO!lz+>!AGGVZ|S zTCS{gmgv5(ay~?D)$<*dd-xX4JXN0E7YRNA@Vu%GUtmX&R+N-Ct!jH6&Q()4XS$HJ zo-U3Y$&BjCc8ha(w$J%gg1jJ4hz`tv+6VQJJIK$fz@FoqYvJ8iF>gvw=j-F89UYIY zm)A|pGPXx<_=7Isxy|DYkPTUHV9K%Qaqzp{x75D^%WY?!4HQy)L`&Ba!!;ylpaWRA z5-@x%+iG?FCJ2}~!BMXsTLn)&t`CDxCRW5Kc&)1it5ER%Tso^}gGB7$YVg`7`~N zwT8PHBcPrUuGz|#p?5n^1%}&uWXslR5XN3cm(6M>4>#=(Mw5OhdmyKopR~oq`;QjD z0zFd%ab7f6Q3YTQN;QdQ)KEv`o~nW?rN$A%&UP9~Md7VPh4$kq1-DshtBQ53fURx2 zsa{v)u%(-I0l1mJ7!Or+Oi=A|GK73F(4nR#lXN3cqub()ZCs1V&eXI71LYT)2>UcZ z{nveX<}>>Bx~R`ShGLsS_YUNcOINYVQy$)v-}Qq1_B7mqi^p>}>;7Oy?ozFi&f)2yA3SnMOWQlliBuyA z?cd=03}x2slW+;R!YsG0`??692QARux_p0}q1^DsDZ}U!E{g*PpRjm~#vU?zJCZF%m3 zP6r(oPSt%r+bKd-z?g?dY1un>m`$3n^gA+U!Vg+$kn7cFc{+re*M%rPuC^;XnZOz* zrBcDi2;)2To}L2?k-qL~p@;rJ;$?*?TK&tS(5>*mH{E8^vFF!WNQ%&uOvU@e_t;m0 z{5JVMieC#I{U@p-U*^lo{fny0jeCksRs}#y<78kk1?8_60`#v(7upnW5?lF?tSSR$ zww&Y-FsI?fNB?>J(H zVFtZ+c_jJngNR92GPU-Kn5#58*k|Vyuph0aPtRFtRF$xQKqPx#I zexlLSS7Mp&lS**hl+ayOFq}1~O#u5^Bx*T}kD0;=68M~#WR?gavR>@bkh`FJUn3f* z`oxoxC0wNh%zTw}iAIWExTy(|c)U6GJ_PHH1fOoe+F|LC+_P8(jt(+Sf!KvA>O?8JU55wxl)1T!9(6}r+RcO9{quuS; zBV$OJ-$8`BkL2rb*m7ja6=+gzQFl2Zf;#{TKlaBM6uUVvXuWv|LZ;Vkd7o5~i0EwK z#EHe^uFv6^d$G!AGw{sxYvYio`a^pnvVO{mn~)^lTP;ssMW)(caJD5yu{-F1rJXbn zVG>>wa8UF<3RHc(HhjG?rfom1-(h;+&SWW`hn(V0Go>Nx#+tLH3h4Tc#77aFqcN54 z1j?VPA9bw(+#NoQMMdBL88Oq~!_HuNxG4o1Vzl8JG(9vG4;^$fo>hi2h=cn!Of6|% zPk~4ZlN&yA7RLeA@^Q7@N-Bbs+2PeA^1s@ipwRD22*XkDzBEpW4Ch}S^j*~8vowSw zPC`e6szvwo?~k<~)W=_KTLF{A@@B&G%U$6q@qJIg<+k<>TcI^ck^C0Dg7*# zW-X;A)9Rfb{mIf;Tr3$W_ve$lI)V2{CgMsrgURBXCq*7>kH#$3MMNGmae)3#6 z*g-_t8Mfd-TD&6Fi}b5_O~1caPyE2w^;9Dlt5#hwJZ#iEIW85#8o|>=3fEZjh8fRW zr=_(TSZzQmg6LWH{pJxP`W#DzeAQFxOR(Cs8!|m}RWk@n%p`yvxzDVmr^$Q5Yb|u5 z%!$0b2zg}2+$V(Qq2KaTBTmF&cQ{iS;o9U$Wy_Y_(PRs|W2^m_j=QsS&WmowC+#K` zfT?;F$?HxSd~}TGIV|`iQAF&i;bF}M6+5f8CqNQyruOLEOZ<1InV4KB%jeAvDFhDB zVOsEYjw_d3pIqb3>r-3$#92ayBwb~NZ*IqdlX>0Y%x&GjtkK_kW@IExGC0H5Exb{( zdkQG&#jGw`UHjh1Hn6W+$~_{JuHhq_BG68GU%m4}#8W+m*SOf2*xF82`i4t*w?H3E z;$IDZ4ISceOC0s>Z)(uHM81-{~#hO!?kePA8(k#oj((7HhA#y--L&wyPLwsNrN^6Fs{gK zm_*8`eO)@j^ne*>ziou^g;x>BC*N-zEGyoacYpL^fx3*Om~iGMts{gx#n&t6Jsl63 z8d1vWd`(U$r){VAYfr+;xkhQR+;OyzB*t8qA>zUI2M&^>|BogIeKe*VMduG7wVcw zYG~&37Grg!o~y;A-x64PICsuPLiLxdW^A~S&1EkIog@NZ|I0^-fr5^mM;mwCUPDb? zK_=x&Wsf5NriOlyyiev%w$1;*+W4fYQSWKYP^qczjG7rzvocZi$LZ?4+(UZ1p}kc0 z@Ys!qubB^jS|3eXhCA95>?y)O-VYsefmdqrJR|sQ>1C{H;<88%&n^{mx<>npQsd#P z;%x&YmN9joQ{Cyx%?Y9qYWoSvN8fy8`(gG@Mb}xguj=ydUN6Vf85ngUFR}bMW$lV` zK4%u*p`P*m1Ui-!Ia|&47t}|KB&oK@`O@s4VQ*q@M7jJveVM8iiS_6)ZU}MeAdG>RCjIQ*k;fnw#7U&lnHbDl%;& z+%6eXJ>k2Rw?cZ<#TV)6HyOqfF^G((9|DK`*-zC+HD8n6`6e}GiM^VZ9~^2KBQ3dQ z(C6?>uC3f;jXzZml$m|t>m&1PDl6_?rZBkyM?Ws=Z`rO|9MP#6dr?}UTPF^<`4Wt& zsp~KY?o4zC7W`ymRGu^UX>mCYyYCZq>1vunEm>pyrQdov?FXI0IKlMb=~G8@J34DQ zQ&=8mhVw>{>MGymxEQ^8T@&=eBluhqy z8m?$12(P>d({kV|grWL&T<_J3r^cSDBuQ9sQUqN^*Jl0d6kFqt@nd^Tgxlak+9|*!B{eOhLWmH^Qv@MJV2oAyBli*HpmtYAA?yiLd z_uy{9-2%bg-CcuIkirSBg%tKxzx%rTj@R9M{|;vi#yNHN-b?11Yc6<06viI@%z?qd zmGw23?V3E?KtSjZnS9kY!`~@w?9oFds%2NU3HwW;DVWA_)WR;QpXVzgE0usw0lo~2 zn@v>wD|cP4s;>vj&u=>X2@CyGyuw&;U_JKsezew3zUw^hWzaSZZ61{1bF~>fK*bw$ znAq-b=6xFGZDaL|IpqwrXFU3@bO%3a@1-nfvt@?MLjH>GKAlJyt#SxsdtIu~L-^np z`JjZaLC=>$;rpa5AHnL#reF0*=XHZih~vqXz=@pE@d6RT@=@}x6{TSTmpCrix@9j3 zcnRAcsvdR900o)8>%5Zfp`9afP29y4&tCVdxUn7JL~igs(?lPP+4jpxYQc^!NW6e` zrPZ)c3WWbJc7AU~kyU1{!gj_My(~TtEApCSd;srwb${RjjA`G2LwRlegPC z_X461YZng@i(0J2iQ*VHBjW3bhZiBcs_T6n>CFT0)$#I+_f&@h`@Sn9(#66>s}Kho zhaI99DN0&3VVlgd$BBwyBO6QcY6&fmG{um|RpAXcs(f-VGM`K(aa}qbX=yUVO|=P) zs$mEGP)yId%bBKcsTgyjK0`tf^tu4zAQ7=20_vN4;v6j&D#(r>TtP0KH@8prA`fRG zO>-yhGuXCRUgQt`lb7HT`ALDY6u~NAU)^fK%FV-DPS1%9!P=junOEinRPQk<2ie+k z63V3s!)grCdnS1T8A4}{zQ2uT9_<8zkZ_M`ffM)KK$oNa>lG;6@`|??-b%gVeUDo| zUMiZJ4iLV`!S9&g9ba68ArhPY>F7iy&_e5aeOX1K0&YY*#Yp@qG;)V32Ir5&;9HHkBaJ6ymd|It^N~BF& z_w*Pp{xST14-o^7?Pk5fJHr>NlCq4Tpm=7B%KK)J2f}M|qOM*b%Pc7J|ZZ z80adoCSLsLtufmqvF$eBWqK}ZjSD$zcU`tLZ;~e1DiQWQjl>Le>eXdLP7gY<-mJde zj5-7L8Ma+uP{}o0?s}zgGNAC*-;HMSn+XJeW=PG{6>#mNYpN1(wrcLQoGQvZQ|i0m zebJBhSE2Dc?bpuoeanI)ak*Rkbv%(l=3Kiw;Disnq{!TyB%bc8A$e|XYeGOdbDh4t zMAF`Q1FwH`8QX&C&6n}6q)p>Yk{lA9^sflmLpMU{XtgM*nx4R&rNFDPcu`j544;8i zh=F)(J0T{Rkx@8sQ_3>}MVJ<|w@ za^rqXecTT&zRjhx8ios%megmH9D%1ENrE5nSZPC5wRyamO2?ck6Qy79TBYi6h?%aTp zbxPedPR_lFP<9?osx0CSEZjepL1r;YK#{sIJxzT>v&>5@3(e*ZP6ER2mjGP)<@3HR z&Jn^}xW2R0y1u;&tNqD(ddp-zESesh^~q#ZVe501~E!Z0V=utU}}Ers+!P49m_? zeLYpHYcD?MJ(UL%M3C+c$qUA&-QoV9(w092CwqRXG`sn2GQ}^Gok$A#j#lNO|Fx#W za+I@Tyi_dQJ6@bT!pP92C=PpHhPq$C>%2u z;kOXetY#aj6`az=!+mX?MaqDS`(zw?sCx8q=kJ5eMm_m@WC~&pHJx7%rZs7V*#Xj} zdn#lN)tn4&T~vJ!9DAd>4~!|s_qbucLcc;AeIJ7eT0?wJp~w$W_Q=aiH5|3|U`9l{ zryU{Z{x6~8^hx<5cgd880Aif^s@gEV6a@y z6dNCauf^OH9v)YhEPR&BLo{xX+>xsu?hvqVYZxo67LEcfVj@BsR~?c`5}1ENVWbH4 zj0J(OC6n0~6Uu5=zNA5>@2Dr4x^=~5%~`aMOIj9J!=Ub~F{8#!E^>;Tl7!%ui8xfD z%~XY9K)aA)`7d%%tmqs<+Vq?B>i#C}`WVyYLNC$nMD@BGF>X*s5Z3U?qXRFoOCYT4 z8?XW!FYHQwPirSJEC#RwM6{3|&dn?gCMnxlnHJ03#@YDwj1^iX>9Sp-12=Ni}l=A2D z_$Y`AYW?{}w6|^1%U%EypkS@kb*uhU&%@QUj7~$0(w#Pu7d};sGd~rVM1EjmMz-`> zlwqq^|A@3=UB|R}ZulRczkB#7tzv5vbn@ie|rSOH-6r7SN?Y3uj|6 z)pNV4D_E-C{l1)lB4h#(sFq?nJ?L=xbhtm4tZz>;?@M^HYYH`e_Khh*LwC?Ix<94p zZ?2~vU4(eCW-kU7tTca2AYFDUkn36P1=ODm8aLJ);$sF$-|)+u9{=2%bz?ZcwQg(r zJMzAPM&9ilGCw1k@9S-2`X|5yZ%R)z;``zDibR+%B67=pZ4_(4*3(qwm`zm3twOTx zD8tle>I14^C`3-tEDb)l;HFwzgZP7p$CxT@niT_KL%iqd58xZ3Y-#{a%5dda8ub2i zUv8&E-KJ$(J1;XTeoJfSBlrn<-4Kg^fDVLAQ7h+PSRWMXh_jc!9c2|fu!VwGQ?!YL z`{k#8(WP4rC$@CRXfA>`aS?9S`kmmbo@0>5rmB0>5+jKi4?=zmUXE(T3aoZlRFJpBS5f0M7Q%1b+PR2HF{dKS&62_NslyX_bs1!P+Hf(q2$`RmGfOmggBzUj!#)L|o9W zjKz*_)qAor-Ri2RR=JtG6g0={NnFo|^O{RfH17386NLhf%Ts6l*obn)}%CdE_k(Wg4_B;%BYk`c2p zlhkf**A6cm6pQ$Gx=j0l4)%9*ZZNS`$3J(ynq za8U=hy1ipY%TG>*Vq}rc+);KJdKv|xC9$731y$X*7!L2CV@)zf3X8$V;siC389l7x6XYuwb4w+Ux*I86lKtnrDa%1Ah(Ca#fDF*r#v3^R&_NE zA3JgVMAV{K^=f9<;n4Jyg&zeP?M~F2LxTC1uuPdVsTv;|zjZAg>vY}TZ#WLa!%FzN zrEKTN+$#iiRSB|+81IbDjw~pWhbrV8kPvS&$zmZSsxovK=&gH|4w=_GW+p{ zVns#MRG5Q9s^3d6qh^T<`$l3&msy;z<06_|XrKIXDTZ{TcccDy!Y32enm*EG#}kQW zz%Lf+u`JF7T_01NtO6nyEXS&EQ{?xJP{NaKd>|g*=GZpm9JA(5fx;{VRVH~zisDk& z340jQ0cBTg+eRtA<;q8MWfG;8c8k%h-TB=dJvj;G`EB~rm`$V=JdVpW9o2POS9Z!x zo>XqZy2?(DgrBHyRp8hU>^lfvx=bFgLfpA0+!ptk8&|P0O6g+~AJz37Gok*aJTybY zG!hH$^`L9NnxVgd0@J#q?-3ZR{*w1jv*>>iJZva}_bIf%rDKhHrUi3;$h`LE7*PN- zyx}I}L8<3e5W$4-Qq9uCmW;3d?Ds-ahfV_XW499aGy}w&l@+H^-pf3r)LeD%$F)52 z4$iX^fsPp_XNWj78HPWu>6Yps1BxuP2H4BMXC+tL+`hl%^(_p}=p~bG^6dG^y#V@+ z|2`!n@AI}>1jOb?+e|jdWuT|wFU%e{L=?}%w5XVedfi!rwad+!XBBSqf=BJRp1$&g z`eY&I2cQk065+iRpv3*QlAMYCB-7wnQ}RUgTcCJY5bJt2Odz_Ruw}&16g^Ef9tpsD zJ~#V&*(xdI;4&p#22W#Cj0xsSQ9T=J!NWP!NinNQdC}QL8?mY zvBS<+9g&$tfaTRvKee?T5E@AzAGh7<;U$$bt+*^x9Mg8+KHM00#-Va%Td6mR!d5fB zYO_IWBD@;66N>fV%bF`_fcwLwEo=rh1tu0YEOVJM2wdz;y8gkL>ut8as{Qak;32el z&EB~*CA;)+_-@LA0@S{ah`+dblF>dLlm#Wb`fl8WahTyw?AV}u3O-4aW*>7ffRa10 zlIXl1;f~RY30jKr)>N}FoA`XM+cBH;h3-F@6W}<+kyf@jofRYtc=683t425Kb(YVY zic~H=s6Es`In{r*?sCcJxC1z+%xU~qyi(Mxv@L!ahT=t{cOHC<=Cuy_lI^emU^GM1 z-WI|*7UYkF@a4T+vs?FK@3hBaFX!jSwUrr$Dc7u>K{7bA|1gs1-UW9#kZL&4x| ziR;CZm1jeG3c{&DHO2j@emGl`ma)5d6VrPQfcJWT@_V!`>j2dEQu9i7G*9#1m?JUw z&cp;Ro)f*q)gH%iXYRRyKT>!A9@Vws)~5ka0-&c{4vmXPftHPThvy0X@+;`PtJHa2 zdxj1&=QK5G*SRN8sZauUlVN6=g~%&!-gQPKkWe|@ipQ^$d#W0*Yvm_#i6F{>t6!BJ z@4d}82=Q6?`!l^KZZW77_~WYJ9DlXl67L|oDhKA)+OO|u70aZM-}x}lFa!ead=7l@ zepOpNMoh*-iM!-*<2_OlPNwwaddMH!^!DpSyc&+Z*Ql-T;p0J*E3*-B#Qf1z+Kk{@^xRoG()M#0(DF&^c>wy5!U2b5W=Xm?|1n^6v z>gCKAA=r1Pura>GZTC>?blZaT9{{WQ1;R| z=pO0%@$RJcVp3odS`~cn3LTr)eAjuQdwa$&VLK}F_R6jaDm+`r7J?giPvy?HDssOW zv$EnILHoVm>ADYBc_`+6rxiD;g2+xI0Jsj7y3Ud2%mffxf9s|uCsPaCjc2V%9_`9Ml%L85T^P|V_eX; z+h4^^Y!P3vRkwxAHUGzs1U^VprcXua?@WFQ zhaUre4|m-77a7ftbFKor?mr%=?qj2s`8F?vT=j53?d0lZ1NVC|k9vxQqnSj$w+l}^ zrqPK9SRZ02M0O~7#*^H(qgXJ$lbX0_-7?jA*rZl$u0ml?1ImZ{C7~}{pJQ9o$B5{;oHb^>9LP4zvQd=Aw3BLFJ41~Z6&fZTcgO8KFSj3 zCP-zX1EmBgB2(4p9W$2Hr%0YZfoVM1+PmBDqr6H@mW7KSu|~w)vkv75Go`Oc1_wP`_=y0 ztCb>(;8Fa>%z55pk!_+a@Z2WzHt2hV`6uzE@nzYlgmCGJjxAS+ji9<5q@_qO&-gX{{M6X6YX&~0NFnbjGNE}_?(_!oEX-x|7L z{Cs8!6)Wq{Or15;-!0yr(O89Q$*AhB2U%Fx0Hz(Z+HVi%8=zc^iZXRNZ5Xk|Xuc&e zM%r1K&mYih2~`G!=ac}3Xa^>mzI>@>ppk~iY!06C%WQH5LY-lmdP0BTffN7r=;Km zpJ_kO$Z8w%^ltU>?~G1eU^8y;N9zRF{Vi1Qpj)>byvtPZxcC2|3;M;Uca5diu8_H| zmLi3P7;^k`J1E$~ft_S&W>6k*AJz)q&`fK&S16I7ivzwtJTY;0_WU6gq=K`R%P?`Yk6|8e+TwDeQB0Cudpa^K$> zh0YLWEfMwJS3Tf%s+qV@3T^N?tUdnOx%XHF$thns(h%6m692`A$ipgi-j#o=rEnVZ3n4ycQsF zXYZAHP6RQ}`m+x5FP(Q^u!d#`Bp#~Kf&?j+T+9N3Muk~R6R6r=40CC{>K<55K}izc z-D@_B0Z_u8fCAx`;+DR&y(s0JvC~+W&1_p5V2;Z?M-CItb}B4(esxQW)KkSi zlRygfZ)0l@8W z@8>o z*%?fxF^S0j-u3;bja5eCmE?2QfPu!ZY=97T4nS(d;Pt&ugY|SjR3W|}!{(lmh>3i& zc;_R@Qv8D$cW4av{ABwLFh7*30_rkkf(%ZCeQt$OhK(o{*^tCC`!q-!88LtI2Pccl zc*PdHdd!~a z`B^T3vW?snv99es2jCTQp^I7>|J$iGLYNFbhn1svd#1Z16LPcu#r zj)UAH*S8L9dLAn*r36LX#%I1h?Nkhcf~2jB4L8IB9?1+;)R_h$UFEKmyVUsPRE>Vo zZ97ibzWQCuQ1~$TGpfw*M4AwcR(K?g*)pgu{8a=9y}~HG?}9k^BP@OYVf~Q)x7Qq% zZ7WJmdC|mk1og&$_5b~u@BM8DySAu4sL7y79dI~}Ea&@rc30M;f;DEf$wu1IM|zUe z`do(fnhcijQgs-|5)s5}+?tRu$s^~1o~QZw`AUe6t-mQ?*b-hs3(L`tc7x?wh3wl()vS1xU8I{onZh^?|0mDa|h%ckY)-Ll6 zW{fddreW;AU$b+Y1It$(jU!g4XL_o+};RcdP3XA)h6E z*ZT>$F~$ANSln?(<4Xy7U>gv5GfB^402;qSE#m+m-D!Mzl7bmziDaJBvY>>env%5} z+fR);M7L_lOkA)rb`U5Qqp+}hGxQ$YX;sob8NTZY!r@tW4Pmvx6;T6~1C$XiP+9o& z*WU5+aUHU>Y^gL_Oj9?yPLjLcoEq}d=4v$9tZvL#XqKW061PR%l;_;zg&)#}+@-1i z_QB~18}YB_&%_#2QF`gU`+0%+trf|Y_5v*9Gg3jaIVf;aX(luTh62Bnm%O3!ub))L z2cTk=a*`)~4fFKX(i2|Pyp~2On>kOA*_jmr0hiZNOSL=Aai%o3(;IZQpN-O#Q#t0K zoxZ@Rs3=RJL#m3+ZLm$uIQ>`y+XQ@;dSP@vZ~9bK8Og047#L-2O!3SfZl}W9FW~WS z<0xfZHUrBadX~?z+B87AyT#_dl>ZuO*N^XdsIe%m{aIzF&61h0tw~p}y8WW`i_UNL zE~RYlQSYmzUE9@=ns6_#vGEk1(edooA!eQSGi#hH$vCz}H!94v<&3LYLVwoIGv#`u#zvwrl9uoh`j z{EU~%rOI_5ESkge(l?Sy_dKYC8%WNNHXm3_TUb(1BdV<{b=x7($U{c0^431Bt&NQZ z2~9~uL)aJ+s@-HSR#zfOyWU$TDcBp%XNMaACwp|7te|3pZ@xd*jr>Tw)HjjxjA{#| zA3aK4sTh7e=4h{1d~d{fZoS9!JV7@D&gCcQRKDL=p=P)d5p&PI}&n4J_sw zL6cvEX2SnF2Fd{;i?VWg*Ctlu?L`^7r`WY)PHW|wrx!A7UTAaq=+2@g3VpMjW5HRD zQ(g}Hgy%Ee)BI5oT#HnEz&Pd*sZBec&U*x!OydUYlkwR*6^Mf62ZdmAn&dgD7P+tHiM55@|$fo*5oiO$x7hyQ!{>;Ds-*BBYs>CCD(Q z0meF5j=GZ#vAOgt_MLS=^v$9M!gXMndDmW_E_6$=Ymf(Z%BW$qH+U{?*W@m|tN)Vf7Bug7=fPTw#M1@8 z%R$^r**be)kn2~y`Fg7VdYZ#aCm3r_TYm`N%mqh9$pp#qmBgf4(n=%B1gG7CYnJlA zjph8ycZfEkBrNb#`AfiEZ}2tD&)0pf1bUFv8uj0<1YhrUZ4}gwV={O(jHA0 zY8R&!EMyY4zuexCtyH+NMrE%;V@!{&(hi6^TGSvsqpP#Zn7mkRQeEyD_{OqL{@hQ$ z%n~Jfz~L`Ww(%Z%^|5KwPdgBEMnyu~4oOT9dx$EkHyJt$r7c|Q^4qw@hW=Gc5=4y+ z(?OK;`c7^CZ2Kd}&@Y^?@nJ(d_1~V&oY}%qQVO*&VI{$JRat$sGIvgymNgp4N8DOA zIZIah-~4vc0l1oyy`jr9T zO8B?;6@9`U3SZLO4BR?OT&NNrGmnv!`hq)7Vs0o{mrH~(0^qQIMjRx*7X`Tw?rqHwCXuS+fQ z4~p_y!vzI|kCTPcGc9Wy6r+2sn_DO>>%;i6ELxCfHu`TIybpIzKUBH#JE*x=@V4{t zF-5rNC*x6PX|&k&G|T<|sxJWh>)nVdr`?pgz?=(5mrT1u-3$`&+h057pBJJ(kOR}q zG_ws!(5tD%%PlJlr+et(DSj?B!YsY;=u2qBNNcB0;<#{X7=Rt z7plSt_m&q&>zt^NMA*L;2c6>^euoB-Q*uOXHYztm4j&(?^t@gXnAWK zzYzU*o9h4TVg68lkcTS-K&IXd%YMLMp5Ontc`;?`y@;>-3sF13-T&rM=Bu<%9WlhOTfsP&yZJ(^a!CUII8LtulQasW+?S_hgK_a@S%Qn; zYueZHplpTEdASQezpXS;m-7b%MCeFO+%KQ%-c@2CF$ zl{!F(OoZWbwb<(@>?>4Wbw>#-F7nK8rJW-86J)N{j4A97?uf0NlLWwWaV=L7biQ4^ zr{$jV{S+*xn*1dUZnXd-9hxm=aN8m3k@Dv75EY|@nNy~|UQ~@!c`)NIDw$N^UHOYA z@^4~C!3gJdW75QC4N>KI6G{0OZ5+DqY=5%YI3YJB3VodCH-Z>HWO!PvWY$x^`)b=WA}izBdN8=}G{Q-sIr=4tR^Jo%PN zI~7pdcgQcv2b-(-(cQxMHcz;$W10upk=j9WYxXSSLO&(nUX|D}J#eWV89*c|J@@kQz^ch%hB0mYTd*D%f7trv0 zQ?MbxPa10c0%y9q171bxT=IcZWd3tCr=AebFMDjGS_(Q;8xaK%SGUvKmZ+z)!uYv1-{zy?c z+)z>8`aTbDF%VbOjL8voWYtB-y}w0$z-+pu!J`4=uL7QG!uRETlU!x?jjV_lH|&|% z%Iy%WNq-aSH5g4hoNjagx>pb>NsBGlL@=as6KJZPnWmm4^N2{REq82Qv^rpQh;TTgfuh1 zGz=rDU{?Aj*=o1#4xIAkJUaKPj{NTs^(#UN-Pg4@VM-Al;!&KdyyE3I>ZHv&by@&2 zl%~QkBUEj?3Fr9NZI;<{$KXZ-j4x-22#1ngc_=8;gc|9jfN=b2yqBezz*pPp)j^mC zz4xZq#;e(@}`z4*pq5)FXu4zSyk2@gsJ7_*achSRB143XELd{M1;K~Uu zFkqS9Nc4NT)!?B6D_n+oVUfMco9zxXv$87sGHq!)QttL* z(z3Ws4ZW zHRKZ;FU%T_o;zC|)(}k!Oj>57i9Ye}Z2FxC(Edxr;mG2vdh}6&ZHCL?e^vwj^s&&F z_$))CHF2f42LkMxxMXo zRtaxAM&e21p3`w*is+w^^T*RuGXwVJk^JEgoIm7b8Tpg7!7%H)VJLrNt3*%pvTpQl z5V+}O6m+lQS$PKArSyXi>MP_FZ&2K8&?`j;f36ImmoXfvNa~9!sFP-mA}x(t@WW+Y z9focsati(wWKtWPA@WK^n1&=w$|3XAt7j1;KRs+@ zXAP17%YxjiVqy!XaG4}MlIw$fW1$7qjh$ZBq-X@k>OED3q{mldNJzL2ME10Jh#r^> z@Gk5I3h(hV@=v(cc_fuQcd{MKfh$B` zZph5H-lCa(L$YLcP@S#*B3mu%CZyt^*GlHS9c_@rF8Jz~xVrKvl}v+#*(yfR4)ChL zK-g>$yPQp(E6>N6peq)w?O4*Tm_v1rstjpeSA6qzwt|k5DWX&{yV_bd%QG)P51m>9 z9xz)LlNjGG(rW>SGh5{QDBz15qk?{QYRLiW(7Em3_(v_vw{LC8&0L`k<6OkP=8j{k z$ZOQEIWY-pR{ol~7~E|C)8J@)U^S;tlJ&jMM!1#c|5CvE7ZVC|G^Bzk|Ip$U~c_BRC(WO^q?Zn!nQPrs!pceIJefS4a-q+N{X3(@8erN zkJWtGT%cP}vseO0%xmU?UB#Cpb)@k>VoQUxmFrl=HD(Ko+J-QnTSeI%5`-MxjRJ^u zp6%|-UH2B~c%q+_wq!u)NOhux(j9ms&e^Ddk-dCYM90V(me+mMUvfNw+@XXP_69r- zJMaWF_Tc=I1YYR$hVA<5`0|h1^UvfMv7%+^4PU9DTC+SMg_bRad$krM(Y;&~RSEz} zbj+>Q&%eAN4qYRi(gmG*-#hTe(D&V#TsHTEpwm8oP8nDVm%x&CjOn+=rh|>I8kqM* zyp3LIR%bjHO|%_r21{hIpfJ~H3gP&SsnE~3b^kP9Nt>8qb6lOtHnhADnn`~m&KY}iTs^&^6pb{z-M z%46Hvy!Gfan&W=MVo(Q9bsyLv?p;In?R3+-;Gx%i(R{H{-I@8g`DFEAO+)H99ukG8 zp8!^?A&(xIQzwz4-a;sd9PK5cpoq_iF|43V$;tQ_g6l5`H+U_vKO44$IoQMgFCOhr zKjV}n`s1=!>2{c07m7i!2qZ~@KXhvoh?oaCLCbufd7N~-Q-2@RfoA>9K@@NE-s?|+ zuOC0e*M^WqtOVm^pe5PFh2*e&?M+b7`b<2R-&V+CqY-OYfUZ-&Twy@S^sK;q7S51W z^$py2v{ zIqQE#(jkltq0s0jgF^FlxOZqP*$yc>lTS4#N) zoeEoW{k?}!ceExI=Jycdmp~pAvzT&4O+gexQ>9;C2G+OI0Ft=_KDGme=N>yH24`9~ z?6I2LzLx_&{OLTsu_JoUMRCP6T&GXCp_%A|U3c5lG2C7<*dwlhR5x|dm2bAP4bTcY z{<7=b8jph7g!-{odJ!+XakwhY-a7t+$)}ybw^r5VR!X3nDmzY@ zlSsG3)NT`}C|uP_U7C&7Wm3f?p!i6)48hyk>DMYQApvSy@-{+yflz}8Df=8JRhM2V z7Ah(sR~3~F$t))QFVTX3rsaG@$aVQ4;>P`qS5!UamW=}gHZ~w_H(1h*ie(upbuJ1> z1d02b)e|$%(5ScLm-PnGG|B-dHIO;0PO;MgR*b1a@tJ4Cx3m#GU59@g0JHmGUuC)FUShjR}e9D0?B4)E2+Y+5< z*T0Lip`m+B6bh3&Y&4x@Q<&cf-i!1bd+IVKTU^~$QTGOxx@y7ap*B{8aISHWO;qqt z4;R23qK9=urwPS!#qXLA&VxuRi@#GyNCr#3yjj58A{2Ywkd7}{v~d^q5g4L-t)l;O zCm;ZhU1d-rP7r%)Q^j0aUy+{LpCLY&5$|n>OpiN5)x3b*!JKoR9ps;j_17I}1AGqm zyhRo|M*DkjjspQw?Uazrb{ruB;^xrpxEJK_*Oz(XJhzbt>mFiJ`p~1BXXc>4I^yMV zZm^7ifdpO#tY|u#g8z0sQH9FT6ZFA8HSxqPjb1eApDE^FFVB~brhG|$&H%hbsmtST z_*rNVqo|f+cR>cSjE`M0Fso^pm{^vSAGV_@?0_55712*}{Sgc=hw|3YX;~P6V&( zuM+b8`>3xhG}JR1<@}{mSgk&qd{UOJR9ByU-b)%#W1VUNaOi<*;WUsC%lt(x@Us<+26yk3UL>H)W|GEeseA zipEC**Rn{hSk+Q6vz+I{<%OTTfMbl6O|z>mz;fHGjCaz#P^DVvaP!}Tws%)YFA=b~ zMcc3fZlaub9t-Bq5CU=ih=WK`_6#$#X+aIE*B%}FbM-*hzZIYUX-_kBpx{_vTg_=v zt|w0o$`Npw;UTn%jLCrI%5vk~ybS|t#NePeAwebBBbRKD9j1CUqo0WJ_pb&hD=Ph+ zvB`8v;waM7mB?dx>(RWx%G3uoG{3VA`N}2$u;GoYj^|V<#>-*-uqiqSk@S)rM}^K2j_oNGW@b|ETpxeGv4ZaY=FOW49!tfa~IOq$Qn?+#Xey=JI|sI zNr=Y>|DuTQz6<_QgvkmYQqh`pA}5H@j zHPpcqlXR8CRz=?OId4ZJSCV#9AzXATW%1Hx{M4D+=;2H>qtd#b@}4{A8F&N+1=-Km zGGaakK+=8f688-~RSnUhGkdq!x}qp%d5Hd7Z+^u${yOD8XH7rymt9dW=GP$xmaZW} z3l5EryX3rf>pInht}@Z&POUCS_IDkS5i$Xnu_!dmRtY3bOHxP2IQ%+9TDWCd@plGV z-d08*7|e=lJuZ@03ztn9pG&Mo;o>ty-x~)>G76TBHRP(H>U8KZS8t*Wc$EKc?2rU` z15DCOP$FAmkHQQ(3@@Zz!TyXZzkj1NT5if5Zzt=0xw|@chMI<-62Bh%32+KcWhn<1 zDBxaUYrJ941vdD_>6Lf+=;>N4ypE9r49c`$f%l1{BEZi^3(U~rU~fhwGI8&QQS$?` z4hkH+m6N^=65C!8!o}ODZr?$ryJN_ODlpGU$;g~3$}~40fuM&>0iTUP5|WEGm&27k zX=&-pHNP48t&Ry?x$_jX?}veVl9uj(OrF=bN#F>xb(Gj$`m#xDJHY;2D*CgI3JV7r zJXqB>;+2I_N^E1+UA=T$?%TyMP|&alAsZK6Mvv()?FhReUUb&shC~=SZ&8zxm$w8H0Os^@%~-iDs4+h@PZnH9y~doh>u*o`|(7B zx_`5<%`0UovQ&-@v+;}q_EDr4h{3T$eKRUl{4!|H`5C8dQ*1DTV{LJYNk^XRLaANV z)mv85coyZL4}6t*6p&}9)aYTcq0d4la3{-Imc%=wvLsNt$?!T}kL1AocxdPquk}2W`?wR2QIep@`)@iv)Ky$i|4Q}7?e7R9K3Rla!-iVn5Hfdd3;!n7K|bR z>UUGzgZgSC@`IMmO^wq-0wD?5<0zeD9XkZaceY)m#Dp%6yW@_IJ7d${p&&PObj(($ zP7_L!?7hmU-&SE3jEiGq(uBmW=|Ph+iin6v zWA}$#s^jj2bN>0%q?o3rFs=V>74nWu3^|@ZiftKtOfKSQbPJ=@Efeo%C9Pk5sjBT) zW;0>sQQeSQ0E5>I(tihcUC;?-3m$oU>1WG3vibc@8~myMCKMaBWoLD*1BzeP zxd8H=UtjW{V%b31-83mk>wP~IQulYpvW}uDM2^*yBP@ie{hO7MA-1u(Sqv{d;K(;T z#nl;PnaUAVa{*gTvoCzK0qI9MpZ=WF)~t*Bt?iL?^Xt<}h+9A=M7OraeU(MxE+CFwUo`=?3YsF z)3V4(OHfjx(e2kZs0sbdrJhq27B5JmibBH`@jceX#2Bvm*s47w-4#q@^=*0admh`^ zkI1*ndoI3sWG_~qDd#(diSlwwv3`}X1LxMZs~upza*sqD9@x5WZ%<4wj8O3&*8a&; z+d~=!-FY~@UN(n;BoPQlu-IefV;RNN?6ladB^{|k=9R>^w>VAM9;ixgWpU9>#|Jx{t>f`=ak79e(uqkM= zAe)RnnE6w!e*h>k3ie=G)(~Js1>T{&zXa(iXh7M-m?P)s$QWwzGzg=or?IbYLwuHB zI?lEEeL^^AU*D=^gQ+0s*+dzkftEsqx#iql=tis<$|dvZ75Lc)=v(sNJo6ga*wELQ z8rZJas2tTI!qWOxr?t8eG^Msn6KEmN6d2rAibB_IWWjuG8YiaAGp%x*b%}07?1RW| z_d<$`0%w}E5`iTJJv3@zL5hc!w5XmP{;;&pwxm_HuBu+nF859PKLgpn?}*YN)TCL^ zWq!}_s0WVj#M*jKG!*80=<-;GerYoZp`RE2FiB&4LU9sm_$y*oyITjAO0+Fx%MVc< z@4Jqnk$ZCmE7E&HPx}{WM`%rnhM)?y#XwE6XJZPsX_TTD#H5lmWOX0C*WU7`*M%q@ z;Evdks-29E^`Oy|6SQIl*2+bAz5C^f*V_i|^bm2vu8)KLykydyH>G}Oz~qCO)KPE3 zrP*3tZGy;8PnW?|dvrCg^v(8q^zom@=dU~T_)zIhMWBt#Wnnyiz4d4P*@=YH9m*|uJHhbl8VWGHDKPo&#w9DKQ_6MU z@b+4=eMSYs3}W1VyF2gAX9DG+v2`78fuqnA?tia#*RX6?og^?=%b17aID{#DL?T`7 zoTu<7KoJI`$A-b^o#aJdeDZX45&xSEC&n?nkPk$pq(2F_QOGf^b<`tdO8W|aDkFc5 zDliB>>JemuB|fS;Z`(sb&)nXMP-<)Q`Tq!e>!`T0Z4Va+1V{+(ng9V3GXp9R9H8-j@raOmiV@@hRkD^9@{vVYE(c0 zFvP$W5BF?{U4oh}$euryMEn+~-%#YoRKuBl81` z-vAB&(1V(!iFhv~$DYnnx*Jp*JFn;@=BPv#x_d6NR~5hAKg*nwAf1P5!q8H~yBx1w zlP=22Um;2tYT2HpaEm$puEhVyC9kGe#b;?)KhBV;jMPWQa1xYcwaRHtp=^B9%lR8n zDNtvWfmnmsV7;^*Mfy3XzmSQ4d_0+r%VOHrdvrH?MyA}f| zf%VKF$ubP=u!~N?LubH!Tp*o(Xgrp5kiejRs8Yc*sK;j{z?Esaa*R$PpxHQ?^NsJv z&9TD-eF>BDs_gT6Bn+Yif#Jvn9(3!$wI+?cRoa_#hs27(}t{>Q*uLr6v7SJ1^qs#_zY>AA{Gli2O zH;gzCDw4K+e%E)_6Sw-+R*Ug~CgZeE8v=ktWs$~l!EIEIHzlA}ul{ff- zv!cd^hIv-qZRt;w{AYeaexG;u#us$#Qg?a+ya6eGrD|B;e*cNoZm%GBX?Z- zYj*R2oj0@XDt>)d>i=w$TQc2vjb#lR++=ohX9BL<-+FVjqVsg!X?3)T+ULyQxU;Da zS!31o<8}G%GqE=K2mFNFaZ9GLhMugC zlp3$EM%#EIdIu;EE&0%XcCW@RxiA``eX~~D0ZY_aX&QS9e}L$_f_ff+7U0w$3{2Ap z?}M5}Sd44-x-AA&Q7xlUC|&*fyTF{qq4xTuvPv?Z{YZgZB~3alw7z@@YU58sg3sKl z43p3_%axiXe%OqQQkM8V8`hsz52oqRonAq^R4%JBtkKIP*zw{X&#GO|o1sbXW8mqc zHfqkVUkb?ZI9q;;>dG3m_w5auXN%`z-xzF0)x#r8`Swb$(w&>s?yU4bAKF!H@R@RK zq&@9SdT(-uMjv-7yY4}f(wrv+p?U6yp@YlkTZ4d;2C9>m@eV5g0-Qf5hV~T^mev9y zgF@FLAg!M&8ApDzk}&VO2@|wb)Jud8x6!fbRAFI>2WKz6tDVlNXumcMyhHW&u^;pM zghVG`FYR+o$YS$3C{|Pwg9Z-R#4nif^BA!|3Oq`^Wj2Wch!bi=(dF2MF-d8ze}luB zI>yqQJput$x-qF7?hO@8Ywhld(5pB`>uc!Qra$cl5u`;YqzDM3t{o+$wbX6c!oXQO zm??RdJtcFp1;$XFDq97;8`xZpAord8J;iI-@R?;T$6=!$%WRg^T)u9Uch;-!H{~w* z&)NRSE4iJw$*q^CP)#;HwuxAIZr8MPmNzH`t?vy72r{8~1GnUVu;82mVK@h^WMUIq zAm?a_K^coUVUo%;=mvnZoSNR7<4`o>(D7|etZbnY08k99xnyc8Dri&a263Dnq|@9+ z0D81u6O1_bJRrTM9-Cso+LpM!>{L9ha(PZ~q{bbS<#?1l&0BoGM!WXY!M!2?`C}Xp zN8x@c!SN_5mDT?#j{o-}D-$IE!I5|C0&r{=%v{n5*Lw`)RJ& zu!DG*_ePGhI(Z^#_5m87A0yZlC88JiT!gv(ZK2y+(Z#x9K<4(}5$?ZRpDB$p8*5?c z6X$NR#BF7#{4&MIFL2&FI|Q4b~b~sC_;yR&v&>FII??hp$e0#QR0v2$BzR!5FY<5xPe1|p~ zKFMEB&^Pt+)6QAT4Sb`E9Z%cj&`?${()%{dR0@$hHn}I^$rFD2jonbehk5?1M~aco z@@|SxNdEh?X+56Iiv*QvTRWE9Dj|`^{&Mymwa!{K9jepx4*0e{4ywCHj}jO zw0-TQQOPfI6bYevM;2bX6I|L6{Y?ww>O#JTxj#Qwyc+J*wZ8*?{Cn(|(BP0!EVzuC z*C12C@#u4v6wP3py%YirIi}N-wV$V0u*hG8`V!-7+~l9vU}*p-R;Ux});wA#)N)Is`OtE7 zex-s3O8RC_U!2!y?%GiDNI)$&TkcrRrsJB#T6=g3$J8H_A#wD@XGT4v-xXR<-4!bH zr@*HNq+)qen50;)7+r?AWE~{P4EHB!=BD*vfFiszwrYxVK2F|!^dulNB#hdDX$>iU zI_GxHG`iGz?_1%I4NnU;%`4;Syp@&?!BZbyRRy7*XiuHEiTkZqDUM~(S}+1*4xaK)ueL8?vBDv${WGv6}=fb|A_0E z=sW1=VvqS!S&H(kdM3Rv&ZBP80hB=ym|Z(CT^sFi?>9u)TpOpoat=5dabiocz;k)0rs?6 zbBC^W+;ptz-vb%t_?54VJNU;Mk5nv*dz46I2IJt}*{2Mr?hlzOc|eT%r9fA&Bsqk1 z>h;PiGIH{2us;LSENO!V_nI{=D@D0D8F}%xib7GP5%G$5s}RQ{XgR&_3?6Hjk&LksXw#Wd&lVq&_f>VVFBj^7uP^~eP7Lw>KK`Pu|l znT*Hbkw-iqH-gD6bFLvWysqQU)6Vll=i$NbXB|>uzaROBCi?dmlOFqf&6g&m+J7njxSLoKn!qT4gGyH4Dj!FEaNE%8Kz?KYDC;KQP9QhDt>D(I2Dl1}|i_bYT&1n(?K#5s&uNQpCPy-dAt* zL`LSDz|7l&nV5`4EywWq;S=!q7j^MYyRp? zwIfPj=4y4AM^~8_xDO9EE=1EKpRG!O+=BuJ%@;;uZYS$~0r+5S%z>XpOCa$Kt z)mEHJNtQtfIyytbCe_QNPx54~`2l=CskkRn?2VY#20s*>4^pRzjIrWe1ZXO7+tx+N^E@boQ)JV)_bv(>V(fizE-+Noz(GNbtpP2JbZ_bUm& zy|S<&=~pMc{mkT!-uKnjcK(ch?9vR{mN%o`3DnLfpm?|HO3>|IIbfu`pIj=pA z2|!P5@R?;B?oAuyq6ME!VKaov8r(IR>*-_M-ybKXMYPr!esu@Ptn)@&DgO3B(6hI9 zKW<$aYQI^=dtLl{?hrzJeeXNh7EJzRd6^2f{?vL5B)t^9Gvo=g*<0;kGcg1&rHr$Z$7+(mZBh9j`}I@xpsFxf!Z4` zK|`a=7P1?ifaA-79*;j;qM|UNB3^Srp7PJecaulqkPiak2_&IS8Es6Tr_9UWN%x`I z6^F=~_BrGfqL-#H8LdEzBw22dLug;3hh~z@)FfL|irxche08-9b6C2sMX)H$bNAey z9%`nvq1d}nCqi1^8Wf=}kCUSb4#DSx_+1FtTDIBqaF`YieZ|#nF2j8(zSQE43LzJi zRWJl722q?xZ@aHIiT{|Saa>(-0c5Z zerlyP&oMh}(Gk~yqX6l1Vt;&}Q-2y$~w@PINYX>^Y(D?Y42A?k0LkB71UMwJ8)bv>6e2b^gz0ui`!FQ?=kpWM+>sf#$LQ209yDo0dbwH=Rn9A^Wf6X`i7=~Z+uE6|K_hYlE z-X6Y4lPs|0J4o-W~d7561jkZI7g70b3 z0GOH(oc`9l|C43=MsV5mN=p`T~0q9%7^P&Ea+MskvphZ@Nk0b8b;-(<$wq4Mulm zc45lnC&So*7jz-4wZpUBg|;I54|{oW^8DRkrKR`ena}VHIoeA7Z4%Y8)1Zw#lEblY zMfQ-_J?}|-*IJI`QPAGOK`~1y%}VHJF{*TZ9X2@QKd)aG9P9`a*U_HDHzq@n zTs95>pvtk3 zE9~YAb_Bcn(9IzUCs$lOIR^*)8WV$B)-R?w1qvVNnP-UDzGelZ3i*I-p6BedQkyEO zIYwdr(%`h|QN`bL0_(R(_jBvPBclPUk+VyJU2Q?tME)L|UPYqiX;ViU8)l07>owc6bN49Wc_Fp12$PrUe zB%U42$O)QpF2*0wtDdME2nc+_+OrCU%tf=YQnoHBpSX51Ea_DSQg$ZEH>Rg4)+x}q zpRi5-yGZ%3*3GIZls-30mftKo^=cs%Q?Z8!d2~3QDjp(>_2v~LZBy1(9i$neTR9b? z48~T0+sF^XXFQ2zYAn$qqE+TWW|w3Fj0%4Bf<#lOGiNn01An)AJ-*nT>WwmOOpcm&Ub{SwZxSO8k}T4@ zERZ3#I^Z*E?D~y#NeLAhU^XD z^Doxyp7MHAl}^;3EG?-!4BA-7Q#YU#H%>OsAH+M*R^9%w?^E-V%UX{78-R9&}oGt+B zkA-7G&X9C**$kEM2e8|R%O&Z@zn7 znjd98GGcjncgWP*SyEh#D9R-=V$_{(MGKVt^5us@9Rx4O6wjp?07SpqMHJ?o~=I5(kParSm%E;bSZ!}A8_o2gD zL*f=p45nUt7J9l?P`nLU;Pn8MNe{ldW;t$!Vcqc?G`g83G~R$4TN7Jzo<@|H+(C_p zt;cTTHO#G^_f&s^Ir(Z+$rk`oi{D-m6ue6refRe5JTIkrFe{==>&1}dh-P!?T>GRs zq64@DF6I+2JgF{j4w@jsZq=e3ls2KmDtQ2p;gD{bFq30Cbc%0H`%;5HaibKCq8yTW zUGl!l@t~&Z1#Gl}W!LrT0$}sbVqVo$kdIYg^(QSr7-j&evWL6!Z(th7(kAjSy?NUNR(Y{`Q0aYrNn_q<(qegM@38J`(LWsYW^%FpEY70a zducDm%6f2Gn;BfW0&}@VXT`-IE`2=TRldCGnS609C3r+OoN*opZX_v_$9Unz&$`W@ z)q1^4A`&NJw(Rhr1DkWT_>})6mx&n9Xsw^A`oo_U%sv3&);asIvSfUoB%)T1A#EZx z&tf#rXzsFw-%5YAUd;$lgQ6*gKh0HVhZm*A_vHW|VC!qI-RdX%?QPysUnmt+b}su$ zNwW3*24k-(k-#@%^cBZspH7*c-XjWjX0L;Dnd|jx35u_fh6avACMiT0r>i)6Z|YLT z7_Ze!ouAE})u12~@3pO4Erje}aQW5P59$^eoQv}a@bt|X?vNPcJ^?;c8eIxAPb{iK zGLNC`aiz(+ekqd7=N^Jo3oi)Hhe3hM$+;L*Jvr^=_yAS=Yk;6E31U|*)s1juY0 z3+i2z5#GQab)G+?G8>pdg19}TCuj~OxvPL-Ce2lnUPw}#0y2tG9nIdoCq$}Rq(Y7G zNh7-w!Cc#4XCRf&ow3q+;I$A3Lt|+KfrtwvW?1S=qcsc<>&mt@(5CqqN5AP# zj5zF%uD%cvt_jYK^0I!AM?86;#GZ7Z|EUHCk5502RrThz=nS~rwgTSItuo+W%jV_0 zve9{b`MXE*QOAGT`;7l3{ao+w2ZD~-j;cA-?L~vNPVayQ^#iR|V2XwJd2N&FGY4|t zxwRBTWMW*OR;B4wECWFWX#P!M&*#?XN!>Y@T%V)Ul2LEd2gWZ^+!y;}<`(E`Bzs@j*J_#%sP8QKvbWUs-ZCg0l?xbDzw;j}TjnQQ3<4*2w*h5%m$z>r#^q z!8fui)97)=aOiqVLfyCe#ToIlIQ-JHq+Q&AOvJJwvP>nWnv)#y zQX_PSg2xtL$7f_Rv0a9%nWNpK+KW4H(rxDPgiTD0$Kn&8Gn+4?z?*LLCjvb#8~Q3P z6Q3qI7LkeIMfMgtF9d6xzRZWi*9$a2o3<@!X=y!2KnLypb_y|n`qikLFBg$Lk-+SiD39`=V#6T!&6ua`Vd18R;G zmX;^JM|kk17o%<)mPDRs+Z=0O*PJB)@V?z0u#nPQW@@>TXWsRUw29uuuE@ThQ($zq zvb_OzyJse->~5H2d9B&jOZo)wIo;BCZw5ig!cV+}?45CYvapk9od>CZUepqrANlIW zJK34)Ek@<#BTJ#DDIDT^^woIrjZqyIBn146l`%VwB(CFD69AQH!`Bx9KL%pIhT0#a zeo7{UO7_@?i;%{IqR}#)Z;kcZUw_|K2a5uTEB88!wD>6)j=T{pFnId-&XAZB@LCDS z$HwaJP8*p&l|q)KATvR996UVcG_;MWkt8s|mFoaZ2j2KnT9KtySB^qIYkW3$I4qr{Yd=ehP9(iyb*)0!o8-Iv$;bBrkTiLC+qc=ji`hy6 zU4bONBA&{mI$2#b3S|A4Ousq_U+%co`pKx0r{jFW+(+7}wA5lG)fO3b-{l`UqkYM9 zsRgw)3D)9>@*$XgTG(SsaGzLpKeK==yW2ohFrgwFC*WW{{MA?aN4z(1t&N%^B)eo3ur`^Eiu&vfdj9@B_Zt+Je@`M8yaIBR#! zE?7*w6w1E|tJzS87oD&k^@$M}%%SW8mus38JOQH>asosq&#eqncqEYcujU}08?P+%~iMe)FwKFaak%|?~C40z~Yx173S2w(hg3J%XwD+J`@>QOl9 zumrnk?0(y-`PZ`lhSA!i2i7DWJb^>+f;sqgTy6t{b=}%mVfm6>BxX0WND57GrQn{& zWiN#XS3`e&GE^gF)(9Iu0xlpI zy=yq&Yk8l1wgG}8YwY)hAe_a=&lyXT%u1w>Peh!Sm%cj6GLZKk-TuhJ5F;)x<#Gx>TrO?uQzyqiVco28aZm zAmc4_>6n4>utz&q+{-l08gbMO-og=}nk>baCif_{k)E zWnErVQi+T>SVRu9mZtU3F2h^APq)t&4b7TRde?RL z1rh!y6W$8QORQAWQ%$ejS8>R%XG1xb@)wr)lXQ9^2lxE40(Njpk8?$la-& z{FNAPP0_d93IR~a$0fhyBA8s4l`DsSf*xp2ejK)qqCgECo9DfcJ{@s+}OHL~a*6m2wq z%S`66cJL&Uc8v&fMx#X~^1LI&MWZ>4Qrv3PquT@E5wwHwAJ__N z+=xoZb6CzA7HHrZ;0{A9(@%}k1q~~_GoM}}pb%q7B7LDpf7p zKexUTfax%?L3~DDtRAO_OEsb>-BG6tGHMCdH|;7ya&l1)@i)a!mm|slfRKRU%&_+x zg47%Nf7QH;;MG#8*ZA$rRQfKI#)MKn7FbBAqh)3B~OUKB% zMCcK4{r$4i2ai;K4Eze6{9{ug-YcESPs={(vJoJp{=m5LSsO!eQJZa{=|7Mqg%IB3 znC9t-Oa+~mq*yla*J)M<)Q3|_1n6y3QMYnI!;piw!5@HR~{(ym1 zz4rcRsZQyg(A|`Zz=nEH{x6g1T#GV&uj*TBi01m{jryi}!%Hm!2*5Ypq>PiuX)J%Ae{sND)%+9yUo&#>>9?uJu%Q?c-=9lO|9gh}k369e z2G;NT3GIV~{gP^$BS($S%n%EW$tj1Gk@G9MyauqB=&L^RYbv%hlXvK{(7?HRj(~K{+?F7wWkVA(x{680=grsDQzB= zsyuvBQkGw7UR<_QPu*^=HkvkDZtd>g+?z3nxI>A_<{*ZDfA4=vtH6Hx3Q(_GWKLOT zPg71#22T(d8o+Z;J6dTts?lB*pcL_xnH0hmO1C7yA~pgh%vwebq>X5eOZ=q}AKY&W z)}qpZr!LqY$O@#FIY6TRSmjDDZL-~eA`*0oP-lV5gq)R6<`&fAxFO8ZQ9_!|%V|?b zut6)+C1|Q&8d$qEgI;HGZdT>NH+eu&`vLNt);b<2|2@FI4T39= zaf||oqN-KuJbk(4kzcr}=XEga_~nV{+O?>iQ*HxyX-1Tbm|JtPILnQr4Lr`l+Jz?k z)LUEL6s8VSI@wmdg9+arB}IM%`^|gKneO=y@}%^onUm@x?8tHBe)Hz@Fa8f!f(cIW+pbpg4Mfm3&VRPy zMp*Lgb2rFjGm{ujGKnfDDw7?^ePT#atjbExrR4tLZJH$zaSqJy%7n0gE8>*Vy` zZ=|YKTo+OiB-^Sn6nxWllK!X#Aq=Byi=GAGuHPar&K1N2$5PYHQqzWs0qGV{XB;BS z6Jw4hYt1-8sF~vsU&xnhv@w7{^jzxE0chq7C49c&Sa@4Jd zIIW6)tgKgr!V(ipy&l#BOhuW4UB2(+JB?-=q{*2@QAE`wNS%(!Imv5nHy^{_`I6pM z7fuO$(EA5`l@DTzTvp8Q2=cjFRQF|>H%j1FlT)7Q0v3W;zC7e8ch$%AFS_*23yE@f z%PI?kUmWYUV0U|%8me8{Mp(qGh-=XTCZlu%aCN`iGO)uRQK^uWnvN316Ho)e%pI=vl9}C*cSvV_Z*S6WFN)F7KnI zbA2BcBUWAE;$hZ6?5wKKH{&3aaeA*rnM7MjjD$}@`S#Va5rPyV4eOT<{}VW>3Y=^w z4~nXkRG6xHX|gskX`_S>3OaD}`T{8Uhs`aSrGrw0Uuts2D}@oyM?0z!UN=zs-6jtd z)=^qu9oSr}X6v5xVwaNsXY2Ei3x6ZHRRK$j*2;9~WFgLI zuW?aU6Kp<+eDsg@lns%mSgK^=@J50|rKr{mTrxATvvFukdqPu@bsiQ?O)m|WX{Se> zD`SZcdd2W#07+mIUa~d$6{`V@estaAR`0^_%)JJ_>R1SF^>9%_@>~V;30E5Qd~6`; z>z^Z~|J-c}Lai-%B4g9kwH~GEWoA)gXNCkn@|ICI57z{xzUJMTlT>6BzL4m6bXy5i z{g@Q~+e+dUi&?;>@x(4FWu?$UIz6e+&lW79KE=4rxj7!AwE9R^kM3Y?LcKPI=c|9v zBSHwSh#sEPji{F#C*^kd4&##*uVXl|&2eP-Se=L}I>MSm2acdeDs39wn&QQ8r@sJm z91&+Gd_uO)6Bw-D1LsBiv{@`}apoc~!4d5A30xLy#y4vJa1u^e2<0lJ0fismB5`HI zQENMp*fLi3O3Q+}r8#b>mUf7-6gXNyuuZ<3V+!DGv2Rm>Yos4S%1cG*a;&mYHL_Tl zRi~Mh(NrfOc(5Q@nn{_k8sOB5`%9tV-+I6|P&!N6sv6Bxsb0O2BP2DMp))in>>AW! z3_hqkxO3DfJ?jnKH7m6)sB2uYWV3$F{3t4qB0v~w;KYq)+rWG#YFm|V%R~g}t=!u) z^I{d&7-9SGpAQ`B6*~MJEALz`u>VHMlm3YFka=F1Dg+?{!#&SKvKTt$QPf>Tb0EnJ z8Ee5VS7HgqME_oI8*h9S08bZS`SUDWcm;G1?4e8rHXGl~?;gb$zxhiI^8X+*e}G8P zeKUT&qr%gKzR&|i+j^Mi@>p59Qnr+qOLoZvt$1q{0DA4Rg%Ug~LIuyC6T{$!)AMph)qS#)L0i(H+po`e z-4AUQkl($}H#^DSmB_D7Pq>u+`z_rr+cgXb9f(h8ee2SKRiqUQG%BebA1~$x9Z{W5 zKF!S#AH_Qv)PcOHv(?)tUsq2htanSUX0z@{Z3qw6oT4G;^V% zVPJ%ESD4_9xYsrw$YJa^&0UqpWi zID~`WEa(Q}u-pgghKI4SETUr{Q3~x;SV(y}d#MqpLFNK;tIijO1PeitbJER5oGv~1 z^!Y~-;R~_X2@L0K(K^gRbOfyrffK)H59C921q_{K3HSjj^P?bpUfHjrCVct!I!xWf zcw+XhVWEQEJiaM=EPzStNAD7HoJTo0gu4*WTfbVbcTjI7Qux1L!7BoItud50=skB4 zD_RI9L7Rwf%AI3JiJ8$&mE*py23L)tfRh=)~z>*>?YK@5zqFk&1hY)_mV2< z9Ker3ClA}9Qw7k@^vfvM{lP^$ zn6`5OsT9Wh?1;pJ?IaRtd2|&fR*u~zLx3#m@bbTeKEA6|Sv{W{)Pqc;{Ij{?@zNA> z#pN6H3w5-YEgTZmPbe`Y7Q=3ogw*jRjUe}Ckcqv_L*B;tl}?e zHjK+)5&!$0MJ*rTV?Bqk;bCr`z@vs`njw>5ofN;P{DHCL_rQg(uM6SIjc9hPJfpDWe#P9{ zf8nHGr0siuRpUmPGPjViuNLiuz%_S%jup1U&&hoJUnq6B?|hJcOaZgbS=c4A%(d8N znF+)w8!F&c`8hi?t0Ky>SKNA?IUqV}Zhr#_%rk3@7aTr}O z-NuZ53Sy2HL0@A~t6b}G@Ua2~Q8A}DRv$il=cHaLCuHtwkfB+$IT3LwS@!n)ry2dv z>#0n{tP`Ov-}zqoG@&Bz`O>9|mj??sTL+vo`lTQn#NWN`+9Xt$dq(^V0HtzxPSp{NLX2A!b=_`$zL2C@($W}OQiZG5|&7&B3L!&>Ro z*+Fr0@@_PYo!*2tqqHED=fJH4(Yk%xwB>;3V=^to3%U}bN|}W5CQ?m!>CNP+$CgSA zIIl-66fQ`#A_&&{6j0uCwPpSz%jwfvBdE@O9#Z=dU?c{@I5ND`qH|prtM(vN)MouzBfzzVJ=qDzXT+8D=<$Jz~i4*wK8FK!3~V0=C5h6V_# zn@5IN2z_gXD!|MU7Z;TQ@byV9yo4N~=Ex?3E?jt4tA3z>VJ(^TBGsw)uwf2mV}p`@ zhbL@0rsUj-vt3a>dmw>_JxRiM`RV@x&$_~eHx-!jBE&}MXh_}$3e0P?{t_`(n9!o| z6fE(l0aebqz57LDUlFpwbYOVd)N$*W!=p_M5G%@4wiKo^h<+#hR&G59?JJ*&?Q=HT z0F&y$M~|QP@IE_L$=tn6lk=5>E;g99T;nmT`)L2obW_Xoz#lVo^~a*lm(-2soD`g- znTM|qia4|5uA#SB^E4^%FsY7Czs}}ReV7>7v;)ZmE5hDVLf=(C*Wb3m`cvKR2E(fgL(>&LsZN!` zE!eZ1zjEl_OjtC?LX{4u$)S?E5j&o*_z3HHeOtWso3_*|Qa^t`7os@XZ{E^@Td!3F zY*S#GP2OwzTRD$U49}EyoJkZsz!R)Ir63=Oe@{1}L_5beri2(BsEy|_7h2RfJP*#< z5~nYp^R@)_hm=fdS+cI+ZrkfD1M}60(cxD@R}(5C>1os~_-gQl`A<}7`&6!JXsr~P zcjPK;XmOAWIZa8tK5v!d5E2owaufdND*Wd){!92n;kj{#jHl!|s>Mxt{@H>)=^2ed zIo@G>V9AXzs;o**t}NZr#P~R9xeoHW)%xX$6&Ltt1L=3PHDQDx+t?oj6r2cv#4YpE zc0sI7zI&gFH~Z#$i5y{jca}=gJLNcCfLK6s={&$t=D)dxkElYg{b>58JQ2nZGbgl_ zR~)@-J-hMUD&B;Mf7N*opMO7?vc61uTyDsSq@Xo>rVq;^25r)EYig~Av-Jx6eHu{61XT!UQ+v0cP_>e* zB&l@IvD34hGk;0L0dH`Iqx~mGQX`;dD8~#7Sc>Hg%k}zVe;HVWpYEqZ>Bd3q$5(Go z!P3JU-Z6e%?>`V*){fp$Wr^-i_^VXFQu;Je|=HUXxBshhJY})%lv@ z61{k=>Pu9+Rp^rd8pE$H*m(k9@nt`>nZ|&>iqpzj+0FAQgPb;}SG7JGWg2z0RbZ|k zUR3BBQ~mqJ|F5Ti9ndv9TldF-7Rt|?DAn52l>Tnl%8A4FCkv$}iOP)&7t$X+cl@Q$y+rkF*EA&H**wVuGkfn3Trd&PR6cD&{6eKK%24QnLr$CH}k zPKF*Jf~s_bun+wh*k`chua8Wvi>gcVI%K;QD6@3=)7?({1#3pzbQkM~fu25=fEKB; zam1hwtqd9Y{NOmu`zkjI3~FG~xgBjvbY$c(gpq&wJmZv@gK~d(*iTnbd%mxJi|x8S z@TIl?Zs6LPYh&0F(Ou@RNC}(wY;2GPW{5kYvx9>QOJXLWXikMrW%#){;^n?_u1m=} zr;c3!yc&j{?l3(0MOymIh!Fy1)03g8ChaSMtMCG&&BpcMx}*B7!q+WX>Msy(PxnbV zSd2dXolOuDf`k4z1J#cnFNfmoC<>d!T8fd%nbTG?2;MffQ+T`?STL=zycqEWuSQRf zlIoII8I9iFHlgeuKB<}yE^=D-pLeW{7B?(DkG8jNsG+H=>}$E}*i^~tOR=fG)3bgqNz zB={4pXB{!)(;~AV`dF&B3NL?nb^1GokOV$F&`AOVuk$fW93+uwDA-3J5ZQaaQSuni zN?#lHU%+rkEa+IvuR!)MO`Sa@3Q<((vR>G1t*>QWT8~AxZv*!Gf7mFrQ8)X9Ef66} zRWMNmsBiy`mu&e8Cvt~Q&#HHXCsNa* zewwF56jG1QoASrodh^xDHiAqCQOHS`49%6^+%a(+LHleohSiD|0$>_#6UdG)tFZ@A zpxLPF)N~UPN&H=Sy@TEL?P7SCj4}b5L3Kbq@rolfCPoj-azy}aknE?b`8jNT?daYH zh;ld(Q6N3_vQPIoxx-;kmOg7s%rn%d!ii&ap?y}?YSvD>BjIL?$f00xvmXb7lx-IE z$Xy_N^SOaf0awIgw-5+d^c-d+i<0)j~Aj+~0~H`8HbYk<>8=EW;M(E`Ld@bAinZblS~F zQ*|pR(4c>5zktm%LafU1fI_~iFTsw}@xT8T;D)FY!K~f3c-#QiSN~e!G{UpBc4=LC zYFF!V$9j+$cBd9|dV|~mQYDFeVOiq{U!JOruXkA)AxU5)b7906mDArs3 zSWITVJiYm1-+HMKqbsn9x#UYucFf*rQi8-tHdyCwJ1R*qUasp`B`bd zP21{vEeV@ZPg6SSNjA{0Wuk;J^#lb)0#%XNWp1EE;5yi8H!YQ|E#V+Q^V(VN$IfRr ztDk4d`Z7vc`~IX$9fI&~|q z@u@txeUy08HAB9v$w%)K4%uDK>Cd!krzygl`+g9e)^=u|vmK_1j-^@$c7^gU8lTy5 zkMeh|Vgir*dWA!}AyXuxiNQKCy5y15+Rrub!yji!a_Q~tx!)ShIP=-{;=D`@mvgR_ zUicap#|kSD(`ZqBZ6;~7U>qKg;<(7%qh}Uu#oPVcTsi|aLMu=&9s*o?Cf-u1US4>I zIry|quTr*uA9g|M0Dtb~;gOHBo62RA5I@&JzPV{8gJvMoK~Zz@aN!kU;HAtyIM0bj zJ;en7+&UM<{P=WmgKmPDc8wVW!>cPLDAP8GY3+4;9y%|3OE<>kU zmapyxT%^d@NLT8$C~mz2&;0+;^_2ltwcWO)fHX)*H=>kugM!i^Al=>Fwds*(ID?)~dpqG^Z=OAo}4_($moqrLZJBv|`{7`$`iPfW!|Q9oM+?>vtFKL65)?XpW}m`Y0}l zzMIdie^!`FT*<|3861$GQZ9-7>=Hqt#T^QBA)ks~+% zlT^bR*H^90`H0K?b>_F7wmM%Pm4wsEelim)s~Yf4(+m(B9LU2S6& zuXF_JWfNzxTund-)w~Om-;YL|Je8j`gF;8QMVV_qG=2>$(tseKYskT9(GZ>EOUKg(Acim6hI%6_Kwj9SyOkAteIs zEo++(2C7$SKX}VRKxv&9khCYcg8k(N($cwkwzZF^ocH);Vx9@cS=+|)dGSl9b(;EZ zBQJMvR!}9D3`4xlLQ6I5D1U>fJ?C$@I1gnTciw6z9o5Eido#nB;0Jz0v-S1oct~t8 z#Y}!FxQsqi9?(EG;?-Q^4R$%E9z-6Ri8B4?a`H`p?}@sOJfm);S>=uD?i#bvrlGrWas0>Oir_(wU_@z{HFSj5wrj0d5}BD7v!c7Yb#xtogN(xx}0*oo%G5tE#wyghM7~qMQfuLG^ZaNv=S65>eE#gFOHilyP z584S4a@^$9)F_>^10AQmBOJLoR+HNm)#N|s$@tpI+GEE z+-w~hAMmTY*Op$%W7L`YnPJf7=0dAiww#PANXA=~wwr^5MV!&UQs^0P`G^wW)B3f3X%Fi{Viz17wJYgr^yDN%Ob@2!upL zb(%F+q5WkKq5N|MBhtrG@(o?#B9y5t+I&XAAt9bWTceLK(*KRp`Yr0qL_o zapH{SY$|l_*wz{>O67@$9yy-`cltdEJ~|?505vLrjxV#sB*?u56;)&jUY^_+!?kx< z@6uS23iA6i3ruNJYQt~U1#Y=^bZhSFp${gR*2&~~@Z2*0n8nbL{RRdk;X6q_T*C;8 z>UG^9paKTLg!FY83k}$|x6=43{vlI7ROW9C@DABQL;?`OcDtQR+ZZ;a1pnIsysal!J5 zyJ10`*F!Zr{HAyEbgiWFC8%&0B!U0b8PW5@?wBRM?zz|*NtnQ8cV+>|cQNm_KHn?-Nu#LyFa~Cr@ zB3Ycwy+`XRv3{QC<~Zb>id~S?cvkok(7~jPn9K|c)(bwKbuYE1YKN)3P3C7Gl{l^t_|{gec5^VDVd*e=X8Rt(4?9iIF@_{n;k%!@jst(zRGumQxRu=r(RTaZV~PHp!14U#$uwS<40@~aF6vud43<%Yb2mv>}~z~chld51*HoiQQ_7` zjC_?N0I#xIht(~}u(l52$y5KdCgmX+rjBHx(vXdoqj7@=+i0dpndykoJA!t`dH?!Zd^G_G3SFoNC7ncPPsvz!!vR4@z3b++b7DXBiM7)nMu8B$-BBbZf(BKda1OsocS}L zMa=!QW`pN`MT=!H@Y@*9D%KbOTSY)jl%Ai4hob`N{663;-Z=0P${S#)!$cgS9f(S^g3F4{XN;jW zJ%X)s49#KW@hquXYdk{imwPU87imxon}toC;Nv}K_SF=q)XONCFy2FiF7!QA zagLiDXjUJpyzVa&A2BRw&tM451mt+Xg-R|SNU9sU-l)?;)9Nkfn49-nJ;x{8bM?lW zKV@q^yyq9kJ^vjJu;pOfHO;_c&Ku6sd{gIg8 zo7h7QxMJ%*R0g}TSp~~Pz3Nf=VZv$Jo=F~(Bj+Xi%l|camSrAoSmKO zf$;-+qJA>Z$q&ib*eKpQOdgwQG^l{(Y_%0uSs88$O=@>L0w#}l-yE3`zqr{A2Z-yln=pUk9L>+;- z^-9t4q;W6xa!hzl%Y7%oxbYYH-(kcL4GxPxKQFHphyLvcVYND|l%z2;2^g=A0duCX zw%o;V+|3FzHim#`R?-J>J%>t)ysUkKd;5qc%T>MMTF^fK9_j^X8H8$lCkNl>I?rqs zCAOs23`P|BKwbne!p3oJA+QRwvgi&?(PPz;{B~zh<-=8x_X-CC7cYs$6v_<=lmqJ2 z|E2rLpgOB9_Z~@iU3IVY{K?Y(=-@ahN#E-p@jp&AUL1zSTFj0}lqxYgDk?QB?EHd= zNJM@eUe4{F7H;`(75%1c~?6#14O6 ze;fUo?emRo;rn$R*YjQiC02O_egD6v>jo)(Tk|Ws1f&h;y>Ib$IM~*8It~i+Gvy)` zYblKQUl`iA^a0PUc3e1rWU$84+UQSe+qnjP4?7($M`DO^N4E>tM?y%%Eub z@VXP_{1O<65`Xz4T24ny9cC+y4D%slq1D%Bvg__+mn;mS9hE@J_W5K=oFpPG~v+XUKL%iYWMg&&krvp z+$g;ohM0D}y#4At(uK!{cD=~bt-4RFr^;E_l*& z9{mqv1z1}!Abd788_gT>E?oI%+c%PP0*g$w7lH=YDo$@RyH<+tQXO}2OhC! zL1YER?LP`sF^$1mW079fE$t{R*QK@zuwEyQS?mpYtJh_&DeR*>x$Q{3tAEuzBA7TG zZA)PtDJ9Gy!4*aWHiGk8t6&^A>xC-Xlk9TH^pw+KLhtCq8#X~fYDlA@E-;N#5CLqER)wcd3Gc~x+$e42CJ4dTFhyiH86GRvM5R)%Fo6PMX$hFlOi)T-_~IgB=_-u&;8&s1@r<-A~FjypWWAMpWrN z)4T{YY=7;$(wo1FZO$nyDl4+DEIx=TW2r~XOx7`+g?|ln!`a!RP{%zA1V}LWGWkUm zT??iS=|YrFiX>swsPMzp<^~K{}2Le9Q146|~0w zQ-3e&@y7F>7?4ddLpsz*mj*WQ(@PM>Uc5QmOxTq8R6O=egDUt)tbl+aHhT9A5?9hz zH9CO!T{$&tu0Utv=2BDdA2kyqELgq;VWsR}hdSEtm1QOqCR@kHK!@-RA2byb<;2b9 zAP4#9yVrPg_@6EX_@HU0WqS`$Rbs)pdggkL(ssIAcCd8Q^6LH7fpSfDqi$Q_%(1?W zrn4e9>w+4BJY|v4iItNL4R0mkN);(?R04?zdafjlrIu>0!}-`*8{A4&3js_`G(tI0puG z@3x|&r+`H~Y-@LY+`}W>YmDB@m4+bz5t7h&w;7Il4PLY?=bY2^X>ELdg6woWe=XH+ zR*8y}7g8U!H?$VBX}wm_zh4i)BqGskJZ*yv+8^Ll( zyFcB{W^h#JxB#pje0xa(Mh54dD-k@3QvghxrlX@HgCIV*X!f;5^&3b2 zaFJ(cB0I0S@Kc}P>ITu^oBfP01N9bWX7j5pVsAqbPI_!mwSZ+bES5jHdHb==2l}iE z8q=K}Hf0!Gxenot)#C`}^aBxIB*Q#WK?haa41}%=9o4CG(21k4qB9>@4LL3lFz@}@`EFjy3^kq&Dd2g_fURJA|JGAhq^fSV2j_Gpl>A~ zeKBi?oHRQgIXf&jMppeTbm0@}K-smyqjN$PeB)4uMEsbSm!~L~P4MBRBKq&%jO3(k z^u%N#c6Q6Y#%lRPKD{5_aO#T;7mwYw-}mbz=FF?jfz!yA=H?#qdiAhn?er(Lb4r~h(KWZ#x=pJ+n1)Jr^sznWA z_*hJEodvBN5V!oH-*$9jum}|SK{n{k4d8GU7F^JYqqgHRD z{Z<~gmDL7}`nQl6AlQ*p)nVWLe62w*0*WHVCw?Czg75c3!$f6;5zN8P^1btN@jIW& zsq;r(4({=gJl7*}6A(qo8$oa#o)+Bp<9baW+!1l%u)wBx^$e zGf&D?+e6HJ4kmx47-ItL1MlT9yW?JG`E=>|o%L>SCEqG$De|s<#(4h2=`VZBEVsYR ziTqrU^(y5E&rV||CZs@TF0}54V>jOsj@0PW|*9rQ4C5RH)aci*G)^kaK z?b(6A#4^#(eM=l?PhJ3g9>KFB(W|$Wcfg`$XIL?Z?SD4YUlgv2Y!t_UP zE?#uvuqrl3DiMLxrt!^s&H!*T-SQKhrp05w&tr%mOoTNLm+h)m#Y^b7cV(=5>5fsa ziKJ}M=Ls2!ZjYJ^r&yaTVXZbP85E28>o-0VaE~XQ7B>a3oW-R?0`BA@N-5+(h z+y{E}m5U6=>wZX=Yd(Y+^3BjQ*7KxMTqV9-rmKVG8W z_}q3c)>@AI`GJ1VPD8{8AU-lM>OvOh9`as5g4>_2%eik?Ty@_H%QzVd=%=|KaJOur zo7Atkgojt=-;=s7WB;*v0k61iMOMD919fty;CYxYt3wU{wlm<67Ey%}%^!ys`uw?} zt3^Grt@T1FuMK0nAIcX%n3b{f^72FaoDP}N1r#Wzq46=>^sNL;Tsq?sk7gS+U!_nF zBuiIAX^YedAh{YO2=@2#;? z!tJFp)F$6WqOtD&oFP#T1oQa}bLUf(t>@Cxa4M&zfxQ>-bV?v2K_svbKLq+bL)KVWa0JhLaG`H^$DSX@MslNtQU@<_$)E4~M>yxSwoC`= z7=bS#|2|jvf!f`?NXTs7vb;5rR^>+ktxBe)uiNwLZJTNw=blYue%?kp+Ho`JX2CRH zsvB>su!{oQrr#fIrBb4Xt@e7#a;xlsg8i?OI~x%WSkGjEEP;usO4VP(>c3h72U6Es zsY8&=VzNNVu{M}ktNcK!>N6&F>zS4Bxw}(8B|0OYvoLr~@857)V0=iv;}LoI&B3hp z&aiheQ^!yTOu7gMrbaXka7X^@u0vj4o6;$Arsz z(P;HD^Y*6|WvI`-D7n;5P*}n{)znkqG#@O86_z~y_=V)%BELq26)PFJm%!+9(s%zG)+P@Nt`uFPUY;0I<{W6yk zGfa(;Jq@^`&NpKk;g9JmhufaTbw(JcPljq|2zIg|bDV9|>i5Q-I2**HKk^k96G7be zvu+lcjw@UU-ps}9q-RQ{dd%=uoAtCDd$6B_S;F8c#T@Sg-;m+atxp+sh0|ysDuSn# zpP$sX)UDq~BNS1e>@{O>V*GKw4Mo*N|Mxk@hK@ z8mumd*Jaw;t4d`s5a7;bj7C z>&MH+y-dp}{u3VIAtP}8YU%k;K1B*Y%g(jF7fX|R#@@NUOm;R68^QBdYu>k<C+3oR=4c{~kHaR)%X+S?|A|D=dA`-oN$&38`jD>$KTUJ)i3&l0dR-grg~1o-Uon zXZw)#X9;>LM;F3?rOufdK4wie`i&M%O{`6|H~hNAzzC7vM~r&KhDi84eyY&byIAs^ zr0PXaIz{e`8>Is`kIhNr45@?ODTe1OX*eN2JL6OG=X3Hj;q^6jHjx#^GV@M8B^D5D-FU$K}-={QX7l8ISwx724xErtR=Kbqsu2=)YJUbN-R@sfN|AR6AJP3EMBcrM z=hrOE*$eU}=w~x~G!4+HxhN5LOq#wc;LBC)B*_kp7No`qJ-s~0{7 z?$!g8@-|eM3cI*)u$m@xCVZ1Dp8(VmF=9{NfhwJbUhcVaq^Mk>98TVGaade_Q;k+< zXwKEL|B2MD$i=ErD51K?TV_lk9FX~PC z3t8*`4D(>Hw0@~vR`|2P>*V5!H((1M6```^eux(Kq@-sum05#>hRW;vk%9V6j%@xE zpmmmuj(sYoJnMkRB=jj4XlXbHF1o#R2JcS1lH&DA^J&}HrV<35(S>2xO~7nN)|$6^ z4FnmGmPrGT0p)E*Q56XK@b%#daw=b`Rb>>$`*gRZ#AQs=#F?oA@c@t}VdxW)P22tM zu=ji|$yVdj!x5)>=55PfN(|>qPw(Y(7{b47)BNy}1E2d8Cs-bK$2LKe{A3*ooyTE;Pcr(>VlO>-Y^V6D8Kb=lO!v19nZXi4*(2 zzU>c!iFrbd?ZFcmb)4-J>^$>Ka}THquE@v|C%ulH9xRD_B`pKNGwL7syi=(!_fvx0 zp@=OF;Sk5|@w{JY2P-ZlWaXAw`3ps*$w+Fv@;<-v{O7KBPTTMc1vQE8 z{HTs6Sh+*`l1`uhY&`6oW3BpU-=8deneX$Ee?Iu+;osO!ib}?+1rUB=)GV`@TQ^NZ zZ~p2AaD3{}CET_|e&v^XyyM z4ChG&h8ejif;FN3zy%sWDyUg!C3)YM$XFU&VYA3Hm^$1mOuJa4i*QOQv&I`QMr-wZWOsQx`)WtIqo zcxe)bSDmkS!n32T+xup+NEbQOX3@W*jWXRGi@>^7MA;jNQ;Wg(7LI%G9uE(Y+Tr2Eirt1QcI1azxC4br=mpc70Kl+DA@F2D$teYx4uUWdEYuV+b zaya5lpap)WvP7$KGTpxKb>j%|7>-CCaDVYa1)_RJG!SjYS6)XV@4kqEFecCJFs(=w z^xDiON&IU1P#C6hKc?BuShRY`>hb?EqfkZ6?iyuetAK6mE{Y7FC3YeK8wZL~V{Z3Eb)(;^_`SHm;c}1)gheVq#(%w9UQh zmJ3`)PlNi~HJ|qT>vA8`{4wEYFLogzvixp|_6YuC?y0}x#g{;s)x9`87jWv}poz{0 zxa~a=`ylsD)V#4_zEb^<^49bc&cADm(!`&p%Ps26y7A0C%Rc7!&yCL4*c2#Pf?P)v zQ_p&(?3RA`f7-%{?l63z^;+z5dq4_*-A(cA3eksRaLGl>yHMvqd_J00+k9{St^MYG$t;1v^m)L`7J?Lx#YiwD=!)ex|*DihBte|x5`9%+OB6cx17|m zYpQ`;)qirvaa}dU96n|I3CIp?58>A8kL%@cY)$y_@AmWUA%-UDe{rJUuzuqXpWD_j z%I1cS1=+ycXz8Dn!M)rKfLiazXD*Dk($Bu9Mkg`9Cdkx&z552=4ZOH3mMiPvX-oF* z)o^8#fZvOPm$Use@>B>y4innz!{S5_=kyq4MHm}|-A*%5LrA~}n2%M@4xb|qze_Wl zd)TRJ*$18L4u&RlUDwT-HTiM9UH9ww6XxSXzLdz~)g^a)JFFh%d#4PM)_qWXt4(8n z@GB`boVn@u(U82-O|jj%Wc;xPX1i3MRy=q|_e3gvHy9FToATWH0iMn3)?ZLY)bnkN_} z6pgGdvhSyIpN5rf-WgkHmAVz2bf82yGPpdp5RB8z(IYE;ippd}W>@I%>_geKfg_Q` zZi`$>{KRcHf$|YuII3o3KTR^`=^_9@itenByY2nX`Jd_(FhF04A1qGlkUG3=J@saY zo08)nVbpCCNKiGVBH3zxI%-cfsW-X5NNTrQg{}WOjltcIaSw3z}5T1}h%)q4A_Qn8w;*q$I5nhv= zZ{~zTk9|*EP{Y&;zW@fX^=p;J7jL&%wVW$j;3Cus3+`@d?a5`qz_|At9y&jC1Jh?- z%LjVR(Cpf>MOx@I69!;9C7r~3fU}fV{a0P}Fk7E$>wToz_^fnzdtX#s_I+j*s|Gtb zI5;rYHsB8EVOdmYEUt|~Gfuto8!ramjZ|k|myEa2K`W!ZhxKf2(OJ9?q({#C$;$`s zmP^lj(Ce@p&$>`YAZyQ{0BcXIimK)3Mh3OE`w?5_>5syZ&EvjiVt>t{lKE9uzOVL% zxUlqpqNk^~Hd2-qVn4*STRvQ33{-qx(?!*9LG_Mkd!g|Hbw7XPK}O83c!~<4IjASK zgh}x^Ws>nq=W9~53@StUMQ#<|E*nE4N{`)!5vAz3Xd9Fo_U?-ihn3;?1BWW#gP%JI z=5506sUePk>J_OLC6ML;>JcL&-2CAtd)B>-#kkw+ElK(eqkmz@wych^r9Y)-a{GN+ z`+4=wr|5qnYP zGOyoO*i*?gv?p}Si#=V7Ra_{~vw`5a;yh=rGV&U7=na&#Y~+vYNJ_?k_$?)KStm<@ zMIHV<=))HdB>>Qpi~>_T~U6`Kg(ve zOxqIJyl6n?b@m~klv4;`pNLvSrZ9nvRe!%tFMMEnEkiuN&W&{Hf*-~u3Ilu~qn`sO^e3X&GJu{K zgg@0APDnvHZ2qU$JCM47UCi^fma10FL?JV5byf>j0GU0;lNI}b(18m3i0{zxgP8=4 zCj0jLNt{GmK+a8=(*u8M!S4Jm$l_{MwtcZ`v>EVcV~S40pt5hJ$dbg_cZJJ(y2{`AfO9i4>x(xF1b5Y) z@s<|$z`awiJ${p!qE%=ig$nl;f7E;Pj=^(VuPtH+Go4lUcIXYeTj#+CB+__Os(Y;I z^Dig3S$ATC7%W#k6L0h&i9^iIjF-QJK`S*d2!#R8QPKSq z4wUVL$}k80FNA!DP~nT@@2=sgW&||dK6fr7%}`+I$Wc24n=3FkUr<;a85+JCsy6l9 z*x{Pw7o|Gz{GzrfvuIHrV`+WX91$EO5W1#hrWz-hPUV^mdR=J-qbj_e%NtQS# zy%khwhE%*l90Y)B1!1Cu+^h{$eg&juulWoxA>kejqR-QH8`4`KNNWCG z!|==2aX;uf!qx6I{F*rZCeTnc>1(55_P}~?s@gRA;kYhWT0~>rMKb}*Ao<2G+eUFM zVgJ>1Ghr8Bn|z<7UJP=}srPA|wQ5B^R~`8P>RsVFB7v4(S=)of> z3|s{-I6x|ZJSbnvsXGcrf8 zTi0txY*CZnhqlzb+M0TAKnoXmE{Z8Q?JIhN-r{yNZ+Zqj^=a=6K(0U#Z$W-lOa=sU za$cZILs)IKP_-$?&dAS6CcshX`aHBRWimUY`*flB7&yw$c~U|8w{H=kSE#Tv?T;em z9MLbv-)>gk=jKS{8g(5Wa9UpYpe%QHA4<(VD&MvBZS{w{ z{7m5c1nx(XN|o8oAW^!V=?O;qnU zYfK01S=1SYVQ-)yxNw9JV*EZ~M7c-K)RG`G>H1707*^5NqZ>Ka^4v( zFBPYz(ERg-;a^q6zXxTYGJ0-q606tnvJ?}D=@|aHPzRRF;{`;5GkeO3Xq;e$uzg{3 zZAsdkajxcHt7E-&S%6LppUaQYk1twcVNU3AL&8Y?8I*Vh=Y0ivJ+yOq!~a%SNmoGr z@LH7TwY7*&;n*FA1*^ z>*)TAuE1Zqj(@Aj(D40er4#1~xSF8(Ikpr9FxNmsD-VHAwW&flC(AdS&7lQ{F7?#% zJxHMP_?Zw;-mscd_?aL(Nd@#krQ;VxTxU+RGHc5lR+zdHsl!VakDh+34|#&MGW5ux zz1h(;F*0-$g2r~N3hPgB*x4>rHBp$jxPH{p`CaST!yvqr{Gm^Nye{AnLM_@4KnB_BF0yb6~Gw* z<94T*yS_22l}t8w%`hm#jfvNc#v06NFswS3q#v%Ia09G`2KeihxX0)(|6*AF-IRWj z)^T=L0n7uXpyBw(9-?}HwqwPOKWykJ6$tY&79b!Y?D$u+R5aG$j%CPdNT_O-oXllfTtc>pj*WPm@JR8FNdbXNGeX#f8Ee=c)fRNo~w}e2-v}NkuUzz8> z^n@bqIBJy!L9dC4t2Y&BC>H!gsyiy3k7f~@vv_Zq`tK4DcjtO>tO%&_oK7U1#D ze#n2@f#>{{fc|1FhplTo2G{vhQ6$OirQmjbI3#Vx`4h`EY5YXxckcox?W(2ClU)RRIh2(<^K|g{#((Z`u>> zI~&#dxA$)N$McAvL}GPBSzu{Ie6Qp@PDW&HPM~A1CO*y8jxJ*hjwcMBqfm_FjGMHZ zlm*#~mBwlfAW7iaVl2H{Rb2nzi|J$Vox-C(z(vv)#ac`z@FRZ1sKRqGG7 zG(Xn}#FuK;r=A-;I_QscS$PwDG@^c~loM&WX0?$c8gjU@BbrvabZi@i{A!9et|?`P z>{bUql?<~lF8^w-{`=OaW_2XL z&mF<81%pXFSh2!$-w*V&xGq_1NQduY4V}lzo6*i+?T2sRxI#k}wcZebDy&tw-Oltl ztq6NBOfuS4z}zSDTIPJk#m*16Z(VZ3@fqKGeBVtYa6!izhGI!tNgeY+P;=sgJ5afLp~DDe`ALWfD8zI?F9|EpK={}khXG6>lL5#+*h z;a`rXZ@?LTiLVaG$LRslds&`L$`6XN`c!~`j$2TVvhV$5vtUjZIghtdVomd{f9SLE z%cID>9j4$o0*XXK?eSO<6*#r$GQEsiEnb%sDE-b4e%2-*R0!-lsLa$@)jBMFuj3K7 z1Z%x`+4|1Mq^Es)&^0poW6z00=obGqK~68q-F|QOinGo0K2~vqE!`LToD!Kl%MBJP zVI7`I8}s0Q{8S{eJrL|N?*Wqh`hMl;iteKs)sNtLu52ZMW>s`HZ#C))JfE>_=;SQb zymfFhY24?w7oK#E$=qJPdKLH6pi$;%&Bqse$lb=BIiG$mqbMmc%q82C*feO4kl+>+ zSlVi-0ArfsnpQfVhe2BHe#fh&yL6R6K1b21)lLil&KtbEP{?&uPVRb?tA2Ka=HHmE zZ`0W+X9Cx@(4+f3TL$afLwMfT7NZ@G|D?9^+dfMF85BEn@Evu7}G%*IC@SMHD24_zN8GF?vmV^Pp@EUA@rAazWd!NULi z4k)4#f-%W$YNC>Kb!=Tyi4S^?-iYlI=pKKW%9mFa4Msb6xFio+ZwC;xp4Wguk5p^5&VTPJI9_Fz^ea7jj9OgeN zZ>=xzL%6C(6j;w3c&*)FVEv9VZ=JG=9S+BhdetVb+QZl*DP68Q-Bp|tZI z@yy05+gf_P`nOAvDO>d0<{85kuhNNc`gXdaw3%i|DYutY_S?_?QtRQ_l=!&J;D@&iNjlVy{}^*5M6&jaT~kG= z{mfPu8mSdHLg>yfhFH>0yJR!?K1Gu7veoZbyaVkFD(|AX>@!HJrmCGR^|=%W8#Iyf zS!wU}ue1mR?!adW1k++xcM9Dh*gZEa(swCnt@Stg_VnW0qljOB_*HwG$3**{rvG%I zK*mrM9ea4z4AOH+n;4mX^S;d&kwkGZ%<)tyu6#a!KIs1=E%;c!h2!Z+!@O!KFF3utrDY z>-m{O2_-y>i=;BiYkmlE1P_z89iwMs6ZCU)aM!G$&fNugnuM+sdsMsZrx{!={%d$p zl29>;99>TbychLPO$ejj$j^O;H_`6jA7plnAKxgKeFeRiorwRB!s*`$0C+`{6d!nJYIy?KSGmGk|$cj=VJx*ClrlO?}Z zl24=N7pZ8A=60pEN}vrt_c}wqL>a2tHNlahOb7TD>r1IUIxLF6)_RcR=#?SdEOxnt-`ZD#VE;) zfA#NcNoG5L986&u|I?T#^A7m|g0=cmXMxh_TAQTnf2?CTgkjC-u{w}2$$pMKuVxD; zHdgC>DZ3e;n8*Q0y&^lVo8R%WxZ`>zZ8F z_-iK&ef(SG0t~5F5}Tq!?w8fP97_Lt5acA6&*k_l6bbEX^Vp(NBoVuY^@Oby|MfW| zPy0ce`)wL`tgmsPt$=_mj~m4cOi8~?j2Zuau>pIQgSr9UH=0cu+f@=^*@Y{Olz%kE z9Zy1Dk8mK_s?_g(ammE<+B4P0Z_8$`RMV8%>x;wPnf`7AJ} znH%tutnz-emP8Dz7dJ+4|8B3f7e8_IV5*c%D^Ar;u9}c&GJNRHcB?THafZWBXq-=J6|;pdwi@-*Z`c*$H8QjP?r3x3HCAY!4h@WBklqs$L`fdZm5&_rgxp0v zR5+3fxl|ce=V*-^Z?mmsFI9GX`8tK1^E-QIXxJWb!M$GeJ&E0=X35lTRAatQ?n`%> z+Wfxyh8&K!$Km+m7P49(RH=;v<0r)_-yB6aS7bya7*5+F3*&5mnPn5gS%(R`1|C&S4aZ{u&v3~ zMC=o4F)XQQUEfb0g7j5+Ft4&iWNxjk1!M!o!xhrF63erAK!_}~6!DiO7hWT>?AI@D zKhWNqSH4%tmvj96&8F`}B@sdZ{?V==Fh6qaEO9Y1>d2|JXLHF&L|odehe2V>f19rS zPNnRm?eT)8Ui-P8csR;Iufq_F{4V{u#|!GB6u!TRkHJ3%PK0u$1%@zKg)jtKL$6yydJZ~B=cU7EuUOKqW5(6RcOt)yA63~3hH zwHbt2n5mtX$gIh$qKjxq%I4+^q*FK@$z%c8sCs<5k~sKbMB{D*LbuOs`Z=;eehx=r z4_V84g^$~cGXMBo`$q^AgCBXwIW1}y0ntDHSp6HXJNA+L)L?QJmC+GVUV9)EqJ)3J z(0uiwXmLK@Xd#8 zpJy&kkE=J1&Q4aUU@&+$AUz|4$8wo0mck>rTsbrkFa>;}&rND?Z_^a$O};4?k3SQ{ z(fB2c{4=j)G=z>fk)B~Ps^@6yc8dIrB>U5a-(o-S*<-S)cf;+7N=4s_XLanWZP3v{ z-OJLR6`_=;!&3?|Z?2b~cAiIjv0QboO`30oH-eFO<=@phmyc))I|HjOq1ryO_iMAI znlr8L>}@5>Wlc?AlXT#-W<&d6fZd;ipU00XSYSZ@rF5o*8BX+L0rz)029VaI<;GS6UL!@~RzRi`%@<6#CvS~tuBpYC zw)mFYEAMhVN9{WN+gkSx!P{q9B>UcUp~6STkqe0y3)VQS zg4MfydbKT_kO9BQyYDvW*wya(9*D5&$at@085PIp#NN^UqZL4m$SLwVqqP=r$KF4n zAULKwM_KNTGE-+Ctv$`HMKAqo3LwpqfKT)h~=2D#)_WY)Q=b(2~lIN`)=y} zSTj13RC9P|M$CCVMb1_W?TCWb)jjQY=keUUFzpj%%%}F#!08~8Z+f;^FyVEEcsrg- z2A-qEau{0Ll+TUjWlF@yu9LE! ze7_Vc3;IZPzLzh)A9j+^S+I7vF+!$i`0-3PCO{pf*Uq(S+3z>a@9kd;jmF=*@~kgE z(=a6cFCW1FC2qg)`bjwtIcPG@VqA!cGpVx}X@Ip>YW@#jZyi-tyS;x)my`%7Esb<{ zN(u-f-Q6MGDbgXG(%s$N(zQ3;z3E2sU7T~C=bZ2F^_+hk+c9+Pz1F(deb0H#IX_ng zU7c{V`>$@1JWFlwP5VIC`cP?D`xJ0_dO(;oZxZZjsn|sT8B@&WxPZ*So0I)9)iC2f zud{!BO|%gIMZbcd3%4heNF$NbDGSDc7#r@&Ymv@q89bvOV}#ooYX`gVE_V;pFLGSa zRGsa*IPpIbO8Mx`P|pn1;jNsUkXQV}xrl_m=Vvx$u|`;7Qtj2D-4#Pc@7zWo=P9w$J^bKVaw%Pi=h*vWto2Wo z>p}~D`c5 z&H)#TEGB@7Zpno#V3QF?a5+ez^1q=+fiuIm?L&rovSB^g^n=N!S}{oE8#W=w@C=mH z6jgQ#Rkeucd0ct?Ukdww{w6JzZgYvt{iCPfwUxqrEMHo*+T;xPO0WNa`x$t8V}^{+n4rpMdc~6W5*DF5q3~KGnDvJK2idnW^%^?A68E zHsWjFIj}Mpxr}^#FwTod|Br4q4|ccX3w5e#|NTa0xFY441*_GLnp>ov-e%17F>>NwRLp5r$bV>+LaCU=QN`s_!DVnV)C8q+@>l!BC)658z z66O7mz37J!JCY%Yp?covP@_a3dj=lbC8Pag+s*bY#ush}E&3bto%b!7U#;!V&a=Pr zI(@n|UR%4cBdjWFJW}&sXx0_EFKb$FNP44H)6DyO)wSKSumN*b`YrSSTp{YIKGkx3 zb!yzNhaOHX_-A&DJIm}17rND40vaQWLtY~vkR{y(j0e{YyV-;e-JuCd?@~&ILfIxbJy+sq+{(U0A!Sp;x{#f1 zTR3w!B};cMGO>v?E}27@M+Z$+qArWDXw z;dy3RW_toKvu`!%rv}mNXC)s_q8M#8v^VV#YlV@cGaf`5Mly$Tb(fb2J8_M1*qSU> z_$A$Nh})s%2YaciVjZo{&c_4i!@_F+$05Mx=R+HGE$%IVgq>%JbAzx zD9%vCEhNM?_K&Z?N%PGXynNg0!E`g6Ln3QT<&(@e=aL;2-lU^brvR!IA&&0c z|GzddGWm}%^m(%ChgCn4ip@UGP8$GyswOa){3qnK6?J|2n+i;vs-kcEH4w;t7$mrm z_*i6FMZAhY>M&dlX@+Dm*6FSv&a2|MujGAwDaFqE-%lOjr67fF8-257h>kr)IR%ku z(ln)+HY2pvR8%kQR%H31?a1rWBLtESR8vRBvi5Hu#-#bu{al3*0{b;Q37Y$( z-gty7(XJ(uXg-oRV3`zS&hZu0;A-nF#i?vxH=iY|M*ok!u^giyPh1U+wb%q+z1Vp& z(DbKmEBJkLp1(~wejPCyq4pWvVGDZhSh3xkl)|w6Tlb)RDfD~ ziz&xyTE*0TutQY9SItJJfab?F-P*nFe7FPTVHROZ)aJ3zfxq9rmOP0bY&NzQn9N0; zf!6~(vi_T5^UwV&IY}s)BZRrp)wYyP!8s~oh=5YxVd4IeiTKA5(j-e8oC#Fj<#Bm4 zjqaMp&L?OEL((7CnPlw9@3wi{Bkaz5+Z6frRR+tI`E%#>{)ZXZ3#3?5l4J{0D&Ah0 zrP_uxBr7nanlx6zoL=Nmuhtc5^9vJ#cs0AApBWWp7m^QL%$7p!!j#@t9tWExj@St; z0_q_m{{OHMB=LO-yYANjR)1HImjt$U`cy>v9&5%JGc=z(&e~GFEz)qp@&&GrTJ4XR zD3KrrjInnz2wCoI!k2-ES3}2vDOCK@A7l7#|K4)`@L?PgT6w`=!MaxUK*gz6WFC_zYVz zigc>6U$rzVhDJ!BZnde4C5LilVWC0YIDmesP;RLxZSbLSNsj?b_XkYd+UwRzjFXe8 zu0;P5hSY)*I5)kWt43T<#s8gy`Om2OhfpX6MtF5{ccJpnC?`DXGT%=T4vqIfZ$pEK zg=I#~&g_;l69QjzF@eo{q47~aR0$i>tY0DRnM`}ht%9Fj6H(MeU3XFL{=PZ?{}1CX z&}E})rOP*t%0+RvTNgOSQ(`&NMDx*KD<3sd?nAa4v4<-qh*#Y+e(Vp0>^PCaHZbzt zp}cTv3y-DmA7h!=nwI#V`9}e#FZqMwEwGD2TZtCk}bWqN0tC> zlCqKDwx00#ep8|2L0#U2%6H`F@60^mc^~7GT2n!g$L2NRM?zY;HBUElp2Fhxs~=;@uEYYmcC#CZY+q^8wNX-r)s5ZM74Z9iPK( z$sRb&CFFT%9ZFFTnbe=Uv8a^d#`5{Gc4|0*8i6*94|Y~>{?EAje?*+mIhOA-26Zm) z?QgFZPOEIo6dddDVF=AKX|l9?5TL_xhrD+ZokJxn;rgq0pl{T zB(nT%dcMi+Tiln!bqUkwgwmjBLfRBF#NIpg%;gwGOB1<-`NF2@IxRx;I5#!=3#YLx zGFEum&v!4EvoTo&n+vG0hyn#g_{0L(K(;9Lx`pBb-?qMrX@2 zz@6)UbA@Fjr;fp(e1k9JC&SRi72jL?<5v;;byu(aK8~8;p92-@+A4$6CWEC9kTzMl zg24Jm>EpO%5RYo~w1thsTba!c=yJp00$L`xyt-1NOT#-6d#%>(V@MDtLXk>@>vtZn zE8lhIimI}whCf7qg$DKZj z>8{b-OY=H`!&v#wP-7M8aOt9ctO@7K1BLUtVa^%H{UiDQNN$;*ET`19GeJq0Ydy-h z?)}llYU|G6gfwSdr;bi1ecPW%TDYU@;FnE(9Qeei=k6=F16u z8hOK!>sP0m!-z#KAx)R(iZfK~&v;?@_Y8I!6vVYdh#h|5>4-*fl)0$+-F+YR^ZA^Ed+Z3p3e9+M4T7c6Q`EYi|B zU1m-AQ#_WTgyeATP%K#9Ahy(SWPPdFk5bAxEXuCA|3UdxNF*oCNP%z^(2n^4TJAP3 zQu~l!xMYi(lIMiaCl&sNNe1;;P!tt3^P{fQ+}$}JGFz+VePQ1`KMyL4cP{%Jx}Z|f zOGrCS>dC*-CaOo}x=4YzD?AyxkWh@PqMd!T!lv_6;FQa8yCw$oVhNY$m8lbS=4j!u zAm&gwPB8+dYmf#%ZZ>_(=Wvv&}A*yWdc#lkc0<)$xH%W5iJ)$nM4aU9Ud~5zNm6V-`%KO zH4!g*&PmKixpi9P`P*E5c`OELAQhDeJrE;**l5IQkue!_1hXwjESLz+)?2*E@>Jl% zOn7STcYUIEXR^?~06=BiU%K%j_CC=KZChqX`(gKv0%NQ<;g|0Uqb`FPNMvc@HY6^= zRUt15^P*LW1(p|)SjX8{5r9ZDumqm$&m;WinrG&>|vUobJ zJFl5uk>Vxu!>ny!qV!?rbm-yG^rgj-g13%)q;vM@d5Rxv3AiQMHjTEO~mbB#`4t>mW0QX6_r=O*RL|{LWT?_ zl5F)R1dplmfn*@k?7HxOI$?y;DNe>f*M&NzE!y<6Z3UNa$Pta;q~^WB~vXSse0sn%!{*~d3cHM_HXz8TcX!YM?S(%T3E0g1)O4#bLWSPAZ7_vN zsM%ytdpmI^SXh>(1vyyy^y{Hvw(}mdpbjS~$9b?o%v)?RWYvjZnwXr}k;L-SaaPrL z@$qFA9QuptWc2)NyPfelT(l;GcYP#54VrZ;IBf=0-AKzWuxUBqMaH@U^JOmlzss=y z`N32jdRFams*O7SgIsl!#%sjMPXylnw8LC+Bwa|6u=<*tN0GWvu}3&9)|KGl3pRGw26w6Rq2ma$oz-#BOOVOiOmLXV z)0hF$$+yuEHfzySk`Wo16L{!7OpmWQO;=h*v5{F&?)Jya%{$^X%S;7z3MT4J{k=7@LOdO7mG zPsBeC3HOVAKXwp~(2)QHsJeJ&uAVFh5c4cCIkt)nnd@4I2XPiRbgfJDI0Pb6UnIX<=j|L^9^y|UK#?b;#yDP#E@OdH+n(zqo%Wh$klZx}(;ydl42z=)PgGD#tF!T*J-VtWL$bQe1 zo#Ntzm2o$iouH6c82n%Ev-&<6(=0L52-1ucPQvzz-}`xS^Cm|j@HTce>2tQljoK0f zJqpe+0K(kKgxuX#Cq+=$2~9L24%XC?73~WTnM4_NeP0$V6r4bnHpMoJqxmMinE<^% z31z-u@{^QY;Jlq#0lR%W7p+odi*@w#>8X91Yh_UG54W+Z)TG4U=yCawB+UhjG*i~B zrPb=6)p!CyX+xY^-?1HLeqd3pj)vFi(p%9|Q=v*8nYi`EV7MM<((wfX0RZ3~OpMVg zJDgRsYNd+vTU!wVC7P9}|NbB3}uPdugrec>TsC zwbGT&YttM@k~OfwNQ6k6!9KgWCzWb*&XU)TqtL;c z;1(S&s?$Z}Z4rVWwwoW@>xB57JFq(fMHoh9l7H6b1iz}*Z`1K+UQlq|h55g8b$@Fv z4vxnPGi7Z`^vji1wYm*WVS-iiRY=gUW*&pFBIJ*gC)9~=E&|hW`Wu)ZzYdiGbMm3Q zDn%B`%UMlsPPy17Zp0)%Z?IglB5SOD6^d4`G)QU}IR zt)+DuA%lzW@e00m)9eR_@_JTJXMekToymk8wqMzQ@`52BcY`g{axHE$+h_sfCvuWv z4v8?ULX#p}c+al}L(cD7R%qwyv)%}DA*PMb_@;0Sf0?0c&$WSCGc|w}T?nIqNE)#B z`tfh%x$k1#(t0`>e*U=vlgoKCMl9R5eyI{Q^rl_7{9Ov1uD0d?7N|>3TvG!TE9G9bjrR2(muRbXu-;=)SHMo(+V z|3Lb)@E{NGiw|DRO@pYaiU)7Ah6iscr&fOZQE`>o+u{j?Q$Pa1x$?qd+yIzu%Kprq zIzIj7kg)(?Wku_ovYJu#s{+yDi=o)b4>7YDVRUkwI*@oC%zmSdahK1?`GK?H6Z*&C zsQj=0c*gMgX+cNU;a_wx2Ho9WEVO-vxghU2tuaiYiy^rjFqD2PmB961gnDW*-8C=; z1dNIv%VN>yfyge!AcF=YKJGipqC_lwmbNtZ@jb|7sslzd3~h@9rba`%O-JuxJn9AF zje4#$g={^Yav!}8U09QmMV0^Jm5dijY&O4X<5hg?3`zbpMrG0^TIYEdBgW_bL=arhQ`Tbfe}{OWx&JD|-K^P%rHJ33{ywUXAXo z32`?%$d9^*+fMAVdwagdsNYrR$h zR`9Lxp!u-<$L-^X5Ii(354fnALm;}d3Qm^5=w-Cu(x1mNB*NQ=W9(Qh~l{q+4&ldwsPuVWgjgVb#;PMsi4c#DELT(|&EbC@k-_VjByr<14L5 zdlv2ZBAH(m&_c}tU5naIDS>UiBO~B7VUh&e95U{;cbKu}ISF!|npnZhB8fqIu!Su; zf_K{@XJ6p#L5Sfa(7oX7VmK@zg#Yb?#G1+LtriD-EIqIWUy z;a~d0cyos5@AA00d}opg_eXFPswGq`G@CEH(M!_hI$LZQLIWar%DyL04Rk*?Bbp~yV0Yu|Bka%%Ovwi0y8(D*OLPefA7E|D&b zm+#IVCU0rKk&!(LQ6sB!K_I|Xc+p%`>IttpXU zYYKTkmwDom)Th~5RmH~fcrMU+##CKsBnW9}q~I}SoGsX`cKDW}g4 z^lQr$5#2+gv48!PuqnZ1d7F7tLM2Q|XRJ#8D!#OXQd+H0`Bv%ETN>$hM_eai1r_8&3X5aN;tMNDBJOi7{Sx8dtpTzktH0<`YTKgn4B38>ggCHHw=BME zTFW@l?lvx%yUiN<^rEH1O7Ct`^_{8IIVZnFC!ct(jrbr4Ta5P*wX{NZM?j)=eX#<2 zC<=G0dvtm&Hmb@MXiy-R#wS@H5rw9xkokBm3#EM^Gn)b{qwN8mjW;<-77Fq2f>Ztp zQAPP{bNSE1L}(O|ullez^|By-$@VoHAzPEg4~NZ$Zx(Z3hron(=Pm-(m$~tTtO&+P z(<13~d186BJNXMd>(Hedj9m?u+@DRDDest=>hxHkyAOTrFIW@&8rQPn4p);xT5oxN zwfY5?Ax!>EHB~5KI@->Bv+k9wm=MqKTg}6p*@))DAIr(U;CB^(^RQc)fzM6wCD zkOrnV6v4m@GV^@NuXX4zQpQ)_J2m~>w@d2FHPv3)&f_rrm^(C)<@2J! zH{BL(CiiFYYcMV$P&SsA4S(h6=)BPfzNO;*Ljv;4&A`Vnc`BrTd@T&<%@sc0 z?+6zEKHo{X;^E<$zB%0h#i?kkDf8-m7tR12T%M`&O#tt0dP+{P6rde@)5Ol%kMV;J z=BhzRRH?ci?aTV73RcE%X)TZ1?v5&w9v|+CvE=xe!^kSkLGLdC3#9L6H`H{u8o|xy zqr4!X#ko7{8DEpk<7oay4p}#R(Qb$ylAEBdun#W%vQTfMf<^U7F@{P`x!k3fcG^Pw z9FN_@Wltwvy#jDEsUW`YjNCS1{b&?PjoMd!cx;Rf(*WOXK1-hButP^&gh zLea!5%PsCprUtj0uapC0Q%V4P*m9N}3=J0LM!SBUhQA)cwT{ScK2055WIpO1e+P$} zkc=?PcQ!x<$xE?jv9JR?Ak1$s_ROCit~qhRm%ucSo^s_vh2FO&0ObP;Wo$DAlchO6 zqll7OO(J*zN3Rika4Er94gm7=mvVVr=a%_Nl^(d;tJ+hFm*#vCTVwO1jfQ-5f zyJ>(@v=2Z5Nw@{b9_5eM-cKq^0BJ}pSV+iy%zZig5x~T>=2V1)q`4n)lT}IQ0#kl0 z`k3ng7A11}8L3+9bg2Fd?E%28xVOF<4y_F}=>}j-_KtH#2{ecXQfMNV&y2#$Jvnc6 zLXWR?_Lm{GNsc8L8K(*&1ONubqU@W{Gc2%AyFgg;TZ2l(2XD{J2LNs)SGo??uu;O* z@uPV<(HdH|z1Zf1u{4EwD-TD`pl6Cr?lu4@^Hyo_sQ0x&c(X+j!R@tyzfvFo{Nf@N0GSl6snm{V(RD0Of2{Jg83YsUV zDtdgQ}pRP=Yq`?Renyj^L9kyWOVb7u+LazWg3f;CC=tD6Ri{ zt~)uQWAc!`u6c0|6AwOfDhP!QKigKai6a##@v4rO`@FcJpDh~e3LMC7G;jQTe0-Q6 z?^IHf!GZJ6cMwU46Sa26-&|A$dxevEtCF`>rWIVC4?evo{-sX@+o3D9o=GyMi5nrK zOwG6Pi%_6dYiNC~!o!vosKS1E)lt?DklY zLuRb2()q;9Gb4T52*K-1R$@272NHp+Sp!f)`~8-Fp3+#cgDe}wtCYtlRqd5fW5f#Y#L*-HN@DS+Iam;z4~1W*grho73y=p--ke0vTgyJ3EaEz)M3<#AAgC3 z8HI=?2Xu=}(~2+!3MS6^y34fDX7|dLIy1#?Y7LEuL03s&3(ghP<}0(kvT)+7A=Sgz zyn|%Yvm{y$N~edTI|FF%VoVGd$iH)1FBGT(d1Qsk3#yY68_Mn^44-o<27j}IbsGUq z4Nii1Tj<;~!l@ozn;|OYLD5$QKRQqtTQZ&jTaM==oK#>Y)A5Yi&&Xw%PEKh6%?tv( zR!J$TG+_oTZ5O$WMpdPGm`+6L0P-C9uo+EaxIsd^!4;$G*$sgjQ9G_dJsh;Mryi_( zlyJJ=<6Nk{C!10G&~c3^ zClZAtxFmB?FHMNjK6)*|$K^2MGHE|U?;MNz(nj_9hLg!jJ6qjH8aAm=mh?+QJd|m-)hymBoqbU8DXc&f-WN&teH(cHXU4k6A`d}qwg--kyfW&ON zzU{K~gibFEi&rA*m?#)pW3ir#p_4z9a4%PrzTa(>d#9&|Bh@UTI@{TeLdNJ7OCI_L?q9 zYdk#^W0xi)$y>?a^=9vQzn_Zi*0(>Da$ggAcJg#YF#yA|?5j=BY&Qttb&;8(c=)NFN*|LI}wd^9Uwthiz`2E49k$rFK*S0#-@jRYSK$4^2Qnm!T zjFHZ1lct95ebY^htbLIY=`!bsVzC*j1Eb?R$vN8|}W*r%P zkN3hT-noe~^eRF`SzAy$tM!rj$Yop{%AuQF$3s0rjS^?X{DVs&PVX|vOqSnCqx!KB zmnF41(duKF*ETS#RPkY@qB|4;mhXpTKuoaE?9Z3}z3{Fe2WwtXNrGdP5Iv$U0$Gpw zjPON(>X(l%0}?#|0%W5Y=>GS4xFnu5-78^yQ`+JNjhv#Y>Q6j-ZhYMxx~Pexl9(%=ap;l{rU@i|8IZpii_#mFxFrjKMFi zv@B&b32l!91L9C6bm2wZAp>|GKV0&gaKC3R^srw>I65PEGOn2t&$AsIEk?HAm%ea= zk^iKD>6LzhPvzpe%@cHZ#xf!+aZ|Tojp7|6cJ_5sF5dBbB+bliJwcA#zfXBQ30-Mt z_YJ&qr=iS`+Ty*w92_lAKUk0_rl~N_P+UGfg0<*t+6?6{@Wneos#82r_%ov*Nn17s zg2|rId6Dezu!Lr{wUd^z2t_AkGrYrx3#{RaDAqWIt2M`DOi(P$EHqs`GGP%JXwIY* z?VMh<`}?vws&%!9tFvLxhB(-$$iyW*Tf9LDN1vg9kvyemAg)b3Uf`SCy82ex2Ov~J zvcl-Aq)=O=NYIRSnHG+(YOU-t7zofHKzxt7@ye&e8Hfa>*qmTx5))s!99rlU z1O8E9CR_={=|L^`!`e#wEapeLb@JP98(b%6@H?lW-KTFPyR9$O8|-F8vyW%gnFc=m zY-qP7P{cv#1RO**kFmKT%qOzMw(GI)&t@x+cswNQQxm@3kT9pQ)-T$HYM^%iEh*EB z>YB)@poCLeQKyB_8b@>plsw5xeoCqEC^B!k2nK{*CSwp%OB~;8Sw8&sG!GwVDsaBU(k{H z$vKO*z)wt)gca(*C(gc}6&LxXExA~|Z~I#Z`E{k)2juf%Exwj2AGJKr`M&}awR@oET~F>z%fBs52yt|Rr|HJ~+*o)N%~h1-eW%}B zg#!MSZtq`iP0p7MH0vjR5CmpJ5DQ*pxYE2uUX~2Ac|sql)nj|xsR$dhlNaiV(>`gZ z2KiLgxt>MlYmV6wNN>wgyZ<%Mxe=||1puRtsWLwm#H$)(_F)9M zVmrkK&FZwJCuwS{={er4B#mp!2~mp1p!vs*k$odFM84*6W|#WJasoJ7P*Xn+vyK(8 zTY!5eA8N6OWP+b@=H_RHUa+)q=rRA2i!=6+RTF`z}S53u+?AKa_@=v0d59xce?jG15eVZGPd*YfEqC;bosx1~fF6c!$>0 zjmsvc1i@!v*&^X# z*C?Zzs9d2&i>!%ji+E=CY}C>6{#LDs^BJW{X_8dV<0c?jq}JTxxxYdbwdeV(ya|*v znQBVEj~(HlmJCcMq5j}$_f(O)j#!1Yer zmOV-{wi-sPcAiq)$Wscm?A=<-l!deF93;JErDI($=SGY_!sJmaZr;mF(a-a0tsK@C z9n`||5`=iRU+EV#G9+J0QaEF%!CnD=JxyY6wbBIkvj5fwl3__qt*vs=y_LCS7n*pc z{e=x^=|=Qd6E*{C&rp(bpjhd#eAOYb%uDRM;VWsTxdY%T^DOBis)-8W;ODCOn-^h2 z2S3=eTu&e(D0{XyS0mqN@plt3Jl`#*ND2LvLUa6>b|d8^L*{FT_-q@9jj}CucmuuY zkAxLH%#!=t=%I;KNr5UGpvovOMhlLDid9rJRp^m{kk>}(*?Fw%C%6~Z{+bKNW13}X z=*_`up9B*S8kt`LxpLrGqk35GB9Euu#OUt*{Zs{TZ!KS~aqi zK4cD>fjqzD?Lr?nGt_z)`637)VU*u+{pD%z03=N{F}!-UIU zetaN@H?)V_^5`gYAyh8id4YbKmUc87^;)kDt3Xs9oocx-zrOS1?Zg6yT3apB>bJPE z?kKuAp7s(Q=iEzh)?yH5+*58RV@kcPh$>(Zqm8TY4uqD|fB`XaAB7K@J(24sW^mDS zYAu`0MiWK_cfvda*zc&Wb$_4j)eha!RA^S4;;<|j5d>c1^V0-p@6zAbn#!Ll&WCOt z6TaYD#UR~4yV`)MyxhP2-87lOjub}7RK$*;yj>9QxM@MAkhEme;EE?$RvsWHtHdjB zqs3IpFNM28oHKzOwjDyuF;gq1%Fx`anLdhnZ*uSrS0K-DoAav|qdETA4%+#uB+kpF zubd}7U?K`V5{sKz*9atP3P?jtynnUg?;Ms|$suV$b7l_k+#BdehNny*DshX)w~S zVM~V|(cJl+?bOzC2*}(W+f_juQue1nF4=P61VAba0TR&bczk|0hd0)S9mF~|;7T2E zd2`f;WnA5Zfuh^_0`)zrkFpvz0m8q;*4M8R*(%OfL}*J&UBBSe_qRVywvd_0j7{Ch z;tgMd65kfe7MC&XLVj8|B-1w^HeTlCc2f?ch}ukOMjc(T%Ami#6jG^gC&b)4XnN@+ zi#{{Mp_q>UTqim)^*NFbJgJ4#HSvIXhefV5N#)xnstZIkm1|`h^$a+SRO{(q4xblq zZQbRa`X_gT{9>>wM$76X33)|nr4yHuVUl3Hh=#0<9svP8|U{@w7 z%%Gsn?(d7#u^|rhkB{? z_T%cp7tym*jTMG;O`>0~teJ3=jY)+jWnl84t1(kBUJm{w*<9d``l$Q4F%+cb%1$8- zF8&!!m?)EC% zA9|E83_{;1SwGtGcNE+Jt!@b(+sB56=elgsti;QIXbr$ERUaOyer=NY>|OO4P#=+} zrl7U7w9M3$WUs|ma#%--=XHP5o*x&wt(0#G-~-@`?&31(Ta>tzm5*zH9jZ`cf0gdR zjFz+3+y{TvBYV2$S2%w+v<=YvzWs1^eH<*_;byjfCeF6NVwHEABEZ%rRb~Xg7>Ka` zm@@JG|p z?QP{|A;5rwMas=`NUnQ=c~4CsSKVLNQO7x2I#u3(0dg1~qY>0D`FUnx5Zv}j_v&EI ze6~XG9Xx@UEublUKP~aqYIZ8rux_LoIho5~U=vzOT%wL-;D5_})4 zb1`sB70=vW*GVAs*TJ@7yH2T_Te<-w2_(!yV*UY=*2|)>F&QhmBvO<@tN65tht&C0 z2sS)!%^30~G=W!3077M<#Z>U3!)w2g~w_;Lvyvm>gGq5dvfz{TlI?fw&tKB|-Fdo&v6dY{?OT z;1}T2zik7&Zl&jASvrPN70?*HwE-i?zTx`>n95-twYnvLE# z#!1eFypfO32574F&)WE9E~6(+yfWR{RQjXhlEaj2uT0|KN7?u$tTB3erZL(&!>ZP8 zJr?oOiA>Cxu|X5M_3BP@(zpDAR`KDBn77B-uvL#5bj4bD;+S3GyLh|RQuD<%SW?aM zc9+i7@4)L%M6G$5OL&2-!{=Lew22xr9lIeD;@k)-)NAjglIU(&-s$C`k>|b>G!t2} zdl@4{`}|5i3){%VF2fKK5^n(V9+uR;&0hXFTYT%z;uzz%4d7>|+8!$QTphw~JNcf7 zG>Y7RO6BT!#BdJOT4h=RuZrh%t5WN%Om?2@p81wIOole_0tko;0KRuZugt6SdyZ-% zskQQ*XM2XC#cVaNyM>jaXPIzq>*-2VKX5;GH?lnby4ZUWLqHu!zvYaH_MT!sN?K}Ba zv5!{k6m1nzQU#GJgU83eyU+WD&LPMK?||ve;%-1i`O$T10kA?Zc!!rF$Aw>b162^L zyAA_p1ullwXXEdrXS951?>S!MX0$ZRPooHvP6WXl;qy)bXS(O>Uot)Fx{)^lV#{tX zJ(I@fji%C4+17_c8}#!jXdpY5pxXzUPN=!epLur`WWNEzxF+}xSq4jHHpWKHfu;Mcm1PQ^W)tI{iWM=-1dSK4U$#=Z0nQ7-tUoq z>-9R%9*Xbz(tp$e25{K-+oB~b|Do!obv$-&c&}LBLMd_i+|7)XR~W7^#P=QgM7dvC zG0}6mnPO!raVW?QPu5Lnerp}xt&X+rmYh=S>tq#vJ^eJqb)<`{$iVK}G$A(Za8&LtjKx`ebbIcwoG0Ce`sc731W_ zIojQoC%&8QM|S#{_AG<*mZx;;)n5YM$tol_&7LbQuGL)(2gPMggR;uAd!f}W=Lf*a zn)eAXyQ>Hl1PnCvH+&NL4S?`PkbV^mh3_Hsnpelx$us#|$Fm4n4EVq5m{E#N*%CGG zE0?Mp!CG?C)9~r{AUbZvsq|HyOeloa&PI{GK^a`Lbh;+X<@)B9?s*jqni-JfKUN89 zdBo&w2Wn8DXC-)AkhK$ycZk+ANpY`*)9%y9T0l;6FMbwfkxi1D(HQYgyO6I*R5u2G z?oH*>dibkoa{!20Z7slsW^gC^<{Gcw?%y!ie&xICi1%_@w|7q?TbvYo{t}+as z+e3oKkUrHD3o7HP$0xq&;o`KKe~sTi5Jd(81nWapb#1al6>gZfJ5T zgnI{_??gzIpuc1hs6av{r5Tl;3%z0EflG@nzx?b^Y}ay;{e=!jpMYu;QT)iOj(){T z7$RQFUeBxQbasQzqlLmR`N_A8Z8!StfQZY?EFfwf6oKK~E9~=pN`mlLZp5q8DlG^+ z-GTlukRKe%^a`MRL)#L!yPk&Iczh@t2-g|AyM!UW@kx^YUHzI7=O0Bw;1}C$*MGTM z@%Co7T^Idc`<->@lzHj*`Qa$ZeIC+56;VD+M{CmcqKv$V1WDL8EbSVrxvJa_(!G+a zN`rp9D;LUVhSWdXR%{4cy(v#UJ{Pj&qnKp?i^p@Z-F%vFqu6(q6tPCFrtPz30%Ss#9h_=MdsG%CTI2 zp6?N%Sv9STvv|Dv|M>dKfT*|aZA4O}yOnN`ZV;peq@`P8=&qp!q`N~}y1QGtb0~?S zo1xKnyzl>znuhO)oAWX6m1mH zBr7$p6B;uXr&QQ@Z(@%zU?(V=h;AYl9>^O+8sqNT7{$`K4omqEc8Z#fu0fB8iDH)3 zGO!TOF#X*tWPn$ZOki6#I0+V->Pbvf);TVhf}&6{G>`W7)6ph8W+u;H?W|>rqPBY~!Qsml5V#9bI9Bd}iN5e}2$ZBv_S-A_ z&pq38>s|+x)A`w;qP2$8;C*~iFD`jhp@B7R4HzwN)tfR&m7oWcmIPr@0uL2!L1@*0DE+j~_GbG2 z@3?--K$z~J>xy#{pCy&XbNVSll8bd8IxO(b=(As5`xLXuq+1xpXCMSkl1MZ^0}p7D z@6#K_+S`*~KW({OkC%H}mA?>NL(P%^1Uhq>#U;|S$l?0{i_-{4Mj5uL?Y08NvHC#0+fehfEFY%n~>xfY+Vd3&@gU&sP zsUbnUJgA6FwX=&K`7n%WSNB$*r}WBTrM`NO8R-9xcx6S&Vz8n8V`qb5Eg|xrQj_3k zD9}M^LMrSdvf)Pr<1~M&K1&n}zj(KNAV-ZdUE;34(S{%xii3wj@_rKb7@_~b*=gNe_`>H1mUfuFk&&vP%1)ol8TTaN=m+%KgWYduzvha2z>R{i?lY(>!nae5 zKZU+L)cQatC5QW>_NU|=cpwpBp`Nq_)n-oZo@PHkA?NX4j`18x&Hr?;qE#UbncYV< zYg_bUgL3wf?thUYS0CTwu=rsTYeHe7Y*&9g?rmR3Bw&-n!qfY()aYaKR`#MzHu3Wh zjo9Dqz3pDNMDRa~SsP<0gD*cDf?dLSW8ukVdu^mccA*T8l5~9v2by<311e`<9z$`s zQZTu<+k)C9DMj&#U1Vi?U--^5mw=6KH~J5K7C$sYH>o@Ap#?XR;lF`<#6);a7ht;* zA&FW06ZrW+L41uInN+m_^RR~`3AsvOV_5Un_!lim+Z<%>z#HBt{V}p;@XU;!V58?7 zh6kXplIKDXa18nO>sxN5@-YuwWdQlk<@8_58P9KxrA-M4xUEQY{jiWs04*n@*@Njm zjsC)>!{dEH{_OENBFrvQj)>sl#eRrA;_Cnwzs`VzEY%awtuS@A;BV$vm=;A!h$5{6 z0y7}9jF=C_b+PqUUu+pH$|i9mKh1p0=5XGR?M{El6Q$(e2%#Ja8WK2}S);t^yvME8 zegF|G{A{CJV)C+@;QzU$0gyK=UaG255#l`2gA9l~=m}jvh}D@-Qq)0fkeqdOk7#W{ z1&0IG4(!W1S%x;5MR$f6g1XKyib(f%xHsLCg3;7QUh#>Hi7^E9QWFZLucVm{gGZ(1 zSk>__q`%I=RcaG#u$Q;5+u@A*A(EMwx3jIThGUNRilg!M>a-OVE>|xgquJME1+Tq| zPI{FPKkk|0u~Gr0E$5NCoQadN1-E5BzPB8Hlzeead@E-ZYpqPA7x+C`mD&fICWwCZ zXVJVL9)(?ZFXarhKU@_BT2H<6?Q+eT0!Xuo;e<&!+PRGUYpYL0g^kr8!%Ga&#xG6C zshQvXJO(RY5f+tVok@1(`SqIU=et5BSBPv-Vx`>3 zT9uHMt4zt%ys)UB&3n5Qe1X(%!pX*(?~P7%M0w4itwT+Z3WdQ#?iXKaDXpslvHfYk z{IH=NgJuzI767&LleNvJn2_dY>VG!8Phh}8%Q{+#xGYr{xV>@DHo{odHKTlcZXfmU zrjokPk=7}*M;%DiOE>@0BqkK6&}Utu8BkzaZOu=qaUW|(ce6hUXVE{sr>}39u(Z0e z=+skc8r=0KNrK8?HY z9HR@935u*M_@R}AvwS=0*bl&g)Ce*mEg&=-&O+DoS5^7kYc|i`m(D|t>VnFY6SlDh zL!%Dej13iPBHn)FFwQZ4h>d|`Kb%9o7yTjA_#|~XTbe=rbVTOW65B;Gclm30R^hsA zZ`+}y)s!~QT1rFqq0%3uwsp(o2BC<{W7juy$G^#mH=cj!1}cz8^U!QHn$M?CedX@u3|cUccv+((^OJQNTAhMO<%A_RHg z!JIq0l(y)ksQ&G8wvKg1%#bu5Y2WTBKeF`($|qBISH@=<{p{VrBD8J18D$l<#o!I9 zLs_$3;O_U!%uQWPen`irT~8EF6yVtd;6rH_zh|94}P$q+57H^PNS% zqc&~On!6uR(!EV6TrUQcH%VsLbtBub|6A5p)}SFfZgy`o3NSVaPlU=(j_@3H3reaUY^;x@~h59TMuX>m9G>Mw*fMR*?T@c`T2vO@)L~j#eCfU zAxCL^3Co-(*SKemctpZ4zwYj{t6zN1h(NdPWecrOfyR8Z43A%|EVQ$pLOyv5edTo< zjEf1f=RllY*^fd_f<|Lm%clQq-Gq8b&}k0kA((_@9w8Fs`(lprM2IUDx@BFJr_cl3vE zCsc+H>o8_?{FMr%KJgwG;VSoNFC%9#zSn>kR!D!{H$K77o{5RJVclCoRx;GTz#G4KG;?D^&GuvIr1?4kx z`_8%B^zLFWOxoKyFLk1DJ?MHJN@_M%Be`*NH*=CG^t&g0{p(1n0v$;ZD24?CvrUb9 z{@?=m&6Fy?Y%n9TjxScf%l7PCUHskIgcP63+*0B_hr7dY8T|OqJPBoQA*Q#mRM84B z6f?oXm)TCw!M~j}(F`O}-Yfyv@}~V6s37iA%W|a@TbMC*+YSEG>5L9n7Y{1S}9loDu?pO&2g&O31 z_KS7Ckq_({7>%I=Vts!vda0zLQWhk+Hy4BFB49y|E16w4*tiozH@qxJyo_~=v~z)* zaQQ-z)DHaGGDHhgYt`~Q1xDVFz&%)NYxd`{X``d+3@CYOSLq~hab^t-6lD!s*kQ)FUHV|v|%w#t@Py8Br;}_&IVZWRUopuJ~Awxjh&uEMesqpPf zx#PiLNrv3b5%Wr^s=>C?FeHdaU2@6vP9Ns$ywM*rio?KTak6bt7<8RJboqqoq2y_Y zh4fBO)qrH24P7+;>LvSQbo8JENyk|3WegLn9*vl#xXxj~PS_W@)P=6=;bwP>A07!Z zfDW$dZLs<1q`m5&O<)pB z1Sz*PyA~-5{v1ujAAOx@oK8^jY~5j^HIm4=(qo6r7RLcNQK%m8JQc?|Ze%)KcYRR3 zRHE>CAp)dcp*usLWNVA>#+h}K5qnHVa*)G3iXQ)cjSyVktIu12P~c1K&5F3*g>Ek@ zA?>y#*c(xoLZb2#`?Ie-x1!O8Mzv_Mn&jQV{Zs>VRs(C?-PBh;^*>a=ABY2*c0fWJ zA>meQ11=rRc#D7HLdVAZgyng-%f@eBi!S3Z{CZR;)?zbbMPNpX>^`;GX7T7t9ginP z|K~Hq^iL1$uPP^hod{SZdd!pADu2WW&O)5aq%XT=!5vf8+X*PROAA;m1!KW!o$fM;|fb*Xa_|N8A z=m#gFA7wvj9!s)EP^z$rfO>mKR1EYI_Mu=rP- z$xrB;&$9^d`)Q4#O}ADL3)ZJpkaA61WvOtHZ;Qc0KQ*9qj}nt8jNd4h-|N^cGFf*^ z9@?R&HBLthL1#IddIqGW)0H}F4a}hm3^pNy)!ESf5Teg$as${Hz)l|CSK4%>1*5ZTrzUTN)E#PHUMM;_NQKbC^Je0}c`6jZaLFZT_Eh~JX2xt~MlZl#1_}I?4+__WBqS{8^gLn+uXs=)Ee9-b- zi=lJFiuj!ePV>Pig7W3d&)DlDiEUk%f$Pt<@}8$IQ0lPmTz5symyq9D28X)0g9FzmO|CvP7(#j~~9(j)B4(qOTcnf)bolx>w>^l3YCWF0v-Dy;6yR+qVJi zD=RL_?r8AgwdmcHvpK-yWvNkVJ+u-WjpEhXm^6{O| zr0t~$Ne9o^-W@^W^^twK$HmC}mJ=1^1g|c~Y9!97FR#|9E&%{dAvbB}YQ+S3#O}X1 zIr|z;Ut#@YQ~i4harAJc1>u^%%V!(DVL=ZcL)yW}PE^W##5mk&DZQyY>Ik8*4BO3t zxP8+nsOwB#`(}W9?$DJwwA|aYDuGIg6cJT^MBr~}hBzMKF}B^ln7yxKQXcca2UJC- zWMSBiylEu}s`tOa+d`x-E-94L%^!#9u;ri&b$$)evHTTmow2qY@L3xBo3UirTevfJ2}g4zjBHlm zWr^i+Bj0|TXB(de`FF3=ZQ9u={GUofwLgpO+QZ2{pQSNk4M!0HD?R2jA3DvDhLArn zIvDI4GzkvXMK$0>QCk##lJ8+^ER<_7vvJXVr3{deZgB0j%VfAuQeg`mBN6 z;;KvD;MiwjxXeGOI&W)Ki>CsD3C$?Gi6 zDmpP<-{UNAc5Z1;rc}$bm*k(w5lm>=RKkk0mhLI)?xQXCf7M*;9VL>?r+k_#XHdm0 zhKW8UgU1ovmEmf`jCfZ>twbrC^UnGDTZKpYU5fr2AH#C+-kup}>Tx0|U!*i)ELI!h zTelOTA1?|%I9qOL$1rlRyMkAnfa#8_FxO}eMiN^s=Oh^V{a+r!%zhTRo%U|_AXW@3 zH_Igr(ew2?WfU94Ms3yao8y&DIuWnY5n2_JjOV;ApoN>xYPHii%j)>mfs0SZ+i){G zEUf-;XPQM9nc`UR1w*oTP%9Z&Tw7CUfNF%%ja6?Lt@$wd(h&a9X`H!Lhx&3 zhX*OHUq{6bw=L2ge&YKmmA~50y~nLT+?>Cv2etW_AA1Kaxh^s{2eD*PPJLz~MQP2!}GnK*S4n4*n_D?t{UXy|1sCcjepQr=37v|fL(H#i~eBQ8|H8BlCfZzT#m(^&PYbGw|S4MU6)Arp?mMQ=a< zXn#G7j#pT((1$WjwXMUzAVVAquU6Vs+(5#o{;-svMdvMW$h3w(Czh%JQ)bSeE3c_4 z0;q@irCBE7$kn3206M4T&@BIz-YckOMfELC@=pQzLskr)!FpbYhpsg}B!!}9T#s$o zX5M+6p^y8}jvH%rbE)Svy(5dWAM&3U7yihJK6aRg_r)&1(q=>H6hxa2cvjebhUwbs z)B)6b@w1X{K_`Dt8pXCicKkzbR8|PyA5=_td@tkw_iKH@r7uyKAZpIfx5du z=f@!Y^P)8C$E6unsRFm_0h$oO%}HqX!QWNdA9^yw)u`LCd9Yg!h<|#(`Z+Y`5#Z=_f9Ke{D#qV zRUZm-J5;>x9?)_0-ND*@^LjUL?N5lKnAYu)#c7cZl%|m5;^8ZTy(-OZyp+_wg)h2W&pMmSPcj zYIv<==Q24>9`f%XNq(u*6GReO%9Is5;T+%H-=jX}+gu&LbuZ>wJm)IDtaBY--2j@c z>3H4vU*lvoZw2(_2eS}tVdvVzw%qb2_5=ANKNT?-W%AEd_2-e7@jMpE&7YV)e(T3U zv24i7FJ8I77^=Y==^GO^70odyrwwuDW#Xl_BIQ4UaI)#?;oiC1RI8Pl3fu`fTb3+z zWI49h8y|k;_`Aq%yNegM3P!Z#J4-BrVgaZoZYdfTW1GuxI3{uv5@r*OaKHPd`vTwKE6vT3LLNq;_ z7`ev)Km#61DT@TDPef^6%tU!uojVq1Sylwu7xtV9G!>M>I|dV+&33TE*PPItit?Qb z3!IRrjyqzbLNb=>=CiJjW~wvE_G?^i-9U`U|0S_7e)$;DvFFDP%O>P}UXBMQ_G@QS zp@@+pT7u_yDt*|tg4%+Q;ETR-#dg(JyYuahx-y>b9muIA-{9;pe$D%Nmj;?o05#Ls zxP<``du&?brmW5^$#Tilm8aItN=bI*HuwotBSptWmp|0DH)>f)N~(QoJ9w+FM>a}< z!C@CySt>eq>&qLl;yd^nr0unhci*@BB6f6icW-c~ERh<8Yl%)k(|{y0zLL4cZE?;| zklZ&#Hbq%!H#G}ZNxB@jieDd37sqoW82hdYtjX$1;V(kI`(SeWxmOBlR3I(|G`v4P zZfOeLo}c3yKi6na9*CpG9KomjZvJ9S(=!F3zYGIkt z6wS8k8sNU{r$gV4)0xXsAmDf$3WT$MCUcgHlzfRIxm@U)K)S0HWgvLN`(|BR+x=H( zooy!9+H=kKh5Dg_G9wf?OtAlb=g*Kz6MQ*e!(Di9O`#0w)92jaQ=uNi8oylG!1@lq7I1uvaD$f0 z-bU}JfAAG0b$WMeP-QzHl8=<_e|Ai$sVh< zqcr3C$<4Xx2j=G0Gr|VNuuPijVXyn%umrLH#AN?6xqcGJUlHYL>4fZ8QVoB3DeDBl z2pcNaL_u1uQ>)Z%y1r|$U3>PCiS8B!5;-{r`qv*O6>0b;%3I6Dd45m|LF;>);^Q~9HnA`r zbUZRyO|SnCW&Qyoe($sM4yn@K;muqTm`|5B!@7K?A*vJ{y`?2ryDF4;uWYU@!oUw|pdng(=|B`Gtk$n-b%y7pe^U>CXMMTyLsFMSw}@b%Wry2j+S-@xhMykDO{`!5&?-CzA1zedg2+>iCz zNck-VO90Qf#yjQgRyp(rZ}MIDPp_^_>&dhuuVOuA$vy@aG_PD8H+*BH)7I9;;~m`M z=2kiN25zmuUH0VCM!Z;S2YR+dn1B8YrvKXkE~vvPWQNnCg6y4? zgut7XL(dzn12_dn25v7K0)Z!3Ez};MpLRtBKa5QQ7s^gQ{FL5*4$jHuKvFM8Em0K2Ft2j07n_OZzlaeioz7qE&-l};FO27 zJLWD{S8(ZvO=UkF<2#_tB4i=By4SUYoSc1D%laH=1RJi~jbG3?w35^kNz!Puk5hCu zAf|X-eG3%fo5{bA&%g@1ViRNB_V3QkAE)+}OOD3z13r9{B9*lHCyPBt@z78<3(BD3O;1N(02C)}ktxZ~sX z$(r`zO4?&h<`H@Z;~y*h#{)*e;DrK{i7^MDbPZ;5p63|Yf^SZ1+&q5QKA$eLyasP@ z7+c(mvgV-qHM;XOpiHQgQDuc2n$J0e*G)09WSgbC72KMmi~IP&rqUE3o}v_y1q(rk6zI%Sr>Q;_u`p@31IRYjLou}koQMCJv)tRiN!7Ima2U}QjI znjp}8j@TW{K0nSkjw@4E_NBM`ypN# zZG`#Nt=o87TNRkU)S@9@I&Fz(UiX-eM@WO{Xs;4;PESv>Sv^hfE@hq%{2S=9AHBgh z`Nc_nIYl=(wN7WQq}iZsRQIup@UqYAjmI04%Iuv5jKcK#(j(ouWg!j^jHj^>dkP(hsNx}LX&gve)sq}NwEqfH-XbHQ`=U6idY{zelVd-Mp!oN1S z1D7`)v^d9G9hul&_)6A(>gBlUsj|jRjAu7C+mW3FEEJTeb*z{s&O6sJ^!@y(aMb#l z;uz~NYEnYW>kGM ze(FrGyy7kQ*(zYSwYjh~gA|NlU(O2OiS{KbSLju7ANkB(u%7iB9fsK=G}V+it!kF%Sgir!BFHs?o86sRIs(74K7Cf+1c zRY-}fVkRunMF(@MrL{ok#f5pr_c3_jVQv|Fjv3e*p!)A*3X%CXhJn^b_9<$1R&4^d zb6=+4trVEa0$lE8xWg_Xx8&9J5@1uNr&g{6A~C73l5CVHHaHn6xkeDH=DRCi?|GDa zBDu?%D0)=WeS(_6mlp)-FtulSV*ak{>MwT4FwpZHfUPT8y1W)A*LMt#`aMjJrH;Uw zQWWifG)_>FHF$)}!$*@5ul+tVk8xk_{-_&Q#cHFP$aT}M}D>)UbPU_W_m+Q@l1S050FR3hTcwL`(HiTfF=cnEOfgu#k#JoyctWW6id z(-pHUid|lEv{lLcWQNIZEN9xYSPtkk=#deux_RxbYrzc%tL;OAy5kHxT|7@M1p@_0 z(&DoNTpA+Cy^qB!)-wejWygu#jR%_rkh|{>=YsGTu`0zW=x=v68n%_^Asy_=!os*E zvq^I+p9q^OOx1oSRY`jNA!`2xR6tOIw|kx8g!oiIv1jvGF}Hr)!KS@DY2lYr)2bT9 zSZJn1ZIAxlZP8G+dsN$Ljkj=&UNzz%ETuLmB2$ZV*k;X2SE1|(1b?~hHh}WfE-FFD z(g9Wn(IqCamgyFnj?Ev+Av3@!`yP9WAI70J#*Q^qke(lL8bc~<_&w1i4s^ZEXgsW- zvIz#yG=~n&MCYEFw+d#2>T?UGWyTBPAK(6mA^JD<4EoYZahF@}IC&=iQDT(qBLV~j z(TI>6){9EDnUI*pK>R*onV6M`vlIKJ~)>EHhUzkT*2*VYZV0KIze zwD0EP>kCZ>H;o5Afg*RQztl5|Mi(0BkqsKL9$D$sCK4N}i#2P(HI1~DcJ_j|kynmE zG{iTvt#1*~y8jvXL(gzQ4Q^@4xTPJLps2nG(#y3t29}MRZJw%AH^nx~Edw-9W;IxE zgcgjaKpV|H+!vC_%U^lbIxd$>bl-eq;$ba+5hy9mq~VHiIPJU`YXxeNrF!bfU|_>A zr6rCu1yKo;EYqr^jQ!7veik~ktnDa*+N^7(2C9=SiPMJUCswoMGV={=?!P~0n{I*R z7vf94U%PX&E44oyhNW1)%jz!{d?nu^Qvg9v&e0lX_d-oUCw^*C15fPaJZiLAVGtac zFey@rPgi59@T!19``GGA(wboo3WxIzSQKzskXKFH;wQvM2ZtQAiUs94N_!7yDp_}n zMg_Mo5mLC4F1GM3qG0Kjdp4dHNx)MF_Y8>&4m``DvQn+Yy>^zISgSiK ztdajEO-zsMI|XjrjMe6Ok1}!eAu>G5|5WZEKrlz%E=Wg&Z|ZuYsj$j( z;?TMY|MX7Ik(hMHYf;CPvJ&tPJWAC|n^G^}H-*lm@xE-!7PWDiTqaZ)kgd;(xl?$i z`I8~ncX`1J)brYcFQ7&RYA}P$NsHJcgZq6nn)zC9=Rzvj#koKiPQy*$={fCg@pL%LA zu%K|!;(!9V6M&-i()MEA^ts@9@8nz}H@_569Uu)X;4gDYop+{hZ@rxElz$BM3P?ec z4CIndu-x^=cEeC@;1=3vOAwMhQrYQ!O1(qfYVKF-TIQZbz$5Dx&4Y98dz(xt7X9+c z;`Ak%uc)%+sf-!(B~8Wkq@GQicRY8x<+60Bh3_st(k0=J@t{+~Qvf}b5XGN^jEAyK z{-$jI?Yvy*{8U@@=BS4Xg%f`6PaOmzZwQ-XJLjtHQJNG$WL;Q8H?ak+3K*ehv}|aT z<2KkQs>LVlX*c8j+^zhpnjgxQg*$c#P&X|(jqfS>^jncIh$U<0EMu3g#r=3I4+Nl2 zQmMJ+ssqG40AAa40b-f{?(j6xj0p~YPBA*#w+U10h1-^HbhQBqSE%%beq=^#^?TYH zph*6wUNelJ8rH{mCE><$`7JoTx&mtY``Sl!i6xA(mV{y3S{7ia)*w)(Pd(^OOU}#W z3HW-&Ntd5(3Op)`7}DLio2WV-w#BN56MynL7LMU^T#Cy^mgLDyOuuX_ZIi?v6cY+6RWT^RUFD#*#&}MT3GZm(xTs>-&vukK;yUe!2<>7CYW(4ekhPZo5C5@C&1+ zpGu+p?!LrdUo@l4IKzxEEhYB3WJR5|lD%o&GEVSxzcFWe>ROC)8Z!D6V@G?6bK!^$wIHPqemz!yr}=4rxEaS-sClkS9Cd z$mcg?XdTY`s#SHk2Sye~jh!?yVlMY_PEokDD^wvH#*OBGw@80Fh;yO_u{K5RGLOLF zv^-Zdhz820QVqo3;@H&MzdjEVBo;q#5XXO>FdSy2Pu?3*pTzFX5$#Vh-fvMqkf7cr zjdzUB(0r#%zOQ)j1ts)<>_*ZcR)Md`b`^eXU&X<_xUgV*NQ3(DzQ7CwPnu+|b=X>> z``9Sxnhe12=-eYeBH=;PD$e}+Q5|`iCCJmJfW7MM&2@cy>kE^tt`{KCf!+_*#((to zfBy?Eul-ETqmOfAJLSL4ep#xLXac?KwY+ZI%8#hI{#0QWzeAslD2l+ll`l;8`TPUN zPrr(uqzs?*lDiq2{1tZMtv2`V`mxr;Udw-b?*H-WNBYhng(y#^te|BM*1dREp*yik zL62Dg8Z#&z`T>8HsE{>Rf5DEls(D5KiY&f*aDQ?{s5i4d12i5tz7J5pcc1Gecv?%A zyv9(3i5V-33UnLi(q|ZnIx%FKT{?@LWpY-ifQAJ8wY{Bm8BP{+^CizfkaJbvt93VD zRo9}{VFhn|`8`y&-ucoiaWA9if8O(d{P(|oEhX5Zpr!5MOLfo;Q$qcW#baI+o9Lzr zxkXdam5cj$A+vUuPKXXcR3lChT`D8V8&OF8FZ1aKv^SQ5v!|;hfd^pJO-9P#e+sz# zOA0Ezq2^mbu2bU>3=ySf5@4YBkb@LnMPtz|L|W2_mSPtc|Wc^W2xv>*}gSsfHX zLVKUg7R4vM5g6DejA-vg>Ht{{XX`qKhL^b;0MB&Crt~pq6Z&M3EcRE=D>!5!uWr4w zeFHpnU#m8f=ZWbF*f@^199eRA=eNqPNA;S$CszwItfo&(E)ee1A~_lXpQwnQJI= z1xpPG@*Lyo7vY-c)IZRkf>jHWaC7fSWmd8|P`sJTClb`c6Z zU#Q*PboDwiYls4rmaRc=FRrEwtvj04TCAFvybc9#1%L9}7P!-x{db+hA12uk9@%Lx zVNbzfZ@c_+i**N!)_&7os+ZkTq6{Att(Rc+<|nsUl6CF(Wy!8uCSmxYF%j~RNW?72r^CyN1j(GwM zas@40_iKKJ_c`dQCHgnLN`1xBr~LuG3f5irpuQQgQ^K-BD&VH9U8Qj7jl}?oX_i@J zc0jO`ZqIrb!9S&h{-xMJiK5!XhtrfGml_>A&<=*a%_h5BdWEKn_BN>3zK*j&NV~(L zeSet4ls-ACwiv5e2JFFaXty~Xf>Z0i?SuAeUG0PHs^?XB@lQeV2^}@h$5B1Yr^KOC zfqG&NLHC=Deg(+AW5>GXwexktl1p6*0^qKMDZfXyJHp=$w7OqVh$!Glax|i6%*i`l z%R23xa>|^t+;>KEiB!9YSJ4}ws;|+0dcdXZ|Cb8*n|Yp~n#lJlz*{|WUurn4jx^&VrDY@x)2U1f4wHg$Ro^mpX}*H}h(BREouI>Z zt>brU;IcT4@U#sOz_1&2R0EJ6c2K)$7`atf7JLbGRGAp<27mFlWv7TTg1*$#g}9*D z+_w#QLVifU;(LW$)Ag=(P7x88GNPY`eBZ-;0huLQb5y?)`cp&evGLdof|K|w9u@@U zC4l~g5SMM}?1+`e`Zj$MN;#o`2?ys7!{4yF9v>VAxLKli7?U-4Qdl$pF*y4#Jt`Um zN9B+v?g@A(t_D0O zB~W9lV{=d=5AAu|4$+#@eWbuoel6FfgF!jO&f2gOyo2vjw0|M`Et?RIL#BP7mQ-d_ ztWV}5|3H>jX5ex$?<)Rjkeb)LQEdJ)C3lKb?h!PfQ}t6x+tdtU%5nIjp{%^3v5U_p zR9LSLto&V>?OHw8jH9VJPPG7%)31NV!`&$mo1sa=lud$&xRUhSIw@AaSIJ^46n zUR9#$+J7f0A}d)LEB9nTAUYwh81SBV%%7C}qVs4Ou|_gzL>J^G5lGETN${q>swy2} z>Uk_eF(A_kO6gh+niE>9o{tiF%CZ=EVk8$tusGmO@io-fLjn9Tb%us%3D3)b%n}!3 zqG7CwFn?0;CMRv}4JH#!5C|K|M;Wu>tl{~jUB0mn6D)s-w_xbrLO(CVQ(J(hEnoYw zkU9}2{qW8u5MDIKnFaDRIW5vfr=eTa%aVFnA1*vJWO-NLCe*4=;MGDNUT>84*`mkq z>=D#(O)m*&bPwcNm#`EbXhU>v{s%C639CfM>KZw0DCdwcx4%>fYEF^a*+>7CE?gEd zQCDW=Ah-Ae%wmpm`rRcgNs*i%$%BnkQjtyd`i%YhXJob`&*&zH?({M?2gXwZypR9~ zo}@}bE}r6{o~?KJQc}?-oN~);UPhVtsDoLzl?2Tp4y*N{Zu}5HTE`2Bh1QsET}+zK zU<`BRy)fbPCR}9jVlmde`I}n$3x>JS_>BT$^BDlJ))Q9wAj=v@>7i*zpNob)Kme4v ziUo2ERnPQqf)U8aaub9|mi@r??N8!PbsHE6WA%1 zq+rG`@@j2FI6fnX6egMj5BW|H(OcO;vQO7tZEGZ$NLtBoubmnmMh>l z*P3H!J9gJ;iq`?kTJzvAI=dpLTBjQ~t(A+h&L3d45-uncHqhc1fvr)UF*q~U;D~nK z7Of{e?p1$kGh8;?H414nEtEr&*1vuvL)rzKf=yiZuf0tABgv0AS9Nrh7$(Q+JTk*Q^I!dz1Jn z+4Ak&N6Jfc%KuydF&$qzU8!MQhAbS{#kb@PJBoYgFnKOI-VZ~EN+oSZQ^ym(1Ilpn zR4U=viqKoYRlJ+3Pf1hdo0y;Fn$Kw`M1Sh?MPF&d7L3bgCkSg-C~H|TBudGvKic4& zpLR5&YK<1F)-5R^XBN+mOY*SExGn=Ug|ON6iiYBPQ`N zUQ`@KcV;p0j5{i9xu8k%$1G&riPSReNZGBYP<38vPS`N^Wi#}h97(60s^-Lhk0^xi zp}tH_hs};cloX0^ut-HHB@^;2I-<>xTU~lQx;x}x|Hqo0|8KBt;BwSKb0WH|WuSmc zy%$H$+Kq|I@5Yacp-c2Jdcl3jQYozOb_}tv68yYZDfG%BYv}(+ucpDyKB+!xfK$Hj zPb>P;bYGdeQBtt*&Yb#$gPfm^^^~(bh%Pi>4oTcW;$g3%C3~929=Pf^x#o=dWCr1os_LRZ(*%zZGj|Wl&=E$M;ZW zCxP-N^Wua=T8!t;RP#Iq6c3keCP&@LKA@rQV9#yn$=w}OP81Se)iofG7TebkT+dGDs^J{cq z7-Q7+#QV5$?_chZoV>gMwzo1ldSYR2pCO6t-(yV*-9}8Ijam3;VZs&$fRhq9v&OgG zlTOOpU9?+Pw}YR|c^X_}>`uRg@}NmbUK9fCbk#2kw`XW<-dvB4zvj{8DZ;NP2iyki z3_1k6yG2K|-u!zZw5=hPv-Mtv{;ij044MME2v`Bq2#$h4uf$t(r?$?Po*>;-f! zj)N-?m8yr9PsPjDtz~aI0NWA$tqf2PIX5QK(4u5Jr$}*uIn!6sJ`(w&EmJ5%cpO&u!$CiM5)`wL9U zQ+*FqP<|v>6Fx(a4)^k!1@}L{c)eLK9*wBS^69CY5iNiVGEXScY6jbk=yvy5p+Hre$x+hU;Ia$T3x2TW7+QYSz@#%GM_svPF|NuM`5?jF|7OHAzVD z7X6pwSqljlom{}trV#lS4CPax3Y}^OM0dwh61rkb;_z|z70OzC_WbWr zsy`f9Xg0zTc1j8mQLAc+4dh<2v#NJ#t7EeXwTX8TSHxKk+pu#{g=$i{_mO1<;9={` zFqLas>OL`YxQuGF^&@cw=$}6I@NJsseE#ah{xPG*% z6wXZHr`ChkK+v$si&P(&O_R@*sy!&g%>9&Kii9WcSC&qzELfv10ApuCnI02@G z%Yn^ghIvkD1VWXWJe~(y0&dfX2Qiz}#VN1avrP+x3J?mpmT{a(P`>;cU0%MbomhBB1 zzfCPND%P+rFZyh{GBjhDn8_R8`Er*IwyqHK>V|@11~o5Aw80jp1XMZbf5+fIsW;33 z?xcO{UBUQ4q1G8tX|(!{@NW6>8kbg?IQJ^qJhgL!f|`DTQIk5fLPhumY?I&I3)0L$ z`;s7P+Gk&AMnylK${Ol=PZ<=LM#xX+wL_Dw=L}wB>>~9~Nu%g0++6l+bt6bR-KE$# z3%OQHL*e|7>hdp5sT?-%qtq-KQVO8HsL>%#0@8;ydM|X}20r5wMv5Okxu@L|wIa+d zZ98#y%b%n(?_`E`jUS1c@5}?9iaNLm`&kfeaKZwKjP>t1eU7}#Sj&~PQ^ofe^A6_? zn`D}+S^MnCn{oH2Y&dX7e_xv986zQ)fFZ==1qW&~VxA>RKs}Cccw7bFCUrIEQxa%E zjP)_`impwKI1X^uqHM{?P(H{oPg`8QxpbI$?i90c5;G)D_Sp*loDm=I4=w=5XV_!Y zh9$q|iYiw`wq&#U3aZ!NR=_F=AAMUAhY|d^EzSPadN8nyn8_LgPRvef_*B0lC^d&x ze(ZUe<~E;$J#;3R{~ur99S>*UteYx~8GY7Mr;J>aoelRf-!B}_ZF z&k5Ea0tX!41fx>~<^N=R)w3k(qqOD>j5fmPCQ-S=52>P>`(G+0E$_#uJxq*BDxUJU z{JSUa`;l0CreTG_)zbNfIz0|UW&u+Pm=nflKf-X2oLR7Re#ZBpfp32d?a@mK9{@eAX}C=P6{dh^z)MzgcGHd znxm2>+FtQuZq$hr&T7E{#(S>Wich{a-Hc#7L@-FISx>W) zT7c~1%F2QA!)HAj`dCeYOs4?@LE4t%j>ypL2}m zCwY8vfwJZC&{1b|VgwiKLQ)J`RQqnPM9|mEMKXC?HAp{HY^YeISi_;F7C$K?4>h3b zz`SITg;jIX-T782*lN@Kp|A4X&OVHx!K|Xl{i3Vo5&fTzWfDMW=C4^5c>DZE*U{K~ z%SsfBMa)Wky+seiaf_YO&m6qT*Z@tyGuvLV#d=U!Uf;<1pt{0dbgZX1pCcm?^4HX$ zRbLbDB4q+!_I~X*k%K5q9md6S@s7tILx?LS0SMsB=f}k7@v#jg$pLY?XuMWhzmW8{ zu2pQX#8cLN%p+Fu4ww{Hw93QGdgvOJke!Lc6U1n%lHa|X7owxNLiPfXSO3HDB{Rh2 z#d8LVWMU8##Hf@!sDU}RY8X@nLo!tU$=SENu?tqEuF(j2DJ5>t>8y!16iWaa%$RGcy({fj_|)^|C&H z_Qb)2#pujZrcbHJ3mZ$A#tfdS=wlL(KDzy6FK=VQO$&n`fcKG2(!p~Y2pkeOXDOt+ z`o+*|785j~MC}cyf$s;#x+8VCQpHadz5IUmseW|*V*V)N{-2!Xl`visUbT7kL;X_7 zmLc@OcpEEoq(+tY%;NeAt296n==gCY)4C6Tv0wbfoCg)Oz}t^{2R3nXnr_=`a!+>} z2OT@ZvFGMs7O+vYG?Q4l+NfU0*n6m(oM(}%;;?I1Hn3(4Ybwk3BD|9pwWJ)Gn^)5iA-DH z6dTlSQ2ov)E{g-QcyJy(O0B!YjL%3-$*cM>E?5*r>!sq2jxY&HyhcDLg{bo&yW~Qq*6T;e#om9#M$vRz&P-W?rsN_q- za}(w6D3C6Y7Rd(o6s+rcAZ<*V|><2;0JEZ^kYq}(j z-4X`@G~+D6EuIKBn1;(pKsSOdOvWVI0v;R<@}u;{zZ3OaxS}In_zt7x@m;o^GE*Xo zXVP?Ya`chAcc#+P?RQZaN3D6pT>by)h1#1$yG>{-MW=r_>N$a1pH`N$_NF%X0$_Gi z0Iz=RNK6oxc{aC;g47X)(+?Yu8J4JB@Ik2@VlAOu3);76N>PUmhat^1@h zdpqG>c%giI+A`eYG{(*7cwq>?X4G8fa>k3gg$LA9CgHs{P_`B6?dHyqfl14okm5o+ zETc6g2qfRWqM#nGxw7juCUv5}pDQB{GYEWoIfumJb^NLEib&~)7{5**#QbD6W7AQ0UhN|^%J0j(ak_V<^%PbG zaS!}Y#&f$f$yyuQIa_QfUyOFp92W;YH8)C}24RuU;6Vk6S*?FV-jJW@XBq;ui)Gf2 zo_19Ae0v=K+WSW0Rd?S)+osdZ3GIi@4Mm5nayo&1(rN7!ZI_IcN$1yxZ`h#i(k#*A z(7md2xM7S#F0hFn2c17v#BbruX~ypYuMWMuc=raXyc&8kEl;f_dvcuKfZzQ~Rn>A< z`wlG+J5=dPF=XVEJNabEMShQt<3`mecG5`R$rhHH*23EZQ)YYksU(-Xb-ywc(q}ps;IoAhj>-n56o!4Y_h=47xrxuKM1f4ZYYK^1zz89Kw2iW|0;n(CyGH9DKR zJhr9$F?s~ry%YQEq&C+lglI6m2ryjZlI z07D@9Gqg-Y{!JI%KZW5U5volG*+HR&2-Um4Vz+iF(#GQV&Nt`kH+=NGA}$Z&qaAzh zidagOY;Ys3?wd9=%sj!`3A|FhsGYUu{~oYt#ielc(nPuRb0Et={UO@}^7y6ED^O-n zlI5og%?XfBqaOKzB)3a+&>P&3@+V=6VA)eAbg8)jX^8%U3^2pl zx(g^<;w80=$U<%U@gl+}^f+kwAK1}XILyvDAJi;0Tv=&?+^Kg0j`=sdam-<(1SSPM z?Fu5=zfwx7J(i)xnPrRP_!4n28jhF^F_~+#xsgc^7R>zOt)?Z6` zdTl<^n?ia1POz(Vb@0)gTsbUJZQsd74Zx+;PCDa()9MuX6cAnCpw++Kw14!xBFDZt z<^o|62=QO*!0{lqL$=%QQudb*)xPR2N~2{y-svf`hq38N1CdTCtw=ARxd3-Cx{J1g zdtv9C`Sgn)H8xo>U|cXi&nRRwx7BR32|>O*9DG4$YXeLpgRSif=bl5>EhVP&U7Gir zqjW#E5@qYc@X9*i)JxVtsX% zVi*(6^d%CNZ|K|e29-;ZT{65CncHLDFemWi)iWvIO&?#qSfm`K^1iQ0(e{y;GiZkv`GS$)!^^r3|bNcwdKooo*v!ofx35OzSW*L zF_^lT*ttJ^@GM7#Ddd%_)FuhSQp^9jUtFN6<;5%iwPn;ehU9^)UZ3nu>sr&T#2MSJ z(|=T|Z?Fog!sCJ=-SplbY9Nndded;qKeCS|Se>gzdS?6R5_g5O zT5y+yy^svKKL{46AGmsMj);+P!7ay@sKmgh&XC83hdz{nYBlg3^r)Hb=OiA4_P43 zVBdFI0~gh3efXr50xs`5P|;d?e#*8zx0wW9j3sG#=KyDq{I+3-LXw6|u%;1*i33oz z=_UWBGesNx;C^pzbfR)!)@YM0xYtE%m@RZ=B~3cbGgu<^N$6pIA#G&Hv2B}C+*5e2 z)_l^va_{q-;>b3H6zBH}5Bj88@6eYt^r^pgtT#Lf05ET+ds|AtGj~%b@Ti0B?7STc zA0uQ$4|gWr&iSz%^>Q51L8qY|?p7%U+62|f-x=$}`11o;z~8{GYW%hjENz2VM4`jA zQo0kO_2mGL61r^@f;0@08fvOmMX*||ZZeuz`10DPqU_D;M4Q^xYj#r(O{{Vw=>^VO=hi-6t92NbyXi>sz{ z+9?iPE$l68`+lL@^U?eLm+f*=%yJFjEjqy7DMdEWUNo4S&@etfFMB~HAM7fzhbrKw+dxiM^2w(f4;rwUo!5y z*}qrfn~U@UT#%IkfF{o*irLR7UGis7*?z8;c;N|~(ecYR#$V&0LR1EUdn}VlNd$tM ztWV}ObcuYvQkmN}PjfWpkr7Z|ovjk*w84%%FtCsfX_4ALesyaN)O4EBhms<5Ak;oN z+vopMd^RnAskCcz;`y!2W9Ta$Pw@5nT`S+7R%Sy8Fc=LJ&X4LNn3boY5 z_sxnQ*Ndp0_>QYjRf^0lO=Q9i@eC6ZqK`)dtlTRcA-MgwAO&zfw;;U<(HDRq9$Kf9 zZmqp(XCaX4evD$~nFXSTV{{jYUc+^QR2_p}d++nPQ^<2>;y%a)4AwY91=h!syWBo)=*PP7tRaIz2dOxPbCi77>U%)$B>E#}qpfWbgZl!$BFm?`dL4P>;Mc2t9txsc(?~Oh6fd#^kedZ~vY#ZaMF^@!X zib{9U4x>=2hVFK}BcqMA7H@Gz)l54gjHS(U4CzU*`L$&$iXm1yoPNFl{~mB{G(UYcW7 zINAPLvD~mB;DNI;WB>?xH{mGZ-IhguTSl(?gED<<_AY#o5S#HEt$)*>G-^2nF}>kj zsM`)yKYG}`roPX0le7{fU$rug=9{i6u+W<}Y|xn=B=o)pCG=v?){s*b3|;FWj#;W| zv8fLbT4`}GRXb+%^KYL&ye%Okkb)xehxZM!FnnU{Oxi+Dh}zM3k(f1n#F7czYc zL|NYQbz$ZMllgro<26J}o3G9Kbtd+@A_V1TAVhYZBt%a1|RA( z8x%Uo7{>GP^70hd@p=G+Oh(V84yF&bX<#$^6S-%3$5UG8QH0~;3p<}h{>;-DbwakD<{_*n^_h${v?s5E6coPzB<=d z8Z@dQSL&{dv6~yb^ZtFser#^Q9w|-+yCEDo-84Epnikv|EG&R?tx}V&l|Ox_MeQEg z(;Sm~wuV~>Iy@!HD6IskxhfP5dmt zwlU!pxD=0&?%E05B7Xz?G+JQR0-!wMPIpD%Y$?w^QU03!C@^1z%bGSh=5E#^MX`>B zH~TxhfZ*s0q@;OLpU+;cx4Co>vZ)19-(N0b-Zk|tz_drOA<{0K`?xam!A;GL*f)WJ zXB_sTH)I7MmFAS?<60j`OP!Ut)LkmB+UxJcOMBBsP*S>o_mI`+_WkdhMJH z9f10+aPuR?wfwqm8Gjgw-W%xXtl!)Z9{d=;w^mh)S+2ttVDs#-eFgH;FraxJ_GA=a$S|Iv zvY=~6^X`S!GLD1qbm2ZGe3X3k^L6D3e$y6z4uv#)Sa$nNX#cOryoU!m{-o<*Z~MvdxrY7= z$SVQ$K&O}Mp(#Qds80qK_HjWVeY4C2vL7Z$RcvI0o7%$sgSHb>%j&i(PDqV1j1fEO z>hsgshl%iWZ-_Z#6bRr4rb4_#e6cm5DP~$tkg70?q7@Ra}sop4)~$xXbd|@M_$L zXMe3*6tBz@oGoj^yVWtd8`*QS3JN_mGyZVoM&lhA?@mth)wKOF5UgOd--Z$GiVIdPD~xr|TxE-+tQxR;x6ZbowgkXe}6Z;mO5 zq6cA3O!R2RtCz#f=V`Cl`c=>6uLPD>Ca7nyn*+zkyE?+7Hx@>g79tya65Dj{$psa9 z=GP6%u6W*yov3@x5dzzkLEs8tn(O~asXbZpE~EOMIE378jq2ah)D4Ar-wo*4XuniLL!HF;uPcH?81R!wvsymZ+pe`K_+VJ>%ecY74ec z9Hfg$a;25N#V(wl(&jeVUq6lM9!8r**zcvn5j zY|gh)KFW+(>>r=N;7NNITK^<>6@g02DXm3_fgylV`OAZJ@x8-|iBeA` zV4=hN@CeZC>g`tzcjj(o{RNE9z2-`t|LQWQ%7#2>iyyyVEZEm$B}HZPb8hGb^cDHB zE%oaIyIgSr|9fgH-b#@$+)*Us%Hb|2+K0cXFuzqROF~SFb^%?ufe4*)OJ2X)_G5NP8 zpMzT~_=3yoKtJ>%DzjV&JEyz-P|GjP($5y4d)L9l=voqMT0P@dA}b>7=0CF8Y()k7 z$nx5ojACYa>u64kI|vKf|_vneV5b z>uYuHZOy~5JZ>jlqf3ou4rnivZ{D0x6!vNa;BMD!0TJ-j^7W)_h9F#XjR4?e69@j{hKs>noMp!1*?= z*2DNJ2YLft_VF5&2+bFS$(z#oo zn*oRP$;G7Jjda>yqTM8r@L)bF3Ebgy4!B=8NdZYQ*Rx{}Q(=Vr7!LQq5k%9LS(_c%tJx+mM*Y|7uW;R$ z_Ubuyjn|0=mtS9+t%l-%6PPpo?eKa3O=g6L+NYrU@=q&$v<-N&HAT-=GSECR#|KQD z6}6O(gF+t*nAzU@2J_7FKA89b(y$pdmWFou9yGbErht8uZO4?_h%dUHtr5@jd0W(v z<|{eS7+QK&rg$AUoy8Q4KVr+kCo91b&&Gdfk0c;_J}#;AH=mUWmFu5Q^h>~Ptw1oh zMk|ncCo01H>+=Av<#X(~gD&pLJsJNIU(UeuXx6CHxXb=dzI8JPyUTIQGu7RVIxW+J zp|Z;n6*&urmwG04ah{Fpu(w#+xyuD-u0)wLD~TvnewkdMl|UWeWn^LQhvuy>!-HCh zu6GxCh%=4THx6(QqW!C{{nCrG!r_O)sChBmv_F{3eK@25V;-4z{AY`?t)*Ib0_h4r z$F}AOV7{g{H@EQ)v*#=YuI|H6G-m~#;?KR-ny+%P>=DJ_?hBp z!Y)4GS4sO7;^<%`dsP>Up;W{2*a~rK=Chrim(z7(Hf28jK$o3N~y zr$!6FB$|r_WBia~(G5-m{!oJnAHvvEm4b(=YTGL}_T@%pH=}e<8;inW4$ZhXXijJ}E0g*}fV`z!i<(ee3;$amBu z@rvbn{G$8i>2NIKE6AgWn7Og)xazX!(jv?6f~YH!*1{|+bRn_}T?y~nU8K zFQOvmp-URJ((o$i(WxS8b{M~AN(sZ*_c9yX({k@F@vrnjtCBl(pP!J|+-5%a%ah9= zd#9H9kUdKL99p?4(BLt_EJnSa`}sSbk9~5<1EcKUqvxrn31<;uPtp7& zy`sfI?47vOQQnMY>Zw&_GP!)gfoYNl zbNoYtENoWONrXRogs3zl3*}k>6J}oZsO)j#qg(09Kk;47n(XgxUj4{9bk~OGJDax4 z?F-x8Bfr*W3ICmuZ-?2pBlZ~Fnu?BL0vGv=enLMay!nxSP0eTcwj4J&Gc+eTCosg% z^P3tHF}2XLKYyAnK$CmCEbw>lAf&ljd%oGvGM{nV@;F=`?6uL*znyN^^W`dgQXd{( z{%~fmVv?7G$C_a`k(OpjGU-3GWz)<8j91*7X`&uY-NaXB;#o1jN(=bX;CTx<=E~f` z_rKS*TFyYUxA$8lEdty|e8%aDW+yin0Tp)pvwG>m#uE!lxVGRqAgc9pZCoYRr3zYn zJm4Uwd)AE#B&*%Rtwv6dj6KFM(**z}sy%BqSPip_w zt;fnflh2h)kg=rSDEq(plqbv@e zwx9WnBXIcryNfe<=%B-LOTG)l7lwc;SafkP+?0*Ys; z@Y5MbyPJ}!+FB^%DEU7t_DDx70u^Y1e z7Tb@`?4X@90=(a6^VfzM^vW^U+x>3^z5E`3MwR)krE1OV;Ba_)OFZtZti|=R4n!RI zOW^%CZywK2RU%1Gb$a91u)JwfvNEpeJ@wpPK=-}>>`U)_u~zXC^++QE_eV&GHu zu}Yy3f{pj(wFEjucCO!_(wnhpA(}o!3Ud7lk^k*Qt9eL{ugw*iyQ=M^hURv9F(PeF z-O|S*_o{s)3>58si_iH1`f6Uul~(-kukp9XZ!Cs*wRzHJ!*W*1XTm?Xsve;y^;5l! zI=QPT{yZ6UO46tx>(s69__3fGYTO&lgM~cw`(P#(;`|l^%QyY24!KbQ;kM&R7w@?C zdYm%4@#SIQt;M2mSBg4zzk5%e()}N@2+d&%rdrvD8Nn@ND`h1C6@7;)bCl>7UV}&5 z6-iaSa`*vxp4TEHc1*08-iRE%F)ijX&@z;4Nz9;wyCFUBQbzrpBi(`bkK$i=t(chh ziY8RW(WVb#$;Eo+Q1=ij$AB!Ho%Y>ZD;{$OL_>oiFqAf4Bbqsda|TRqU9*X!N#1jc zc%UBsS!`TadfN|Yym!ixZb$V;s`wQ`{9NN(5Oi{2senPsX~Yug;(~CYDAK_XP3i|0 zn-11d4IG+i^*CNRw~l~_1#q`=KYZw9^|Dk!?9CKi&BgPk*2CeUxPPRA|LStJ`?6MZ zn|m?EKT05tDF;t`D%DL-yHsF`hYY^J?9cD?cTV2J=Y>$dva1zyE+7_>CaPTg3wU0V z)T1{5OQI?r?sCKU5~rPiXmj@eA>nh6L{G=%vyU3WTf+c~$@(#I(`V9mHmwtKQ2F;z zbgXD2E5)9Fjf2P?&E{)POGR=y?S4D<9|zOsc}-sAzSxS*p4j&NXPf+AZ*hfimHoYQ zYTc>eP2qhgVjnjrMOx`ff_N zGY-Iv1y?rSiDY*L(8o^RY0nEFv!`n7tc#Vs`f_G?zIwBaZZE9fdLXONi-_Yu1ulVr?)zt4H_F~4qR*? zegRE`Ul{krCgj@jyZ(bo^REHKuuUzijA99md2a&vS!4Fu%|kR^|Eb|diCoJJEK1N4 z+BNgi0^JU_LzzecJYUuXK455VBW zk%}D!H!X@{nBcWT5FUK_9H{nvANlvhp_>HdMS}SCMM#zf>GIX8H1oU!gG0>&t4E^H z#L_5Xx{9x;8*tk{4odqP@j z=}(uhmGu7T60dv7NQ2;cl%?BT;_!p}XHT!(Nt|OE7R=rLlKO2{y$h6C_g@{>Ke9%p zQ(qr+)<><;qu$5nbost*p5jzgq zeLL{iPaxy#J5GCqoq)a^XdP=v_rz^`jB{q-?Hj+sj(;5%Ue}50DmuE(`K=PT&+hRa z)HQp&dv(nmfY-_Jnq&E}T~qnP$LoDS!P?LVUeRPO_THs8V%Oc(7&m7bh8sx%ikTd3 zADY5%^POuEjC$-R2ZMm!kFNbv*Z9wY_x4(n3e)njsb$DD261s5MBPMfMjNyppakB~ z%AL3^aIbgqM#-w8<6>iu*qYXz8;&=9o5HPl_TR57be}$IquqslpR}|1tMxyg|Cc|* z1}VQA3&h>`<7#Z$p?~i*cyar2+co(sY#(|7$y_b#-r<9`?iDO}LGiC;H)^jCzu}IK zKQD`{rC$`4IP0dagg!!}d%;C`t>X;$(?4IOCsr92jZU&-D8H#0v{;w7d&*l=)6>*7 zo#~WOhEvIpT}~Q9OJs(f^=H$(F{@W=roVE+fMckRvZs@}F-H61jRwIC+;Np2N*F7X zKVb|j-0f$gKQt=j#$9xG4VoNqMjMS2#~-!iJ7yvdJr{P@00}QY@g*#ZV?{kH~l1lQz$YK zUu^jWoByX}Rl`XJilU!?;S)vnE)}h&<|^|g>$CwKu5R;K!0Ox_=UX)wg!O*#xyevrD`;iNxk_|Ms@TP1di6n`sfrL?{9op?UbFZ5Vbw*9)${SKcq1y;-u%^pne8x4GH;YwutY3bRrr(X< z4$UERUc1vBkU6>EIyhnlQfc6~C9IF7zFX^4ZH`pnnn?Ww3TWJ-zsA1wc@F37`?Hp6 z0K;6f2UI%dV$zrI^~qG;udm)DfIh>)uk#QijqfO8dBI{XJRWAb!8n`Nsd2&IM*c}C zVPV(iY&pKOGtbzqc^}YV8LsA&9) z2n>hOSCPgw);!Cv0dKfEW7<|Q$Z>=%|`@EuodG=b2Sr%95{bI?!JP&xJ%}HTq z+NcR|#*8PQbsNR|wZv@Sb=bI>>*{X&=J&fO-rpK=TM8#$5@O=i%!-kYOA3hr-g0y) zGf8OxZt+xl8p2jDmCml1dFXvYXVWQcJ+5&(@6d*;B|8S z63VhFlt|jPn_t6o*3h2bk;@+!AcHU&^)$|5}Dt*>8 zy(zj9M9pJ9;DA?pc`_5RdelYKo^Q?@*>v}{!EF3AztsAYmYh`Fs;hIAM@FXcX2}V1 z=saNk3+MZ{D@e(hir>!XC=-nO7amr_g97JnkG?2K;NGmiq@MxF2`N)N%gD^o__^s< z^CJgu{2wT3{YmG2?C(S0_2rwRSfG=eVS={7DX4{qvE%DgPh6k1-Y%u2`$R=O{4mu4 z7;CTi&hB?nBy97E)x2CbM|IWcK|NyKxDxY)BZ_!I-v{CFM8ys=rIPtMe24)rG%hF5 zHBB0T>t=N*f0|*yw4>apeMwU(a)sP-le?tXB{Re(1tf0HywXePeIxclFZ%a3S6xT) zZ2!ZN(sV1Kcgyoz&YJ|&O|HTErM&#NVOy#(KDHc1_?K%`ZF^X+{3BN?IYbuvTXvHun63Sz^5=-`#h7) zqo&s9&e^l5v+|>6S4r27byx0LbvBnJKTMFx{<>JMSaJ`=pg|MD{V%4~U+X{r|J$z0 zUd?`ls$M=beF}e61bdDJw}99*XbtU08{Mtn0JNw2^mvH&1xs%nci(tv?C|#~#rWN0+?s=BE67cJ;-PSkC18^^ci&}VfabU1)anI+lpR0cTftC#QKnRQXjGOj z*E#Pw3JC8H52OjrE`zU>$w)t}bj?)c+5SFt_N?uS{bFNuY4yu@XQ_w>;K*tTk;9*W z1zwQ*Y=!rT90y?Dbz;BgxpeHFhSqtRr4u0K1_F5$M!yg*UqM2M>m3kb9Bg5^yulkv`J}ppf{48THSA&*n#@fd&)m4(8 z+?-f99-3V~7$Js{{JvBW8-68VYc`zAj$crA>K(5iU#klKPp0~6eCZ+ z9Sa1h%hsD@i@4ryH0w<}PeZ*HrmGjUwGmEr3m7bwx~#b&ktjXo4cGV@)g#kNSp55$ zM!_prog79ZNQlCu2CK`24ECR3l#?~D2TpO?%O-WToQ2t)r^&eJ_&kA;CkCpTO94HK zWeylO6g|G&umc3T;Kg0Od2Wh)Q;smZdT;7!qe+Cfbxfv0EIfh8v0rA|m(p2o&#()< zCkDFV^$$w;f7?(JJBtR1QYlR<4ky8qe+$y^m7_2>%m3HaL z=kxv1t(j&zdYz6I#|b+w!uaZ2%A50xO@f1 zh1X*?3vp7|oD48GSXXe^o;pue1S@@KR}Z?W>>oD%Js``3kneeuBgdDLSkdbz9*4qx z_h_6=HPZyfQdXM&2h;rLHCFHL$%$GT040&F4&KCi7dA@{MrhU6kpWHUwe~(dB|Xjy{wE%-*wU zyVF%5eY*~!PcB_Nttkb@Yaj+vOTNs#?i9`>#`H}R>9$TJzM=h+ff}~Kar4tfGv!Zj zpr6>-|1!?M4YKPge);SUb=sEGH2P;HvCD_VrSa{DKb~&ZrybF+7>6DkE7@hZt#PVR zh=uo>HsH4b+7W-h8bn&^{53SHo2J)>$FRbj%pPC7c)$dAw6Nr_kw*_`Qs5`+e5e26`52 zV$N}oGtKYVt*i$Woro}2aXU9-VX$kqqHT%xRU6e&)u!%^r;yr)gVyq9U+(`Dqoe9HpzXH==jAc8a7@ z&YIs}@?{%Y{-xsg`izPp&G*CzzBv}&x2|s*nQ($g4x_O-8mP0Sp@pFVK;@4z6R+D9 zS>E^7L4C_zK3+ICIghCoxV$^g>0PBZZ;0oe#!H_1-X%19rBpM={n_`+Fb(_bqru(M%V_8X{uQEq}dhWxlwNuJE#SpVS&fw%MuAXX0?SNeFf ze3N((xz(4qNz)YEaA>@S7(D!SV&Q*cg7+N-p7oa>Q@!CLx|vM}jg8mh;mSzYJhxyK zcUCDv#&2_LH`t^}%*A`^lOcGgXx*bc<1-Q{V+R^tS1bgoMf;{Ct=05wZFkOii&tbvH# zhd0(mo)5CO_SKb6Uo$u0*kidfX7&!IJwDzXqYs}LS*Kh*N*}%=TZa1J^>pl~Dew2% zcpx+J`Yx>L72NE-cPl(gg`t~VUL254;`MvWPnY`pHs2X@%19ca$vnF2VV7*5?gSwvRwq=-|?@8k)cf@N{2V9am5Awx2!R6UkBLBx|{^>*8TkgLYzgB+o zSfF@XZd|ppsw_SQ(e<&-=b0pTL!`gFFs0pU9y)`DVKHwpKuMY;+lR#*p|%FNQAs@j z$d3?tWWc?p{4Ekq=nNoOyCPf9mx47WYIeHj1TRr5`%`W7D#-zxRntqY0;Uv=g8>V% z^KJ@25i|hL$$dQ9+Eirc^jNH8$nVy{?2FwT{RR%V604lyJ~-k!iOpo>cg^JJ2dg2> z50X_zp1W{%(uj@OrNfEJAGibb5X1E#S-Vckk-a;=3+Z^{Vj+k%<|$kMSZe=ij&Zh6 zH`97_e78I}xtq^>$EvvgaFhL_%Wzsw!?5R*uUuMjiR_2b<0;bM^Vft~ay2hivz)Kw z^1aRKAp=Etn%*+KXDZAiaA?^pQd26zO7^E~qhMu2SZMqlR}s^%&sS$6UrL(No#Mu` zD@NLf%!*yy;DxG_b<}@xvv!8l3~}Y^LS z{W`Pu|DUy30q3u?ZK+J`yekuRcQ9YOb3+-E27R z6mqaA%x#P2jiz$k#yifYU@Wrj2<5Q$ z*pcLSXa1C}dJ^*4109t#!n8EANI<*jHwGwQqBe1T;S^j@>(4~Be|tMcRn#3168Meo z!H&%%23DE`YM%%mrw#)S;nX2t*Pr-I#bd64g))y2Lin||bOfYfoVorp=AoOAt!%$J z)#oZ&t$Ta@TZ_3DW2zGJ=L2<$G67Wwzs;~&1i8cNF;T+8?{>1$_!Ut41^UJwe4iZu zA7Aeo*VMMH4{zBjDk6wmP?2UsrKuntBBCN9AiW0>0VyHUYZ4Iw5djtHO{r3nNDUAO zDor{hK!6YeA|;fB76J)L-gu7Zo_)^$-uK)3z>lms*PLUHIi4~o`4ise4~}_puSJ?) zlCDhpteG4TunpC7FcC~KS}}+-8oQ65kJm_m3 z5<#1V4P3biFJ*Lw>z%j#qtn|p$xz+v;{P6`pNK^0(As_av_0T9iR{mn?&)Ico`Qoe z(#e*gi14Q!fga8)kMz-#b~0(jY2OT|_zoGXIBDE`6$D=pd&X3c^5zp{^msjYN<<~> zLsr1I+(5DjcT+p1PY{swUww8wLKO2>YOK9>i|lrXPRK@)D8TUPee`4`bXd+2V;CuZHR5mi)W09o&P9}tq$OMj zv974nR*zvdH?j60jIdjm*VUEyDk=DS^gFM?zyve6aL+!+H^()%Rvj_NP4-wgWYt}) zaA{L|+yQZpM>jX9nbm$i$bAuoCP#rm8a;~TUbVQ4rYG&?UsK%FC7-hm~2utsF4O5wMhg@Ci-^E z5p%*;W&eWKFS1V$Pa-OQ5LQJGj%mM z9`T|_)Eeqe1Y+!ORBbR%+6)`?Jn(Y2C`5~;yI;B|Qepnqg=;rb!@a?@2nLVG-4o$# zN_z(?qj#!|0!hvSL33mC!6+Gm01l*=c^s4LhuJ+I)M}!~i&F5!BYHt2aJzZ=-L=FC zDvKQ?*z0%soUmd9*E*!tA#kltjc2x*wgNtzF@K98Ezce^&c@6>ns(e>0&I{T+j!gj z2)TEIV@V9%=;by-r}&F1do9!*0mWC-}~ve*CY)#d>P*CY_dX-E+cnGJCNr zSw>B%@(Vu9HdSOF*N+yBKYeFV1zjs=P^FW&ay`ZxRq#3KN#<$9A}P>YjvJ`L_M4mw zntq(YzG8-{6jm!4Z`+tU4+r-oFz4I#zqQ|eb*~djJ3k|Jt!6_!Pa>lS%%jk>{YcdY zc)S}B_9K?knxrkE9N7+u68gdElwehe^dkL*_Q-{=vY8ZV&MKWkfUWY@xm~^3s(ZAK zPC*kP^ogW8tS?2jd8ca08_hpVQaVNS!#jwzOoq|54$O&}U$nlzcN08(8wUGmw_==f z>|Z10Zcg=sjZ+Re+T`XE51qUn6Fsrn+R;aRGH0T{+}(TM*sIh|xuz`ByawKGq?>^X zDWr*I)cO0K{Pc}p$jP#sbEKA3U;oHWUJkgMq#XFb7EppXS&MWJ`Bk?a9^KbmnMQ+0 zE5OlG(bV{qxY#dYY?)r)dNph$i`(;=|N3?M|HX;=V5;fHZQg?0G1QJ z=>8F=wTpWmoz#ic{8#vS0%E)bdy6ZCUXK|hE#EFq z2VSZe*@rw(B5Fh{+d73l0XXg6*MFm99jKTFb&%QTM}2nw&Cx!=N-@3_jmO6;+FOhx zg>OmR=RrTip1V>sU7!%|)YrQ1KcSH0 zTWUw|Yj_uyf~M59i21Kdd8axA85-*Aeu#V~z8i##S833IhS}*6!{t?XU!HB*l&GU( zRt>%kc8D)+5y8zn;rcydJZFyvuSE?T!IlfBclqR>YET;uY;XPB%ud#W2hC{yNPD>H zF+eq;2cwdG`}24Uk4auR98pRcElVuDVAD(YAT-P4o_HQMOtnZZY#%(n?EP*2(m3U^#ID!z{!)$V3n{26kP`p@>&IDaiCri59u=z%r>q^_ zhbx)jhHizX)+R=mOrd`FY=5}lSJQ@PvS$Y;UTidkc1eCtJJ49O1;@@(wn~hpQc_?3 zEyVW0ZEJqzP?JqunY);(_Nwi(dN&;<>K9#|Ke>$o#0>YQs@iGSogN-4FbpzL*xBaQRb|!ZUuC4&hqAMhihl5o*dK+=CCL+BxdH33j0g| zXVY=l05#^ieHB=(!YhK2JZSFl5^i1bNCzk_^6#R`9VtnxuQB4Gp9nr3VxH)$gN=;d zCR^u2QUhmrijuqXV{R4;i*`Q(ZOJ=(eu(T1Jk#)a2yG&z9HXi`vjk3tY5ns zpSkq@cG}A_Reh;SNO#xk@%uN5y{VXuKI~t{s^oJh+DNy7%S_LAR<|P?z(Fx(IE&|> zQ869j6hV$a*eP%HY>JY>DV~Tg$&*$)3cD>@0LDUo*2`UyT;Fkb%5!iyPR^mI*q$2( zBcg6i%_PR<207rliFM5z`9#lupLot^CHWWM=pOOWSgAGx<-VM&vA*ba_d{&^SZmSr z)B1GAYt|ijK;TR{GS2-N&Lt~B%mac}YGwG^*pi~F^>Lsy1NS5QjeSmje9+PMdH?Y3 zgFiwn-EW3+bDNh^-0hkOV0^*>5`-MJUhNaL1 zpKtRia0`6&!?K;0YcLr=jfO~56 zR$4NyuVKNpxS_$fOgj3@hrQyV zs}ZJ2`Hj{Xa#7NbNh3W6O$@*T?M@*{u{RFpGFO_Lf8~@LfdYW-sm4vSGU@98uA3O8 zP{i>mwTTH_J&W3z%dCg*UTEt|)=;fKVuR^b-U-;cw~)DqYb0Rmop=4$WoJGFVx-9$ zO8Pptf+7Vn4v+g3XR|z|r2)-N6G`E_YaLDr;6`?|-3)7@iNg*>c`d<-wFK|^Ex^6kD-3D}>1ZB$6x9q^o{vN{# zZj=^<-q~99tb`lW4>6o?K?rH@6mU z#9NX0yqmyrZSP=Lxz!WeVeIlzXrWz=M>0-DY5zeMMU6)5l2JgrH zJV)k9Mo@~KSH34hLpA>6Y`zLyIPxyFa9u{?>xI~ zeTvvx28{cddb(#x;Dx`*qqsW3iYf}#9bIaHY#36Wys~ydzgrHZk}w-9BE2Vj)o7op ziR$KRi!{#+H$@6)Ze%Q)l7tTT?PA9YD;r+Vy+1T(9J;pq9%qD91i7R`$#pap6*Mme zt+303^?Vmbsp-mwQ0gI38^7+{@(d>52IAZZYXiWtlHzQd8Sli$dN#hXf=cH22}Fk; zoOE2*UZjuuAsd^{i-&C<4Qip`d5`vXVYj1~kbY%blLTfR-EWh}5jo(Qz>rPy%*lkZ zJW-Q;ZUH*EX1wD5pJCg}2Xz*ZRr=ljfb3~%apZ1Etb2@pdA;~h5@P29a9|e&V~foYcH32+y_#b=<+luWF_Y0a}6R@rpHGD)Aa|QN{xycnmjJMufsdl1eW_lmaylsg9 z++}a)vNrBKM~&MZL$(zT<$20^79dnye)V5q_#*O~#48lt4d zVLcqXHETVZRWmyPMTIhh-m6>+o~5wvnyvXb!(9D#<BXGmZCbRUG zUmJ2;S918_Ka$j_C%`COOyo04evxhAsZ98I;m(_r47pN-2LcMGHc-5F^W+cBUZxHs zKIa;FY&u^&mPRk%>l-_(bFsewoGYO7wotcnwC&~oEpa?l$-T3<)nT>j zo%Tdv`VyUSaYi*YXW_h2L@~1+!1Wgn~t+}i=$GXVD}Yi+#eWSl{bcg3WH#gWK4t{)h{Diyf9k@KjYF$|Fz z-1;WLf^$xm!RDST5BIB4y9>cOPjqi(S{OcO7`ORm@3(@D-zwo7Kg}Wo*pcWTl?KdL zvmP97zT9BPnr{(c(>e2&NGJx;-R_bxPvUIrX|?$V6!k}yA?hYrt^JBxw`Cnts19tSc)fS{vJP*V3*(=OU4h@jaCO$b;6wurk)8A2axZ-!keeJj>;G|x-3tI9fImu~IXmG+hMc8b}1p!sJ>d3gSiI`L(+m{9$%RT(({p@Ei znv@p&H|kn+feS~5tOGck`%Jr zE%l-oZ@Lxg3x2vv!l@}E6WoRL?nuyILa!ToEizxLf3$(9g4El&+b2HOYpcLnNxz?# zp^~07COgM$IdS;d(!AAkq*2buzIxroEkEBXz)^Vbw@o)J;|==bqKZp({U^gJppG;Y zfi7AeUo7Z{eO#KGv19v9cL27b9sQ_ysfs2-e-5-yp<(r=w3JVpq?MSJeW*Lx?`FAa z(79n~E`;GuUvj)oQ2kV!-M>@cAQA0y4zC2GqX35ym_^1wvk1{1(z!2DIV+b!^WdB_3R%rey4%@7l+TS+5H1D{SkL<~mydbd{BNkXk z@Eb0jinPQ_HMb}&R;=Q3|{Mu7ChiJsFCud0|<=nvxxM^3k6s6@MQjJ%R@J$#gu z>-8o~WNqgQpT9yFoj)iPCGzN9^b#xtH2S!N@Z=JE&A_e)6pFxyzbUg@rgun|m)4G- z2(4MSupmbMDhx@(X4moMHL0rQ;OGfIJdr{S!p{=Fiwzs0mzNXN8EK6@LVDZW7_~R> za=YV3oMRo8-3gF`l(jD(?~XSaZ`L+Q)L?H9oip|XrZRd}Vk6w&kXC23{Tl6R`X{G1 zZUO+~pKz!kA`^1WR4#7ur&wXF98AD_pe@NE+GLS4;0Nd~+AM z_EXe8b1$9fXIx?j5s$T6ti82g{dO*cr+Eg2N>;`m0sOcd^kW|2xM*;)et)`v6P6nu z4*M(E2X?ki+Z=P3@ZDDw1_n?mcQU^;ByJkUUI#Z)DBCMw19}Y`)UaI)@FB5^IX!^< zJhs}2-%GG7Tj`%L)5EO2n^Y9{Xi(sGB(;dlR!R*MW=$Gchwojc@}PZ*GQ-!(GA+y1 zE?umwo_pfAk_J|y%Uzq*tfsq@ir`!%Jmlbrkbpw@vwKTAMSOYHa+Vlzn;0=Z%TrIn zPiFRShZYn^-F&z*{zJ#Lq3E~_N`g?>X+=q9FLB5aL{gea_)Ml)aa-o$h9=lW`%|Sn z4{Y(#oB3=8{(`_OT>mtc%?^6bMd|?A%J@r+IM^2MT_UjF|A9mEL-xY?!;mEcX-j}2 zGh!-$PFh9a17&LcR7kcGR`n*J^kmflL>o?u+ zi=NjTU6`0*O^+%rz2zYb<{At^j!2UX|EbJT2zv8Nyh%%dHPO_wB5k|pJTvVwIs#zi5~y0!_CtDjZrYiL?N#F|8bLIvne@H)Q>tjKe6E1u(!SBQbLtN{+T98 z{NZcUgmvLm4)9$Hx-}!_=Nd7%gUmBBh1VUHs11RTbgdYP-ck`r<-hzSRC(@-%|Oe}Cf~gzZu^fC zYaoa|b_^gQUSK1a`hBez%z^65BFC3!ZhLDr%JB=CnC{VnFQrW4a=TcI`y7`9yUVs> z#Go%=FXvP5wFz;!rhO5nHg?x=*4!16{cD8`7T4Y)C@t)%wy4mATiIr6J90))6NeN& z*bTUQ64ENK#ij1Vcvwn6Wt;nvO}5ZG&!Bz&SPhoF)=o99A!lUx(k8bkr&)YsCMvh2 z3Ns{?La>(|iB6t4T{um{JNnVLd83b<+I%(z6_~KEp-)ar<-3zw!fOjw<~>7AxgkQc zdhindru%3=M+Y)#)khWVHWMwZxf)8b>Jqp+tZPo|eeRLA#Zq_FmMEL}{`(%%uK%!^ zwY9-pxb>ElMNdf=4DKxLse_U$YZa#fQq` zKP^U^pv2E(vMEtyr6*^vvh43K{Y+UHXgSgy_c(}VVU@Bx`{S2pz8`e%&Z1HnN>b!L zITnaWh*z;YqW$xY>CRaJfO!|adtqLs2?5;7)mm%)k!lUTxZl^W>O{*q zN{o}5HhUp|M}>bVhO&a|Q?075Ae2%6Jfit&Msb%coxA+x+6ps3J6t>PM*eRV?Ug+| zOa|#HecAWBAuO^Ra>2>x=oOQ3g!-@$ysxV?#8dIU7lh{FIn&qY$`jmXO5Q%9Z44ldUCJR6)OTuSq_6#pD3w%Pn(Ja5PWA$QjyTe!%I(4cJCjlB)V7WH9l zl}qZC(taS>_iHv4RylSC8uBX zXPaMxv3KSTZn~EEzDjp5-;qr1W?elXH!lub9yE>2PgE~#Y@vw1^^8D>we z2piGP6y!)f)PdqwbWpxH-zBhjZRyij4hW6VsE)>g^!>ffi`a8;q=6M|3%CZ8hrB<1fH|ydSzz6dW{hqP8^sq;n?gkw32o{DFPSkTg8dGz9~eY z>-W1Sg3t%eSY+#QHuJjj z3BiEJm$Gj5C2l}Zq$E-DMPw*#TU=}|0*!02o9+7!v=Gq()%}{|nJ-iBzaiTb&%bc% zI>+ph1u;`t@4!;E8_6R{z|(v84<6#JFeIlZxck^z8O|#MbB2RMCPjwb!6ozwy&xaFZ946#|YlaxD!tYT&P|@sY(JY7($U9M@PZ{rS7AT;^c5%OIXH? z25W4%e+tQJ>670bOw1BLcQLnp=CB=;H}FVksFIi?G&M82NPtzg1V&nf;g8d0aj3rn zk>PSrL2a{!{YU0L`Tj~+yMM4xyhsu8V8v2~vSVAxv)yqxl3^IP47{##ZKSCR;bsOp zA{I8`Qy(w>Zo7rbiVu@NN)=f5Ha()bJvC7zzqUmHCqwZA`}1#gJKdyxY*|F>k78j4 zcL3u51{40oylRG`Kk!g`0f@JXPLZ(<(uxxtH1-VYD^?jc9s{qtaV%cNe@+!byx7s3xMg_iujyjQ>dVh7FNjo=l(DE8c7 z{!&FaGP-~E9^oy}ey^$ARYQTBPHBM`Dh&eUu&g^PdA-7~LYt4wRbz&qhZvn%HfptR zFnEiXj%{E#2?NO2WT8pgN`c4F)nIH~+%t@KZjSx?{s|pStpcnOfL_SM9;s%pYW_kC z*}R9@H7mePN6753DQ)Yx#B^a-%42^ab3lxheV}_M5?xe`D_i$O;JHMPQ@36cwQJCX ze)Ln-<<*wR#2+d4?W08%{$%V&=D0_hFM+;4cwk{5^2p$u>k>S+BL#-^jxeAkGP-oz z0S%hNeSL9wwtl6- zE`;)Dkh&iKPxWa5_I4ssDn@Jsrxg+2+BAh+`&Gx}%y3vTLEXJ7{ey?+z0-P?3E>N0 zl}X`MrUZeGWo*&V&dZY{;i}I-4S>yOeflpFwHcpGP~2b_M_4g);gA^nUtyB5=L(9k zE`TmYiBgb?*Y@YmzdoNl8(~6?aVj>21b%l-?w@vI!fR)sa3QJR&5P4CctXE3SacGJ z_IO8|9r8F!HWy7x6F5u5o-(9Y2ixOup7-NTtdNz}nmw0}4k}7WI=%YZ=f5eLhcb}G zpR)`5runp638=@l(1tlIlY)7^v1COh|% z?V}c^?N~-7%p6c8JY@{!lWzlelqy`EyGGC2Iq- z+^4!^z`n&%q>L5kuKn2dptp-6@xbSGlfD~RA)%nBf3rbbVvYJZfcbSa&w=2`c7$M1l#AohFMM1U`NTTZEj5QVr0bd{257N4=9tTSw z@my2Aa6Lksd*Ce9mq`7b3MjIFcsyvooMp%JPoUZ(GmSp&V^;IY4=M)^epgvZU|+%` zo40K1%wsheExQZOi7TPx(9iSd0i1LL;~X*+z?|5a_&T|-^V~OG0qqBifnbh6P)EOY zXI}P6tt+5rmcRjp-Upd~C~WsW;~HU&YpspW=tvs7K}lNly7($lV!B&yws1dTlc}q1 zZYg39kzB#LxQNJsqAEwZXHI}SjwO{#avx~XXb$cwaUX2OQZeX078Wq`L0*$Rnol{( z;X<}mwU_-9l0Y@oh7eQB3s<_R9y4_kT~+QmcC|H_SGF%`$9$tKnu_Zj`Q{3K7q*hl zaChjX%D5_$6O@!(lvG4>h=y!8|&>`%r}*{z&D>;tJSQ=o;OH)|5oHiZi1{^ z_-Ql9m`B;!@~_+WRYUMX?*lx0_e(upaEeLo;YQ>>Y>Z_I*6k;5lNhkH{)p(eUM7=z zHtB%SJ&6!5rw1Qn^rRds>Gx*Lw+#Ng-&d=Z>c}3;jXK6KNKM}NitF%2DwN)te`tG5 z^d^3-PlI}Rn3L%_@_t;-!FJBP)XT@U0=^?@Fj&OQzdY5X5d%ddG3)nIa}EHOSI^!% zf!ui1AbxXP#>?U8E`Fzf**~euir|sWPP?ZYpeO~mJ4_Y;c+FXMSTBgIFNPj(F37Z? z2cM?v(kH|4vUZk}s@P`EaBugdmQvX)6x4oD=AU5Wm%0PxYcM;Sx($YlfcaF0+kYk( z%S0?vTZh9@(X>Dz%o6RvSTWL;IFw<42Dv{ed8udAFwM^#T^XBs5Rn{{T6v*#rP&K8ia=7ubm=%C4-{j713 zqx*kXCMu2w&%~>QzM5rHo0|y`Ih5$9v3c8y({S$Rd-apLccewR?KBchjdORF-8OQoOxt)m z?;e<}`e6qY_|xIyG#5z{+cxFenpa@wv-f62{WJi7MD9rPO{Oq)5&m0S0;=~~oD+{g zsYNTyLQ?3G+^=p+v#uGD7)ajJ^^)S`9J#e>5xHM{kcrxifg*x7(95{xSfW;nt7xGW zfYV5LqxpwLX2CF2G(agazLexT=+4r4&b|nbzR=tpzXAP}KvDwC;O)Fyv8&%(X2Icw z0YogfHBte3@l`j`BGr+CXxzP43^po^U|uiO`AsLeqQ-+{{wXQ20|o~%9d13^DZVU_ zlRM$H5F#bC5D30rZ&@Q&+vHxc&Li{_e}6AZ`*`<-Q5QzAc5%58?@)ex0eK6O-}H;{ zZ*jC(4(*cs=h^ypx@mMbpN~dCeH3jMtUW@roY1i)F|fy8dG@Z$Q?T!0nJ3&?hws{us7#1J45W$<3qqqJU_IdQxaYB&N z40Pqj&EF#Ehvj>L8~t)&qa1JBVm}uxl#T(-9 zN$S;>1FJ{ZoOF?0iVTJdj=B;+L~gC~LB?`Z5NSm`tQzf^@@{SKHPl3SQkWBM(Ufak z>T^ebRKcS5;>L8e0FfW>nA(i^#p%+i(SIXJ&k{L@{p>s2rKCZ{H8;*>+`;tF(gV0S ze(2lE@Vt%BT`FfT9K2Ob;H-UX_5(1!(up9{1eSJiy?kfe@}nF*X5wSPBXTsq=!62I z@f-PThv9Q0H^*6HTo?R3aQv3yp2l%tfCA=0_~c4sw>*2gJ6ArvQ_g^~a%|@uKD>cN z!1QC7^>7q0hc2bD(=rmEbP&a!bEVtP`YmybuHIOGow4QWjXtAd7VasR^jtKCa0jJe zHh;u9ETJv#|KM(%;ms=0aH)lN+m=fNbG!^T=vQ??%s_NMSXxvAO{vXfn)l)x>mAKJ z3jUS0p8o*$2K*o*0+~%~|3K?@VEG3H7xX?5w1$ulwvNTu1D8;Jwd1K{cT2BBELr5j zhCIy0ILO`QSEHt+gerV;d9>rVlI&=@91#Xs-7hi;wlWV_Nif6QmM?+DV zMfHoqwTQsgN}x0Uq^R7u(bV=aBY%R`3!CiVtZ2?MFSQx)GU-wki%3Ta*ac-V9zd!&hRk8Zd( zHk5IQ^Cvx(XmonYxHm(9rSFIe-*H^8%H2omU;hZ*h;I&T-W0Ge=PM5pOP=l;_l0?3 zcIGIXRT%Szmu^k4HP|*<0v(_w4lFxXGyb%3+YKNG`n<5=$$Yi?sgD#qLwgDyyTL&1 z_9$d$1E{TC$8)FWlc1t^p`FzFB?K3SG5D#qv0Sy$D8xu-#z=5RJBfOzRW6r0rWa%9 zEf1@j`M@O~F#S^PDctpn^*;>JxGzd-t|u2&kBZULXADBRpEjR5_~`e^y?sz~mbeqZY`Xnu z2f-%H>^;zPJi?p)h`3z>VlbZ!k1RfiTzKDoCuFz~IclknaIddu6?KaZydy`~5Y$;p zj7d+w_}*YI{z$Z3p4>aOb)_Wz_@PH&nsBCJv-Otp9TftUMjW>ZNxIsT5(15my($?T2r5}a<;Y?Zx0dpL`R=gB{tsIESG-Dh5r%={`0 zdeg65GZ?$-tj%%{!i%*j-rhXP&Zoiv_OR{4{o?P-e0TJQ-GXTuR)~qOEAz0ThJ6&g z_v4Gd6@IVpOmS~w)P)<2#MX6(hTX}r!Dl=N4&V5YiWx0K;02qM`WyQ^t1pBRR7=gq zZ&u44(c!9iO5YpC-QM5=^NNa@dlBDGzB!a9q;4nv@Xq$Vj&P~hG0~wLpw|$OR6JeZ zx*5`Q%8C%5qWu@@c&b!{Fq;chBJ=g2FP$YY=CEvPnIF%R_mD*LEp5l#2&u;4w z1|GG2WmB7(;%;Y1XM1^UG-3rL#??FQfTHvUoc zRYZLm`@BDVYp*nEVJrSP$ZL^sqM6Agm1rd+%;p#uvcmQ32Ik=i>81;1W$hXCeU6+v z9Vpk*Kt8o*E?>VPKS8^l7}ifL?0!!WYifwf4xDW3OdjQWg_>Q{hdCL`%>9541V=(^ zOX8Iy0!R~(`RuMao&JI+>-L9;1V`ow%7Z%CsF*tiHFSUTyW0CY=2x|MD%a(h%EoX$ zR7CAN%rD4|8k+D&gudWHp~6;a8SXN(x1W(Tce!Q)inpu>rcjrq51@gQy_zw>1m&3E zztAp}e+Z+$8y}2_$6YTVpD7@J4#qf5Dx)z&MU;2UVJpJnM_?868i92|)$IY@ zmV@K(U+hiqmdB{p<=>UKmpcCX-N+@+?8nGgvqJB=D9fJw4Y`GR6Yt~NoVKKFquh}w z???5@yha)u_Z1#wnc}@BQ}u(@w(4dWhol2Pt}}PTfQR1`lIjmLgC$0WKhGFxB=6oV zrj>`>bKdqRPy|@JIYI0l%8PyoqC>%shA%T)Xl7DR}sWbSrKtFY6nFtds*4cN8=?0G1!jBWMWrQ%#lg6jIG zc03X{s9Qd*LxetsWaHia9CSt38@d0pHbFwTd22b zNL)Z8X0!4ta3#2*)GE0hN59*2!a5?0^>`#Qr8{{c&_%x}t-D>g4Lj0jwRdaGF3kXl zcX4Gcmi6whLDotO>^Zwi?hn6kj+VTk^j(3PWUP7AC`0Tg@vl)<&iLVXDT1&>$Ks9~ z|MCI=_(00&$z{Bwg*s=FTh~jeqeHIR1+*&`*_s$GpuaEBR}Cw-@S+(r4qf)iO%IY@ zL_3%{)V~m^w^YOCjyA4Y;hDHt!N*ue zV*!FnM5QDcw4VM%=pS;CI@mU7afTyIl?inu0dvm=Ob5fRIdBnNxJXl}0Io*aS0m^d zCzLYSaazL-QRlU++`%}ECA-BoGlI$poxb(s>T3l_-{vzZvlpBY+^Q6S;4jA*!9}ov z$xGraqtD(BJ0E)jn|{>bOPh$(dg9X@2aSPOq8$^+coV1JDzOEF8>DgI>^?uPgU6FU zBYYN98Fq3{%mOpU93PH17rHo;kL(9l^6PwSNGL&9#&wkkmcBLgDNXux1T$=z$?fcP zL?+R(#zO&xKCkVx7!dMy6Oz; z@`_olnd3I;9qBAy3qPQM*&feo&`GRJhx{p=5Pstl2BZUQ49ICuz)AN3WjG1iBEHafyo z!n@yM-tUbselyTw4PdP1KfM>6wj*0~UE*=b+y+bPqa7z288#21GMU+u;Y;;$T=q=!6Ty_gRhS`&U029a;9pX;3}X3iwC=5?(( zD=jJy#xd+#@qB14ImmuT7G_5u1XkCAKF9B~CSb1rW%b-6|MaC3@=+ESDT)!N*|P zhcx|C#{^Y!=6)J!tjuX|W>8j8lRu%pgSRO@um_ZMx10bZmwzfM0c&Je(#yuyvWXeK7R9y_g52@7>xoGLtd~Nq@xq}z$bofu>1muRi)kvt*bYUvc_m`J` z7%kbVJToB}X;C(z#)TFZ^7DQ7{B_qH7xzGLAa+D7aP(^9E9_Q5ih=~_Y~*zbkQ^Zx zw(#?_a^Pl0qRs}sMVsau*3%)A#?GSEUs-f(sYX5;+F7Doa}(R^o!n^*7!Nu$^BZpG z(&s1lwtoa4!NrWf9}k(n-fO2m{KTS2(CD2)Y>sYY@Np%(vL#3jt^`#0Q2wty8DDti zzh$UJ3Fy(fJ6KD97=GX}18QU?~6Sg`F5n2`AQOE7fq zHtvi2KqqpzA0PsB-}&I4ZOVC0AA^ie>s7P;=RV-0|5wqu^~=2t?s1U|2}H(1hkBh{$TW#S(dmt-}&8j|M>22PO&gLC%eRT@R zRgp$07}Keguw}Buji5AKB&B_J-hV1~;6LoE|3??~A?2C-3t_AHP;wG++?;vhxNuBVI^{Ba@`wTNOiB}1K|@~;7JS>n^I~R)=d{cE7fsoFm6shp zbmb9x^V-Ok_(HVWr9Njq!@>&2heIkoJk zf22Dgn0k-4QznUB+=2jl#R{>>?h6VETdpsLjh-*=obL`*H2yC|!$02q!TBp7xhf?L z-Ypq*4X2chsxDbHX*1jex}$Y|!hud2>+rvNSU}wm$4JQ&3wd@~FIC?2c?jZ`bUd6I ze5|tNTp>$&LtdHVB62YXy~C<4y$O~e$-=^)M)3GZM-wdk~ zwS_%vua<6>x?dJ^9K2S2H((;TtG>9_PNm&GxwBc{Q}^cy5gqLw=hZKwJ*saylzXUa7N@+yz&!u7^|J$g3;l&SqgwhJM)ea0^eD2m&o_L>79%mt36EG$M`W6hN z@Pis&QX_lZg=&9=6F&ZH^)SBm;AhgE_D}PlO;Y{q7gt;>Z)fhkK?BVcu!3Y%GNCkA zy~Eu9`{7aqCq@(b&$)_=*0AF_o`+%_<`i&E$J6Ff6wykKw- zj&}qBg}KRnT+GKMz3=BX8C{oBYq?4)+JpP2`hTu{NMb@_9~|r2y%}TAzvI6)Y@8uN z_1}{$ao(I+i)lzMrp;u<&E71~0N6sW zt>r3f@db$URX8tIQe5OLCTI)STwq0F+2QT1bA|DCCjX~Dv8*c(2q#e3vENWR$MlGS zvtK#R!X|_4ABaB`&(HQ-^9JIn)xgFS&yUpibTb_iZww#*h&*z?Lhuk zQY}hb&S>fG|HSqGNB=Gup-hg+h-<Qm^>hity$j)>?NiB-PGnSvscMP<~c#nRfDu40w0I!1xSe|`18?YB!2^#fAZ z+k1NM2U`SCO1DZIfhKLeo5VL)9aPMM1G@fUUlg+Hl=C*RvX0yM;aLWP6m#GT^UrL) zIimOX#rQ|uYTx8y?Eh_1p>%%0Fve>fQ{o{1!Y!{XKC>OGO~d}F-8*}4Ac$p$m@AcB z$|sNdx#PL>wS7fI=_}u@D6@mvSMW!o62m_o%Z1^FTR`VmZXe!chn_rb7K`QRz7{u`$h8mV$X+iDalyd z4}x}8!X3&%=&!b#7DfC2pO5idQeXYzi)82z$=W7)g(vAk-}28)O1&?Gd)?o7Ev{RM z>$mAB?Vgp9C4xeyyUx^?p*!}X=&SPcSP6$jb(R^<>G=x$tK@7Pq1e`;3Z$#wPU zy)2Fw5fQM+3whd^=Oy(#b-L{Gi2KJ%^UC~>m?Duy?;LV(^xOZRZ**2LzzSKnf6&4f zw7G5q0x`T^=f{D@E=sVIpk-DAVWcwU+)zyR^N?4!`2>Zf8h2hh9*&((zmk(-tB&l> zkTe%P`Mb>ZZ`uC8jR;zfUeT{zk7E`Le9rOcvEHTN5y&$5-leWuBN?$5Hko>kF&4ts| z8;{@k_f-<>Om6Pn5Bxk4x4g!Aiq#b9OWFeI*N&Wz`-AuU8D!Kdrvvq7VM;{gEOW`IXq*zwm_^n+rnWs!5s=8;nyl=hrZ=3zU9)%A@s2lpf_pD+s z->sTGzI49nx}n~AE9qXAj7R{)b1W)$>Yd9c+JTr|m3Fb%8_F?ul)hVA>Qaq}9^s0K zZhzl^jm*CLC;m^{W$Pztx1%t2Oav!X}kP;nmm zCD$nY)yEon8=hkVf$ab}3x!Cai_7e*xTsII0sZrFL&Spo_V(pZ3nl|)y;n9USO2p3 zuW|fs7Vqu7-y?tAH61u!IHo76;2SeDyQWl6&M0Xw^(R^bF}+Ga+EL?lI}+#>K`t%s z{b=vDdvw$RFy`|p22|D$j zjC||U+)mwwi|Qi^TbDDlv-y3zN3WR3dQtZ)BifqaCI8v0?Qdh0d?1)6<4{3RHS~W_ zL*vA}*~+gTUmEa*d_T3%My0ed{ihvRdlu_$b#>PLd`no4&()u&CkxiC9UaoFZ5s(Z zrRQqoWJ8Oi-EruSzVQnx&TcaI{{HX(c3g99;ch&X-)t#8VyogRE7J=0%=ayn1A7W> z3Ce~lR!AF)< z8`8(mf9KzF-~afGUw&M8$ap9gRVwJAaH-5?>|Y+{qr-3{N(@A#bed*AbTu514%m-pW5UTe-d#+YkNv>MbJX*JzJZE!2} zddG~%+>4JY^6UEeX~vz=A0Fuc#G-#5Iv|X2B8Oi*E(gLiDS5+>NfDPV9a6(JYpbnR z0vPtS;|ly*S&kJyeT%K{{fh;PX;KlJ{6nwS*L0kz^n7}dd*DsFK2MQj{opGKI=T3g zU;j%xWH@0kol%!UaGR+QST(yz0taXSmlj7;17_j0+&p~ z`6bbvatXO@W=PWA=BndTOdrMt71NrZELV_BFe~%3oSh@LDt!5`hMxcVb?i8PN?b$U z0Xd}kk*brtvFf_{)9|Y=@XTI(-dV#I1*{e_-cU@+Y`5$RK3J-=+cfA_9an`JtU~

rpX?@EMjQM^0g(A1jd&I<}J)`Le$Ppmz$XX3U(e7OxrxJG-$X%NjVZy z3bav&dVzK=7p~RPD_98~jrpd31vmm`C-{KzV){|I8_Eb=Ih>BJbl6JHld&wfA;zNF z8WiC-e>qz~PG%QEco7K^tp%Nf+${|^xMuBZ-tsS{L@G@hVhA?zqNZMGt8gbQ&nE#c zlEnVcu;t)R)H@_A!4)DE9lLl{<@Rc&b?WSmw%issoUp-`QctK0$V=(3rILsN9#Y|K zQTNMJWj0@wg6a|N2tKDSTKu95JcvRc{rS$^M$Z7l>zmPQ>k@-~v}>k_Mr_qg&74UJ z3`6Pu4#!KL*clmam>52p71Nw|CZE|1kHvJE{`TKXn5SYH@s&G0Z^osQy z+!7=9aFR9l_io6oS>03Q3#hW3drE#ak$hL+g`|@ zOIH80@&GggeCadUt<>|eI*ZgC{?o% z^KTRTCMRDMh)c^U1ymW_dG@+&zvZ4o{~n`Ai2alxa>?TF&0(MBJi^uGLBM32X{IoXA?_(H}MqDMnO>psBaRd#N*%4M@SQOYMj?()Ap z1jo;tbhQpL*hH0zr63==u{PuQeM5P(zI`Zg4zOg%l&f|0zyDE3+=4g;_mR;JNherO z_^Qr!v}Ga7!vgfRQ9DX4+aXD(-#mNd@0mCsY5hb0X8;!Yk|~f0Pri+C7MMwCn8Di9 zVoa%3W-m)tZ865B=$oU}VD7C!t*RO=h)C#B*CEhutLfU}g4oy&p#1HKCJ4cg3+26y z$uOQmg>myt=2_Ga4e9HdUC2XLPbZ~HFXT_c`Fa*0Rrya|B;q^msUihZrrk$~(4981 zCYQ1VR>s8*zRVk_Dh&gOyg0nNv#G@zXH!{>|M@&WLo{+1=+{ne-I2XqhfOs;tG&9h z#y7L7WL43mHj3sEAT+*uHVmWs?YOIH4@obKkB=HZyD0O8h3LYj=^ZVB&;a5@pKUNp zutW8E90Q<95fS;}%kz30>f)&Xi#MFFHo(tHO3@Iej-NtARnDfU{fR$-6ZA%GV`A30 zHyr1Rm2mo+B}YIx3#B|KQ$ImN3R-XK?VAMV>_d&f7X>*O_^xbOHj>H*3-DZjPY<~w z)P(UsURM6fnq-+BP;+3CJ|xqV99vt#TytJv<3I$PzUCG!<6534h5VGCkeh=>0<^t zUE4VvOtmr*?j@Yc_@Hd>ZEd8OFDEv0B(va-CW*dP{3hxTa|i^;HSB8NHYVRa+_;(2 z>zV4jtV)re4}L4V(f91HWjjfbh_FqCz8Bv)b&ZLVPNl2nF83YRo^bfux9)5WTcw#? zuX;dhUiPggZdlEdyp%B|D9UXjnS@1}11APbichod0FZ z{TEPVmPHsN@~}t_Fpq{=?_9IRzjuyY;BGjhxrXf&I(jrt6#~T=R-hHGv$W13Vuv-I zQJ8wWIGEQ#uT7SiMQ)l!R#$UWxk`oaT@{hWU$+5HKEsHL-EOXpi`8ODlQ$78XdjZk zO+MynN!-&bx!~C=pWI}*9rpOEsKFw12||~>>DbX09@vHTCbu(A2Ru?3F`egUWF_?q z8*7p=Uq5ciu}e#>ru9%88~@d>5)#u^?w+kHS9=cYE72Y2-80M0s-3b(t6)Zl5WcD{ zJmpU9NPz;5veP*s_`idsTRw&5GF~vcOLA|CX=q^(6DhpAUOcY&HL*BOX0;t-_&>wM ze(4vgur3YsP9ugkP+@YZr)d%2q6;UF7C7)l{<(ytB4?;v_mCihQJf-~Yyy zd4VOuZ(2;VAy9da=(@FXPN_1{zFWeREH_CVteZH4|94`FL`9r@q6KX^zn#9&#>HC4 zI8n&TVN^f&&{XNm@S99QcLH!3yUqJYd)~pm0D{BBUFZ4)w1$LPu^&KZWXKcLys;|6 zXLP6ZJgMt+{8e%rX37OjReb*odk|n58Ze733&kko4mKJWHaoTNW0vlW6h(XJI{_@Q zXKT8WYiz`Fu1NU1Xi^D3L2O=Ii;mSCXVbl*ij~-zF#QgJ;oMzI?{N-uE|#&J{XII( zie&s~Ute)eNwi2KnjYebadBE}Qk<|F%v05nz8_B#jd!4HO)**@f7h+_gxFL2c}`J; zA=rZ?f35K;4?~wtLT8H3%Ygq`6#pq;G17dus+!qUNbNFDOc_#E-)}5n=6#%#Z=Y{u zicTizQj;V|j*Z@5N@jjn`Bd4jKjZJ;6p$mvKN0pu5#B) z-UC_gq6%H)jT^2lK|f+Wt73#?sHZqa3HjecR~zjM#Ywr4_6)?qBSj}?ewJv)sWR|X zxo5r4R@M0j%L!2oai9jUHl}qP$Y43bT5ZOUOW4Wso9obEHqO~0X54{FHE*j<7qx8W z_*m-Pol>D;{4ItVwBROu_fFW(kxTsH^s4rvqyGDBQb6B}M(WVym#k$pnp(5ADs##5 zCh~hgU;JFBO27%BaiPXaZP>bycI`_>ramvabT$?h%mux(!W~upaoy=GNV-v!?qI?H zM0tN+1`)2Gig3zL6YQ+_maMSdfLoT)MwyqTbor|bA4;`XUTrF>)77|cSq?>LzeKu5Hw%oaPIOyauPPmGy$=8m((KKbj%Ve_;mFe;R&x@K!D}Bd@Qz1w z=NehuYTaW2k5ChB^=XIfiD&;?jg5q$N>Pye8ODJttg|6Vr4N(R42xu6+&Q2Oh;^jndBb zD&Rr^V2@`r-M(UO5s|5rq=wJ(Nz2shBD`cUW6%BU?NEz*q&UUj*tYU=)X#-1xhr5c zj^=J9-BJgJB&*q4Yi8l9=O2dghB|#L$E^+&;nQZ1&Dl~bMUD9@SfC!^K%fua`BEj8 zHau3aU+4_^wqx^lY3S4D$kYUEL~iHtw&C!=r;fUcej?GF*}#O+WNa-0k0AfzfiWU= zV!*GNS9b*B2|ca5A#RocaqsD_ffLvRe)&OF2OB&~ZlM6&a9^kuq{1rh^-F;oK%-gBI#) z1k~Yd`FFLv^Eu!<-_#vP{M$>1^Xood7^sIwN2CvK;d}xWZCs7hF^&6h$$@6#GzLmXzs{Dp$9VvE*9J>4i%Rb;IsA3 zNeiK2y?6gy)%h1X!Cb^ZqlCj|T4s@X@=4eflD!9I4yW_DJ(uAmlO0>Es*RCm@&XVU zP(&zgB`9fZo!tq{C)&m(ITLC~3YoSeo-pAMALg}^73 z!QfYDfB!{$bU2dh*Wkon8wa*IOY8usmNr?>aror$z~@xOD6YzJ8IZf?LZ9r*+U(I~ zX-F-T{D(X`2p#N0yC;2OY#cRfKt_D#4DELrkBy03((S%N?%#jM`Lf}@5~e})S~)Y^ ziT!{UbFC~4)_*^{o;(d|4wj8Si+{$-)wrN%ET7H#H8=0IXZ-6sT&Lv-hF^lBY>v~L z^O+Sa($x&HbMPk<^t!xO%Ic;dxmD-Sxi*C^+s5Q~DmO3w$^{KzBdm@vZd`w89M)tk zqcGsEcF9iu3Z0WXOx^jV-VF#b`(`gZMFDxUmDZVJC;F-n^jlM#Jh32PX6DldUG28) zQ#zGA8|(R@w8r=!dnP$T!CQGZsQ)^T<3FX{NtlT08|&5ioaR}995XGxRI5yGe4F>iW({XQ zS9#46Tm0=M8rc+MNSW;(yqx6e+>{Na1d(-Ut(TQBD6-XpD z9}R11_(9E|e#v%gRDvDww6X7H1BiNT@qYcr8(~SGjeH}xb>H(={ray#ah=#t?>akd z(#b(`OM{ysf~#eFFxN9s^Mf7xq1{@N;zAY=AOdsd^r=90GAJVIp5QVDO%P>~_O}sr zA-TpTrjEs+%XOO@y(zdFW&2wwVShkPSa{1~Ga7iv>rLavRMz0EXVE}SG551UR)CFB zJJu2)ty3%9)F|Ci{j%k1PnHuVW7gkkV&w8Q78L6^+^ysg4USFKb~S5(s-8{jxJSzU zHCM6wpgE5dF?dPl>aMSPl&D$sYiqg0olj~iiuGh-cobO}TcbM{trMjdOJvDZQiRvN ztWx@YPTIf{ET?l;V-MDpCg7)3vvsJ(W}-6FNmZ`4D@u>3SLvbu>t2L}_*w*wCn=R3 zMevo)O-Yv)@_yfE95;|3N`}S?H3RI--c|oewxTs zlBb?D6M7Flw(iuMy0I72bH!FAu;;L+#oiMR4QC4fI>Yj@xMCMZO;F?Qvz8itb`zOC zt&1t>#p_kOFbM!`?;|Eb~O}@ywPf#uf^JO{mbFo-DE%VAYMt7Kd}Ek zuDR>I0cnzv#Q=2+5vlDS<8OvUla!+Bc8I)e*`6_a&|~>@e6l=R8)@JD-A3nU*CsMI zv!2G61sCO|!7u+3b^nx|e5lUjeEd&Cm`SW262)=wM+vnbh@`@FVlaC~V>F2w#$!6B zoR!W} zvecM#%gnLuFHT=ER*~RgT0|hm_p*Z-sty47gHhImk_Bv*;K|iCkygHLmCERf}d{r<0bVUCPFk+`dOIV+A-Vd0aJ}VL`PS2l= zD;c}ipuOk+lx*8ja5FA^r|f$Ci=oyzcQ0j1Rb6*Y=<0Wjr(<5acf%au zA%7web|tfv9WCU@GjAW@v0aciUsTpY>g}gjVHCu__FZ`%SBiYp!EhdfLi+!j4c?PxMK1r9uQSqog{15n_qZ=Mv!fe`_1xNl7PNlb#=RORz#*Y= z4lsTSFrfDXw}eEnHuQR(BrguIiC^uMwNj1>FpLR1v_B!P!xp-v=9#!VE>Q4d9F9rw zK@z&ZG`^sESoLv!*lWGxxtP+n1L?G-KU}3ZJ{;@c%SgSw5JSQHZzVk;59!ggeyUh= zwPx0msTR01W?yHTriVVB#^I;ZDm&hsJ2^3QP13zE>oTqwA+)+|7Be_054r-Ig4m6gEu^|ApuhzaS2kwPcAx#*Q&RazzWE&Zo$!$1O|to6R( zrl|Fr>7tYBenb$M{;2zC!a)H$xLY`=Z@r9YO_*NU)W2WTk1JJrsJT7Uf8g>5hoL8S zNr)I-Ju^$em5*b~-C5WA?~2D4cLe?mFhFChQj}A8J3o6KFlG4~|J7GS8KJ;CG1sHo z&eF{5sm;NeCVscvFr?`$mHdT5xTkn3^FcOj9W(a}yM;Tt_yx(>4LkOLK;yv8hqw`O z;WrPwG_1WY%%5iqRW$;b@gubEgf``7w<8Q40qH4=)_yHky`d9Zc>XTr;gAZr1?x|1 zc-MMA{Xn|Q)^ykb?ti~xz%>Ds!oA`KnRu594_m^M;cLm(m0^!|xpN)|&9Jc?8RG3V zC*A*!hixT#%?%REz=al9;8t_lT)ClmLCRC4Zn-wdsKZ6_IWb5Ry!Y~hM>_`st#E+o!(LOVkXd_oY8Cu5)JLvW2DP`jqkbce z?3MtVBd6MGy1~xpW^P~jVdFtSx@rn~|116WeUHxlX7qi2_GbiCW@Lm}X21?njP{C9 zU1#|;B&!8%oh;&?0R+k9i}WjdAR3 zuaF$EJEg8`rHVsdU%f@&sL(%q9g<}uj1%jg?2zMjttpzUxkAw~ys7$R^F$A`R`QEY zlDuEu)_eWfLBp}t5n-TK@#Ds=`~6L3zV*iSp_DUckZ1As!R$Z)?)j^}T76gYm*mu< zOJDe*G1TrzzqYW+{eM#*`q8E#Nutrxno8in6XJ({JLTo*rO7}*r~MXy>i_%~p1i0d zF9*NApz58A`lR!PvjVgL=7nzB*rA>2qsq^7<2fyV^XVCFpYCW^i@UL-EL51v@dXI5Zc2DWrYHZ@!TA2 z7Q^TO8|tji=n696?7Xf%?qe!~xi0GNwLaW0$g4ED0&b@Z?=;A4_erUfR=Y$HvQyi{ zu?2tAXqcsfCK!u~SJqyOB0LxQ^Px*^Yisj$2X2WqM}His#+UpO|n`G%kpf+9&W;C&TTIiu+DuV=6+5H3AQR+CAA233aO_gOcU%q zK`F0_vHtEHKpgXBO{V_WvA468gIgqDm~GACU`+}Q$A`y93)0!IgfTFtz_A(LULJMd ziM@=Isn?0--{-A9Xu0_?rsEKPTacHw{hu$(?uohtpF|d?Y5I)k7w_YW{MY9*I)ZU^ zUf?U`+F*ZvqT9=+*`IuNcc(#D;=&Eip=5 zYbri(Nu~IMD?8^AKn^p@qyuOg-^1PZwK|aDO~Gq5kQ?Eh^B3oid-tJG+Q)~T2DfS3 zHRn(#G%iDpqgBalAYYSJNkYn2!|)m0*UVBJg5yp7k)#bSaZn#Nwaqhwf`^yhe*8aM zW|J1)T!89&BxE8hf;DN5QSwBGd0>DBXFJG{FYkjw)krHvz{|2hSiLdim_^00@#t+M z{TVIHgfh{QRErSJ%?b>5t=s%NGN%}^BHk*I zrC-_?Nt&7-;bPIJ^+=A@I0>*3i(GVkU)!*9N8;TII|Q!U*1hk35B$~t|6`zm*S5${ zb-RQ@sH40d#IxYmS!agEL-Bit!|oIh^{j&(hRqT7JlQ3(n$^8XXIzXe3#4m?2zdo$ z9; zodDP4_*wk5i9TBZUqI%hfnsaDp)CY3Yj5VJpj~e^5AiCx+PWaMm(Z2)DZq(g?S`N#cvZ7bN+&F8WWMQCX)_C>k3!RZs!84%5Qz$G8@A}rfZdfye5;rB@FHI4BTMbPF0xbUrIUFE&3l}Z5uUaj;SEGeX z_kp#UAU}wAdnmv^JU}3k(Qq2%BBZ>2zj@*Q#bKg;0SJDkn1aA;19Z0e#XvOt0gU_K zN0iYQ`SRkm(~RBYmd)okcNNN}#R~t49qjICA`DvIS8JHQBVH67z|qrG06oN!HmgXT zr#8-J&0FZDeLq`89~?KYa`%$Kp&*!EKUzspG;f?0D)}an!u_L!q*Mug-MHd_LX5{d zfk{#HbNA$}$j@XrS!92H(cIFZI00kDV>)ZP;fYjd9;`XFyTJ4OnD`J+WISrWb2O1n z-rK{@Nx=2c>TD%~Esh%k%(*Cf_Qn)KFjSE|d)6<=&TbI+meYGHCq7A>SsY%IFPK^o zi2rnI(rG$oLn#F<$FXH3ezM2&OnuxiXJuuduorRm5lJg1#(_|{ho!~qsoKx%Q!1Kf zBGNM!d8rdTtygkSGFw&KZiu^sFN7cuJ`Wc@C54=^Y>m5>dJ8&EgC^+H@g~q zH8x8zP^%^RCHmGGg1J<f9{ z{>k^Mo#|=o!`;CoH!$kPR9!xMkj-mLrr<75=puY$sSNl5q^OL=2Gu%?tJ!UGc!p(>jAKq?n8g!k=E|z$( zi5p^D@U{4|tS{}Czme^M<`JOj+s$(rTF`$c@60`(V2^0ajZM|w@2*k{HwK@B!Cfrn zfk@IBgz%yAP+WA}l30zXn)~DURuUzQ4iG=Q(G4)XVF!)PkRQiH==f`NK3%i`2aEqO zU+81#amv)dGH#MGKzS)700U}Vi#xurK$S0A(tJL<{U-fNHd7<&oH{hcW53?AdQwf< zEDdbh&swwKZuO?-5zy??3IcvkT2pA4)bASL`L%l@ckZj^Gxdn~1*^k*T7uoHy|o-( zF{KN7)Lb=pB2w=5v0`XU<7ga-Yyc8IBWF;#_w`m@vR!8AC=lR;Pef$KOr|l|nnv*Z zj}B32SxdpoR~zZ-z+{G?S>U8`< z0Y8(c7gY2PKN=8wvpE!dECYS{kvoy(`E{hE>=0hXAXLM2pIUe;M>1!J%bDs5t4(+- zdEZfL1R%ZRfv=PGgNwbGpe#i$$0yNm#KzY1BY|7K2GUgV6Tt9_inEcEruV&l>lhoj zZsl&1fAK1+0Vv;O&wRv?oB55VVU$t|9sIk~JYi7PX9HINM!16w5f=3xZGM}rL+5}%n-$Lx2F$3DE~%c< zk~@I^i1oF3^8)|i$s>Ar77tZ#?vO)qF>YM+Z}S*Jz+hH{{!G6=WjL!Y^C~I#d`|py z(%n3SW`3-V1=>ZdpQ3%Id*B{_aoEG0p zHo!dvi0(R0q3h7+lA{8|!}&wSS!4~Q=DHfHv%^;SUJYy&U(W|r%RF{pO>YMhBo{&5 z5^-M~$0iD4EAH@{dc1w^h(U~4IsE)oM`NgWOcZW4{Cx-2h4&YC6OP7`(V>-Z_jf{7 z-?dry8o}k?Z;OeHfwB#RCIabJAFNoFM{r@Ja`=h64Zsv-9O80((v??8Jg83&x3+_g zquzG!^17acui*5S88i6wM z!_N?M$IyoD!Xg5f<183}@U^@iZWh&XMqs`$=*Qtfws2!&{&^yiL$Nkq8sv^)cBD6- z3Hhaz_8SQOSQg^3c$eUY?f8!ypV_xU_qKy4)%9U=F630(ASJW5in2_NX>3ZkpvG(( zp>(IAx9f(_Zn=$japuai1)NPzi}de4q&kni4^pn4*7GPRshRC#dbr&XJ_sAo%am`M z94*$Y8nfOt4dKse@TYrXvj6A{SbJahAB6KhKv-Wv;a0geUPovOzMRcq; zf1$n=;RV6XO?H2(!=H*XUmVUIN8;r0O(CQ`bzRI~Jzj(f#y;^3)}XplG8@lr5E8$o z&|j?PP@U6@(L-}yK2m+EkbO#erNe``3@`aq740){$1XW8g)_6F{o0D>9BLEaJ!2+o z*L1us6X|;DQ`UTBxtdE>W}D)${c9a`{W*qoU*;GvI^Bo_O=(tDbP;KDyUaWE1TPer z{4(rQt4%~PjGb+92{LzvI(2;v zhoEywV+s8_1(xSTQ<8o}U8L@2rPJv`<>i@@8)NQ!M3(vkdiw71R^k!vd))EbWz#!i z5xxt8<}@6xGn^?vofBT3=^GSCzDC8)9VWpSX$RD2kQxX zC76g~%ma{+8tKnH|%VXgEjY;yO19ujjBi8QPml+N=^u>-p8WRk(M?x z_l=G|dq%zB6yY&0w4 zm)G{NbIfnMaE-D{ZK=j&u5`gOo(nQ5?*p9d8LEf4F2O|^f#0HPQr>O08;Pwlw9 zpO+sJh^@SjgKk<~B6VtPHn@bin=aZ8e14Ii5>K3v@M#~4*V)9~w|g>cMv3;t_XQ4R zLSB&FYHj0Z?3T(Xm|N+vHAMN0$)5L%WW+3^K;76LJ}Y&3GUro!3`8Dm8J21ljqkbD zy(MC;OOKpkM`^LpthK5CnH zr$G$r<|Cg{WZpqplty*6S@oR-(rRN)FR_+6%?S(o#RWlQiNm3xQUbf7*!Xqef!SrO zGom-~@=Dj83)!yyh@IZ$u+P0Q)^nHsL%HujTVng1=zg4xXVoCLGB5KuroBZVqIeOq zXLnnziWWs(7{wB=!35@YyYJhLwm~wMBfXLkm&;|ckJs&i>V)0{N0B*W!T@Cae#UIZ}%mS>1x!Je!%#vx? zOxo~<0~{bweyAA&_AwW-)C0>*=cWwpz7DZG$SwnI^TY`5lmjjO^4rvKx z2os8_$u+>U=(#?^Pwp8K&x99SjT=5ASu-0es{GXjcl3LFe5Nidr^z(#08J@N>+P|e z&3BVP+UK`Lo$1y*2)8df?O3s>zQf%=vJ$o9`h9%O=f=7d&zyFxUgn-5!VhEOj32Um zErd5Z`TZ+K*(t@ww_2CT8z_xDeA>)wakl{o%B5;gva%dPm8Pcewj30ReauP6z~Qtm z%SIgXkfoRE+Zh1M&<%5IRj}10>G4ZfMa0TR$Nu53e>qCt02G^s;05pocydX<)!Zz} zZ1cIj9?HRVcnoJT^$HwM$u z*sF;Z)>{7thvpp6=Dt(1!IkY*&A5BN3t%=8sF{*JaA8?PSxJ>fSRp@Fti>0JZtpgsAUZ`aG@XYRVvt*WJ+ zhL~NI-u7-ZSlh91F3=-w9&11e&?9_{Gu`iucX`KuHlf@+jKlkb7-eI{UY3056?~}@ zKpHx{=hk&-v)!acgw0daTE|l(Y^m{WVASiwK8Q3`T9KNj2_Xphw;y0P zobRq`qLvwS_GT;hG=X+%ZG=kvhB0#7M} z%~41xC+e)sn}jG{dA@|oHyWn?>tVi)bnl^XC1^lPDQ=|*Lpgu?TrpnctF7jg{MX#X z_(k)l+qvEO6oY;} zjm##vZ9)D&M-e4l+UyAD`AV!$`d-Uhh?jz02I%1NHd)|r9lX{P%^2&*?OhG@0eeJr zwmLfvud~TWoX;>A&NIHN!(~c>**SizR#08M#RbmbNVFaW4?CEyyLA3gWb~0Wz^6Ug z1|{)K0MXOUSYP~gN%|uLE*^o1280kNwz3Z^mHn(I1062SVhj!wN>Yh-n)87DC;hY+ zTnJaN6M|MyIa}zx>YaRuO^BEa)Wtm7ng8Gk=*fALx0I0{&y+a7P6xPqcBkt`8ak|4 zrdqz$`;0lHa89}3jk63Z@2a}) zk0mld{jXWyYxDp9amj?i6>OgnoY=kG{ z$`hqtj}eyDrIcs!jBUGDaH2p#8z^LCmhjUr1CQ=%c1jz$1#^1#C&+B;+MB!&1E>;& zyJ?a-B|8DCtF-*a@kbEA3jf&C_NNl$Sz@1XLA+g~1R#qZh+GIN6aUuMOox~?a{EYQ z3j1A(D^%&Fw|Z;U00#kb6`pXgxcIUe6|0>6We(XPCK36<3}c{IP^)ptcH^2MtlVZg z?r0x>v)kR=OlA2%l%X}-z zf7Vz4>%0lyep-7A5Jt6ofV?m#c#2QE8sBX2WF zBeNN;om?IL-@6Zol<3-kw*!Xo(KD|B9h?_2p-3wa7Tc4SnEY)2y4iz64&@yYPOszjD-U1-E)bi4BCC-94gOOP;kSwjqnX8ca9Hyruf%OpxJel8`!ILyby;;^` zq3!wyujI@78i7G-kX=l_Lid-P<9#(+Nb#7zKeCZ~<^H1Qv!SibyZRcwKHBgJ-W-vP zTF`B1HNt@CBQ*Cz3&pH+fOgNI9nZqv`78y>&Q~}zPJG{V$4llOc&{qcL5`5XVE%K{ zfrQNRRnO;0^F7AVf>To^^MKZ?*Zd_$o#grbQM3=>Q$F*q`Tlyx;@3oh@}wlO?hhjY z342IMg!4x&iR)_b<6uH`w@kK;CO+6ez_6EV-Y%ub?lRXy5SwS_7JQ0X20kzED5iqI zQ6Rdmb={iD5x~j~Ghxl0qf3cqu}X!m;WWc=yoH?mJKTU+3pI^(<{hpJIb41p>!LLi zcxPWyL)-26=k&Z}INYroj@xI}H%&CDt7;;rk!cjk7}|hwXcu{C9W}n)$0k_w)^12E zOHP&>^Dg22IEubbge`F*T4h0{j&$;y*3ZhOGJS8C=$L`kstBf0J7?ueUMz+1Tu~m* zZl0I?^thi*-Dz0R)$%jN=(N5M&{wqo70@mq=PXNzFhGo0ne2>OU|a^KGbkq8wbVP_ zO{~~YMzFO&S9NkqyrG}tKuWQDie>3p4$IM~08pZ(X&K@pfM4XjTBUkOa0v<#%}a3% zf6J`FL6ds`P%YmEnpc==fVKh!E#wP!bv>|2QYmSBtwCws3i@!<_CcPy1qs7^weOs7 zhBkx1qcVXa+Su+5b#*#kg=t}5mBeqXTp0L0A(DuUj(9&kUl+v|~Sx%Czoh-(AGd1>y2>d{t{o$W|}RtzEsmn;xIG%A*j(;u&4 z6jn25)-@`b(YG;~Nxc$9$8S|xzcDd{ZwipU^%-$C#-2`yUPED8?8Mw!^a@$V;}q*~ z=^fKKIQ4>=WcXPR7so%^iPURhM7hiWL=E+Y)A79p*i<=vUX@CDRE3w(qc+m1Hx4#5 zrJ}t>q5l4ar-D^XgzkIlphfTO8!Y6Sp3;?!)D64Vo5q9 z9Snf+0#(52t;r!HDD#(Zo4%ZGIj=q?+@EmSI~qat&)V5v^;o>oy9Uh!5$kE({bmq$ zD9T5~7Em<2a8yD)SpNKGNYQoPLFy}5S*ZK;tjq`2zi3@GsM6}))8zNmkk%O&DCQLk z0F9vP7&VG-JHM~S2pVxlZ8KfJVvWkPBpW4sx20gKWUow` zG9m+WQB!3O97j4AH=?}MN!_HT0@fTyAg76Q0s-B6!)fO>ym+(eIpjE;-M$X}~Aviw@ zI}k5dHqoe#wQkA(+hi=0ORHVeQMV;PXn(|Mo)?$w#HwQzqutB#X!b6QLN8U+(O(Br zhJVMbCjhY-xg3Drfn*p&_}qqUU{O z@`nDwNm}g*{{T!^0>^L}`vzdW2R?r=!r6W7tr}iANq=aHr^F0XRnyGb(q0TeGChEF@jdC#wh&ta)A4MB< z+p3PXYEnGIq(4BZ5WmfoHU&-fG!f?^uJyp{plE@bQa9C86wOzd2&Iqnhgk{P#K`xFzg>5;_W(*B`_zH-8=mO zP>~CODeM}+#B%ST5(iy^tOC`M{xfAWYWon!4m1Wg)$%Hr>`3m9CaiA{ZzA0_^7awq zqP=#F(fK`&5nA|e0EUsDB?+MD2|U$TNrNAGdHw++?_WMgz||mAddz7K^BznefO;py z>I!O%&N`g6Dk~`STw{`3yoZ{^>kf1lX1UXGJpdgduH!eH3slsW#9rK;^Q$!4MJT@D z0rzUkA7y+8Hun7p)569*rc}#E{OGlswF~Y=oHNcr!?%xY+V-Q4qaGYG2==q$ibg;A z2ThRr0a8l-;TyD{$A`XqmL+4BocN)VWS4sy%TYAedHM3@JwO>G z0z$CQbUYoE@(@Fvr)~4$mN3{w=%O*T&pZUqOwVp$cV?XM?J{<6WvZ(dem{ zD&1HgcKx4UoM1B9+yvfPoho@ zky;jDWgh!>L?jWQyEw?^qBsB$kPYURjS<0xzZ%e15mLF*=-I-kbp7UX6JHU2Z}HP1 zJBk6lzBIu7-RqEiTgJ{1u#T%TpirYd%x;)9)7K`qg-ndWK!3SLvbXp%^pZn48!ndww&q3q8{-kaR6>&kif?r61 zJ%nXiQo7i^VR|R;TH3Q8Xgx?6M&9@P0TsNRTfCoY^4wV`a5-(`xdDi_WDL*{(5SlX zMTUrtg(ZA@sH6QWP~YKFsz==p*PaMY!lBOj*Icak*n5V=0MN3q76Ov~e2jnYo_G|P`U0IZFr2bx_I6!1Mo)uuu=Ey;-h=Wu(LpM$NthNf{R20~gPJ=KiQ-Tr}Y+en9+P5>i*ONMzw?o*}6 zTs9OP2cuCuy=b`Mk%6WCV4Woz)V}`8VWB)`#BepEy3_csF348<{z6U7Y|{v+Hjx`> zm_&*%ZM^xeW}fr%kKu2ci}&Tk%9ny|eyzN;tW^ao39iQgd7c4I9Tn&z&cF?gaAA}w zdeonBVFs;_Epz9gjba*pcKV(yRQ@zBvl~x5%L9B&>ew|5g9wjhuaiPj(@0v8>U4ZB zRbSZas$_n*!(Y8_Z|yqlc^WTfAA)f4ToN98WGNUm#6-_|RI*!DGHnm&EV0$8sKcIG z>J(NO7$}N%LJ)g^KE~*)=Kd;RbbM~-9{PUH?i@Le+j80MBc23gO8s|dLyqw;e(E%t zEhn=cBBozF!#uwfC)~H%uUo}h73;Cdekt^QCo|@P+mi`h==LbX41l_lwZ^x*ovK9h z(6$R7!issD&sLK;?f-Q9eaCML4uxk)Se1Qot6SZMfbm`3;1$}bugIv`syx>Ri|O9d z_PQJ!owe8*O}S~tz5M*~cXM6co0l#L4B43b<-@;@oQA>U9T;qosgH;<1tySm-ODPN zOV2~zy7z_K8PIuM$Zy*y2`j@si3$6%Y&7;IB;d;-XcL%xDZ^5}S4eMrV#6?C6kQ__ zWS*CYhD`8OW7IE$;f26mWo+|_OYI1HIEwQDzdPJ3emH0_&F?L>b-VQNCrwFCg2{$< zP-uBPxmKURlUiKbcZ*Zk8GOYCscN2fVzFvcd-e$~IXzv-KJ*A%T%)ZOzaz0K_F8F% zZ`oNcB5M)<|2TW=sHoTV{~HM@X$c8I5KvlLN;;HMN*bhV=BNM!aPS*CF<2$qrRWBdBE&D28OW!FoaOw-=_79cpo7Oa%F* z4s9RN&Jy+eI40OhoS^j=boPzkU30sp$L3}(EaO=|X}bxuRXpIDbyCQ0VaWXIwq<`@ zK?0Fq8{m_J{b)qEVB1RHY>?wW?wzr0s4n`xZi+>UMzX2E*<(?6Ag$_m8`^v^O3;Tf|T;)Q+`nL;&!>nixc|hewh6SQMPYM_u&su3@ zKTaO#R549U5P=0cj-7jmoV9drAx{jaDp;lmrAmSwR8N{IbXIT964xTCVb*H ziTU8U7n<6<8Y5hb`JWy(2vG$^2OzbR^Iq_)lEtEB1o>rvJ z2{qO%_WD3^L_wv1X2*WFd4%2k=qjwpHXHBGpDB%BwY7iyC(Ad~3}Ta}d>0opZyAqr ze8b$*Ib%Tv2}w~0_pvex}T%_LU zm3^a}aok;2hq2W5D8UTW1hdkGO?wqT6T5C#T09r%#&8py_(7FS`^pvXZl9eM#XpS9 zuL(&{5`0$nLgfYHTi$PZL)w}c1PA6<@(%gciDjRU)uan?`ivyF#+K4ZaBe?D&fd)K zT$)r3hbyGanE7~>5}yHub-%OlV2Y5Dw{>^)Nqo^`%kxG3E9;A!V%<+48#AOLLB|Qw zNa8-=1e<)s!8I21+RDOz5Citb-=4H_5a-bdQy}DSIW11Zim6Th?c^HOsr8BtOqYF6zUZ_p;jG>GH;YeS;xD4wlg3--a|j@gP)@?RIjIP zdLdvwD$>D0y^A-0@z}5h_rIr)2#Q!;RK=yTXUEA^$(c>DIi2|kd|1` z552)F;vLmWEy#3t@u`H;npE*rLlQ+crOLRG4)qoFIM}sYPujjkNb=m1@-7<3!Az6@JV{J89=8;X0pU%mA4AL!u-ovZ7nc>|Zr6IKHaIigZ#y@S{|C*XV z{t5DFsWES-ot+ugF){`eBQ0`JrbwAJAAE`2tdl9X)(4-ng^bxus&d$jcc-OSATh#5 zzY32Mk6}fxeNsKjZAA2)=cj_y?ShVqmev0*<^IcKT}U^M2_6Ov=kOKC=GGSW@%7;&GgwuSd1PT+WKrv_Ke(LBD7v9wm`X@{>Fot-wg; zo&BE``~SA^uW2ZVjfc3^q6x=r4^!V)^O#I#GDhZimuH$O+zm9yIZg1B6{{X|`&gG> z=^@-!G0A2%sSO=?ErX19w!56#n1r3N#g~n;mF7V-`#one@Hqa@Vun8gd>YVp_map$5?hbNP(?&TeHe@TP!b5>yMy#qu<6ax-VkMH15U-6h%Y(Si+OATp z$E@$`6g~{gX=x{w_g(zkv)sHc@9%{&qGVoU8M%?a!2KzaQtn7&hO1SB@lzrzzuHgp{p3#+p8@HHdoV(Ib*Oo=fLo!n zUPri+Q&m&aoz{*F`|S}-$8WPPV%BfB#Ps9-DDnPvu>EbgSQ1bKL-6YqUlhnRR|c)8 zO*OaQHVUkIIh$Wk|G7xhz3pk{5cNCyM*M;&db6pk{9g*?IF|`nFfyUZ_qgL@yQ03) zsi=4ryxE&B9Z6A7`Bk#^zYhXl3Gx?AR;DA)4q7SJovJA7uIg%`F`CM}-xG$kWTmwJ zDZ4=mK?SQxz*BO?(o$Z)Nj(Z4+3EUOvVMlM){aYXwZ(E62LHH4*4IZSGQT@sV6-|% z)8;dk8(_o0oHD%=@&~8KtGgX$ycTmuoetSD$w8sIuc4)rj}-cxUORkBZEyX7Z~*HQ zg34it`a|W{pI>>Gs&`ekW~=W+*)>uA`|Nw`KBvG-T&nlBO0Qf`X7zBDDV=I{ERWjI zY`a*kPM>i|d==&MR21S%Kh)l5Xr1Jp5!0yfR1+>R@)mt0A>V=EcX_0H>RO%#2F@op zS>%5lHt-_RIpQahx8wI_V5^rj1LCSYCjrdiOZQ2_gfyKU#swc1JwmcYQcn9|7Nb`x z3Gr1LL}G4yLn-QYLJ&E+hp;}}mfEotR)V#p7yGx#|NDwKzzI_2iYgLvi0)fRWuiBE zF%I#R4I`UP+v8Ck5)}&$Hxx9aa8ZndyXI?qczk#}iC|&2-N47{6Ri*Lz05oK`_DGx zRoM+izKOIemzAX6*B-MT3jg<0n!*EKB$e4PAuFaPY!@wQyxtGH{eiQu?{p`k2C|sm zgtcbFyTqiOxVN6+^h}$UQd1Arlc#J^94WE6O!(c*7Ua}C_vji<&5KrI*<%0wV?Y!S z<&J;-qw3t3J*m#LzCr>%L;6LoPlIWvXu`ALU4X)5ge0lV`YLdgq%MGbU&)XEOHqIPeo3fd$F9~m% zbVcU!diUi|eWc?oWGIRJ-pW*J-(yj5CH1>?Uk4OOk?UgY-LBeWJs>y@jUKw>1@?zG~n#)D1OMwN`t%>b{-yhInhVRh_i1TZ=k+^5|ck zPISZ$^$$IsW9%b2By5PPMe}CTsEsQd_x#H0|IOg~b;;X>2+~^=D>+A7`%yYZ^L&w+ zf8J(+ZawtoNx%7sEdg6%+Rn*QY1bs>0op31AD5uK2!}=)_kC>k{m2g71zDSTma5xI z@qZNI|MOz;#t{-^m#LUQ8@_h7StxnMqd_EJ^Lgx*?g5x=(y zHO-mwy^&YaJ*n5-tvzG7TKne_EA9^@y9sZz*5*;5+WL z`wIghPg(UFK5>UJ+eM6znM1QVQ0 zF_dEQnv*a}%_*td&t$7tn39+N?yz!3y!cF8eLd}zSuMP0r~SZQ1uL4hBJ7DCh0MFy zG-K#e+C8a$K{$D>lMMt}gkxHyF}S(re&QlLAE}lJ#01CBMlm~WJSHWP)jMG^Wq*^l z{ly*vuLqFa8p2uO8C|m6C3G?4L{o{xSLc$WuLwqdFFG=2|K3-;SE(+6gFuR5lWbKd zgZfaEo$yV6=qJ*dQ*}Q&PR0b5HkfJFbM4Xx|1@lWPMY^?6hV?QW`0V>F5#GHzR)M3 zJ(c+`iNVWy7GjUXcZmVhKD3+9U{G`|P7jmnBiCItuSvqb!|XRcIw84yu+Ea)$ccQ1 z%_FH_brb)v7`)%hHEza>xTff)=uVSilnyCIi_IF(S7ug0ov|m>EYc=2$D=oq`tAs^ z$~KWEtJrxhTa>S5?aS-XW_F8q-hFE^<(HmYEMEpuswu92{Tt((r5YtDU}~$z;rPYT zz08mct|h#r@u0cE#xS2mtE#ev(b*gNyC+)?tFEMUcqm zEKi||(j{^yn(i4LK3uC%7-MSSwEd0QD%psOEmWPzH$BEYQuL6w=LwAoy2|4YrA3F? zNd>5OxZtj6zLeG$u_J5 zPBvNj&TT}D#pNfWsb_gxL7MMnU{^dP%A*cRoW{TLp1~-V=F($o@^2Gxxwel4;}mJM zd*w!3=2s?fn{%E_QZhQ2AmE5Cf+LT7wd=dh7!j#-vi!W1zq{H4c}6$MB_S5JHxF&u zZoS!4>k_SY4kBC$bkLpNNzV(bWvzfrGX1Jc{re65$K{>wE12x?dZj-wE!j9Ovp^@3 zt$jVbi9lo_I(FQ2=Z9gsZM5rVpbT(+g_}F)DYk;=s^zng@o>l@=Vko_MK!mE+hRh( zq;n?i59!=XM+|B0XExsbDgpcZ2K!_01u%P8^B8GfMo24@*K{n%^5S)t6z9dfw=2oO z=@CQVm>_L}aqOw^nKqFQhP}Su$VPI`aZsdp7?>g(VbUl;AGx^3`YR^&kM>7PprB?_ z3Do^bu(X|w{p$|yosPFP)rjheedEU0BX4)a4=rAeCW&7V^{ITkHe6ja<|O1<;1JYfCbY+e~~JgMhJgXup{rw)h{RsG$EXrw=`NtWBDG>Cf&V^LYQCj z-V0~ekRQswgBgSvaLXHLL(GUmKZyL}9hWUK4}z-&dIE3EHFpYUxW+6Phu_ZC3!5DZ zza!V^;{wWVq9n>BAyHivUo(y|E08@25 zKs!civGcS?bbzt?-jH)h_hlkE5P8#lSGVPi3AH!%$CsQzqXj5DKqdTo~O93%tob+dRqur5i?FSK|I za&O3dAc=K36A*Iyc9Gv)Z?|x>Ur^j_O`WI~DwQb`iCzza*d_oO=4PAnN_LzlG z+K13?ItjTTZvgdPt_Eyjd8KOQCPe;^mMM5Kln*{>(WENo%Rb22$w0w(fTGuMp@7$Q6 z4_@nWHao`@As0gRDD%5S#kJDc{omZk2+kq#N=~rx#V`rRI1=+X3ejGLKvCfdYe(S& zB{(h%S`qry3(xZeL0Sb0i=MK`^4Gt)vAqz5;VD^qI2iUEsjadhCf!`u$9m z4V+3aLN*E4I!xR}aCWBv7)NTA_8mg5|2}Bm??os^84m3_Yxy2vk6ZRz5@^CsfGUit z1jo(3PLz4CUEJuN3ReO6oCh{NDmKT^Z`hTYK2p}S;_HMmEP@v&Fj4id1Q(1XD$W)$ zzl}O_z&rEkYiZ?y}87d-RO->mLVl%Vma`eZz0LIZbY@Gw>M89uzr zU0$FoFAvtID%}orm=0qk{;VV5kOya(ErdbLjLz+h`<~kjXMdq7xt%kAghim=p!hA# zr(a3We_PQJjq=$0V(Y2vPu5$u87AI1RWD4r?0c0b6clvt+PIIoF#0UN{_N;BDD)%p zCEBVFk{sQ<3sRtqu;|2am5}L>;jW@StA10Qh+vYG&nG5>VFxEhy4Z3N*UaBs8In5e zIL5|FW6!_#Il`W^%W=R~b6&29K90k}q&Yj%3+P3C(T*X^124MECo~f@*M)~ZDI_>h zBBH8=LR%pVt37G1Ek5b)Zg%)p6!wpPgm*RK^ccm6OPU8SnMkBP7qvPPmjtYh$^N=n zrq@9&hzbl~IBXdgPFBkYNBui>=fjN1&*%@CA|I-J8QI*Nt~pRXR*SA#qAM2j{9RTm zJ;cj6ueaL0jNvLH;lUM6sBGn*#av{D%+=DwiHCZ^cpLP{`J-Xw+Y24W{nskv3|vjxNDll^BL%Evz|}x zYDF6KsjCjs9%<*(mrIz3YKM0|>LfJFnHkMo8VJiT%RV>Fp@N1b&h=D=R^`3(2+cL; zWB8pLsft+rS*UtPN2DYJlyxRzrE2)uE|@B6ONly$4O53aonaF~O)kdawe9+#Fq6D?~f%w$MUcRrCR(ZL&k zfj$$YxKR>jsOtZl-4un59rhBM52?vXnhDPhP7~1TjEr5DBAXECiwuIARq%{ZhftU} zM@u10I^IU+f6rq%QZMbrD-sT%VN>#(#~8xk$!q)E04aR zD@ofqnVK}%oY}_IFgn0k9POr&NGLKMFlq2K6qG0a<4E4TC};)`8z-n4*P^Ul-AMBw zpB&56rW}3aST&{l_2TQLE)H;&*XOaj!sMZ1mOA!gwvFGK)ZHNWOTST`IOCb98-G{T zr$4u`fSTCef zo7v}H+0!CDjpIh_a>PO@qZT<;XlJqGFQ)z74I)Tk$p%eaYY*J^K(H9!nF#H8+>vSQ zCkpL*Lph%??BoHCCq4O-bD#D&AB0!i6cZ|UGtUV!gNCQz-@`VP*<)?Y$T|kh^Q=Py z(|_|+K_P<4(2doq1FbMpcc*J(i}5i|oYU?RZ^2HEH_$QL{8scP&axmzT+pyZXn6jW z69IIri*c}4s;RXTk%PD|(8tiCg3$KTyZ~vO@;^iMzrT3zViTd&;ewAK`Z8`>T8SQ2 zbzi_7)gu$FYRH+ymj}zG0LS?y=l!vCG;6d~7sW;c4>f;2@6~5rovx+}G9RJ-Cf-RX z8&V<`+TSkBYn~|oqqp%ZR)dLh^rFCTmrm$NMT*25X5GBFnq?g<>JSEK77?L?J59xM zRPCWfyq$HFvD56Tv5tI0M>TO}7k;PB@swH{Uqf$&oyBaW z*~;5l$ZVPJZw-|P4{BR zrPzt{HE}-Y6fE>b3B*kOkmJ>mrFEHg#q6q8pz{#l*||&LQyTGP#YWWHBrNYx)uK1m z==Z{$@JwZ{_!_d<@yjC#6hob^Z`<=&@EV6p{rd_B*^F24gWRCQ$&jW2Tv}N51kFp$ z7d4Lv{Z%&G9l}PaUvSfTu|LeWN;Y2EK>UpNdBf0pw5Wh?(10~}a=dKAV8*^f__sEg z7{!Qa$leR{Wi@kEjSrh2)p8llva0SZLXR=ulsW|MwLj}TTkOA3WQEFI$2caQv7C}~ z!F{gvNm`aNiwm+F##LYLLabVW)Vi@S{8<(Dzv~*j;_Za0pJ04I)FuwKKYlqHS-svC zMtDj`tu}F7sy-Tsa+N?NYKw7dBf-YPhJ9Em)DgNKmW8b>)uFE=eDFd3Zmf+4Ov-Kv zit*bC!p6G=82jUuJR=feyx4M<_8rkPoH-9&pfByoEGF`2_MCwHa|J?V5K(5FjDWWi zFVj#|?D@O#U&r+3sWxJ6wVy?rCgW9J)~|_wy4@pVG|cqo0x6o57%H=oVtu=+gCUNPaj>=RFKUUXzWL@a7QjD`&3lPY(Un>m>E)CQTc!0}SYNiL^{)j- zWHd~+vX?=78Tnjm{2~%b7=?-BH(2H58!8?ksBF#Le%obM0a2~_on=7b?SXBqvUfLi zCR{aYW|bG4cjnzTZ3R=gf$P)mZLLFrIQqMPw=GztQC{cYorx@Z@swA4cuZc5p`rrOn&3oAvTa(FFZQbQ26nZG zhCK5P=}VXa8u$DqUc`I*!@g3TT<63lix&ym2`Yxn2DQJn;kOaF7g9k4Hpc4W|9({K&JVAHN6|Y<)Q6y;)9uOg&(s z{;l=4%Hsig+nry$fdbDm` zn%fX#NOz0FL&oF60@clK4#tsoty#tL>HL?DVvEt@R5k67ZF%Og{ww_!VAsR#0))=%TVYl)ko#t82`gw1c^FLK0hkGkcrVnpU zclj9a%?_GFve7D8$DsDm>$k+r#mwEzrh(_bT`G!*y)*_L!BrrjW^o_6RYQfdBEgdG z8>h;;nJh>w8Yp%tpi(7l(c*95p`=*8@WM`YeJpxfyc5GEf$;D9)cd8(r`LJAOfmZ8 zCF3Ufv=N2&_axqHnSLu5ShBPf zt~~=vxlOWb8CKLA=8?;3%^ahYR^StFQ1bsm(|$~8Gd1x3D^U3tb)Hua*k?ImoYRsw z<%cqk?PY_0e)s-1Qu6VF1?)18aD$Agg~AE=aukrKi>%?&OQIMG=(w&qV+rET-S0C% z?Ee_k7ANkOs$f4Exo#vK9MRgg`|joFfpS{fWon=jvK*WCj@TFSe-TRF|uzqObt z)fC?Uxe&3bylIo2g!>XWUho4rnJFlWGzEM$B|I8wc?1LmQ$Q~Wv+gWwX9viGvcrDf zEc=Lhz}bA9kR_@n;V`L-e0;H0kQMNn?FB=TXo8{s8O~4DEdVA!!vVqa7*v_E!x~i@ zE|lzn%7AOdmXZB|?oX2EjnUi4LcI@^cDX>dqTI)SI%~S%Pso{Q7O;_THTiHJ2OEYv zY#x6fjki`O7Rjikq(3TYQ`MQT=qAY8GAqa-M}CB8tQChC@=E)FhzgbU<%g5DgD0j| zED~0PO9T^zJ8Ayd(^4g3C$E?$ava_a(BGKft_?ss?~%S zy0i!2J7+t<>{X7Tj&<}COeekCSBr#Su@Cl|P-#C3HqE9tw;EHz6eodu`IKwK$=9d86krC@<#0|ao<@VaM>>14*R&qKF4b12pq-_5KY{fjOHgrXl-Iy zUhxO=f<{SduQ~ThT~vhnYc3X+Pc(ALc|docTo`Baf18Qu3E~ZVU{6cSb%(RJWiMZ^TzN0LueglziS-xDo;Fs=|^3RA=jd&NSY$fqY6FJw2KU_+Nei$uv@(_AVo zDl+ke)oxl+E>==^iM3rAf#BziqfqPvLSYUFe@l=71v;8X$d(@vMrm7sr!h2gNOE~H zDZG!>0<@zzhEHVR(|zFX6Zp?ndr5k9QO7>INPz=Qw*2tRWz@05 z^~#-Rk!QyWW`UW(*9}wEIYeGJ489S27k!CabCiRgC!0$(=qw{iEcxn*=AvEK2W-}w z7Q7(6BdEc(9+uQ^q^R5%3<1n(RmLUXTd2%^1LV;MC@xZ8rJZI@bnZTrZQ`3cg&^5DKi5r7 zK$WC;E660h^2NVsXx<<#gD=L6d-rvFoApnZlNYftmsB{Ogd(aX@SL@WVaYHz$zi%H zW6pVvtnA14OV^t2gLjV+*8l~~^!53vuQ5||m1Bu?MB^i>{Xr2dXU9;>BzcC@2$ug) zbXm|)?%cVPt)lHdHX}(<5!SN1knEpUg}qQl1`iv?hdInTtvreE>z;xbsKknq7?0e} zADXrp6o15JJu1_KR}#eZvRWTUV0Fs(wy}4JPIxYU3oICgc3ka+rpc4=TyUa@x4!)O z-CYp@`}D_`8?AC~EszrD-VoHK7#zdq2hPrx-l^QEmSE<`KnSOj1aN{{_wm5t+Tz=& zAjiLg!S1HBiqHC=Z!HK<71cOdq}Nv^8^Ta5xl8ueIGk+l2K46**?0@#h142LzllU! zP}}FEv!HOxT9Ls}y+vmKX{VhOWGRc-$VWq8Lk>8IJKry?qAxfpe29265xFWG)w)7= z@~06qN^8~qI4gUq#3&2@Aa#E^QVx2B-%{YdV`kdyhic+3%G7XW!`uh7r;Ym**x!dL z28Gcv-g(QQ#;Q>kANHvsW0rU9EZ@cMsK@vhVJ>R=jfkdx8gKEwQDO zU1MJp#%wz_d5Qw1b;GSPh|}*!yYJ3pF_SaUNYXU7MsDu5N@+knYU*BeRk}l)#3{B;ZIn z-1cEqY6Rl{AiU>n(4w3>zzKUh98vt9Hvo4iTm$K#(ha-H`uIkSp9}a6D!PR10+r?f zM|%SXh|aDB`1wsBJd?EmA{a>7SC_5NCTDZ?0KXmD2gGESNU>D=OLfD~>AwJ|fJz_R z0N)o2q9)`F63g0`FmV2_`imS|$@c*0PWXRiFXH!G{YVlUj{mxL!aYKl6%JiE%oa zGD^g3xUm|PqLaqCu?ZXGxGB(>&+0^fykM%zP=i#hj7k8hsTx+gT0YloM}EcE9HyFW z*5g(@xkr@>@j&D4Y}tBAb!A$=#=rNtM)MFk*|n4Qtmf9KHDJh8s?sn8!g1j%_BsJ@ z>d#wHZRy&kJL<7X^>f4@OkPEPK!K!iQ(^r<@mdROL(o(ltPQ4;@cZsZFZ~I3apo4A z&;?cw1m2x~kp&RR38Js$yaw%m7L=w(!X2f69Ol7Wp=V>HKIiKm${pULsiZ4r^j1~KtRHQf)Kwzvp8*BmghCmvKy zihh@)e?GGrWP}%GsZBjmKE>B$V1KyTT1(Nr>u>OoMFZu(DY%%fH{dHa!(QXZALoF8 zPrUF$J4ItKx7`f|7lDD?<4pJQ2^2dcOD+ZChgKj+YxpM!cR4u?Q$5O}dLuLQJMIex z9OcvD8xF1&Xt

As~dPIO2|gBtXRI|}qRxXQ%2I5cnOLt%v&uZd_q`dSP0w6=-c zYPE!0@Na-&j*4v>;t&+oH(;zK(hS|jH@ombXOe$*-X#|*_*#*C%M9R>6o6tm!r4hl zpUdOQ$$mXGFai1b8{dLKpm3{;MzkC~-!^)d%T&Ei`2})6!JpDiEt`t_>I6V|!w%NS zJ=9yuL`c(ErsseiCC$fxHDa)}12FJ4Cdde%~ybzC7uAqjRy*9n_t*lwKDRp2RpTfq8@}d zmYqG2zeJ6D0O+dO>RmH;uovmeFwif{Za8~)oLPR=m@1(P68ad2Z$-)pOexBU+M3fD zukC%wyd5tDkA(P*y&Ha{dkXp}eBkpsRReiYm6`&+s?tmwX?r_&Wsw>VR~jpKrX2mN zv`+J<=DfNrB^CxdXG#W?*RxlOFGn0%5(;J4)f-+E$E0UO8D zYIdOLc3Y+*|7jwrA*R@4Hv`!Hns5xg$mMEap(=y7PKAsE=OiR`tk$J&c84m!RUbTF6vo1!hJO*Q~=dYrHvT?`%JvG zjf44yyJiY32YD;K(dP^j?xaDNz`hvZ>oui*yLwFP+;9ooyIVi@k|w5wU};9vQx8$E zwf}sGAi#ycw1;r+VI#tWD<(x%LZ{9^Px1wYWAH6)S0G?Y0$cvdv>ikI#J}*1X}b;7 z)4NGn(MBD7xS;fsOcT$H^Ny}NV;BkE@)EWE!|+mV+9C-?M$qg7`((1gF3P7$;E)YD z!S{*La01R-caa5DiI)*ljC6bKbJ|2n(xU8F1=mXAMp+%V9RWTkBw;we zEr1#*%sbC428(cTd)SYHw(0tL!}7x0Q=dJzHQr5d^{`C=ds92|wKuG5ccBg99z+Uw z(k~Wv4JO9Jmaegm=Zl*$_Sv-9$ARcu{U^9#GJkT*E0B1he-Kzp>A5h>Oq>ca&m%V5335o+CZhVd3o zdIBoz7T?mlIDha;`3`8dxf}d~-4BWhuRRv)bO2@R$rA2mj*1%zDUPLx8)TTeVLD|MKYK$~;{6yK#wAOq*X4<FiTQuxK6 zzd~?E{2aLugf2=Q5CR~l;JvLNV}w~iA!XHNTJqZ59cr=MZ^X9$g>zk zH43mMSriN!K*87E!Hvijgt))mB#H*)M9Z9kr^=*?+t}Ummo{a>mu8`D^PWu z$wl%C1WfM6!EpRMfCQo_2oI z7<7m+;dCe=+>!1;c+B_76*zydTt(-(NFf>fTGS0GPH};@L^qD?UP&+D${YB3o>>$S z?_Xj%aCgA%F=_*9HvW~oFaAXndd)FtZ?cz3b1+Duk4d%N8MseZ!0d4lqNQ0Y@jTuY zW6rYn`Fm;?K>e%JymI{ppgFU>sWa_Ba()3$rTB}cj)q{Tf;Jws36AgA7n204&0mnYOk8Cj%1SUHzL_~OiuGdkNo!Oa zG@*tIU&QU|?5Sk}yLD0f#a8~*4mdq0n!%Tlz8wrOqp57+BH6%wvFJDH_W1v$t^ zfu)S*i7r~wnb$@;93(en5j~)OH(yUui;JF()zYat7dxrzPvO||42#uru~4i%VnzWt zM}IGe2aDJDw=^R$+#?K4X4D?B?&0k2l?qU>RD=_R)Si3}KS7=utoO0qqEJ9TsXCU| zCSRWzMD`;XALBLtJCoLlc~tm~uI63imwBm-$8rklxx;M8;PYSNtPb z<;2t1FQ>6CMgq~ej>wg&-0}0d{+CE=!o~DT!|56TCw9h$u(r`G4$*R;NY_>)TM~N- z17=WmxfI5QvQ+mt!s$~R{<{v-X+Mdy?~c7rALJPqwO5a<__^qo8aqHM=KOa@Wp~4_7h|CLrt4Fk_K+-v;}mr&bts zx3Y`Y(?Zz8`K}_`RkMEZj2X)DuC(6P8ZPOo9L#8rbJs(|lbj}lnQrYUUZat^ zIkqsi0@fb=SR&NwmNzUz*GrwK()qRNm(R^QtR)KJ z%A7~&lh@%%-yn^cqs%s3$E|e8AlB{Pr^t}6#=)ni;1b4~`!I|}Tf4ch7jCI~ z_q>--uac2>E3f3C3AEvQ#}Xei7(=9CV#^AcNL3)X@rQ4xgvtO|d(SP?x;Z+2fojE> zz2yrSs9t6$Jtq-cF|deY#xN^D&UsXqJK@^IkD#F|Xy-S`J?D0K zk9>cBBv9m@Ix4??2izr?9G<0R;v8A@KvQA%STB*tZc)pk7JMdv;;p%52K`5pE%h&w zEzN(CY_<1R<^R8fY*~MTYzc+1W2{$2$B@HaB&yULVfJ;qhy944)5pyPd|&jJXZr~Q zo|_E`!gB~Oz_mEI|{-Htkie*3mBioe9W?uD6)talSn8s7t|@05L#EeB5AW$xo6#~%iy9H6aY*VcL2eS0ouXGVdJ zSO_Z+OTU}R3Yrj@js1AMGg{C^492jIrVx1d&YH&0^3iIZx#I78;GU?CPP_?W99L9S zLK&=&Nff1b3^Y?{5fZ1LK=-bal1<`K((On?bdzlH{sOyW(KUUmZ;KkhV=Y3E~ z*u3x&cb?ebpp7tAe*_(DyE*8@P+K>F3waD~0kgYKMWE|oIeQFG~gVjoxAiNl&XjYWj zdri4-WuhS%eYpGQ7|e)O)0JJr&u$;1p9EvPQZ5}UIurh>*+w8LBwH^=Sw_o%@WJkx zeo8lIVRYPCOhgM00_thJp8(sf^|QtUP{F38>v(#!F@o<#;K(+wcv#IV0{gx-YB8aN z`c>p`wyAQdGemC!Qb}JAl@CaQSvfD%&-32%a4gz#*U_QaxrkQLdO5jM2~1_;hFudO zLdqLo=uW&@HV`!%Qb~ktaGz)0`xYM}xwC5FRM4%t zpiR}7_y~r9m@LX_Jr&xwC;Dkyqv?zwRyT;#NIn7jpJe*<+;T)d0U*EJeV@ypEf-Ll zCSe%fsgYRZyF#fVjuTwO0n4wK4SnVqEvfaT z1erm-f%PfOF;V067U?)qKdLdPZ>P9WIeto3PKGw6I|IskcWMbo@h1;2$by|=fb?yH zx<B^3brfj7gJ2dc5nZCqmjq9d#gOI2K5pfbhTSxei+ye@=EcH`19GL%} zOhh@kuM1Bluk~;yI;9p#aM+B5P}%HuZMh9bJkaJ1;=`w$wbF*}RcJcG$~GSJJ7j$Y zl%LgOU-GL)lsnjc3u(IG`BWy|pAI)uhA<4DFgCqQCb1$i#Bmk^OhR@ zDk2l-x%GC1%01xH)zg_c6VxBg+-zios!%n~r$hT&vSj!$h2^zS`e;#9{yK=iye?tsqLa}mn^XMKpETJO=7JrtD z>Qv%2uBn|dI5yz#e8ua}SpX26&h=20A@bbGBOk47`_+w-b(MCbfi99WiL@ zCbD?X9o>4zsPzt0w$lnp>@j%dye9o$~uKzc%HvMSK`ayg8@zb028=LU~dZLM<;KyERc z8f()ZUU6HNPZ@4vd7g<(_lI$PHJ)A1xThIpr8yc%1eS`rCwKZMNaL>6>4G#_h4jz= z{ujjU6D;>ypO2zWSV@H#TLBhpb_01#cPzlk~FbZVl3tZ$n0@Z1ip4?-?-&l5CUn%QN_ zo7ozfy2+fl`aHclKc)*kv40t8R*A{;Wu}WbfkoG6;`ljzenz;!g)wtMEb5u`oMpqc zO%3-=9u;`!=~nwjswjJ@c&Th3V+(hRtqXf`vzMUE`r~28*yXAmWc~KAL+a_Bz^yX6 zr_+B{LkbFjy>qS+H*??n4GvEOs9P}p{G4p(4sRLWds2HC_b{h^GjzSGy7;cRW7srx z$fwe)i01)YLG!HMm1tHPeaq`~orH<&>$ZZxJ`i~h=h3}TLW+C@GTJ?)xWv2TEkaR~ z)&jWjyCQDB6Ll#dpgNI@Yx51QCgQd8N_3s_caUGem~uTeRwul_P7n+_pFF}1@c5`c zE8fv|6F2Qc;4(1=XG%nNl?O=e8N0x(-CIY*l(B08z`#`DQ(}m->J{kvaQL>Rysr6W zM0O9(o2R0dT%+fClVQj6Cz`bP`#E;0+`L|8)ltqAl0zpbU-kZikBuWH0~+aY(n=3Q z$sNa-igs=~M$xtHw!Vs)^3y8SE$DFgG6_E?PBTsgE)zRUkaPk`NR}B=vcn1T{$(!+ z_zVEEFRpHt=Pf*0L=wsZfj}cw&-9Y%qSP^uK(CC*MWA$LhBJtg5~(8Y^Mc4_*S++svOf-+2mE#cYD@yh+A3Z>Bh=1nh_F~>^NjK1RDpY^yQWc

Dz+Ievo`Ixi{OQ0mj~>+7$6S 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

Mr?E}HIpUJ99JwjF}TVmwvd)o#l@u}}6Km&s;? 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

+ +
    +
  • Welcome, Flux — the new servers in v6.2-beta.1!
  • +
  • What's the problem?
  • +
  • Using two operators improves connection privacy.
  • +
  • SimpleX decentralization compared with Matrix, Session and Tor.
  • +
  • What's next for SimpleX decentralization?
  • +
\ No newline at end of file diff --git a/website/src/css/blog.css b/website/src/css/blog.css index dcbe842785..f92998f301 100644 --- a/website/src/css/blog.css +++ b/website/src/css/blog.css @@ -238,4 +238,60 @@ h6 { padding-left: 1em; border-left: 2px solid #c0c0c0; font-style: italic; +} + +#article table { + border-collapse: collapse; + border-spacing: 0; + margin-bottom: 16px; +} + +#article th, +#article td { + border: 1px solid #d0d7de; + padding: 8px 16px; +} + +#article th { + font-weight: 600; +} + +#article table { + border-collapse: collapse; + margin-bottom: 16px; + border: 1px solid rgba(255, 255, 255, 0.15); +} + +.dark #article th, +.dark #article td { + border: 1px solid rgba(255, 255, 255, 0.15); +} + +.dark #article th { + background-color: rgba(255, 255, 255, 0.1); +} + +#article tr:nth-child(even) { + background-color: #ffffff; +} + +#article tr:nth-child(odd) { + background-color: #f6f8fa; +} + + +.dark #article tr:nth-child(even) { + background-color: rgba(255, 255, 255, 0.05); +} + +.dark #article tr:nth-child(odd) { + background-color: transparent; +} + +.dark #article td { + color: rgba(255, 255, 255, 0.8); +} + +.dark #article th { + color: rgba(255, 255, 255, 1); } \ No newline at end of file From e5c83b20c91b3a030d4e12a473782550d2d48801 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Mon, 25 Nov 2024 15:52:30 +0400 Subject: [PATCH 065/167] android, desktop: fix operator disabled indication (#5242) --- .../views/usersettings/networkAndServers/OperatorView.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt index c61a9f5ef7..e5d6f9150c 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt @@ -145,7 +145,14 @@ fun OperatorViewLayout( Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically ) { - Image(painterResource(operator.largeLogo), null, Modifier.height(48.dp)) + Image( + painterResource(operator.largeLogo), + operator.tradeName, + modifier = Modifier.height(48.dp), + colorFilter = if (operator.enabled) null else ColorFilter.colorMatrix(ColorMatrix().apply { + setToSaturation(0f) + }) + ) Spacer(Modifier.fillMaxWidth().weight(1f)) Box(Modifier.padding(horizontal = 2.dp)) { Icon(painterResource(MR.images.ic_info), null, Modifier.size(24.dp), tint = MaterialTheme.colors.primaryVariant) From cfc21dfb51c58c2d129992a475783195037f1457 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Mon, 25 Nov 2024 14:15:32 +0000 Subject: [PATCH 066/167] ios: address or 1-time link (#5246) --- .../Onboarding/AddressCreationCard.swift | 4 +- .../UserSettings/UserAddressLearnMore.swift | 38 +++++++++++++++---- .../Views/UserSettings/UserAddressView.swift | 6 +-- 3 files changed, 36 insertions(+), 12 deletions(-) diff --git a/apps/ios/Shared/Views/Onboarding/AddressCreationCard.swift b/apps/ios/Shared/Views/Onboarding/AddressCreationCard.swift index eae64e4465..9cf755be78 100644 --- a/apps/ios/Shared/Views/Onboarding/AddressCreationCard.swift +++ b/apps/ios/Shared/Views/Onboarding/AddressCreationCard.swift @@ -87,8 +87,8 @@ struct AddressCreationCard: View { .sheet(isPresented: $showAddressInfoSheet) { NavigationView { UserAddressLearnMore(showCreateAddressButton: true) - .navigationTitle("SimpleX address") - .navigationBarTitleDisplayMode(.large) + .navigationTitle("Address or 1-time link?") + .navigationBarTitleDisplayMode(.inline) .modifier(ThemedBackground(grouped: true)) } } diff --git a/apps/ios/Shared/Views/UserSettings/UserAddressLearnMore.swift b/apps/ios/Shared/Views/UserSettings/UserAddressLearnMore.swift index d4bc0959c9..22efbfcb85 100644 --- a/apps/ios/Shared/Views/UserSettings/UserAddressLearnMore.swift +++ b/apps/ios/Shared/Views/UserSettings/UserAddressLearnMore.swift @@ -15,21 +15,45 @@ struct UserAddressLearnMore: View { var body: some View { VStack { List { - VStack(alignment: .leading, spacing: 18) { - Text("You can share your address as a link or QR code - anybody can connect to you.") + VStack(alignment: .leading, spacing: 16) { + (Text(Image(systemName: "envelope")).foregroundColor(.secondary) + Text(" ") + Text("Share address publicly").bold().font(.title2)) + Text("Share SimpleX address on social media.") Text("You won't lose your contacts if you later delete your address.") - Text("When people request to connect, you can accept or reject it.") - Text("Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address).") + + (Text(Image(systemName: "link.badge.plus")).foregroundColor(.secondary) + Text(" ") + Text("Share 1-time link with a friend").font(.title2).bold()) + Text("1-time link can be used *with one contact only* - share in person or via any messenger.") + Text("You can set connection name, to remember who the link was shared with.") + + if !showCreateAddressButton { + (Text(Image(systemName: "shield")).foregroundColor(.secondary) + Text(" ") + Text("Connection security").font(.title2).bold()) + Text("SimpleX address and 1-time links are safe to share via any messenger.") + Text("To protect against your link being replaced, you can compare contact security codes.") + Text("Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses).") + } + } .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) } - .frame(maxHeight: .infinity) + .frame(maxHeight: .infinity, alignment: .top) + Spacer() + if showCreateAddressButton { - addressCreationButton() - .padding() + VStack { + addressCreationButton() + .padding(.bottom) + + Button("Create 1-time link") { + + } + .font(.footnote) + } + .padding() } } + .frame(maxHeight: .infinity, alignment: .top) } private func addressCreationButton() -> some View { diff --git a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift index cbc3e9b79e..8f212d8678 100644 --- a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift +++ b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift @@ -304,12 +304,12 @@ struct UserAddressView: View { private func learnMoreButton() -> some View { NavigationLink { UserAddressLearnMore() - .navigationTitle("SimpleX address") + .navigationTitle("Address or 1-time link?") .modifier(ThemedBackground(grouped: true)) - .navigationBarTitleDisplayMode(.large) + .navigationBarTitleDisplayMode(.inline) } label: { settingsRow("info.circle", color: theme.colors.secondary) { - Text("About SimpleX address") + Text("SimpleX address or 1-time link?") } } } From d912fe07a1f37a59ad8ede0883e7ba4c61185675 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Mon, 25 Nov 2024 18:51:49 +0400 Subject: [PATCH 067/167] core: fix pagination indexes (#5241) --- simplex-chat.cabal | 1 + .../Chat/Migrations/M20241125_indexes.hs | 50 +++++++ src/Simplex/Chat/Migrations/chat_schema.sql | 49 +++---- src/Simplex/Chat/Store/Messages.hs | 135 ++++++++++++++---- src/Simplex/Chat/Store/Migrations.hs | 4 +- tests/SchemaDump.hs | 6 +- 6 files changed, 186 insertions(+), 59 deletions(-) create mode 100644 src/Simplex/Chat/Migrations/M20241125_indexes.hs diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 1a65b87d0b..23071423b8 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -152,6 +152,7 @@ library Simplex.Chat.Migrations.M20241010_contact_requests_contact_id Simplex.Chat.Migrations.M20241023_chat_item_autoincrement_id Simplex.Chat.Migrations.M20241027_server_operators + Simplex.Chat.Migrations.M20241125_indexes Simplex.Chat.Mobile Simplex.Chat.Mobile.File Simplex.Chat.Mobile.Shared diff --git a/src/Simplex/Chat/Migrations/M20241125_indexes.hs b/src/Simplex/Chat/Migrations/M20241125_indexes.hs new file mode 100644 index 0000000000..2115de09a3 --- /dev/null +++ b/src/Simplex/Chat/Migrations/M20241125_indexes.hs @@ -0,0 +1,50 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Migrations.M20241125_indexes where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20241125_indexes :: Query +m20241125_indexes = + [sql| +-- contacts +DROP INDEX idx_chat_items_contacts; +DROP INDEX idx_chat_items_contacts_item_status; + +CREATE INDEX idx_chat_items_contacts ON chat_items(user_id, contact_id, item_status, created_at); + +-- groups +DROP INDEX idx_chat_items_groups; +DROP INDEX idx_chat_items_groups_item_status; + +CREATE INDEX idx_chat_items_groups ON chat_items(user_id, group_id, item_status, item_ts); +CREATE INDEX idx_chat_items_groups_item_ts ON chat_items(user_id, group_id, item_ts); + +-- notes +DROP INDEX idx_chat_items_notes_item_status; + +CREATE INDEX idx_chat_items_notes ON chat_items(user_id, note_folder_id, item_status, created_at); +|] + +down_m20241125_indexes :: Query +down_m20241125_indexes = + [sql| +-- contacts +DROP INDEX idx_chat_items_contacts; + +CREATE INDEX idx_chat_items_contacts ON chat_items(user_id, contact_id, chat_item_id); +CREATE INDEX idx_chat_items_contacts_item_status on chat_items (user_id, contact_id, item_status); + +-- groups +DROP INDEX idx_chat_items_groups; +DROP INDEX idx_chat_items_groups_item_ts; + +CREATE INDEX idx_chat_items_groups ON chat_items(user_id, group_id, item_ts, chat_item_id); +CREATE INDEX idx_chat_items_groups_item_status on chat_items (user_id, group_id, item_status); + +-- notes +DROP INDEX idx_chat_items_notes; + +CREATE INDEX idx_chat_items_notes_item_status on chat_items (user_id, note_folder_id, item_status); +|] diff --git a/src/Simplex/Chat/Migrations/chat_schema.sql b/src/Simplex/Chat/Migrations/chat_schema.sql index 0dc68034e7..6f944157c1 100644 --- a/src/Simplex/Chat/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Migrations/chat_schema.sql @@ -627,17 +627,6 @@ CREATE INDEX idx_contact_requests_xcontact_id ON contact_requests(xcontact_id); CREATE INDEX idx_contacts_xcontact_id ON contacts(xcontact_id); CREATE INDEX idx_messages_shared_msg_id ON messages(shared_msg_id); CREATE INDEX idx_chat_items_shared_msg_id ON chat_items(shared_msg_id); -CREATE INDEX idx_chat_items_groups ON chat_items( - user_id, - group_id, - item_ts, - chat_item_id -); -CREATE INDEX idx_chat_items_contacts ON chat_items( - user_id, - contact_id, - chat_item_id -); CREATE UNIQUE INDEX idx_chat_items_direct_shared_msg_id ON chat_items( user_id, contact_id, @@ -887,26 +876,11 @@ CREATE INDEX idx_chat_items_contacts_created_at on chat_items( contact_id, created_at ); -CREATE INDEX idx_chat_items_contacts_item_status on chat_items( - user_id, - contact_id, - item_status -); -CREATE INDEX idx_chat_items_groups_item_status on chat_items( - user_id, - group_id, - item_status -); CREATE INDEX idx_chat_items_notes_created_at on chat_items( user_id, note_folder_id, created_at ); -CREATE INDEX idx_chat_items_notes_item_status on chat_items( - user_id, - note_folder_id, - item_status -); CREATE INDEX idx_files_redirect_file_id on files(redirect_file_id); CREATE INDEX idx_chat_items_fwd_from_contact_id ON chat_items( fwd_from_contact_id @@ -926,3 +900,26 @@ CREATE UNIQUE INDEX idx_operator_usage_conditions_conditions_commit ON operator_ conditions_commit, server_operator_id ); +CREATE INDEX idx_chat_items_contacts ON chat_items( + user_id, + contact_id, + item_status, + created_at +); +CREATE INDEX idx_chat_items_groups ON chat_items( + user_id, + group_id, + item_status, + item_ts +); +CREATE INDEX idx_chat_items_groups_item_ts ON chat_items( + user_id, + group_id, + item_ts +); +CREATE INDEX idx_chat_items_notes ON chat_items( + user_id, + note_folder_id, + item_status, + created_at +); diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs index ab8a52a98a..a79eb98f14 100644 --- a/src/Simplex/Chat/Store/Messages.hs +++ b/src/Simplex/Chat/Store/Messages.hs @@ -1145,27 +1145,52 @@ getContactNavInfo_ db User {userId} Contact {contactId} afterCI = do getAfterUnreadCount :: IO Int getAfterUnreadCount = fromOnly . head - <$> DB.query + <$> DB.queryNamed db [sql| SELECT COUNT(1) - FROM chat_items - WHERE user_id = ? AND contact_id = ? AND item_status = ? - AND (created_at > ? OR (created_at = ? AND chat_item_id > ?)) + FROM ( + SELECT 1 + FROM chat_items + WHERE user_id = :user_id AND contact_id = :contact_id AND item_status = :rcv_new + AND created_at > :created_at + UNION ALL + SELECT 1 + FROM chat_items + WHERE user_id = :user_id AND contact_id = :contact_id AND item_status = :rcv_new + AND created_at = :created_at AND chat_item_id > :item_id + ) |] - (userId, contactId, CISRcvNew, ciCreatedAt afterCI, ciCreatedAt afterCI, cChatItemId afterCI) + [ ":user_id" := userId, + ":contact_id" := contactId, + ":rcv_new" := CISRcvNew, + ":created_at" := ciCreatedAt afterCI, + ":item_id" := cChatItemId afterCI + ] getAfterTotalCount :: IO Int getAfterTotalCount = fromOnly . head - <$> DB.query + <$> DB.queryNamed db [sql| SELECT COUNT(1) - FROM chat_items - WHERE user_id = ? AND contact_id = ? - AND (created_at > ? OR (created_at = ? AND chat_item_id > ?)) + FROM ( + SELECT 1 + FROM chat_items + WHERE user_id = :user_id AND contact_id = :contact_id + AND created_at > :created_at + UNION ALL + SELECT 1 + FROM chat_items + WHERE user_id = :user_id AND contact_id = :contact_id + AND created_at = :created_at AND chat_item_id > :item_id + ) |] - (userId, contactId, ciCreatedAt afterCI, ciCreatedAt afterCI, cChatItemId afterCI) + [ ":user_id" := userId, + ":contact_id" := contactId, + ":created_at" := ciCreatedAt afterCI, + ":item_id" := cChatItemId afterCI + ] getGroupChat :: DB.Connection -> VersionRangeChat -> User -> Int64 -> ChatPagination -> Maybe String -> ExceptT StoreError IO (Chat 'CTGroup, Maybe NavigationInfo) getGroupChat db vr user groupId pagination search_ = do @@ -1363,27 +1388,52 @@ getGroupNavInfo_ db User {userId} GroupInfo {groupId} afterCI = do getAfterUnreadCount :: IO Int getAfterUnreadCount = fromOnly . head - <$> DB.query + <$> DB.queryNamed db [sql| SELECT COUNT(1) - FROM chat_items - WHERE user_id = ? AND group_id = ? AND item_status = ? - AND (item_ts > ? OR (item_ts = ? AND chat_item_id > ?)) + FROM ( + SELECT 1 + FROM chat_items + WHERE user_id = :user_id AND group_id = :group_id AND item_status = :rcv_new + AND item_ts > :item_ts + UNION ALL + SELECT 1 + FROM chat_items + WHERE user_id = :user_id AND group_id = :group_id AND item_status = :rcv_new + AND item_ts = :item_ts AND chat_item_id > :item_id + ) |] - (userId, groupId, CISRcvNew, chatItemTs afterCI, chatItemTs afterCI, cChatItemId afterCI) + [ ":user_id" := userId, + ":group_id" := groupId, + ":rcv_new" := CISRcvNew, + ":item_ts" := chatItemTs afterCI, + ":item_id" := cChatItemId afterCI + ] getAfterTotalCount :: IO Int getAfterTotalCount = fromOnly . head - <$> DB.query + <$> DB.queryNamed db [sql| SELECT COUNT(1) - FROM chat_items - WHERE user_id = ? AND group_id = ? - AND (item_ts > ? OR (item_ts = ? AND chat_item_id > ?)) + FROM ( + SELECT 1 + FROM chat_items + WHERE user_id = :user_id AND group_id = :group_id + AND item_ts > :item_ts + UNION ALL + SELECT 1 + FROM chat_items + WHERE user_id = :user_id AND group_id = :group_id + AND item_ts = :item_ts AND chat_item_id > :item_id + ) |] - (userId, groupId, chatItemTs afterCI, chatItemTs afterCI, cChatItemId afterCI) + [ ":user_id" := userId, + ":group_id" := groupId, + ":item_ts" := chatItemTs afterCI, + ":item_id" := cChatItemId afterCI + ] getLocalChat :: DB.Connection -> User -> Int64 -> ChatPagination -> Maybe String -> ExceptT StoreError IO (Chat 'CTLocal, Maybe NavigationInfo) getLocalChat db user folderId pagination search_ = do @@ -1565,27 +1615,52 @@ getLocalNavInfo_ db User {userId} NoteFolder {noteFolderId} afterCI = do getAfterUnreadCount :: IO Int getAfterUnreadCount = fromOnly . head - <$> DB.query + <$> DB.queryNamed db [sql| SELECT COUNT(1) - FROM chat_items - WHERE user_id = ? AND note_folder_id = ? AND item_status = ? - AND (created_at > ? OR (created_at = ? AND chat_item_id > ?)) + FROM ( + SELECT 1 + FROM chat_items + WHERE user_id = :user_id AND note_folder_id = :note_folder_id AND item_status = :rcv_new + AND created_at > :created_at + UNION ALL + SELECT 1 + FROM chat_items + WHERE user_id = :user_id AND note_folder_id = :note_folder_id AND item_status = :rcv_new + AND created_at = :created_at AND chat_item_id > :item_id + ) |] - (userId, noteFolderId, CISRcvNew, ciCreatedAt afterCI, ciCreatedAt afterCI, cChatItemId afterCI) + [ ":user_id" := userId, + ":note_folder_id" := noteFolderId, + ":rcv_new" := CISRcvNew, + ":created_at" := ciCreatedAt afterCI, + ":item_id" := cChatItemId afterCI + ] getAfterTotalCount :: IO Int getAfterTotalCount = fromOnly . head - <$> DB.query + <$> DB.queryNamed db [sql| SELECT COUNT(1) - FROM chat_items - WHERE user_id = ? AND note_folder_id = ? - AND (created_at > ? OR (created_at = ? AND chat_item_id > ?)) + FROM ( + SELECT 1 + FROM chat_items + WHERE user_id = :user_id AND note_folder_id = :note_folder_id + AND created_at > :created_at + UNION ALL + SELECT 1 + FROM chat_items + WHERE user_id = :user_id AND note_folder_id = :note_folder_id + AND created_at = :created_at AND chat_item_id > :item_id + ) |] - (userId, noteFolderId, ciCreatedAt afterCI, ciCreatedAt afterCI, cChatItemId afterCI) + [ ":user_id" := userId, + ":note_folder_id" := noteFolderId, + ":created_at" := ciCreatedAt afterCI, + ":item_id" := cChatItemId afterCI + ] toChatItemRef :: (ChatItemId, Maybe Int64, Maybe Int64, Maybe Int64) -> Either StoreError (ChatRef, ChatItemId) toChatItemRef = \case diff --git a/src/Simplex/Chat/Store/Migrations.hs b/src/Simplex/Chat/Store/Migrations.hs index 7218706239..9a91c7f970 100644 --- a/src/Simplex/Chat/Store/Migrations.hs +++ b/src/Simplex/Chat/Store/Migrations.hs @@ -116,6 +116,7 @@ import Simplex.Chat.Migrations.M20241008_indexes import Simplex.Chat.Migrations.M20241010_contact_requests_contact_id import Simplex.Chat.Migrations.M20241023_chat_item_autoincrement_id import Simplex.Chat.Migrations.M20241027_server_operators +import Simplex.Chat.Migrations.M20241125_indexes import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..)) schemaMigrations :: [(String, Query, Maybe Query)] @@ -231,7 +232,8 @@ schemaMigrations = ("20241008_indexes", m20241008_indexes, Just down_m20241008_indexes), ("20241010_contact_requests_contact_id", m20241010_contact_requests_contact_id, Just down_m20241010_contact_requests_contact_id), ("20241023_chat_item_autoincrement_id", m20241023_chat_item_autoincrement_id, Just down_m20241023_chat_item_autoincrement_id), - ("20241027_server_operators", m20241027_server_operators, Just down_m20241027_server_operators) + ("20241027_server_operators", m20241027_server_operators, Just down_m20241027_server_operators), + ("20241125_indexes", m20241125_indexes, Just down_m20241125_indexes) ] -- | The list of migrations in ascending order by date diff --git a/tests/SchemaDump.hs b/tests/SchemaDump.hs index 4e63a31001..d13dc94b63 100644 --- a/tests/SchemaDump.hs +++ b/tests/SchemaDump.hs @@ -103,8 +103,10 @@ skipComparisonForDownMigrations = "20231215_recreate_msg_deliveries", -- on down migration idx_msg_deliveries_agent_ack_cmd_id index moves down to the end of the file "20240313_drop_agent_ack_cmd_id", - -- on down migration chat_item_autoincrement_id makes sequence table creation move down on the file - "20241023_chat_item_autoincrement_id" + -- sequence table moves down to the end of the file + "20241023_chat_item_autoincrement_id", + -- indexes move down to the end of the file + "20241125_indexes" ] getSchema :: FilePath -> FilePath -> IO String From 7a91ed2ab244a6ee08365274ceaf2b9f96ea7cc0 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Mon, 25 Nov 2024 23:20:02 +0700 Subject: [PATCH 068/167] android, desktop: view conditions as markdown (#5247) * android, desktop: view conditions as markdown * better animation * unused * open chat links inside the app and removed divider, smaller font * paddings --- apps/multiplatform/common/build.gradle.kts | 4 + .../networkAndServers/OperatorView.kt | 93 ++++++++++++++++--- 2 files changed, 85 insertions(+), 12 deletions(-) diff --git a/apps/multiplatform/common/build.gradle.kts b/apps/multiplatform/common/build.gradle.kts index 0e45c66efd..b9b307c8f4 100644 --- a/apps/multiplatform/common/build.gradle.kts +++ b/apps/multiplatform/common/build.gradle.kts @@ -48,6 +48,10 @@ kotlin { // Resources api("dev.icerock.moko:resources:0.23.0") api("dev.icerock.moko:resources-compose:0.23.0") + + // Markdown + implementation("com.mikepenz:multiplatform-markdown-renderer:0.27.0") + implementation("com.mikepenz:multiplatform-markdown-renderer-m2:0.27.0") } } val commonTest by getting { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt index e5d6f9150c..4e0fc6ec79 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt @@ -16,10 +16,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.* import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.platform.UriHandler import androidx.compose.ui.text.* import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.DpOffset -import androidx.compose.ui.unit.dp +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.unit.* import chat.simplex.common.model.* import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatController.getUsageConditions @@ -29,14 +30,19 @@ import chat.simplex.common.views.chat.item.ItemAction import chat.simplex.common.views.helpers.* import chat.simplex.common.views.onboarding.* import chat.simplex.res.MR +import com.mikepenz.markdown.compose.Markdown +import com.mikepenz.markdown.compose.components.markdownComponents +import com.mikepenz.markdown.compose.elements.MarkdownHeader +import com.mikepenz.markdown.m2.markdownColor +import com.mikepenz.markdown.m2.markdownTypography +import com.mikepenz.markdown.model.markdownPadding import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import java.net.URI +import kotlinx.coroutines.* +import org.intellij.markdown.flavours.commonmark.CommonMarkFlavourDescriptor @Composable -fun ModalData.OperatorView( +fun OperatorView( currUserServers: MutableState>, userServers: MutableState>, serverErrors: MutableState>, @@ -610,13 +616,20 @@ fun ConditionsTextView( val defaultConditionsLink = "https://github.com/simplex-chat/simplex-chat/blob/stable/PRIVACY.md" val scope = rememberCoroutineScope() + // can show conditions when animation between modals finishes to prevent glitches + val canShowConditionsAt = remember { System.currentTimeMillis() + 300 } LaunchedEffect(Unit) { - scope.launch { + scope.launch(Dispatchers.Default) { try { val conditions = getUsageConditions(rh = rhId) if (conditions != null) { - conditionsData.value = conditions + val parentLink = "https://github.com/simplex-chat/simplex-chat/blob/${conditions.first.conditionsCommit}" + val conditionsText = conditions.second + val preparedText = if (conditionsText != null) prepareMarkdown(conditionsText.trimIndent(), parentLink) else null + val modifiedConditions = Triple(conditions.first, preparedText, conditions.third) + delay((canShowConditionsAt - System.currentTimeMillis()).coerceAtLeast(0)) + conditionsData.value = modifiedConditions } else { failedToLoad.value = true } @@ -639,10 +652,10 @@ fun ConditionsTextView( .verticalScroll(scrollState) .padding(8.dp) ) { - Text( - text = conditionsText.trimIndent(), - modifier = Modifier.padding(8.dp) - ) + val parentUriHandler = LocalUriHandler.current + CompositionLocalProvider(LocalUriHandler provides remember { internalUriHandler(parentUriHandler) }) { + ConditionsMarkdown(conditionsText) + } } } else { val conditionsLink = "https://github.com/simplex-chat/simplex-chat/blob/${usageConditions.conditionsCommit}/PRIVACY.md" @@ -655,6 +668,44 @@ fun ConditionsTextView( } } +@Composable +private fun ConditionsMarkdown(text: String) { + Markdown(text, + markdownColor(linkText = MaterialTheme.colors.primary), + markdownTypography( + h1 = MaterialTheme.typography.body1, + h2 = MaterialTheme.typography.h3.copy(fontSize = 22.sp, fontWeight = FontWeight.Bold), + h3 = MaterialTheme.typography.h4.copy(fontWeight = FontWeight.Bold), + h4 = MaterialTheme.typography.h5.copy(fontSize = 16.sp, fontWeight = FontWeight.Bold), + h5 = MaterialTheme.typography.h6.copy(fontWeight = FontWeight.Bold), + link = MaterialTheme.typography.body1.copy( + textDecoration = TextDecoration.Underline + ) + ), + Modifier.padding(8.dp), + // using CommonMarkFlavourDescriptor instead of GFMFlavourDescriptor because it shows `https://simplex.chat/` (link inside backticks) incorrectly + flavour = CommonMarkFlavourDescriptor(), + components = markdownComponents( + heading2 = { + Spacer(Modifier.height(10.dp)) + MarkdownHeader(it.content, it.node, it.typography.h2) + Spacer(Modifier.height(5.dp)) + }, + heading3 = { + Spacer(Modifier.height(10.dp)) + MarkdownHeader(it.content, it.node, it.typography.h3) + Spacer(Modifier.height(3.dp)) + }, + heading4 = { + Spacer(Modifier.height(10.dp)) + MarkdownHeader(it.content, it.node, it.typography.h4) + Spacer(Modifier.height(4.dp)) + }, + ), + padding = markdownPadding(block = 4.dp) + ) +} + @Composable private fun ConditionsLinkView(conditionsLink: String) { SectionItemView { @@ -706,6 +757,24 @@ fun ConditionsLinkButton() { } } +private fun internalUriHandler(parentUriHandler: UriHandler): UriHandler = object: UriHandler { + override fun openUri(uri: String) { + if (uri.startsWith("https://simplex.chat/contact#")) { + openVerifiedSimplexUri(uri) + } else { + parentUriHandler.openUriCatching(uri) + } + } +} + +private fun prepareMarkdown(text: String, parentLink: String): String { + val localLinkRegex = Regex("\\[([^\\)]*)\\]\\(#.*\\)", RegexOption.MULTILINE) + return text + .replace("](/", "]($parentLink/") + .replace("](./", "]($parentLink/") + .replace(localLinkRegex) { it.groupValues.getOrNull(1) ?: it.value } +} + private fun changeOperatorEnabled(userServers: MutableState>, operatorIndex: Int, enabled: Boolean) { userServers.value = userServers.value.toMutableList().apply { this[operatorIndex] = this[operatorIndex].copy( From 1f04984a3448e43514ea588c3ad571300946222d Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Tue, 26 Nov 2024 14:43:39 +0400 Subject: [PATCH 069/167] ios: offer to create 1-time link on address views (#5249) --- apps/ios/Shared/Model/ChatModel.swift | 4 +- apps/ios/Shared/Views/ChatList/ChatHelp.swift | 3 +- .../Views/NewChat/NewChatMenuButton.swift | 14 +- .../Shared/Views/NewChat/NewChatView.swift | 58 ++- .../UserSettings/UserAddressLearnMore.swift | 32 +- .../Views/UserSettings/UserAddressView.swift | 356 +++++++++++------- 6 files changed, 273 insertions(+), 194 deletions(-) diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index 4bc5917ed7..6b6b0ac03f 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -845,7 +845,7 @@ final class ChatModel: ObservableObject { } func dismissConnReqView(_ id: String) { - if id == showingInvitation?.connId { + if id == showingInvitation?.pcc.id { markShowingInvitationUsed() dismissAllSheets() } @@ -898,7 +898,7 @@ final class ChatModel: ObservableObject { } struct ShowingInvitation { - var connId: String + var pcc: PendingContactConnection var connChatUsed: Bool } diff --git a/apps/ios/Shared/Views/ChatList/ChatHelp.swift b/apps/ios/Shared/Views/ChatList/ChatHelp.swift index a01c81bafb..776229f60a 100644 --- a/apps/ios/Shared/Views/ChatList/ChatHelp.swift +++ b/apps/ios/Shared/Views/ChatList/ChatHelp.swift @@ -42,7 +42,8 @@ struct ChatHelp: View { Text("above, then choose:") } - Text("**Add contact**: to create a new invitation link, or connect via a link you received.") + Text("**Create 1-time link**: to create a new invitation link.") + Text("**Scan / Paste link**: to connect via a link you received.") Text("**Create group**: to create a new group.") } .padding(.top, 24) diff --git a/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift b/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift index 3ca3e0e4d8..6f973983bf 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift @@ -18,7 +18,6 @@ struct NewChatMenuButton: View { // @EnvironmentObject var chatModel: ChatModel @State private var showNewChatSheet = false @State private var alert: SomeAlert? = nil - @State private var pendingConnection: PendingContactConnection? = nil var body: some View { Button { @@ -30,12 +29,8 @@ struct NewChatMenuButton: View { .frame(width: 24, height: 24) } .appSheet(isPresented: $showNewChatSheet) { - NewChatSheet(pendingConnection: $pendingConnection) + NewChatSheet() .environment(\EnvironmentValues.refresh as! WritableKeyPath, nil) - .onDisappear { - alert = cleanupPendingConnection(contactConnection: pendingConnection) - pendingConnection = nil - } } .alert(item: $alert) { a in return a.alert @@ -55,7 +50,6 @@ struct NewChatSheet: View { @State private var searchShowingSimplexLink = false @State private var searchChatFilteredBySimplexLink: String? = nil @State private var alert: SomeAlert? - @Binding var pendingConnection: PendingContactConnection? // Sheet height management @State private var isAddContactActive = false @@ -110,17 +104,17 @@ struct NewChatSheet: View { if (searchText.isEmpty) { Section { NavigationLink(isActive: $isAddContactActive) { - NewChatView(selection: .invite, parentAlert: $alert, contactConnection: $pendingConnection) + NewChatView(selection: .invite) .navigationTitle("New chat") .modifier(ThemedBackground(grouped: true)) .navigationBarTitleDisplayMode(.large) } label: { - navigateOnTap(Label("Add contact", systemImage: "link.badge.plus")) { + navigateOnTap(Label("Create 1-time link", systemImage: "link.badge.plus")) { isAddContactActive = true } } NavigationLink(isActive: $isScanPasteLinkActive) { - NewChatView(selection: .connect, showQRCodeScanner: true, parentAlert: $alert, contactConnection: $pendingConnection) + NewChatView(selection: .connect, showQRCodeScanner: true) .navigationTitle("New chat") .modifier(ThemedBackground(grouped: true)) .navigationBarTitleDisplayMode(.large) diff --git a/apps/ios/Shared/Views/NewChat/NewChatView.swift b/apps/ios/Shared/Views/NewChat/NewChatView.swift index 4ca33e674d..19e810d034 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatView.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatView.swift @@ -45,32 +45,33 @@ enum NewChatOption: Identifiable { var id: Self { self } } -func cleanupPendingConnection(contactConnection: PendingContactConnection?) -> SomeAlert? { - var alert: SomeAlert? = nil - - if !(ChatModel.shared.showingInvitation?.connChatUsed ?? true), - let conn = contactConnection { - alert = SomeAlert( - alert: Alert( - title: Text("Keep unused invitation?"), - message: Text("You can view invitation link again in connection details."), - primaryButton: .default(Text("Keep")) {}, - secondaryButton: .destructive(Text("Delete")) { - Task { - await deleteChat(Chat( - chatInfo: .contactConnection(contactConnection: conn), - chatItems: [] - )) +func showKeepInvitationAlert() { + if let showingInvitation = ChatModel.shared.showingInvitation, + !showingInvitation.connChatUsed { + showAlert( + NSLocalizedString("Keep unused invitation?", comment: "alert title"), + message: NSLocalizedString("You can view invitation link again in connection details.", comment: "alert message"), + actions: {[ + UIAlertAction( + title: NSLocalizedString("Keep", comment: "alert action"), + style: .default + ), + UIAlertAction( + title: NSLocalizedString("Delete", comment: "alert action"), + style: .destructive, + handler: { _ in + Task { + await deleteChat(Chat( + chatInfo: .contactConnection(contactConnection: showingInvitation.pcc), + chatItems: [] + )) + } } - } - ), - id: "keepUnusedInvitation" + ) + ]} ) } - ChatModel.shared.showingInvitation = nil - - return alert } struct NewChatView: View { @@ -84,13 +85,12 @@ struct NewChatView: View { @State var choosingProfile = false @State private var pastedLink: String = "" @State private var alert: NewChatViewAlert? - @Binding var parentAlert: SomeAlert? - @Binding var contactConnection: PendingContactConnection? + @State private var contactConnection: PendingContactConnection? = nil var body: some View { VStack(alignment: .leading) { Picker("New chat", selection: $selection) { - Label("Add contact", systemImage: "link") + Label("1-time link", systemImage: "link") .tag(NewChatOption.invite) Label("Connect via link", systemImage: "qrcode") .tag(NewChatOption.connect) @@ -157,7 +157,7 @@ struct NewChatView: View { } .onDisappear { if !choosingProfile { - parentAlert = cleanupPendingConnection(contactConnection: contactConnection) + showKeepInvitationAlert() contactConnection = nil } } @@ -197,7 +197,7 @@ struct NewChatView: View { if let (connReq, pcc) = r { await MainActor.run { m.updateContactConnection(pcc) - m.showingInvitation = ShowingInvitation(connId: pcc.id, connChatUsed: false) + m.showingInvitation = ShowingInvitation(pcc: pcc, connChatUsed: false) connReqInvitation = connReq contactConnection = pcc } @@ -1278,9 +1278,7 @@ struct NewChatView_Previews: PreviewProvider { @State var contactConnection: PendingContactConnection? = nil NewChatView( - selection: .invite, - parentAlert: $parentAlert, - contactConnection: $contactConnection + selection: .invite ) } } diff --git a/apps/ios/Shared/Views/UserSettings/UserAddressLearnMore.swift b/apps/ios/Shared/Views/UserSettings/UserAddressLearnMore.swift index 22efbfcb85..414e7efe85 100644 --- a/apps/ios/Shared/Views/UserSettings/UserAddressLearnMore.swift +++ b/apps/ios/Shared/Views/UserSettings/UserAddressLearnMore.swift @@ -11,7 +11,8 @@ import SwiftUI struct UserAddressLearnMore: View { @State var showCreateAddressButton = false @State private var createAddressLinkActive = false - + @State private var createOneTimeLinkActive = false + var body: some View { VStack { List { @@ -44,11 +45,8 @@ struct UserAddressLearnMore: View { VStack { addressCreationButton() .padding(.bottom) - - Button("Create 1-time link") { - - } - .font(.footnote) + + createOneTimeLinkButton() } .padding() } @@ -76,6 +74,28 @@ struct UserAddressLearnMore: View { .hidden() } } + + private func createOneTimeLinkButton() -> some View { + ZStack { + Button { + createOneTimeLinkActive = true + } label: { + Text("Create 1-time link") + .font(.footnote) + } + + NavigationLink(isActive: $createOneTimeLinkActive) { + NewChatView(selection: .invite) + .navigationTitle("New chat") + .navigationBarTitleDisplayMode(.large) + .modifier(ThemedBackground(grouped: true)) + } label: { + EmptyView() + } + .frame(width: 1, height: 1) + .hidden() + } + } } struct UserAddressLearnMore_Previews: PreviewProvider { diff --git a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift index 8f212d8678..28301c5ddb 100644 --- a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift +++ b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift @@ -16,45 +16,28 @@ struct UserAddressView: View { @EnvironmentObject var theme: AppTheme @State var shareViaProfile = false @State var autoCreate = false - @State private var aas = AutoAcceptState() - @State private var savedAAS = AutoAcceptState() - @State private var ignoreShareViaProfileChange = false @State private var showMailView = false @State private var mailViewResult: Result? = nil @State private var alert: UserAddressAlert? @State private var progressIndicator = false - @FocusState private var keyboardVisible: Bool private enum UserAddressAlert: Identifiable { case deleteAddress - case profileAddress(on: Bool) case shareOnCreate case error(title: LocalizedStringKey, error: LocalizedStringKey?) var id: String { switch self { case .deleteAddress: return "deleteAddress" - case let .profileAddress(on): return "profileAddress \(on)" case .shareOnCreate: return "shareOnCreate" case let .error(title, _): return "error \(title)" } } } - + var body: some View { ZStack { - userAddressScrollView() - .onDisappear { - if savedAAS != aas { - showAlert( - title: NSLocalizedString("Auto-accept settings", comment: "alert title"), - message: NSLocalizedString("Settings were changed.", comment: "alert message"), - buttonTitle: NSLocalizedString("Save", comment: "alert button"), - buttonAction: saveAAS, - cancelButton: true - ) - } - } + userAddressView() if progressIndicator { ZStack { @@ -75,39 +58,22 @@ struct UserAddressView: View { } } - @Namespace private var bottomID - - private func userAddressScrollView() -> some View { - ScrollViewReader { proxy in - userAddressView() - .onChange(of: keyboardVisible) { _ in - if keyboardVisible { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - withAnimation { - proxy.scrollTo(bottomID, anchor: .top) - } - } - } - } - } - } - private func userAddressView() -> some View { List { if let userAddress = chatModel.userAddress { existingAddressView(userAddress) - .onAppear { - aas = AutoAcceptState(userAddress: userAddress) - savedAAS = aas - } - .onChange(of: aas.enable) { _ in - if !aas.enable { aas = AutoAcceptState() } - } } else { Section { createAddressButton() - } footer: { - Text("Create an address to let people connect with you.") + } header: { + Text("For social media") + .foregroundColor(theme.colors.secondary) + } + + Section { + createOneTimeLinkButton() + } header: { + Text("Or to share privately") .foregroundColor(theme.colors.secondary) } @@ -123,8 +89,8 @@ struct UserAddressView: View { title: Text("Delete address?"), message: shareViaProfile - ? Text("All your contacts will remain connected. Profile update will be sent to your contacts.") - : Text("All your contacts will remain connected."), + ? Text("All your contacts will remain connected. Profile update will be sent to your contacts.") + : Text("All your contacts will remain connected."), primaryButton: .destructive(Text("Delete")) { progressIndicator = true Task { @@ -134,7 +100,6 @@ struct UserAddressView: View { chatModel.userAddress = nil chatModel.updateUser(u) if shareViaProfile { - ignoreShareViaProfileChange = true shareViaProfile = false } } @@ -147,37 +112,12 @@ struct UserAddressView: View { } }, secondaryButton: .cancel() ) - case let .profileAddress(on): - if on { - return Alert( - title: Text("Share address with contacts?"), - message: Text("Profile update will be sent to your contacts."), - primaryButton: .default(Text("Share")) { - setProfileAddress(on) - }, secondaryButton: .cancel() { - ignoreShareViaProfileChange = true - shareViaProfile = !on - } - ) - } else { - return Alert( - title: Text("Stop sharing address?"), - message: Text("Profile update will be sent to your contacts."), - primaryButton: .default(Text("Stop sharing")) { - setProfileAddress(on) - }, secondaryButton: .cancel() { - ignoreShareViaProfileChange = true - shareViaProfile = !on - } - ) - } case .shareOnCreate: return Alert( title: Text("Share address with contacts?"), message: Text("Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts."), primaryButton: .default(Text("Share")) { - setProfileAddress(true) - ignoreShareViaProfileChange = true + setProfileAddress($progressIndicator, true) shareViaProfile = true }, secondaryButton: .cancel() ) @@ -192,19 +132,24 @@ struct UserAddressView: View { SimpleXLinkQRCode(uri: userAddress.connReqContact) .id("simplex-contact-address-qrcode-\(userAddress.connReqContact)") shareQRCodeButton(userAddress) - if MFMailComposeViewController.canSendMail() { - shareViaEmailButton(userAddress) - } - shareWithContactsButton() - autoAcceptToggle() - learnMoreButton() + // if MFMailComposeViewController.canSendMail() { + // shareViaEmailButton(userAddress) + // } + addressSettingsButton(userAddress) } header: { - Text("Address") + Text("For social media") .foregroundColor(theme.colors.secondary) } - if aas.enable { - autoAcceptSection() + Section { + createOneTimeLinkButton() + } header: { + Text("Or to share privately") + .foregroundColor(theme.colors.secondary) + } + + Section { + learnMoreButton() } Section { @@ -213,7 +158,6 @@ struct UserAddressView: View { Text("Your contacts will remain connected.") .foregroundColor(theme.colors.secondary) } - .id(bottomID) } private func createAddressButton() -> some View { @@ -223,7 +167,7 @@ struct UserAddressView: View { Label("Create SimpleX address", systemImage: "qrcode") } } - + private func createAddress() { progressIndicator = true Task { @@ -243,6 +187,18 @@ struct UserAddressView: View { } } + private func createOneTimeLinkButton() -> some View { + NavigationLink { + NewChatView(selection: .invite) + .navigationTitle("New chat") + .navigationBarTitleDisplayMode(.large) + .modifier(ThemedBackground(grouped: true)) + } label: { + Label("Create 1-time link", systemImage: "link.badge.plus") + .foregroundColor(theme.colors.primary) + } + } + private func deleteAddressButton() -> some View { Button(role: .destructive) { alert = .deleteAddress @@ -292,12 +248,14 @@ struct UserAddressView: View { } } - private func autoAcceptToggle() -> some View { - settingsRow("checkmark", color: theme.colors.secondary) { - Toggle("Auto-accept", isOn: $aas.enable) - .onChange(of: aas.enable) { _ in - saveAAS() - } + private func addressSettingsButton(_ userAddress: UserContactLink) -> some View { + NavigationLink { + UserAddressSettingsView(shareViaProfile: $shareViaProfile) + .navigationTitle("Address settings") + .navigationBarTitleDisplayMode(.large) + .modifier(ThemedBackground(grouped: true)) + } label: { + Text("Address settings") } } @@ -313,6 +271,116 @@ struct UserAddressView: View { } } } +} + +private struct AutoAcceptState: Equatable { + var enable = false + var incognito = false + var welcomeText = "" + + init(enable: Bool = false, incognito: Bool = false, welcomeText: String = "") { + self.enable = enable + self.incognito = incognito + self.welcomeText = welcomeText + } + + init(userAddress: UserContactLink) { + if let aa = userAddress.autoAccept { + enable = true + incognito = aa.acceptIncognito + if let msg = aa.autoReply { + welcomeText = msg.text + } else { + welcomeText = "" + } + } else { + enable = false + incognito = false + welcomeText = "" + } + } + + var autoAccept: AutoAccept? { + if enable { + var autoReply: MsgContent? = nil + let s = welcomeText.trimmingCharacters(in: .whitespacesAndNewlines) + if s != "" { autoReply = .text(s) } + return AutoAccept(acceptIncognito: incognito, autoReply: autoReply) + } + return nil + } +} + +private func setProfileAddress(_ progressIndicator: Binding, _ on: Bool) { + progressIndicator.wrappedValue = true + Task { + do { + if let u = try await apiSetProfileAddress(on: on) { + DispatchQueue.main.async { + ChatModel.shared.updateUser(u) + } + } + await MainActor.run { progressIndicator.wrappedValue = false } + } catch let error { + logger.error("apiSetProfileAddress: \(responseError(error))") + await MainActor.run { progressIndicator.wrappedValue = false } + } + } +} + +struct UserAddressSettingsView: View { + @Environment(\.dismiss) var dismiss: DismissAction + @EnvironmentObject var theme: AppTheme + @Binding var shareViaProfile: Bool + @State private var aas = AutoAcceptState() + @State private var savedAAS = AutoAcceptState() + @State private var ignoreShareViaProfileChange = false + @State private var progressIndicator = false + @FocusState private var keyboardVisible: Bool + + var body: some View { + ZStack { + if let userAddress = ChatModel.shared.userAddress { + userAddressSettingsView() + .onAppear { + aas = AutoAcceptState(userAddress: userAddress) + savedAAS = aas + } + .onChange(of: aas.enable) { aasEnabled in + if !aasEnabled { aas = AutoAcceptState() } + } + .onDisappear { + if savedAAS != aas { + showAlert( + title: NSLocalizedString("Auto-accept settings", comment: "alert title"), + message: NSLocalizedString("Settings were changed.", comment: "alert message"), + buttonTitle: NSLocalizedString("Save", comment: "alert button"), + buttonAction: saveAAS, + cancelButton: true + ) + } + } + } else { + Text(String("Error opening address settings")) + } + if progressIndicator { + ProgressView().scaleEffect(2) + } + } + } + + private func userAddressSettingsView() -> some View { + List { + Section { + shareWithContactsButton() + autoAcceptToggle() + } + + if aas.enable { + autoAcceptSection() + } + } + } private func shareWithContactsButton() -> some View { settingsRow("person", color: theme.colors.secondary) { @@ -321,68 +389,66 @@ struct UserAddressView: View { if ignoreShareViaProfileChange { ignoreShareViaProfileChange = false } else { - alert = .profileAddress(on: on) + if on { + showAlert( + NSLocalizedString("Share address with contacts?", comment: "alert title"), + message: NSLocalizedString("Profile update will be sent to your contacts.", comment: "alert message"), + actions: {[ + UIAlertAction( + title: NSLocalizedString("Cancel", comment: "alert action"), + style: .default, + handler: { _ in + ignoreShareViaProfileChange = true + shareViaProfile = !on + } + ), + UIAlertAction( + title: NSLocalizedString("Share", comment: "alert action"), + style: .default, + handler: { _ in + setProfileAddress($progressIndicator, on) + } + ) + ]} + ) + } else { + showAlert( + NSLocalizedString("Stop sharing address?", comment: "alert title"), + message: NSLocalizedString("Profile update will be sent to your contacts.", comment: "alert message"), + actions: {[ + UIAlertAction( + title: NSLocalizedString("Cancel", comment: "alert action"), + style: .default, + handler: { _ in + ignoreShareViaProfileChange = true + shareViaProfile = !on + } + ), + UIAlertAction( + title: NSLocalizedString("Stop sharing", comment: "alert action"), + style: .default, + handler: { _ in + setProfileAddress($progressIndicator, on) + } + ) + ]} + ) + } } } } } - private func setProfileAddress(_ on: Bool) { - progressIndicator = true - Task { - do { - if let u = try await apiSetProfileAddress(on: on) { - DispatchQueue.main.async { - chatModel.updateUser(u) - } + private func autoAcceptToggle() -> some View { + settingsRow("checkmark", color: theme.colors.secondary) { + Toggle("Auto-accept", isOn: $aas.enable) + .onChange(of: aas.enable) { _ in + saveAAS() } - await MainActor.run { progressIndicator = false } - } catch let error { - logger.error("UserAddressView apiSetProfileAddress: \(responseError(error))") - await MainActor.run { progressIndicator = false } - } - } - } - - private struct AutoAcceptState: Equatable { - var enable = false - var incognito = false - var welcomeText = "" - - init(enable: Bool = false, incognito: Bool = false, welcomeText: String = "") { - self.enable = enable - self.incognito = incognito - self.welcomeText = welcomeText - } - - init(userAddress: UserContactLink) { - if let aa = userAddress.autoAccept { - enable = true - incognito = aa.acceptIncognito - if let msg = aa.autoReply { - welcomeText = msg.text - } else { - welcomeText = "" - } - } else { - enable = false - incognito = false - welcomeText = "" - } - } - - var autoAccept: AutoAccept? { - if enable { - var autoReply: MsgContent? = nil - let s = welcomeText.trimmingCharacters(in: .whitespacesAndNewlines) - if s != "" { autoReply = .text(s) } - return AutoAccept(acceptIncognito: incognito, autoReply: autoReply) - } - return nil } } - @ViewBuilder private func autoAcceptSection() -> some View { + private func autoAcceptSection() -> some View { Section { acceptIncognitoToggle() welcomeMessageEditor() @@ -434,7 +500,7 @@ struct UserAddressView: View { Task { do { if let address = try await userAddressAutoAccept(aas.autoAccept) { - chatModel.userAddress = address + ChatModel.shared.userAddress = address savedAAS = aas } } catch let error { From 345e0acdec4eed587519c99bfb94c23c0e580620 Mon Sep 17 00:00:00 2001 From: Diogo Date: Tue, 26 Nov 2024 12:26:35 +0000 Subject: [PATCH 070/167] ios: onboarding redesign (#5252) * ios: onboarding redesign * shorter texts * updates * more updates * remove extra padding when focused * strings --------- Co-authored-by: Evgeny Poberezkin --- .../Onboarding/ChooseServerOperators.swift | 45 ++++--- .../Views/Onboarding/CreateProfile.swift | 64 ++++++---- .../Shared/Views/Onboarding/HowItWorks.swift | 6 +- .../Views/Onboarding/OnboardingView.swift | 8 +- .../Onboarding/SetNotificationsMode.swift | 112 ++++++++++++++---- .../Shared/Views/Onboarding/SimpleXInfo.swift | 72 ++++++----- .../NetworkAndServers/NetworkAndServers.swift | 3 +- .../UserSettings/NotificationsView.swift | 8 ++ apps/ios/SimpleXChat/APITypes.swift | 14 ++- 9 files changed, 210 insertions(+), 122 deletions(-) diff --git a/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift b/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift index 19d67bc62c..4efdb99f21 100644 --- a/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift +++ b/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift @@ -73,14 +73,20 @@ struct ChooseServerOperators: View { GeometryReader { g in ScrollView { VStack(alignment: .leading, spacing: 20) { - if !onboarding { - Text("Choose operators") - .font(.largeTitle) - .bold() + let title = Text("Server operators") + .font(.largeTitle) + .bold() + .frame(maxWidth: .infinity, alignment: .center) + + if onboarding { + title.padding(.top, 50) + } else { + title } infoText() - + .frame(maxWidth: .infinity, alignment: .center) + Spacer() ForEach(serverOperators) { srvOperator in @@ -117,11 +123,10 @@ struct ChooseServerOperators: View { .foregroundColor(.clear) } } - .font(.callout) - .padding(.top) + .font(.system(size: 17, weight: .semibold)) + .frame(minHeight: 40) } } - .padding(.bottom) if !onboarding && !reviewForOperators.isEmpty { VStack(spacing: 8) { @@ -162,21 +167,15 @@ struct ChooseServerOperators: View { } } .frame(maxHeight: .infinity) - .padding() + .padding(onboarding ? 25 : 16) } private func infoText() -> some View { - HStack(spacing: 12) { - Image(systemName: "info.circle") - .resizable() - .scaledToFit() - .frame(width: 20, height: 20) - .foregroundColor(theme.colors.primary) - .onTapGesture { - sheetItem = .showInfo - } - - Text("Select network operators to use.") + Button { + sheetItem = .showInfo + } label: { + Label("How it helps privacy", systemImage: "info.circle") + .font(.headline) } } @@ -305,8 +304,6 @@ struct ChooseServerOperators: View { private func notificationsModeDestinationView() -> some View { SetNotificationsMode() - .navigationTitle("Push notifications") - .navigationBarTitleDisplayMode(.large) .navigationBarBackButtonHidden(true) .modifier(ThemedBackground()) } @@ -334,7 +331,7 @@ struct ChooseServerOperators: View { .padding(.bottom) .padding(.bottom) } - .padding(.horizontal) + .padding(.horizontal, 25) .frame(maxHeight: .infinity) } @@ -411,7 +408,7 @@ struct ChooseServerOperators: View { struct ChooseServerOperatorsInfoView: View { var body: some View { VStack(alignment: .leading) { - Text("Network operators") + Text("Server operators") .font(.largeTitle) .bold() .padding(.vertical) diff --git a/apps/ios/Shared/Views/Onboarding/CreateProfile.swift b/apps/ios/Shared/Views/Onboarding/CreateProfile.swift index c6760319b1..7665e57cc1 100644 --- a/apps/ios/Shared/Views/Onboarding/CreateProfile.swift +++ b/apps/ios/Shared/Views/Onboarding/CreateProfile.swift @@ -119,49 +119,67 @@ struct CreateFirstProfile: View { var body: some View { VStack(alignment: .leading, spacing: 20) { - Text("Your profile, contacts and delivered messages are stored on your device.") - .font(.callout) - .foregroundColor(theme.colors.secondary) - Text("The profile is only shared with your contacts.") - .font(.callout) - .foregroundColor(theme.colors.secondary) + VStack(alignment: .center, spacing: 20) { + Text("Create your profile") + .font(.largeTitle) + .bold() + .multilineTextAlignment(.center) + + Text("Your profile, contacts and delivered messages are stored on your device.") + .font(.callout) + .foregroundColor(theme.colors.secondary) + .multilineTextAlignment(.center) + + Text("The profile is only shared with your contacts.") + .font(.callout) + .foregroundColor(theme.colors.secondary) + .multilineTextAlignment(.center) + } + .frame(maxWidth: .infinity) // Ensures it takes up the full width + .padding(.top, 25) + .padding(.horizontal, 10) HStack { let name = displayName.trimmingCharacters(in: .whitespaces) let validName = mkValidName(name) - ZStack { + ZStack(alignment: .trailing) { + TextField("Enter your name…", text: $displayName) + .focused($focusDisplayName) + .padding(.horizontal) + .padding(.vertical, 10) + .background( + RoundedRectangle(cornerRadius: 10, style: .continuous) + .fill(Color(uiColor: .tertiarySystemFill)) + ) if name != validName { Button { showAlert(.invalidNameError(validName: validName)) } label: { - Image(systemName: "exclamationmark.circle").foregroundColor(.red) + Image(systemName: "exclamationmark.circle") + .foregroundColor(.red) + .padding(.horizontal, 10) } - } else { - Image(systemName: "exclamationmark.circle").foregroundColor(.clear) - Image(systemName: "pencil").foregroundColor(theme.colors.secondary) } } - TextField("Enter your name…", text: $displayName) - .focused($focusDisplayName) - .padding(.horizontal) - .padding(.vertical, 10) - .background( - RoundedRectangle(cornerRadius: 10, style: .continuous) - .fill(Color(uiColor: .tertiarySystemFill)) - ) } .padding(.top) Spacer() - createProfileButton() - .padding(.bottom) + VStack(spacing: 10) { + createProfileButton() + if !focusDisplayName { + onboardingButtonPlaceholder() + } + } } .onAppear() { focusDisplayName = true setLastVersionDefault() } - .padding() + .padding(.horizontal, 25) + .padding(.top, 10) + .padding(.bottom, 25) .frame(maxWidth: .infinity, alignment: .leading) } @@ -191,8 +209,6 @@ struct CreateFirstProfile: View { private func nextStepDestinationView() -> some View { ChooseServerOperators(onboarding: true) - .navigationTitle("Choose operators") - .navigationBarTitleDisplayMode(.large) .navigationBarBackButtonHidden(true) .modifier(ThemedBackground()) } diff --git a/apps/ios/Shared/Views/Onboarding/HowItWorks.swift b/apps/ios/Shared/Views/Onboarding/HowItWorks.swift index 9a0ee4ddeb..66e63fd9c2 100644 --- a/apps/ios/Shared/Views/Onboarding/HowItWorks.swift +++ b/apps/ios/Shared/Views/Onboarding/HowItWorks.swift @@ -40,8 +40,10 @@ struct HowItWorks: View { Spacer() if onboarding { - createFirstProfileButton() - .padding(.bottom) + VStack(spacing: 10) { + createFirstProfileButton() + onboardingButtonPlaceholder() + } } } .lineLimit(10) diff --git a/apps/ios/Shared/Views/Onboarding/OnboardingView.swift b/apps/ios/Shared/Views/Onboarding/OnboardingView.swift index d004e0306f..b2b1b8fa68 100644 --- a/apps/ios/Shared/Views/Onboarding/OnboardingView.swift +++ b/apps/ios/Shared/Views/Onboarding/OnboardingView.swift @@ -24,14 +24,10 @@ struct OnboardingView: View { CreateSimpleXAddress() case .step3_ChooseServerOperators: ChooseServerOperators(onboarding: true) - .navigationTitle("Choose operators") - .navigationBarTitleDisplayMode(.large) .navigationBarBackButtonHidden(true) .modifier(ThemedBackground()) case .step4_SetNotificationsMode: SetNotificationsMode() - .navigationTitle("Push notifications") - .navigationBarTitleDisplayMode(.large) .navigationBarBackButtonHidden(true) .modifier(ThemedBackground()) case .onboardingComplete: EmptyView() @@ -40,6 +36,10 @@ struct OnboardingView: View { } } +func onboardingButtonPlaceholder() -> some View { + Spacer().frame(height: 40) +} + enum OnboardingStage: String, Identifiable { case step1_SimpleXInfo case step2_CreateProfile // deprecated diff --git a/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift b/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift index 91a755459a..cba290c286 100644 --- a/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift +++ b/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift @@ -13,41 +13,55 @@ struct SetNotificationsMode: View { @EnvironmentObject var m: ChatModel @State private var notificationMode = NotificationsMode.instant @State private var showAlert: NotificationAlert? + @State private var showInfo: Bool = false var body: some View { GeometryReader { g in ScrollView { - VStack(alignment: .leading, spacing: 20) { - Text("Send notifications:") + VStack(alignment: .center, spacing: 20) { + Text("Push Notifications") + .font(.largeTitle) + .bold() + .padding(.top, 50) + + infoText() + + Spacer() + ForEach(NotificationsMode.values) { mode in NtfModeSelector(mode: mode, selection: $notificationMode) } Spacer() - Button { - if let token = m.deviceToken { - setNotificationsMode(token, notificationMode) - } else { - AlertManager.shared.showAlertMsg(title: "No device token!") - } - onboardingStageDefault.set(.onboardingComplete) - m.onboardingStage = .onboardingComplete - } label: { - if case .off = notificationMode { - Text("Use chat") - } else { - Text("Enable notifications") + VStack(spacing: 10) { + Button { + if let token = m.deviceToken { + setNotificationsMode(token, notificationMode) + } else { + AlertManager.shared.showAlertMsg(title: "No device token!") + } + onboardingStageDefault.set(.onboardingComplete) + m.onboardingStage = .onboardingComplete + } label: { + if case .off = notificationMode { + Text("Use chat") + } else { + Text("Enable notifications") + } } + .buttonStyle(OnboardingButtonStyle()) + onboardingButtonPlaceholder() } - .buttonStyle(OnboardingButtonStyle()) - .padding(.bottom) } - .padding() + .padding(25) .frame(minHeight: g.size.height) } } .frame(maxHeight: .infinity) + .sheet(isPresented: $showInfo) { + NotificationsInfoView() + } } private func setNotificationsMode(_ token: DeviceToken, _ mode: NotificationsMode) { @@ -73,6 +87,15 @@ struct SetNotificationsMode: View { } } } + + private func infoText() -> some View { + Button { + showInfo = true + } label: { + Label("How it affects privacy", systemImage: "info.circle") + .font(.headline) + } + } } struct NtfModeSelector: View { @@ -83,15 +106,24 @@ struct NtfModeSelector: View { var body: some View { ZStack { - VStack(alignment: .leading, spacing: 4) { - Text(mode.label) - .font(.headline) + HStack(spacing: 16) { + Image(systemName: mode.icon) + .resizable() + .scaledToFill() + .frame(width: mode.icon == "bolt" ? 14 : 18, height: 18) .foregroundColor(selection == mode ? theme.colors.primary : theme.colors.secondary) - Text(ntfModeDescription(mode)) - .lineLimit(10) - .font(.subheadline) + VStack(alignment: .leading, spacing: 4) { + Text(mode.label) + .font(.headline) + .foregroundColor(selection == mode ? theme.colors.primary : theme.colors.secondary) + Text(ntfModeShortDescription(mode)) + .lineLimit(2) + .font(.callout) + } } - .padding(12) + .padding(.vertical, 12) + .padding(.trailing, 12) + .padding(.leading, 16) } .frame(maxWidth: .infinity, alignment: .leading) .background(tapped ? Color(uiColor: .secondarySystemFill) : theme.colors.background) @@ -107,6 +139,36 @@ struct NtfModeSelector: View { } } +struct NotificationsInfoView: View { + var body: some View { + VStack(alignment: .leading) { + Text("Notification modes") + .font(.largeTitle) + .bold() + .padding(.vertical) + ScrollView { + VStack(alignment: .leading) { + Group { + ForEach(NotificationsMode.values) { mode in + VStack(alignment: .leading, spacing: 4) { + Text(mode.label) + .font(.headline) + Text(ntfModeDescription(mode)) + .lineLimit(10) + .font(.callout) + } + } + } + .padding(.bottom) + } + } + } + .padding() + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) + .modifier(ThemedBackground()) + } +} + struct NotificationsModeView_Previews: PreviewProvider { static var previews: some View { SetNotificationsMode() diff --git a/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift b/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift index 2d90fb2fb2..b6d4c59279 100644 --- a/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift +++ b/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift @@ -19,21 +19,26 @@ struct SimpleXInfo: View { var body: some View { GeometryReader { g in ScrollView { - VStack(alignment: .leading, spacing: 20) { - Image(colorScheme == .light ? "logo" : "logo-light") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: g.size.width * 0.67) - .padding(.bottom, 8) - .frame(maxWidth: .infinity, minHeight: 48, alignment: .top) + VStack(alignment: .leading) { + VStack(alignment: .center, spacing: 10) { + Image(colorScheme == .light ? "logo" : "logo-light") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: g.size.width * 0.67) + .padding(.bottom, 8) + .frame(maxWidth: .infinity, minHeight: 48, alignment: .top) + + Button { + showHowItWorks = true + } label: { + Label("The future of messaging", systemImage: "info.circle") + .font(.headline) + } + } + + Spacer() VStack(alignment: .leading) { - Text("The next generation of private messaging") - .font(.title2) - .padding(.bottom, 30) - .padding(.horizontal, 40) - .frame(maxWidth: .infinity) - .multilineTextAlignment(.center) infoRow("privacy", "Privacy redefined", "The 1st platform without any user identifiers – private by design.", width: 48) infoRow("shield", "Immune to spam and abuse", @@ -45,25 +50,19 @@ struct SimpleXInfo: View { Spacer() if onboarding { - createFirstProfileButton() + VStack(spacing: 10) { + createFirstProfileButton() - Button { - m.migrationState = .pasteOrScanLink - } label: { - Label("Migrate from another device", systemImage: "tray.and.arrow.down") - .font(.subheadline) + Button { + m.migrationState = .pasteOrScanLink + } label: { + Label("Migrate from another device", systemImage: "tray.and.arrow.down") + .font(.system(size: 17, weight: .semibold)) + .frame(minHeight: 40) + } + .frame(maxWidth: .infinity) } - .frame(maxWidth: .infinity) } - - Button { - showHowItWorks = true - } label: { - Label("How it works", systemImage: "info.circle") - .font(.subheadline) - } - .frame(maxWidth: .infinity) - .padding(.bottom) } .frame(minHeight: g.size.height) } @@ -89,7 +88,9 @@ struct SimpleXInfo: View { } } .frame(maxHeight: .infinity) - .padding() + .padding(.horizontal, 25) + .padding(.top, 75) + .padding(.bottom, 25) } private func infoRow(_ image: String, _ title: LocalizedStringKey, _ text: LocalizedStringKey, width: CGFloat) -> some View { @@ -104,7 +105,7 @@ struct SimpleXInfo: View { .padding(.trailing, 10) VStack(alignment: .leading, spacing: 4) { Text(title).font(.headline) - Text(text).frame(minHeight: 40, alignment: .top) + Text(text).frame(minHeight: 40, alignment: .top).font(.callout) } } .padding(.bottom, 20) @@ -121,7 +122,7 @@ struct SimpleXInfo: View { .buttonStyle(OnboardingButtonStyle(isDisabled: false)) NavigationLink(isActive: $createProfileNavLinkActive) { - createProfileDestinationView() + CreateFirstProfile() } label: { EmptyView() } @@ -129,13 +130,6 @@ struct SimpleXInfo: View { .hidden() } } - - private func createProfileDestinationView() -> some View { - CreateFirstProfile() - .navigationTitle("Create your profile") - .navigationBarTitleDisplayMode(.large) - .modifier(ThemedBackground()) - } } struct SimpleXInfo_Previews: PreviewProvider { diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift index 8b6421b502..16aa98bc5f 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift @@ -269,6 +269,7 @@ struct UsageConditionsView: View { } .padding(.bottom) .padding(.bottom) + case let .accepted(operators): Text("Conditions are accepted for the operator(s): **\(operators.map { $0.legalName_ }.joined(separator: ", "))**.") @@ -277,7 +278,7 @@ struct UsageConditionsView: View { .padding(.bottom) } } - .padding(.horizontal) + .padding(.horizontal, 25) .frame(maxHeight: .infinity) } diff --git a/apps/ios/Shared/Views/UserSettings/NotificationsView.swift b/apps/ios/Shared/Views/UserSettings/NotificationsView.swift index b9c92c9919..ee43a24557 100644 --- a/apps/ios/Shared/Views/UserSettings/NotificationsView.swift +++ b/apps/ios/Shared/Views/UserSettings/NotificationsView.swift @@ -243,6 +243,14 @@ func ntfModeDescription(_ mode: NotificationsMode) -> LocalizedStringKey { } } +func ntfModeShortDescription(_ mode: NotificationsMode) -> LocalizedStringKey { + switch mode { + case .off: return "Check messages when allowed." + case .periodic: return "Check messages every 20 min." + case .instant: return "E2E encrypted notifications." + } +} + struct SelectionListView: View { @EnvironmentObject var theme: AppTheme var list: [Item] diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 51aa9108a1..f8cc2ac8b7 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -2158,9 +2158,17 @@ public enum NotificationsMode: String, Decodable, SelectableItem { public var label: LocalizedStringKey { switch self { - case .off: "Local" - case .periodic: "Periodically" - case .instant: "Instantly" + case .off: "No push server" + case .periodic: "Periodic" + case .instant: "Instant" + } + } + + public var icon: String { + switch self { + case .off: return "arrow.clockwise" + case .periodic: return "timer" + case .instant: return "bolt" } } From 25893177d01ec84cef8331a5a3f970c480edc6d9 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Tue, 26 Nov 2024 20:00:39 +0700 Subject: [PATCH 071/167] ios: view conditions as markdown (#5248) * ios: view conditions as markdown * changes * removed Down * refactor * unused * react on theme change --- .../NetworkAndServers/ConditionsWebView.swift | 83 +++++++++++++++++++ .../NetworkAndServers/OperatorView.swift | 45 +++++++--- apps/ios/SimpleX.xcodeproj/project.pbxproj | 21 +++++ .../xcshareddata/swiftpm/Package.resolved | 11 ++- apps/ios/SimpleXChat/Theme/Color.swift | 17 ++++ 5 files changed, 164 insertions(+), 13 deletions(-) create mode 100644 apps/ios/Shared/Views/UserSettings/NetworkAndServers/ConditionsWebView.swift diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ConditionsWebView.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ConditionsWebView.swift new file mode 100644 index 0000000000..1e38b7d5ec --- /dev/null +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ConditionsWebView.swift @@ -0,0 +1,83 @@ +// +// ConditionsWebView.swift +// SimpleX (iOS) +// +// Created by Stanislav Dmitrenko on 26.11.2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import WebKit + +struct ConditionsWebView: UIViewRepresentable { + @State var html: String + @EnvironmentObject var theme: AppTheme + @State var pageLoaded = false + + func makeUIView(context: Context) -> WKWebView { + let view = WKWebView() + view.backgroundColor = .clear + view.isOpaque = false + view.navigationDelegate = context.coordinator + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + // just to make sure that even if updateUIView will not be called for any reason, the page + // will be rendered anyway + if !pageLoaded { + loadPage(view) + } + } + return view + } + + func updateUIView(_ view: WKWebView, context: Context) { + loadPage(view) + } + + private func loadPage(_ webView: WKWebView) { + let styles = """ + + """ + let head = "\(styles)" + webView.loadHTMLString(head + html, baseURL: nil) + DispatchQueue.main.async { + pageLoaded = true + } + } + + func makeCoordinator() -> Cordinator { + Cordinator() + } + + class Cordinator: NSObject, WKNavigationDelegate { + func webView(_ webView: WKWebView, + decidePolicyFor navigationAction: WKNavigationAction, + decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + + guard let url = navigationAction.request.url else { return decisionHandler(.allow) } + + switch navigationAction.navigationType { + case .linkActivated: + decisionHandler(.cancel) + if url.absoluteString.starts(with: "https://simplex.chat/contact#") { + ChatModel.shared.appOpenUrl = url + } else { + UIApplication.shared.open(url) + } + default: + decisionHandler(.allow) + } + } + } +} diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift index 83152a001f..c544d8724c 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift @@ -8,6 +8,7 @@ import SwiftUI import SimpleXChat +import Ink struct OperatorView: View { @Environment(\.dismiss) var dismiss: DismissAction @@ -342,6 +343,7 @@ struct OperatorInfoView: View { struct ConditionsTextView: View { @State private var conditionsData: (UsageConditions, String?, UsageConditions?)? @State private var failedToLoad: Bool = false + @State private var conditionsHTML: String? = nil let defaultConditionsLink = "https://github.com/simplex-chat/simplex-chat/blob/stable/PRIVACY.md" @@ -350,7 +352,18 @@ struct ConditionsTextView: View { .frame(maxWidth: .infinity, maxHeight: .infinity) .task { do { - conditionsData = try await getUsageConditions() + let conditions = try await getUsageConditions() + let conditionsText = conditions.1 + let parentLink = "https://github.com/simplex-chat/simplex-chat/blob/\(conditions.0.conditionsCommit)" + let preparedText: String? + if let conditionsText { + let prepared = prepareMarkdown(conditionsText.trimmingCharacters(in: .whitespacesAndNewlines), parentLink) + conditionsHTML = MarkdownParser().html(from: prepared) + preparedText = prepared + } else { + preparedText = nil + } + conditionsData = (conditions.0, preparedText, conditions.2) } catch let error { logger.error("ConditionsTextView getUsageConditions error: \(responseError(error))") failedToLoad = true @@ -358,18 +371,16 @@ struct ConditionsTextView: View { } } - // TODO Markdown & diff rendering + // TODO Diff rendering @ViewBuilder private func viewBody() -> some View { - if let (usageConditions, conditionsText, acceptedConditions) = conditionsData { - if let conditionsText = conditionsText { - ScrollView { - Text(conditionsText.trimmingCharacters(in: .whitespacesAndNewlines)) - .padding() - } - .background( - RoundedRectangle(cornerRadius: 12, style: .continuous) - .fill(Color(uiColor: .secondarySystemGroupedBackground)) - ) + if let (usageConditions, _, _) = conditionsData { + if let conditionsHTML { + ConditionsWebView(html: conditionsHTML) + .padding(6) + .background( + RoundedRectangle(cornerRadius: 12, style: .continuous) + .fill(Color(uiColor: .secondarySystemGroupedBackground)) + ) } else { let conditionsLink = "https://github.com/simplex-chat/simplex-chat/blob/\(usageConditions.conditionsCommit)/PRIVACY.md" conditionsLinkView(conditionsLink) @@ -391,6 +402,16 @@ struct ConditionsTextView: View { } } } + + private func prepareMarkdown(_ text: String, _ parentLink: String) -> String { + let localLinkRegex = try! NSRegularExpression(pattern: "\\[([^\\(]*)\\]\\(#.*\\)") + let h1Regex = try! NSRegularExpression(pattern: "^# ") + var text = localLinkRegex.stringByReplacingMatches(in: text, options: [], range: NSRange(location: 0, length: text.utf16.count), withTemplate: "$1") + text = h1Regex.stringByReplacingMatches(in: text, options: [], range: NSRange(location: 0, length: text.utf16.count), withTemplate: "") + return text + .replacingOccurrences(of: "](/", with: "](\(parentLink)/") + .replacingOccurrences(of: "](./", with: "](\(parentLink)/") + } } struct SingleOperatorUsageConditionsView: View { diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 4f03ced132..0ffe9d1f40 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -199,6 +199,8 @@ 8C8118722C220B5B00E6FC94 /* Yams in Frameworks */ = {isa = PBXBuildFile; productRef = 8C8118712C220B5B00E6FC94 /* Yams */; }; 8C81482C2BD91CD4002CBEC3 /* AudioDevicePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C81482B2BD91CD4002CBEC3 /* AudioDevicePicker.swift */; }; 8C9BC2652C240D5200875A27 /* ThemeModeEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C9BC2642C240D5100875A27 /* ThemeModeEditor.swift */; }; + 8CB3476C2CF5CFFA006787A5 /* Ink in Frameworks */ = {isa = PBXBuildFile; productRef = 8CB3476B2CF5CFFA006787A5 /* Ink */; }; + 8CB3476E2CF5F58B006787A5 /* ConditionsWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CB3476D2CF5F58B006787A5 /* ConditionsWebView.swift */; }; 8CC4ED902BD7B8530078AEE8 /* CallAudioDeviceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CC4ED8F2BD7B8530078AEE8 /* CallAudioDeviceManager.swift */; }; 8CC956EE2BC0041000412A11 /* NetworkObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CC956ED2BC0041000412A11 /* NetworkObserver.swift */; }; 8CE848A32C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CE848A22C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift */; }; @@ -547,6 +549,7 @@ 8C852B072C1086D100BA61E8 /* Color.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Color.swift; sourceTree = ""; }; 8C86EBE42C0DAE4F00E12243 /* ThemeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeManager.swift; sourceTree = ""; }; 8C9BC2642C240D5100875A27 /* ThemeModeEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeModeEditor.swift; sourceTree = ""; }; + 8CB3476D2CF5F58B006787A5 /* ConditionsWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConditionsWebView.swift; sourceTree = ""; }; 8CC4ED8F2BD7B8530078AEE8 /* CallAudioDeviceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallAudioDeviceManager.swift; sourceTree = ""; }; 8CC956ED2BC0041000412A11 /* NetworkObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkObserver.swift; sourceTree = ""; }; 8CE848A22C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableChatItemToolbars.swift; sourceTree = ""; }; @@ -636,6 +639,7 @@ files = ( 5CE2BA702845308900EC33A6 /* SimpleXChat.framework in Frameworks */, 8C8118722C220B5B00E6FC94 /* Yams in Frameworks */, + 8CB3476C2CF5CFFA006787A5 /* Ink in Frameworks */, D741547829AF89AF0022400A /* StoreKit.framework in Frameworks */, D7197A1829AE89660055C05A /* WebRTC in Frameworks */, D77B92DC2952372200A5A1CC /* SwiftyGif in Frameworks */, @@ -1072,6 +1076,7 @@ 643B3B4D2CCFD6400083A2CF /* OperatorView.swift */, 5C9C2DA6289957AE00CC63B1 /* AdvancedNetworkSettings.swift */, 5C9C2DA82899DA6F00CC63B1 /* NetworkAndServers.swift */, + 8CB3476D2CF5F58B006787A5 /* ConditionsWebView.swift */, ); path = NetworkAndServers; sourceTree = ""; @@ -1183,6 +1188,7 @@ D7F0E33829964E7E0068AF69 /* LZString */, D7197A1729AE89660055C05A /* WebRTC */, 8C8118712C220B5B00E6FC94 /* Yams */, + 8CB3476B2CF5CFFA006787A5 /* Ink */, ); productName = "SimpleX (iOS)"; productReference = 5CA059CA279559F40002BEB4 /* SimpleX.app */; @@ -1326,6 +1332,7 @@ D7F0E33729964E7D0068AF69 /* XCRemoteSwiftPackageReference "lzstring-swift" */, D7197A1629AE89660055C05A /* XCRemoteSwiftPackageReference "WebRTC" */, 8C73C1162C21E17B00892670 /* XCRemoteSwiftPackageReference "Yams" */, + 8CB3476A2CF5CFFA006787A5 /* XCRemoteSwiftPackageReference "ink" */, ); productRefGroup = 5CA059CB279559F40002BEB4 /* Products */; projectDirPath = ""; @@ -1516,6 +1523,7 @@ 5C764E89279CBCB3000C6508 /* ChatModel.swift in Sources */, 5C971E1D27AEBEF600C8A3CE /* ChatInfoView.swift in Sources */, 5CBD285C29575B8E00EC2CF4 /* WhatsNewView.swift in Sources */, + 8CB3476E2CF5F58B006787A5 /* ConditionsWebView.swift in Sources */, 5CC1C99527A6CF7F000D9FF6 /* ShareSheet.swift in Sources */, 5C5E5D3B2824468B00B0488A /* ActiveCallView.swift in Sources */, 5C2E260727A2941F00F70299 /* SimpleXAPI.swift in Sources */, @@ -2375,6 +2383,14 @@ version = 5.1.2; }; }; + 8CB3476A2CF5CFFA006787A5 /* XCRemoteSwiftPackageReference "ink" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/johnsundell/ink"; + requirement = { + kind = exactVersion; + version = 0.6.0; + }; + }; D7197A1629AE89660055C05A /* XCRemoteSwiftPackageReference "WebRTC" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/simplex-chat/WebRTC.git"; @@ -2412,6 +2428,11 @@ package = 8C73C1162C21E17B00892670 /* XCRemoteSwiftPackageReference "Yams" */; productName = Yams; }; + 8CB3476B2CF5CFFA006787A5 /* Ink */ = { + isa = XCSwiftPackageProductDependency; + package = 8CB3476A2CF5CFFA006787A5 /* XCRemoteSwiftPackageReference "ink" */; + productName = Ink; + }; CE38A29B2C3FCD72005ED185 /* SwiftyGif */ = { isa = XCSwiftPackageProductDependency; package = D77B92DA2952372200A5A1CC /* XCRemoteSwiftPackageReference "SwiftyGif" */; diff --git a/apps/ios/SimpleX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/apps/ios/SimpleX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index c8623a95cb..7fdbff38af 100644 --- a/apps/ios/SimpleX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/apps/ios/SimpleX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "e2611d1e91fd8071abc106776ba14ee2e395d2ad08a78e073381294abc10f115", + "originHash" : "33afc44be5f4225325b3cb940ed71b6cbf3ef97290d348d7b6803697bcd0637d", "pins" : [ { "identity" : "codescanner", @@ -10,6 +10,15 @@ "version" : "2.5.0" } }, + { + "identity" : "ink", + "kind" : "remoteSourceControl", + "location" : "https://github.com/johnsundell/ink", + "state" : { + "revision" : "bcc9f219900a62c4210e6db726035d7f03ae757b", + "version" : "0.6.0" + } + }, { "identity" : "lzstring-swift", "kind" : "remoteSourceControl", diff --git a/apps/ios/SimpleXChat/Theme/Color.swift b/apps/ios/SimpleXChat/Theme/Color.swift index 3e8fe1b6e7..f307eaa5aa 100644 --- a/apps/ios/SimpleXChat/Theme/Color.swift +++ b/apps/ios/SimpleXChat/Theme/Color.swift @@ -63,6 +63,23 @@ extension Color { ) } + public func toHTMLHex() -> String { + let uiColor: UIColor = .init(self) + var (r, g, b, a): (CGFloat, CGFloat, CGFloat, CGFloat) = (0, 0, 0, 0) + uiColor.getRed(&r, green: &g, blue: &b, alpha: &a) + // Can be negative values and more than 1. Extended color range, making it normal + r = min(1, max(0, r)) + g = min(1, max(0, g)) + b = min(1, max(0, b)) + a = min(1, max(0, a)) + return String(format: "#%02x%02x%02x%02x", + Int((r * 255).rounded()), + Int((g * 255).rounded()), + Int((b * 255).rounded()), + Int((a * 255).rounded()) + ) + } + public func darker(_ factor: CGFloat = 0.1) -> Color { var (r, g, b, a): (CGFloat, CGFloat, CGFloat, CGFloat) = (0, 0, 0, 0) UIColor(self).getRed(&r, green: &g, blue: &b, alpha: &a) From 8c1abcccfb67c4c12c1a416dd3f0d9645ab054ac Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Tue, 26 Nov 2024 21:22:24 +0700 Subject: [PATCH 072/167] android, desktop: scroll to quoted item without known id (#5254) --- .../common/views/chat/ChatItemsLoader.kt | 10 ++++++++ .../simplex/common/views/chat/ChatView.kt | 25 ++++++++++++++++++- .../common/views/chat/item/ChatItemView.kt | 5 +++- .../common/views/chat/item/FramedItemView.kt | 8 +++--- 4 files changed, 42 insertions(+), 6 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemsLoader.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemsLoader.kt index 22fba59004..be09c04ec1 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemsLoader.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemsLoader.kt @@ -10,6 +10,16 @@ import kotlin.math.min const val TRIM_KEEP_COUNT = 200 +suspend fun apiLoadSingleMessage( + rhId: Long?, + chatType: ChatType, + apiId: Long, + itemId: Long +): ChatItem? = coroutineScope { + val (chat, _) = chatModel.controller.apiGetChat(rhId, chatType, apiId, ChatPagination.Around(itemId, 0), "") ?: return@coroutineScope null + chat.chatItems.firstOrNull() +} + suspend fun apiLoadMessages( rhId: Long?, chatType: ChatType, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index 79f97d9f6b..875868103e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -979,10 +979,12 @@ fun BoxScope.ChatItemsList( } } + val remoteHostIdUpdated = rememberUpdatedState(remoteHostId) val chatInfoUpdated = rememberUpdatedState(chatInfo) val highlightedItems = remember { mutableStateOf(setOf()) } val scope = rememberCoroutineScope() val scrollToItem: (Long) -> Unit = remember { scrollToItem(loadingMoreItems, highlightedItems, chatInfoUpdated, maxHeight, scope, reversedChatItems, mergedItems, listState, loadMessages) } + val scrollToQuotedItemFromItem: (Long) -> Unit = remember { findQuotedItemFromItem(remoteHostIdUpdated, chatInfoUpdated, scope, scrollToItem) } LoadLastItems(loadingMoreItems, remoteHostId, chatInfo) SmallScrollOnNewMessage(listState, chatModel.chatItems) @@ -1042,7 +1044,7 @@ fun BoxScope.ChatItemsList( highlightedItems.value = setOf() } } - ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, highlighted = highlighted, range = range, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, reveal = reveal, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp) + ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, highlighted = highlighted, range = range, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, scrollToQuotedItemFromItem = scrollToQuotedItemFromItem, setReaction = setReaction, showItemDetails = showItemDetails, reveal = reveal, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp) } } @@ -1867,6 +1869,27 @@ private fun scrollToItem( } } +private fun findQuotedItemFromItem( + rhId: State, + chatInfo: State, + scope: CoroutineScope, + scrollToItem: (Long) -> Unit +): (Long) -> Unit = { itemId: Long -> + scope.launch(Dispatchers.Default) { + val item = apiLoadSingleMessage(rhId.value, chatInfo.value.chatType, chatInfo.value.apiId, itemId) + if (item != null) { + withChats { + updateChatItem(chatInfo.value, item) + } + if (item.quotedItem?.itemId != null) { + scrollToItem(item.quotedItem.itemId) + } else { + showQuotedItemDoesNotExistAlert() + } + } + } +} + val chatViewScrollState = MutableStateFlow(false) fun addGroupMembers(groupInfo: GroupInfo, rhId: Long?, view: Any? = null, close: (() -> Unit)? = null) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt index f0c85736af..bd79b78c45 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt @@ -71,6 +71,7 @@ fun ChatItemView( joinGroup: (Long, () -> Unit) -> Unit, acceptCall: (Contact) -> Unit, scrollToItem: (Long) -> Unit, + scrollToQuotedItemFromItem: (Long) -> Unit, acceptFeature: (Contact, ChatFeature, Int?) -> Unit, openDirectChat: (Long) -> Unit, forwardItem: (ChatInfo, ChatItem) -> Unit, @@ -155,7 +156,7 @@ fun ChatItemView( ) { @Composable fun framedItemView() { - FramedItemView(cInfo, cItem, uriHandler, imageProvider, linkMode = linkMode, showViaProxy = showViaProxy, showMenu, showTimestamp = showTimestamp, tailVisible = itemSeparation.largeGap, receiveFile, onLinkLongClick, scrollToItem) + FramedItemView(cInfo, cItem, uriHandler, imageProvider, linkMode = linkMode, showViaProxy = showViaProxy, showMenu, showTimestamp = showTimestamp, tailVisible = itemSeparation.largeGap, receiveFile, onLinkLongClick, scrollToItem, scrollToQuotedItemFromItem) } fun deleteMessageQuestionText(): String { @@ -1087,6 +1088,7 @@ fun PreviewChatItemView( joinGroup = { _, _ -> }, acceptCall = { _ -> }, scrollToItem = {}, + scrollToQuotedItemFromItem = {}, acceptFeature = { _, _, _ -> }, openDirectChat = { _ -> }, forwardItem = { _, _ -> }, @@ -1130,6 +1132,7 @@ fun PreviewChatItemViewDeletedContent() { joinGroup = { _, _ -> }, acceptCall = { _ -> }, scrollToItem = {}, + scrollToQuotedItemFromItem = {}, acceptFeature = { _, _, _ -> }, openDirectChat = { _ -> }, forwardItem = { _, _ -> }, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt index cfaa3eced5..c2df11a8ea 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt @@ -39,6 +39,7 @@ fun FramedItemView( receiveFile: (Long) -> Unit, onLinkLongClick: (link: String) -> Unit = {}, scrollToItem: (Long) -> Unit = {}, + scrollToQuotedItemFromItem: (Long) -> Unit = {}, ) { val sent = ci.chatDir.sent val chatTTL = chatInfo.timedMessagesTTL @@ -130,11 +131,10 @@ fun FramedItemView( .combinedClickable( onLongClick = { showMenu.value = true }, onClick = { - val itemId = qi.itemId - if (itemId != null) { - scrollToItem(itemId) + if (qi.itemId != null) { + scrollToItem(qi.itemId) } else { - showQuotedItemDoesNotExistAlert() + scrollToQuotedItemFromItem(ci.id) } } ) From 15fae29e5b9f8a32a513e5a7c026001f844b3236 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Wed, 27 Nov 2024 11:16:22 +0400 Subject: [PATCH 073/167] android, desktop: offer to create 1-time link on address views (#5253) --- .../common/views/newchat/NewChatSheet.kt | 2 +- .../common/views/newchat/NewChatView.kt | 2 +- .../usersettings/UserAddressLearnMore.kt | 62 ++++++- .../views/usersettings/UserAddressView.kt | 153 +++++++++++++----- .../commonMain/resources/MR/base/strings.xml | 14 ++ 5 files changed, 187 insertions(+), 46 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt index 02996381f8..72118224e6 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt @@ -174,7 +174,7 @@ private fun ModalData.NewChatSheetLayout( val actionButtonsOriginal = listOf( Triple( painterResource(MR.images.ic_add_link), - stringResource(MR.strings.add_contact_tab), + stringResource(MR.strings.create_1_time_link), addContact, ), Triple( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt index 61403e07a4..e08d46d880 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt @@ -98,7 +98,7 @@ fun ModalData.NewChatView(rh: RemoteHostInfo?, selection: NewChatOption, showQRC val tabTitles = NewChatOption.values().map { when(it) { NewChatOption.INVITE -> - stringResource(MR.strings.add_contact_tab) + stringResource(MR.strings.one_time_link_short) NewChatOption.CONNECT -> stringResource(MR.strings.connect_via_link) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressLearnMore.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressLearnMore.kt index efc161ac65..ea0cd4fe28 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressLearnMore.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressLearnMore.kt @@ -3,29 +3,57 @@ package chat.simplex.common.views.usersettings import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.* -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.platform.ColumnWithScrollBar import chat.simplex.common.platform.chatModel import chat.simplex.common.ui.theme.DEFAULT_PADDING +import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF import dev.icerock.moko.resources.compose.stringResource import chat.simplex.common.views.helpers.* -import chat.simplex.common.views.onboarding.ReadableText -import chat.simplex.common.views.onboarding.ReadableTextWithLink +import chat.simplex.common.views.newchat.* +import chat.simplex.common.views.onboarding.* import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource @Composable fun UserAddressLearnMore(showCreateAddressButton: Boolean = false) { ColumnWithScrollBar(Modifier .padding(horizontal = DEFAULT_PADDING)) { - AppBarTitle(stringResource(MR.strings.simplex_address), withPadding = false) - ReadableText(MR.strings.you_can_share_your_address) + AppBarTitle(stringResource(MR.strings.address_or_1_time_link), withPadding = false) + + Row { + Icon(painterResource(MR.images.ic_mail), null, tint = MaterialTheme.colors.secondary) + Spacer(Modifier.width(DEFAULT_PADDING_HALF)) + ReadableText(MR.strings.share_address_publicly, style = MaterialTheme.typography.h3.copy(fontWeight = FontWeight.Bold)) + } + ReadableText(MR.strings.share_simplex_address_on_social_media) ReadableText(MR.strings.you_wont_lose_your_contacts_if_delete_address) - ReadableText(MR.strings.you_can_accept_or_reject_connection) - ReadableTextWithLink(MR.strings.read_more_in_user_guide_with_link, "https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address") + + Row(Modifier.padding(top = DEFAULT_PADDING_HALF)) { + Icon(painterResource(MR.images.ic_add_link), null, tint = MaterialTheme.colors.secondary) + Spacer(Modifier.width(DEFAULT_PADDING_HALF)) + ReadableText(MR.strings.share_1_time_link_with_a_friend, style = MaterialTheme.typography.h3.copy(fontWeight = FontWeight.Bold)) + } + ReadableText(MR.strings.one_time_link_can_be_used_with_one_contact_only) + ReadableText(MR.strings.you_can_set_connection_name_to_remember) + + if (!showCreateAddressButton) { + Row(Modifier.padding(top = DEFAULT_PADDING_HALF)) { + Icon(painterResource(MR.images.ic_shield), null, tint = MaterialTheme.colors.secondary) + Spacer(Modifier.width(DEFAULT_PADDING_HALF)) + ReadableText(MR.strings.connection_security, style = MaterialTheme.typography.h3.copy(fontWeight = FontWeight.Bold)) + } + ReadableText(MR.strings.simplex_address_and_1_time_links_are_safe_to_share) + ReadableText(MR.strings.to_protect_against_your_link_replaced_compare_codes) + ReadableTextWithLink(MR.strings.read_more_in_user_guide_with_link, "https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses") + } if (showCreateAddressButton) { Spacer(Modifier.weight(1f)) @@ -33,7 +61,7 @@ fun UserAddressLearnMore(showCreateAddressButton: Boolean = false) { Button( onClick = { ModalManager.start.showModalCloseable { close -> - UserAddressView(chatModel = chatModel, shareViaProfile = false, autoCreateAddress = true, close = close) + UserAddressView(chatModel = chatModel, shareViaProfile = false, autoCreateAddress = true, close = { ModalManager.start.closeModals() }) } }, shape = CircleShape, @@ -42,6 +70,24 @@ fun UserAddressLearnMore(showCreateAddressButton: Boolean = false) { ) { Text(stringResource(MR.strings.create_simplex_address), style = MaterialTheme.typography.h2, color = Color.White, fontSize = 18.sp, fontWeight = FontWeight.Medium) } + + val closeAll = { ModalManager.start.closeModals() } + TextButton( + onClick = { + ModalManager.start.showModalCloseable(endButtons = { AddContactLearnMoreButton() }) { _ -> + NewChatView(chatModel.currentRemoteHost.value, NewChatOption.INVITE, close = closeAll) + } + }, + Modifier.padding(top = DEFAULT_PADDING, bottom = DEFAULT_PADDING * 2).clip(CircleShape) + ) { + Text( + stringResource(MR.strings.create_1_time_link), + Modifier.padding(start = DEFAULT_PADDING_HALF, end = DEFAULT_PADDING_HALF, bottom = 5.dp), + color = MaterialTheme.colors.primary, + fontWeight = FontWeight.Medium, + textAlign = TextAlign.Center + ) + } } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt index aa9ba70b02..836faee49b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt @@ -31,7 +31,6 @@ import chat.simplex.res.MR @Composable fun UserAddressView( chatModel: ChatModel, - viaCreateLinkView: Boolean = false, shareViaProfile: Boolean = false, autoCreateAddress: Boolean = false, close: () -> Unit @@ -39,7 +38,6 @@ fun UserAddressView( // TODO close when remote host changes val shareViaProfile = remember { mutableStateOf(shareViaProfile) } var progressIndicator by remember { mutableStateOf(false) } - val onCloseHandler: MutableState<(close: () -> Unit) -> Unit> = remember { mutableStateOf({ _ -> }) } val user = remember { chatModel.currentUser } KeyChangeEffect(user.value?.remoteHostId, user.value?.userId) { close() @@ -82,7 +80,7 @@ fun UserAddressView( } LaunchedEffect(autoCreateAddress) { - if (autoCreateAddress) { + if (chatModel.userAddress.value == null && autoCreateAddress) { createAddress() } } @@ -94,7 +92,6 @@ fun UserAddressView( user = user.value, userAddress = userAddress.value, shareViaProfile, - onCloseHandler, createAddress = { createAddress() }, learnMore = { ModalManager.start.showModal { @@ -105,7 +102,7 @@ fun UserAddressView( sendEmail = { userAddress -> uriHandler.sendEmail( generalGetString(MR.strings.email_invite_subject), - generalGetString(MR.strings.email_invite_body).format(simplexChatLink( userAddress.connReqContact)) + generalGetString(MR.strings.email_invite_body).format(simplexChatLink(userAddress.connReqContact)) ) }, setProfileAddress = ::setProfileAddress, @@ -141,12 +138,8 @@ fun UserAddressView( ) } - if (viaCreateLinkView) { + ModalView(close = close) { showLayout() - } else { - ModalView(close = { onCloseHandler.value(close) }) { - showLayout() - } } if (progressIndicator) { @@ -173,7 +166,6 @@ private fun UserAddressLayout( user: User?, userAddress: UserContactLinkRec?, shareViaProfile: MutableState, - onCloseHandler: MutableState<(close: () -> Unit) -> Unit>, createAddress: () -> Unit, learnMore: () -> Unit, share: (String) -> Unit, @@ -190,45 +182,41 @@ private fun UserAddressLayout( verticalArrangement = Arrangement.SpaceEvenly ) { if (userAddress == null) { - SectionView { + SectionView(generalGetString(MR.strings.for_social_media).uppercase()) { CreateAddressButton(createAddress) - SectionTextFooter(stringResource(MR.strings.create_address_and_let_people_connect)) } + + SectionDividerSpaced() + SectionView(generalGetString(MR.strings.or_to_share_privately).uppercase()) { + CreateOneTimeLinkButton() + } + SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false) SectionView { LearnMoreButton(learnMore) } - LaunchedEffect(Unit) { - onCloseHandler.value = { close -> close() } - } } else { - val autoAcceptState = remember { mutableStateOf(AutoAcceptState(userAddress)) } - val autoAcceptStateSaved = remember { mutableStateOf(autoAcceptState.value) } - SectionView(stringResource(MR.strings.address_section_title).uppercase()) { + SectionView(stringResource(MR.strings.for_social_media).uppercase()) { SimpleXLinkQRCode(userAddress.connReqContact) ShareAddressButton { share(simplexChatLink(userAddress.connReqContact)) } - ShareViaEmailButton { sendEmail(userAddress) } - ShareWithContactsButton(shareViaProfile, setProfileAddress) - AutoAcceptToggle(autoAcceptState) { saveAas(autoAcceptState.value, autoAcceptStateSaved) } - LearnMoreButton(learnMore) + // ShareViaEmailButton { sendEmail(userAddress) } + AddressSettingsButton(user, userAddress, shareViaProfile, setProfileAddress, saveAas) } - if (autoAcceptState.value.enable) { - SectionDividerSpaced() - AutoAcceptSection(autoAcceptState, autoAcceptStateSaved, saveAas) + + SectionDividerSpaced() + SectionView(generalGetString(MR.strings.or_to_share_privately).uppercase()) { + CreateOneTimeLinkButton() + } + SectionDividerSpaced(maxBottomPadding = false) + SectionView { + LearnMoreButton(learnMore) } SectionDividerSpaced(maxBottomPadding = false) - SectionView { DeleteAddressButton(deleteAddress) SectionTextFooter(stringResource(MR.strings.your_contacts_will_remain_connected)) } - LaunchedEffect(Unit) { - onCloseHandler.value = { close -> - if (autoAcceptState.value == autoAcceptStateSaved.value) close() - else showUnsavedChangesAlert({ saveAas(autoAcceptState.value, autoAcceptStateSaved); close() }, close) - } - } } } SectionBottomSpacer() @@ -246,11 +234,27 @@ private fun CreateAddressButton(onClick: () -> Unit) { ) } +@Composable +private fun CreateOneTimeLinkButton() { + val closeAll = { ModalManager.start.closeModals() } + SettingsActionItem( + painterResource(MR.images.ic_add_link), + stringResource(MR.strings.create_1_time_link), + click = { + ModalManager.start.showModalCloseable(endButtons = { AddContactLearnMoreButton() }) { _ -> + NewChatView(chatModel.currentRemoteHost.value, NewChatOption.INVITE, close = closeAll) + } + }, + iconColor = MaterialTheme.colors.primary, + textColor = MaterialTheme.colors.primary, + ) +} + @Composable private fun LearnMoreButton(onClick: () -> Unit) { SettingsActionItem( painterResource(MR.images.ic_info), - stringResource(MR.strings.learn_more_about_address), + stringResource(MR.strings.simplex_address_or_1_time_link), onClick, ) } @@ -266,6 +270,85 @@ fun ShareViaEmailButton(onClick: () -> Unit) { ) } +@Composable +private fun AddressSettingsButton( + user: User?, + userAddress: UserContactLinkRec, + shareViaProfile: MutableState, + setProfileAddress: (Boolean) -> Unit, + saveAas: (AutoAcceptState, MutableState) -> Unit, +) { + SettingsActionItem( + painterResource(MR.images.ic_settings), + stringResource(MR.strings.address_settings), + click = { + ModalManager.start.showCustomModal { close -> + UserAddressSettings(user, userAddress, shareViaProfile, setProfileAddress, saveAas, close = close) + } + } + ) +} + +@Composable +private fun ModalData.UserAddressSettings( + user: User?, + userAddress: UserContactLinkRec, + shareViaProfile: MutableState, + setProfileAddress: (Boolean) -> Unit, + saveAas: (AutoAcceptState, MutableState) -> Unit, + close: () -> Unit +) { + val autoAcceptState = remember { stateGetOrPut("autoAcceptState") { (AutoAcceptState(userAddress)) } } + val autoAcceptStateSaved = remember { stateGetOrPut("autoAcceptStateSaved") { (autoAcceptState.value) } } + + fun onClose(close: () -> Unit): Boolean = if (autoAcceptState.value == autoAcceptStateSaved.value) { + chatModel.centerPanelBackgroundClickHandler = null + close() + false + } else { + showUnsavedChangesAlert( + save = { + saveAas(autoAcceptState.value, autoAcceptStateSaved) + chatModel.centerPanelBackgroundClickHandler = null + close() + }, + revert = { + chatModel.centerPanelBackgroundClickHandler = null + close() + } + ) + true + } + + LaunchedEffect(Unit) { + // Enables unsaved changes alert on this view. + chatModel.centerPanelBackgroundClickHandler = { + onClose(close = { ModalManager.start.closeModals() }) + } + } + + ModalView(close = { onClose(close) }) { + ColumnWithScrollBar { + AppBarTitle(stringResource(MR.strings.address_settings), hostDevice(user?.remoteHostId)) + Column( + Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING_HALF), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.SpaceEvenly + ) { + SectionView { + ShareWithContactsButton(shareViaProfile, setProfileAddress) + AutoAcceptToggle(autoAcceptState) { saveAas(autoAcceptState.value, autoAcceptStateSaved) } + } + + if (autoAcceptState.value.enable) { + SectionDividerSpaced() + AutoAcceptSection(autoAcceptState, autoAcceptStateSaved, saveAas) + } + } + } + } +} + @Composable fun ShareWithContactsButton(shareViaProfile: MutableState, setProfileAddress: (Boolean) -> Unit) { PreferenceToggleWithIcon( @@ -441,7 +524,6 @@ fun PreviewUserAddressLayoutNoAddress() { setProfileAddress = { _ -> }, learnMore = {}, shareViaProfile = remember { mutableStateOf(false) }, - onCloseHandler = remember { mutableStateOf({}) }, sendEmail = {}, ) } @@ -475,7 +557,6 @@ fun PreviewUserAddressLayoutAddressCreated() { setProfileAddress = { _ -> }, learnMore = {}, shareViaProfile = remember { mutableStateOf(false) }, - onCloseHandler = remember { mutableStateOf({}) }, sendEmail = {}, ) } diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 8b379d8212..5f095fbf7c 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -675,10 +675,19 @@ If you can\'t meet in person, show QR code in a video call, or share the link. + Share address publicly + Share SimpleX address on social media. You can share your address as a link or QR code - anybody can connect to you. You won\'t lose your contacts if you later delete your address. + Share 1-time link with a friend + with one contact only
- share in person or via any messenger.]]> + You can set connection name, to remember who the link was shared with. + Connection security + SimpleX address and 1-time links are safe to share via any messenger. + To protect against your link being replaced, you can compare contact security codes. When people request to connect, you can accept or reject it. User Guide.]]> + Address or 1-time link? Connect via link @@ -917,6 +926,11 @@ Invite friends Let\'s talk in SimpleX Chat Hi!\nConnect to me via SimpleX Chat: %s + For social media + Or to share privately + SimpleX address or 1-time link? + Create 1-time link + Address settings Continue From 41b7ad01f9708be381547b255c226d7df8e32484 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Wed, 27 Nov 2024 15:51:35 +0400 Subject: [PATCH 074/167] core: apiGetReactionMembers (#5258) --- src/Simplex/Chat.hs | 3 +++ src/Simplex/Chat/Controller.hs | 2 ++ src/Simplex/Chat/Messages.hs | 8 ++++++++ src/Simplex/Chat/View.hs | 1 + 4 files changed, 14 insertions(+) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 5906da57de..707163fde7 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -1078,6 +1078,8 @@ processChatCommand' vr = \case throwChatError (CECommandError $ "reaction already " <> if add then "added" else "removed") when (add && length rs >= maxMsgReactions) $ throwChatError (CECommandError "too many reactions") + APIGetReactionMembers _userId _groupId _itemId _reaction -> withUser $ \user -> do + pure $ chatCmdError (Just user) "not supported" APIPlanForwardChatItems (ChatRef fromCType fromChatId) itemIds -> withUser $ \user -> case fromCType of CTDirect -> planForward user . snd =<< getCommandDirectChatItems user fromChatId itemIds CTGroup -> planForward user . snd =<< getCommandGroupChatItems user fromChatId itemIds @@ -8267,6 +8269,7 @@ chatCommandP = "/_delete item " *> (APIDeleteChatItem <$> chatRefP <*> _strP <* A.space <*> ciDeleteMode), "/_delete member item #" *> (APIDeleteMemberChatItem <$> A.decimal <*> _strP), "/_reaction " *> (APIChatItemReaction <$> chatRefP <* A.space <*> A.decimal <* A.space <*> onOffP <* A.space <*> jsonP), + "/_reaction members " *> (APIGetReactionMembers <$> A.decimal <* A.space <*> A.decimal <* A.space <*> A.decimal <* A.space <*> jsonP), "/_forward plan " *> (APIPlanForwardChatItems <$> chatRefP <*> _strP), "/_forward " *> (APIForwardChatItems <$> chatRefP <* A.space <*> chatRefP <*> _strP <*> sendMessageTTLP), "/_read user " *> (APIUserRead <$> A.decimal), diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 23aa632478..b6f8d5e093 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -304,6 +304,7 @@ data ChatCommand | APIDeleteChatItem ChatRef (NonEmpty ChatItemId) CIDeleteMode | APIDeleteMemberChatItem GroupId (NonEmpty ChatItemId) | APIChatItemReaction {chatRef :: ChatRef, chatItemId :: ChatItemId, add :: Bool, reaction :: MsgReaction} + | APIGetReactionMembers UserId GroupId ChatItemId MsgReaction | APIPlanForwardChatItems {fromChatRef :: ChatRef, chatItemIds :: NonEmpty ChatItemId} | APIForwardChatItems {toChatRef :: ChatRef, fromChatRef :: ChatRef, chatItemIds :: NonEmpty ChatItemId, ttl :: Maybe Int} | APIUserRead UserId @@ -621,6 +622,7 @@ data ChatResponse | CRChatItemUpdated {user :: User, chatItem :: AChatItem} | CRChatItemNotChanged {user :: User, chatItem :: AChatItem} | CRChatItemReaction {user :: User, added :: Bool, reaction :: ACIReaction} + | CRReactionMembers {user :: User, memberReactions :: [MemberReaction]} | CRChatItemsDeleted {user :: User, chatItemDeletions :: [ChatItemDeletion], byUser :: Bool, timed :: Bool} | CRChatItemDeletedNotFound {user :: User, contact :: Contact, sharedMsgId :: SharedMsgId} | CRBroadcastSent {user :: User, msgContent :: MsgContent, successes :: Int, failures :: Int, timestamp :: UTCTime} diff --git a/src/Simplex/Chat/Messages.hs b/src/Simplex/Chat/Messages.hs index 0e3575b64c..274308176b 100644 --- a/src/Simplex/Chat/Messages.hs +++ b/src/Simplex/Chat/Messages.hs @@ -474,6 +474,12 @@ deriving instance Show ACIReaction data JSONCIReaction c d = JSONCIReaction {chatInfo :: ChatInfo c, chatReaction :: CIReaction c d} +data MemberReaction = MemberReaction + { groupMemberId :: GroupMemberId, + reactionTs :: UTCTime + } + deriving (Show) + type family ChatTypeQuotable (a :: ChatType) :: Constraint where ChatTypeQuotable 'CTDirect = () ChatTypeQuotable 'CTGroup = () @@ -1465,6 +1471,8 @@ instance ToJSON ACIReaction where toJSON (ACIReaction _ _ cInfo reaction) = J.toJSON $ JSONCIReaction cInfo reaction toEncoding (ACIReaction _ _ cInfo reaction) = J.toEncoding $ JSONCIReaction cInfo reaction +$(JQ.deriveJSON defaultJSON ''MemberReaction) + $(JQ.deriveJSON defaultJSON ''MsgMetaJSON) msgMetaJson :: MsgMeta -> Text diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index f9ec3f936c..e0c836d8d7 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -154,6 +154,7 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe ttyUser u $ unmuted u chat deletedItem $ viewItemDelete chat deletedItem toItem byUser timed ts tz testView deletions' -> ttyUser u [sShow (length deletions') <> " messages deleted"] CRChatItemReaction u added (ACIReaction _ _ chat reaction) -> ttyUser u $ unmutedReaction u chat reaction $ viewItemReaction showReactions chat reaction added ts tz + CRReactionMembers u memberReactions -> [] CRChatItemDeletedNotFound u Contact {localDisplayName = c} _ -> ttyUser u [ttyFrom $ c <> "> [deleted - original message not found]"] CRBroadcastSent u mc s f t -> ttyUser u $ viewSentBroadcast mc s f ts tz t CRMsgIntegrityError u mErr -> ttyUser u $ viewMsgIntegrityError mErr From 9fa968a5936d9f945b2d257f0db95746fd6397bf Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Wed, 27 Nov 2024 18:30:39 +0400 Subject: [PATCH 075/167] ui: fix marking chat read (don't use range api) (#5257) --- apps/ios/Shared/Model/SimpleXAPI.swift | 23 ++----- apps/ios/Shared/Views/Chat/ChatView.swift | 2 +- apps/ios/SimpleXChat/APITypes.swift | 4 +- .../chat/simplex/common/model/ChatModel.kt | 29 +++----- .../chat/simplex/common/model/SimpleXAPI.kt | 8 +-- .../simplex/common/views/chat/ChatView.kt | 66 +++++++++++-------- .../views/chatlist/ChatListNavLinkView.kt | 4 +- 7 files changed, 63 insertions(+), 73 deletions(-) diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 13b11568d8..c03483311d 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -1061,8 +1061,8 @@ func apiRejectContactRequest(contactReqId: Int64) async throws { throw r } -func apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64)) async throws { - try await sendCommandOkResp(.apiChatRead(type: type, id: id, itemRange: itemRange)) +func apiChatRead(type: ChatType, id: Int64) async throws { + try await sendCommandOkResp(.apiChatRead(type: type, id: id)) } func apiChatItemsRead(type: ChatType, id: Int64, itemIds: [Int64]) async throws { @@ -1368,15 +1368,13 @@ func apiGetNetworkStatuses() throws -> [ConnNetworkStatus] { throw r } -func markChatRead(_ chat: Chat, aboveItem: ChatItem? = nil) async { +func markChatRead(_ chat: Chat) async { do { if chat.chatStats.unreadCount > 0 { - let minItemId = chat.chatStats.minUnreadItemId - let itemRange = (minItemId, aboveItem?.id ?? chat.chatItems.last?.id ?? minItemId) let cInfo = chat.chatInfo - try await apiChatRead(type: cInfo.chatType, id: cInfo.apiId, itemRange: itemRange) + try await apiChatRead(type: cInfo.chatType, id: cInfo.apiId) await MainActor.run { - withAnimation { ChatModel.shared.markChatItemsRead(cInfo, aboveItem: aboveItem) } + withAnimation { ChatModel.shared.markChatItemsRead(cInfo) } } } if chat.chatStats.unreadChat { @@ -1399,17 +1397,6 @@ func markChatUnread(_ chat: Chat, unreadChat: Bool = true) async { } } -func apiMarkChatItemRead(_ cInfo: ChatInfo, _ cItem: ChatItem) async { - do { - try await apiChatRead(type: cInfo.chatType, id: cInfo.apiId, itemRange: (cItem.id, cItem.id)) - DispatchQueue.main.async { - ChatModel.shared.markChatItemsRead(cInfo, [cItem.id]) - } - } catch { - logger.error("apiChatRead error: \(responseError(error))") - } -} - func apiMarkChatItemsRead(_ cInfo: ChatInfo, _ itemIds: [ChatItem.ID]) async { do { try await apiChatItemsRead(type: cInfo.chatType, id: cInfo.apiId, itemIds: itemIds) diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index 1acf08035c..6b287d52a1 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -987,7 +987,7 @@ struct ChatView: View { } } else if chatItem.isRcvNew { waitToMarkRead { - await apiMarkChatItemRead(chat.chatInfo, chatItem) + await apiMarkChatItemsRead(chat.chatInfo, [chatItem.id]) } } } diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index f8cc2ac8b7..1df6d07813 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -137,7 +137,7 @@ public enum ChatCommand { case apiCallStatus(contact: Contact, callStatus: WebRTCCallStatus) // WebRTC calls / case apiGetNetworkStatuses - case apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64)) + case apiChatRead(type: ChatType, id: Int64) case apiChatItemsRead(type: ChatType, id: Int64, itemIds: [Int64]) case apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool) case receiveFile(fileId: Int64, userApprovedRelays: Bool, encrypted: Bool?, inline: Bool?) @@ -310,7 +310,7 @@ public enum ChatCommand { case .apiGetCallInvitations: return "/_call get" case let .apiCallStatus(contact, callStatus): return "/_call status @\(contact.apiId) \(callStatus.rawValue)" case .apiGetNetworkStatuses: return "/_network_statuses" - case let .apiChatRead(type, id, itemRange: (from, to)): return "/_read chat \(ref(type, id)) from=\(from) to=\(to)" + case let .apiChatRead(type, id): return "/_read chat \(ref(type, id))" case let .apiChatItemsRead(type, id, itemIds): return "/_read chat items \(ref(type, id)) \(joinedIds(itemIds))" case let .apiChatUnread(type, id, unreadChat): return "/_unread chat \(ref(type, id)) \(onOff(unreadChat))" case let .receiveFile(fileId, userApprovedRelays, encrypt, inline): return "/freceive \(fileId)\(onOffParam("approved_relays", userApprovedRelays))\(onOffParam("encrypt", encrypt))\(onOffParam("inline", inline))" diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index ca03d0ce72..56e1376ea2 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -333,9 +333,8 @@ object ChatModel { chatItems = arrayListOf(newPreviewItem), chatStats = if (cItem.meta.itemStatus is CIStatus.RcvNew) { - val minUnreadId = if(chat.chatStats.minUnreadItemId == 0L) cItem.id else chat.chatStats.minUnreadItemId increaseUnreadCounter(rhId, currentUser.value!!) - chat.chatStats.copy(unreadCount = chat.chatStats.unreadCount + 1, minUnreadItemId = minUnreadId) + chat.chatStats.copy(unreadCount = chat.chatStats.unreadCount + 1) } else chat.chatStats @@ -514,23 +513,19 @@ object ChatModel { } } - fun markChatItemsRead(remoteHostId: Long?, chatInfo: ChatInfo, range: CC.ItemRange? = null, unreadCountAfter: Int? = null) { + fun markChatItemsRead(remoteHostId: Long?, chatInfo: ChatInfo, itemIds: List? = null) { val cInfo = chatInfo - val markedRead = markItemsReadInCurrentChat(chatInfo, range) + val markedRead = markItemsReadInCurrentChat(chatInfo, itemIds) // update preview val chatIdx = getChatIndex(remoteHostId, cInfo.id) if (chatIdx >= 0) { val chat = chats[chatIdx] val lastId = chat.chatItems.lastOrNull()?.id if (lastId != null) { - val unreadCount = unreadCountAfter ?: if (range != null) chat.chatStats.unreadCount - markedRead else 0 + val unreadCount = if (itemIds != null) chat.chatStats.unreadCount - markedRead else 0 decreaseUnreadCounter(remoteHostId, currentUser.value!!, chat.chatStats.unreadCount - unreadCount) chats[chatIdx] = chat.copy( - chatStats = chat.chatStats.copy( - unreadCount = unreadCount, - // Can't use minUnreadItemId currently since chat items can have unread items between read items - //minUnreadItemId = if (range != null) kotlin.math.max(chat.chatStats.minUnreadItemId, range.to + 1) else lastId + 1 - ) + chatStats = chat.chatStats.copy(unreadCount = unreadCount) ) } } @@ -642,21 +637,17 @@ object ChatModel { } } - private fun markItemsReadInCurrentChat(chatInfo: ChatInfo, range: CC.ItemRange? = null): Int { + private fun markItemsReadInCurrentChat(chatInfo: ChatInfo, itemIds: List? = null): Int { val cInfo = chatInfo var markedRead = 0 if (chatId.value == cInfo.id) { val items = chatItems.value var i = items.lastIndex - val itemIdsFromRange = if (range != null) { - (range.from .. range.to).toMutableSet() - } else { - mutableSetOf() - } + val itemIdsFromRange = itemIds?.toMutableSet() ?: mutableSetOf() val markedReadIds = mutableSetOf() while (i >= 0) { val item = items[i] - if (item.meta.itemStatus is CIStatus.RcvNew && (range == null || itemIdsFromRange.contains(item.id))) { + if (item.meta.itemStatus is CIStatus.RcvNew && (itemIds == null || itemIdsFromRange.contains(item.id))) { val newItem = item.withStatus(CIStatus.RcvRead()) items[i] = newItem if (newItem.meta.itemLive != true && newItem.meta.itemTimed?.ttl != null) { @@ -666,7 +657,7 @@ object ChatModel { } markedReadIds.add(item.id) markedRead++ - if (range != null) { + if (itemIds != null) { itemIdsFromRange.remove(item.id) // already set all needed items as read, can finish the loop if (itemIdsFromRange.isEmpty()) break @@ -674,7 +665,7 @@ object ChatModel { } i-- } - chatItemsChangesListener?.read(if (range != null) markedReadIds else null, items) + chatItemsChangesListener?.read(if (itemIds != null) markedReadIds else null, items) } return markedRead } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index 0cab7ce8e9..a18dd0ac14 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -1599,8 +1599,8 @@ object ChatController { return null } - suspend fun apiChatRead(rh: Long?, type: ChatType, id: Long, range: CC.ItemRange): Boolean { - val r = sendCmd(rh, CC.ApiChatRead(type, id, range)) + suspend fun apiChatRead(rh: Long?, type: ChatType, id: Long): Boolean { + val r = sendCmd(rh, CC.ApiChatRead(type, id)) if (r is CR.CmdOk) return true Log.e(TAG, "apiChatRead bad response: ${r.responseType} ${r.details}") return false @@ -3172,7 +3172,7 @@ sealed class CC { class ApiGetNetworkStatuses(): CC() class ApiAcceptContact(val incognito: Boolean, val contactReqId: Long): CC() class ApiRejectContact(val contactReqId: Long): CC() - class ApiChatRead(val type: ChatType, val id: Long, val range: ItemRange): CC() + class ApiChatRead(val type: ChatType, val id: Long): CC() class ApiChatItemsRead(val type: ChatType, val id: Long, val itemIds: List): CC() class ApiChatUnread(val type: ChatType, val id: Long, val unreadChat: Boolean): CC() class ReceiveFile(val fileId: Long, val userApprovedRelays: Boolean, val encrypt: Boolean, val inline: Boolean?): CC() @@ -3338,7 +3338,7 @@ sealed class CC { is ApiEndCall -> "/_call end @${contact.apiId}" is ApiCallStatus -> "/_call status @${contact.apiId} ${callStatus.value}" is ApiGetNetworkStatuses -> "/_network_statuses" - is ApiChatRead -> "/_read chat ${chatRef(type, id)} from=${range.from} to=${range.to}" + is ApiChatRead -> "/_read chat ${chatRef(type, id)}" is ApiChatItemsRead -> "/_read chat items ${chatRef(type, id)} ${itemIds.joinToString(",")}" is ApiChatUnread -> "/_unread chat ${chatRef(type, id)} ${onOff(unreadChat)}" is ReceiveFile -> diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index 875868103e..2a66daf2db 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -490,19 +490,35 @@ fun ChatView(staleChatId: State, onComposed: suspend (chatId: String) - }, addMembers = { groupInfo -> addGroupMembers(view = view, groupInfo = groupInfo, rhId = chatRh, close = { ModalManager.end.closeModals() }) }, openGroupLink = { groupInfo -> openGroupLink(view = view, groupInfo = groupInfo, rhId = chatRh, close = { ModalManager.end.closeModals() }) }, - markRead = { range, unreadCountAfter -> + markItemsRead = { itemsIds -> withBGApi { withChats { // It's important to call it on Main thread. Otherwise, composable crash occurs from time-to-time without useful stacktrace withContext(Dispatchers.Main) { - markChatItemsRead(chatRh, chatInfo, range, unreadCountAfter) + markChatItemsRead(chatRh, chatInfo, itemsIds) + } + ntfManager.cancelNotificationsForChat(chatInfo.id) + chatModel.controller.apiChatItemsRead( + chatRh, + chatInfo.chatType, + chatInfo.apiId, + itemsIds + ) + } + } + }, + markChatRead = { + withBGApi { + withChats { + // It's important to call it on Main thread. Otherwise, composable crash occurs from time-to-time without useful stacktrace + withContext(Dispatchers.Main) { + markChatItemsRead(chatRh, chatInfo) } ntfManager.cancelNotificationsForChat(chatInfo.id) chatModel.controller.apiChatRead( chatRh, chatInfo.chatType, - chatInfo.apiId, - range + chatInfo.apiId ) } } @@ -602,7 +618,8 @@ fun ChatLayout( showItemDetails: (ChatInfo, ChatItem) -> Unit, addMembers: (GroupInfo) -> Unit, openGroupLink: (GroupInfo) -> Unit, - markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit, + markItemsRead: (List) -> Unit, + markChatRead: () -> Unit, changeNtfsState: (Boolean, currentValue: MutableState) -> Unit, onSearchValueChanged: (String) -> Unit, onComposed: suspend (chatId: String) -> Unit, @@ -649,7 +666,7 @@ fun ChatLayout( useLinkPreviews, linkMode, selectedChatItems, showMemberInfo, loadMessages, deleteMessage, deleteMessages, receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, forwardItem, updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember, - setReaction, showItemDetails, markRead, remember { { onComposed(it) } }, developerTools, showViaProxy, + setReaction, showItemDetails, markItemsRead, markChatRead, remember { { onComposed(it) } }, developerTools, showViaProxy, ) } } @@ -937,7 +954,8 @@ fun BoxScope.ChatItemsList( findModelMember: (String) -> GroupMember?, setReaction: (ChatInfo, ChatItem, Boolean, MsgReaction) -> Unit, showItemDetails: (ChatInfo, ChatItem) -> Unit, - markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit, + markItemsRead: (List) -> Unit, + markChatRead: () -> Unit, onComposed: suspend (chatId: String) -> Unit, developerTools: Boolean, showViaProxy: Boolean @@ -1284,15 +1302,15 @@ fun BoxScope.ChatItemsList( DateSeparator(last.meta.itemTs) } if (item.isRcvNew) { - val (itemIdStart, itemIdEnd) = when (merged) { - is MergedItem.Single -> merged.item.item.id to merged.item.item.id - is MergedItem.Grouped -> merged.items.last().item.id to merged.items.first().item.id + val itemIds = when (merged) { + is MergedItem.Single -> listOf(merged.item.item.id) + is MergedItem.Grouped -> merged.items.map { it.item.id } } - MarkItemsReadAfterDelay(keyForItem(item), itemIdStart, itemIdEnd, finishedInitialComposition, chatInfo.id, listState, markRead) + MarkItemsReadAfterDelay(keyForItem(item), itemIds, finishedInitialComposition, chatInfo.id, listState, markItemsRead) } } } - FloatingButtons(loadingMoreItems, mergedItems, unreadCount, maxHeight, composeViewHeight, remoteHostId, chatInfo, searchValue, markRead, listState) + FloatingButtons(loadingMoreItems, mergedItems, unreadCount, maxHeight, composeViewHeight, remoteHostId, chatInfo, searchValue, markChatRead, listState) FloatingDate(Modifier.padding(top = 10.dp + topPaddingToContent()).align(Alignment.TopCenter), mergedItems, listState) LaunchedEffect(Unit) { @@ -1385,7 +1403,7 @@ fun BoxScope.FloatingButtons( remoteHostId: Long?, chatInfo: ChatInfo, searchValue: State, - markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit, + markChatRead: () -> Unit, listState: State ) { val scope = rememberCoroutineScope() @@ -1453,12 +1471,7 @@ fun BoxScope.FloatingButtons( generalGetString(MR.strings.mark_read), painterResource(MR.images.ic_check), onClick = { - val chat = chatModel.chats.value.firstOrNull { it.remoteHostId == remoteHostId && it.id == chatInfo.id } ?: return@ItemAction - val minUnreadItemId = chat.chatStats.minUnreadItemId - markRead( - CC.ItemRange(minUnreadItemId, chat.chatItems.lastOrNull()?.id ?: return@ItemAction), - 0 - ) + markChatRead() showDropDown.value = false }) } @@ -1779,12 +1792,11 @@ private fun DateSeparator(date: Instant) { @Composable private fun MarkItemsReadAfterDelay( itemKey: String, - itemIdStart: Long, - itemIdEnd: Long, + itemIds: List, finishedInitialComposition: State, chatId: ChatId, listState: State, - markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit + markItemsRead: (List) -> Unit ) { // items can be "visible" in terms of LazyColumn but hidden behind compose view/appBar. So don't count such item as visible and not mark read val itemIsPartiallyAboveCompose = remember { derivedStateOf { @@ -1795,11 +1807,11 @@ private fun MarkItemsReadAfterDelay( false } } } - LaunchedEffect(itemIsPartiallyAboveCompose.value, itemIdStart, itemIdEnd, finishedInitialComposition.value, chatId) { + LaunchedEffect(itemIsPartiallyAboveCompose.value, itemIds, finishedInitialComposition.value, chatId) { if (chatId != ChatModel.chatId.value || !itemIsPartiallyAboveCompose.value || !finishedInitialComposition.value) return@LaunchedEffect delay(600L) - markRead(CC.ItemRange(itemIdStart, itemIdEnd), null) + markItemsRead(itemIds) } } @@ -2406,7 +2418,8 @@ fun PreviewChatLayout() { showItemDetails = { _, _ -> }, addMembers = { _ -> }, openGroupLink = {}, - markRead = { _, _ -> }, + markItemsRead = { _ -> }, + markChatRead = {}, changeNtfsState = { _, _ -> }, onSearchValueChanged = {}, onComposed = {}, @@ -2478,7 +2491,8 @@ fun PreviewGroupChatLayout() { showItemDetails = { _, _ -> }, addMembers = { _ -> }, openGroupLink = {}, - markRead = { _, _ -> }, + markItemsRead = { _ -> }, + markChatRead = {}, changeNtfsState = { _, _ -> }, onSearchValueChanged = {}, onComposed = {}, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt index d071a9d4fd..226030fcd4 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt @@ -537,15 +537,13 @@ fun markChatRead(c: Chat, chatModel: ChatModel) { var chat = c withApi { if (chat.chatStats.unreadCount > 0) { - val minUnreadItemId = chat.chatStats.minUnreadItemId withChats { markChatItemsRead(chat.remoteHostId, chat.chatInfo) } chatModel.controller.apiChatRead( chat.remoteHostId, chat.chatInfo.chatType, - chat.chatInfo.apiId, - CC.ItemRange(minUnreadItemId, chat.chatItems.last().id) + chat.chatInfo.apiId ) chat = chatModel.getChat(chat.id) ?: return@withApi } From ba7abcf6f7da0a8fbf51ee9c8abf100515f33541 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Wed, 27 Nov 2024 19:01:16 +0000 Subject: [PATCH 076/167] ios: update onboarding texts (#5255) * ios: update onboarding texts * translations * more translations * more translations 2 --- .../Shared/Views/Chat/ChatInfoToolbar.swift | 2 +- apps/ios/Shared/Views/Chat/ChatInfoView.swift | 2 +- .../ChatItem/CIFeaturePreferenceView.swift | 2 +- .../Chat/ChatItem/CIGroupInvitationView.swift | 4 +- .../ChatItem/CIMemberCreatedContactView.swift | 2 +- .../Views/Chat/ChatItem/CIMetaView.swift | 8 +- .../Chat/ChatItem/CIRcvDecryptionError.swift | 6 +- .../Chat/ChatItem/MarkedDeletedItemView.swift | 2 +- .../Views/Chat/ChatItem/MsgContentView.swift | 6 +- apps/ios/Shared/Views/Chat/ChatItemView.swift | 6 +- .../Chat/ComposeMessage/ContextItemView.swift | 2 +- .../Views/Chat/Group/GroupChatInfoView.swift | 2 +- .../Chat/Group/GroupMemberInfoView.swift | 2 +- apps/ios/Shared/Views/ChatList/ChatHelp.swift | 2 +- .../Views/ChatList/ChatPreviewView.swift | 10 +- .../Views/Contacts/ContactListNavLink.swift | 2 +- .../Onboarding/AddressCreationCard.swift | 8 +- .../Onboarding/ChooseServerOperators.swift | 9 +- .../Shared/Views/Onboarding/HowItWorks.swift | 13 +- .../Onboarding/SetNotificationsMode.swift | 5 +- .../Shared/Views/Onboarding/SimpleXInfo.swift | 29 +- .../RemoteAccess/ConnectDesktopView.swift | 4 +- .../Views/UserSettings/DeveloperView.swift | 2 +- .../UserSettings/NotificationsView.swift | 6 +- .../UserSettings/UserAddressLearnMore.swift | 13 +- .../ar.xcloc/Localized Contents/ar.xliff | 53 +- .../bg.xcloc/Localized Contents/bg.xliff | 684 ++++++++++----- .../en.lproj/Localizable.strings | 4 - .../bn.xcloc/Localized Contents/bn.xliff | 48 +- .../cs.xcloc/Localized Contents/cs.xliff | 681 ++++++++++----- .../en.lproj/Localizable.strings | 4 - .../de.xcloc/Localized Contents/de.xliff | 687 ++++++++++----- .../en.lproj/Localizable.strings | 4 - .../el.xcloc/Localized Contents/el.xliff | 48 +- .../en.xcloc/Localized Contents/en.xliff | 803 +++++++++++++----- .../en.lproj/Localizable.strings | 4 - .../es.xcloc/Localized Contents/es.xliff | 687 ++++++++++----- .../en.lproj/Localizable.strings | 4 - .../fi.xcloc/Localized Contents/fi.xliff | 681 ++++++++++----- .../en.lproj/Localizable.strings | 4 - .../fr.xcloc/Localized Contents/fr.xliff | 687 ++++++++++----- .../en.lproj/Localizable.strings | 4 - .../he.xcloc/Localized Contents/he.xliff | 49 +- .../hr.xcloc/Localized Contents/hr.xliff | 49 +- .../hu.xcloc/Localized Contents/hu.xliff | 685 ++++++++++----- .../en.lproj/Localizable.strings | 4 - .../it.xcloc/Localized Contents/it.xliff | 687 ++++++++++----- .../en.lproj/Localizable.strings | 4 - .../ja.xcloc/Localized Contents/ja.xliff | 683 ++++++++++----- .../en.lproj/Localizable.strings | 4 - .../ko.xcloc/Localized Contents/ko.xliff | 48 +- .../lt.xcloc/Localized Contents/lt.xliff | 48 +- .../nl.xcloc/Localized Contents/nl.xliff | 685 ++++++++++----- .../en.lproj/Localizable.strings | 4 - .../pl.xcloc/Localized Contents/pl.xliff | 685 ++++++++++----- .../en.lproj/Localizable.strings | 4 - .../Localized Contents/pt-BR.xliff | 53 +- .../pt.xcloc/Localized Contents/pt.xliff | 53 +- .../ru.xcloc/Localized Contents/ru.xliff | 696 ++++++++++----- .../en.lproj/Localizable.strings | 4 - .../th.xcloc/Localized Contents/th.xliff | 681 ++++++++++----- .../en.lproj/Localizable.strings | 4 - .../tr.xcloc/Localized Contents/tr.xliff | 685 ++++++++++----- .../en.lproj/Localizable.strings | 4 - .../uk.xcloc/Localized Contents/uk.xliff | 687 ++++++++++----- .../en.lproj/Localizable.strings | 4 - .../Localized Contents/zh-Hans.xliff | 685 ++++++++++----- .../en.lproj/Localizable.strings | 4 - .../Localized Contents/zh-Hant.xliff | 50 +- apps/ios/bg.lproj/Localizable.strings | 36 +- apps/ios/cs.lproj/Localizable.strings | 34 +- apps/ios/de.lproj/Localizable.strings | 36 +- apps/ios/en.lproj/Localizable.strings | 4 - apps/ios/es.lproj/Localizable.strings | 36 +- apps/ios/fi.lproj/Localizable.strings | 34 +- apps/ios/fr.lproj/Localizable.strings | 36 +- apps/ios/hu.lproj/Localizable.strings | 36 +- apps/ios/it.lproj/Localizable.strings | 36 +- apps/ios/ja.lproj/Localizable.strings | 38 +- apps/ios/nl.lproj/Localizable.strings | 36 +- apps/ios/pl.lproj/Localizable.strings | 36 +- apps/ios/ru.lproj/Localizable.strings | 41 +- apps/ios/th.lproj/Localizable.strings | 34 +- apps/ios/tr.lproj/Localizable.strings | 36 +- apps/ios/uk.lproj/Localizable.strings | 36 +- apps/ios/zh-Hans.lproj/Localizable.strings | 36 +- 86 files changed, 8876 insertions(+), 4190 deletions(-) diff --git a/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift b/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift index 8c9112a858..62a41c504a 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift @@ -45,7 +45,7 @@ struct ChatInfoToolbar: View { } private var contactVerifiedShield: Text { - (Text(Image(systemName: "checkmark.shield")) + Text(" ")) + (Text(Image(systemName: "checkmark.shield")) + textSpace) .font(.caption) .foregroundColor(theme.colors.secondary) .baselineOffset(1) diff --git a/apps/ios/Shared/Views/Chat/ChatInfoView.swift b/apps/ios/Shared/Views/Chat/ChatInfoView.swift index 35adcd49c1..c829e1a2b9 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoView.swift @@ -339,7 +339,7 @@ struct ChatInfoView: View { Text(Image(systemName: "checkmark.shield")) .foregroundColor(theme.colors.secondary) .font(.title2) - + Text(" ") + + textSpace + Text(contact.profile.displayName) .font(.largeTitle) ) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIFeaturePreferenceView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIFeaturePreferenceView.swift index 752f599c8d..2c9c261536 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIFeaturePreferenceView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIFeaturePreferenceView.swift @@ -47,7 +47,7 @@ struct CIFeaturePreferenceView: View { + Text(acceptText) .fontWeight(.medium) .foregroundColor(theme.colors.primary) - + Text(" ") + + Text(verbatim: " ") } r = r + chatItem.timestampText .fontWeight(.light) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift index 1a77b36d6f..107208a033 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift @@ -45,7 +45,7 @@ struct CIGroupInvitationView: View { Text(chatIncognito ? "Tap to join incognito" : "Tap to join") .foregroundColor(inProgress ? theme.colors.secondary : chatIncognito ? .indigo : theme.colors.primary) .font(.callout) - + Text(" ") + + Text(verbatim: " ") + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, colorMode: .transparent, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp) ) .overlay(DetermineWidth()) @@ -53,7 +53,7 @@ struct CIGroupInvitationView: View { } else { ( groupInvitationText() - + Text(" ") + + Text(verbatim: " ") + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, colorMode: .transparent, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp) ) .overlay(DetermineWidth()) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIMemberCreatedContactView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIMemberCreatedContactView.swift index 463695ddb7..e9cd838234 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIMemberCreatedContactView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIMemberCreatedContactView.swift @@ -45,7 +45,7 @@ struct CIMemberCreatedContactView: View { + Text(openText) .fontWeight(.medium) .foregroundColor(theme.colors.primary) - + Text(" ") + + Text(verbatim: " ") } r = r + chatItem.timestampText .fontWeight(.light) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift index 32249506d3..e58ad0f74e 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift @@ -83,7 +83,7 @@ enum MetaColorMode { ? Image("checkmark.wide") : Image(systemName: "circlebadge.fill") ).foregroundColor(.clear) - case .invertedMaterial: Text(" ").kerning(13) + case .invertedMaterial: textSpace.kerning(13) } } } @@ -120,7 +120,7 @@ func ciMetaText( if ttl != chatTTL { r = r + colored(Text(shortTimeText(ttl)), resolved) } - space = Text(" ") + space = textSpace } if showViaProxy, meta.sentViaProxy == true { appendSpace() @@ -138,12 +138,12 @@ func ciMetaText( } else if !meta.disappearing { r = r + colorMode.statusSpacer(meta.itemStatus.sent) } - space = Text(" ") + space = textSpace } if let enc = encrypted { appendSpace() r = r + statusIconText(enc ? "lock" : "lock.open", resolved) - space = Text(" ") + space = textSpace } if showTimesamp { appendSpace() diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift index 693641b1d3..4603a026cd 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift @@ -121,11 +121,11 @@ struct CIRcvDecryptionError: View { Text(Image(systemName: "exclamationmark.arrow.triangle.2.circlepath")) .foregroundColor(syncSupported ? theme.colors.primary : theme.colors.secondary) .font(.callout) - + Text(" ") + + textSpace + Text("Fix connection") .foregroundColor(syncSupported ? theme.colors.primary : theme.colors.secondary) .font(.callout) - + Text(" ") + + Text(verbatim: " ") + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, colorMode: .transparent, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp) ) } @@ -144,7 +144,7 @@ struct CIRcvDecryptionError: View { Text(chatItem.content.text) .foregroundColor(.red) .italic() - + Text(" ") + + Text(verbatim: " ") + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, colorMode: .transparent, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp) } .padding(.horizontal, 12) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift index 6631206987..c2b4021edc 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift @@ -17,7 +17,7 @@ struct MarkedDeletedItemView: View { var chatItem: ChatItem var body: some View { - (Text(mergedMarkedDeletedText).italic() + Text(" ") + chatItem.timestampText) + (Text(mergedMarkedDeletedText).italic() + textSpace + chatItem.timestampText) .font(.caption) .foregroundColor(theme.colors.secondary) .padding(.horizontal, 12) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift index 63d5dc30dc..914f7c8a2f 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift @@ -11,7 +11,7 @@ import SimpleXChat let uiLinkColor = UIColor(red: 0, green: 0.533, blue: 1, alpha: 1) -private let noTyping = Text(" ") +private let noTyping = Text(verbatim: " ") private let typingIndicators: [Text] = [ (typing(.black) + typing() + typing()), @@ -85,7 +85,7 @@ struct MsgContentView: View { } private func reserveSpaceForMeta(_ mt: CIMeta) -> Text { - (rightToLeft ? Text("\n") : Text(" ")) + ciMetaText(mt, chatTTL: chat.chatInfo.timedMessagesTTL, encrypted: nil, colorMode: .transparent, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp) + (rightToLeft ? Text("\n") : Text(verbatim: " ")) + ciMetaText(mt, chatTTL: chat.chatInfo.timedMessagesTTL, encrypted: nil, colorMode: .transparent, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp) } } @@ -104,7 +104,7 @@ func messageText(_ text: String, _ formattedText: [FormattedText]?, _ sender: St } if let i = icon { - res = Text(Image(systemName: i)).foregroundColor(secondaryColor) + Text(" ") + res + res = Text(Image(systemName: i)).foregroundColor(secondaryColor) + textSpace + res } if let s = sender { diff --git a/apps/ios/Shared/Views/Chat/ChatItemView.swift b/apps/ios/Shared/Views/Chat/ChatItemView.swift index 418c2d7c34..ebbc55a932 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemView.swift @@ -170,7 +170,7 @@ struct ChatItemContentView: View { private func eventItemViewText(_ secondaryColor: Color) -> Text { if !revealed, let t = mergedGroupEventText { - return chatEventText(t + Text(" ") + chatItem.timestampText, secondaryColor) + return chatEventText(t + textSpace + chatItem.timestampText, secondaryColor) } else if let member = chatItem.memberDisplayName { return Text(member + " ") .font(.caption) @@ -203,7 +203,7 @@ struct ChatItemContentView: View { } else if ns.count == 0 { Text("\(count) group events") } else if count > ns.count { - Text(members) + Text(" ") + Text("and \(count - ns.count) other events") + Text(members) + textSpace + Text("and \(count - ns.count) other events") } else { Text(members) } @@ -234,7 +234,7 @@ func chatEventText(_ text: Text, _ secondaryColor: Color) -> Text { } func chatEventText(_ eventText: LocalizedStringKey, _ ts: Text, _ secondaryColor: Color) -> Text { - chatEventText(Text(eventText) + Text(" ") + ts, secondaryColor) + chatEventText(Text(eventText) + textSpace + ts, secondaryColor) } func chatEventText(_ ci: ChatItem, _ secondaryColor: Color) -> Text { diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift index 8b988f5624..fa999961fc 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift @@ -85,7 +85,7 @@ struct ContextItemView: View { } func image(_ s: String) -> Text { - Text(Image(systemName: s)).foregroundColor(Color(uiColor: .tertiaryLabel)) + Text(" ") + Text(Image(systemName: s)).foregroundColor(Color(uiColor: .tertiaryLabel)) + textSpace } } } diff --git a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift index 9385633060..59df52df9f 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift @@ -418,7 +418,7 @@ struct GroupChatInfoView: View { } private var memberVerifiedShield: Text { - (Text(Image(systemName: "checkmark.shield")) + Text(" ")) + (Text(Image(systemName: "checkmark.shield")) + textSpace) .font(.caption) .baselineOffset(2) .kerning(-2) diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift index fd72b5b515..ed40c0592b 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift @@ -388,7 +388,7 @@ struct GroupMemberInfoView: View { Text(Image(systemName: "checkmark.shield")) .foregroundColor(theme.colors.secondary) .font(.title2) - + Text(" ") + + textSpace + Text(mem.displayName) .font(.largeTitle) ) diff --git a/apps/ios/Shared/Views/ChatList/ChatHelp.swift b/apps/ios/Shared/Views/ChatList/ChatHelp.swift index 776229f60a..7abab33177 100644 --- a/apps/ios/Shared/Views/ChatList/ChatHelp.swift +++ b/apps/ios/Shared/Views/ChatList/ChatHelp.swift @@ -42,7 +42,7 @@ struct ChatHelp: View { Text("above, then choose:") } - Text("**Create 1-time link**: to create a new invitation link.") + Text("**Create 1-time link**: to create and share a new invitation link.") Text("**Scan / Paste link**: to connect via a link you received.") Text("**Create group**: to create a new group.") } diff --git a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift index d721d546c1..13701a40a2 100644 --- a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift @@ -172,7 +172,7 @@ struct ChatPreviewView: View { } private var verifiedIcon: Text { - (Text(Image(systemName: "checkmark.shield")) + Text(" ")) + (Text(Image(systemName: "checkmark.shield")) + textSpace) .foregroundColor(theme.colors.secondary) .baselineOffset(1) .kerning(-2) @@ -232,12 +232,12 @@ struct ChatPreviewView: View { + messageText(msg, parseSimpleXMarkdown(msg), nil, preview: true, showSecrets: false, secondaryColor: theme.colors.secondary) func image(_ s: String, color: Color = Color(uiColor: .tertiaryLabel)) -> Text { - Text(Image(systemName: s)).foregroundColor(color) + Text(" ") + Text(Image(systemName: s)).foregroundColor(color) + textSpace } func attachment() -> Text { switch draft.preview { - case let .filePreview(fileName, _): return image("doc.fill") + Text(fileName) + Text(" ") + case let .filePreview(fileName, _): return image("doc.fill") + Text(fileName) + textSpace case .mediaPreviews: return image("photo") case let .voicePreview(_, duration): return image("play.fill") + Text(durationText(duration)) default: return Text("") @@ -367,11 +367,11 @@ struct ChatPreviewView: View { case .sndErrorAuth, .sndError: return Text(Image(systemName: "multiply")) .font(.caption) - .foregroundColor(.red) + Text(" ") + .foregroundColor(.red) + textSpace case .sndWarning: return Text(Image(systemName: "exclamationmark.triangle.fill")) .font(.caption) - .foregroundColor(.orange) + Text(" ") + .foregroundColor(.orange) + textSpace default: return Text("") } } diff --git a/apps/ios/Shared/Views/Contacts/ContactListNavLink.swift b/apps/ios/Shared/Views/Contacts/ContactListNavLink.swift index 4b43610236..898a47cc86 100644 --- a/apps/ios/Shared/Views/Contacts/ContactListNavLink.swift +++ b/apps/ios/Shared/Views/Contacts/ContactListNavLink.swift @@ -151,7 +151,7 @@ struct ContactListNavLink: View { } private var verifiedIcon: Text { - (Text(Image(systemName: "checkmark.shield")) + Text(" ")) + (Text(Image(systemName: "checkmark.shield")) + textSpace) .foregroundColor(.secondary) .baselineOffset(1) .kerning(-2) diff --git a/apps/ios/Shared/Views/Onboarding/AddressCreationCard.swift b/apps/ios/Shared/Views/Onboarding/AddressCreationCard.swift index 9cf755be78..c757dcfeeb 100644 --- a/apps/ios/Shared/Views/Onboarding/AddressCreationCard.swift +++ b/apps/ios/Shared/Views/Onboarding/AddressCreationCard.swift @@ -34,13 +34,7 @@ struct AddressCreationCard: View { Text("Your SimpleX address") .font(.title3) Spacer() - HStack(alignment: .center) { - Text("How to use it") - VStack { - Image(systemName: "info.circle") - .foregroundColor(theme.colors.secondary) - } - } + Text("How to use it") + textSpace + Text(Image(systemName: "info.circle")).foregroundColor(theme.colors.secondary) } } .frame(maxWidth: .infinity, alignment: .leading) diff --git a/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift b/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift index 4efdb99f21..910f2a4127 100644 --- a/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift +++ b/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift @@ -133,7 +133,7 @@ struct ChooseServerOperators: View { reviewLaterButton() ( Text("Conditions will be accepted for enabled operators after 30 days.") - + Text(" ") + + textSpace + Text("You can configure operators in Network & servers settings.") ) .multilineTextAlignment(.center) @@ -405,6 +405,8 @@ struct ChooseServerOperators: View { } } +let operatorsPostLink = URL(string: "https://simplex.chat/blog/20241125-servers-operated-by-flux-true-privacy-and-decentralization-for-all-users.html")! + struct ChooseServerOperatorsInfoView: View { var body: some View { VStack(alignment: .leading) { @@ -415,8 +417,9 @@ struct ChooseServerOperatorsInfoView: View { ScrollView { VStack(alignment: .leading) { Group { - Text("When more than one network operator is enabled, the app will use the servers of different operators for each conversation.") - Text("For example, if you receive messages via SimpleX Chat server, the app will use one of Flux servers for private routing.") + Text("The app protects your privacy by using different operators in each conversation.") + Text("When more than one operator is enabled, none of them has metadata to learn who communicates with whom.") + Text("For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server.") } .padding(.bottom) } diff --git a/apps/ios/Shared/Views/Onboarding/HowItWorks.swift b/apps/ios/Shared/Views/Onboarding/HowItWorks.swift index 66e63fd9c2..7452d74e91 100644 --- a/apps/ios/Shared/Views/Onboarding/HowItWorks.swift +++ b/apps/ios/Shared/Views/Onboarding/HowItWorks.swift @@ -23,13 +23,10 @@ struct HowItWorks: View { ScrollView { VStack(alignment: .leading) { Group { - Text("Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*") - Text("To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.") - Text("You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them.") - Text("Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.") - if onboarding { - Text("Read more in our GitHub repository.") - } else { + Text("To protect your privacy, SimpleX uses separate IDs for each of your contacts.") + Text("Only client devices store user profiles, contacts, groups, and messages.") + Text("All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages.") + if !onboarding { Text("Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme).") } } @@ -47,7 +44,7 @@ struct HowItWorks: View { } } .lineLimit(10) - .padding() + .padding(onboarding ? 25 : 16) .frame(maxHeight: .infinity, alignment: .top) .modifier(ThemedBackground()) } diff --git a/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift b/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift index cba290c286..6164fcae70 100644 --- a/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift +++ b/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift @@ -142,7 +142,7 @@ struct NtfModeSelector: View { struct NotificationsInfoView: View { var body: some View { VStack(alignment: .leading) { - Text("Notification modes") + Text("Notifications privacy") .font(.largeTitle) .bold() .padding(.vertical) @@ -151,8 +151,9 @@ struct NotificationsInfoView: View { Group { ForEach(NotificationsMode.values) { mode in VStack(alignment: .leading, spacing: 4) { - Text(mode.label) + (Text(Image(systemName: mode.icon)) + textSpace + Text(mode.label)) .font(.headline) + .foregroundColor(.secondary) Text(ntfModeDescription(mode)) .lineLimit(10) .font(.callout) diff --git a/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift b/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift index b6d4c59279..a8704e964b 100644 --- a/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift +++ b/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift @@ -26,6 +26,7 @@ struct SimpleXInfo: View { .aspectRatio(contentMode: .fit) .frame(width: g.size.width * 0.67) .padding(.bottom, 8) + .padding(.leading, 4) .frame(maxWidth: .infinity, minHeight: 48, alignment: .top) Button { @@ -39,13 +40,14 @@ struct SimpleXInfo: View { Spacer() VStack(alignment: .leading) { - infoRow("privacy", "Privacy redefined", - "The 1st platform without any user identifiers – private by design.", width: 48) - infoRow("shield", "Immune to spam and abuse", - "People can connect to you only via the links you share.", width: 46) - infoRow(colorScheme == .light ? "decentralized" : "decentralized-light", "Decentralized", - "Open-source protocol and code – anybody can run the servers.", width: 44) + onboardingInfoRow("privacy", "Privacy redefined", + "No user identifiers.", width: 48) + onboardingInfoRow("shield", "Immune to spam", + "You decide who can connect.", width: 46) + onboardingInfoRow(colorScheme == .light ? "decentralized" : "decentralized-light", "Decentralized", + "Anybody can host servers.", width: 46) } + .padding(.leading, 16) Spacer() @@ -93,23 +95,24 @@ struct SimpleXInfo: View { .padding(.bottom, 25) } - private func infoRow(_ image: String, _ title: LocalizedStringKey, _ text: LocalizedStringKey, width: CGFloat) -> some View { + private func onboardingInfoRow(_ image: String, _ title: LocalizedStringKey, _ text: LocalizedStringKey, width: CGFloat) -> some View { HStack(alignment: .top) { Image(image) .resizable() .scaledToFit() .frame(width: width, height: 54) .frame(width: 54) - .padding(.top, 4) - .padding(.leading, 4) .padding(.trailing, 10) VStack(alignment: .leading, spacing: 4) { Text(title).font(.headline) - Text(text).frame(minHeight: 40, alignment: .top).font(.callout) + Text(text).frame(minHeight: 40, alignment: .top) + .font(.callout) + .lineLimit(3) + .fixedSize(horizontal: false, vertical: true) } + .padding(.top, 4) } - .padding(.bottom, 20) - .padding(.trailing, 6) + .padding(.bottom, 12) } private func createFirstProfileButton() -> some View { @@ -132,6 +135,8 @@ struct SimpleXInfo: View { } } +let textSpace = Text(verbatim: " ") + struct SimpleXInfo_Previews: PreviewProvider { static var previews: some View { SimpleXInfo(onboarding: true) diff --git a/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift b/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift index b99c054abb..67020e09e7 100644 --- a/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift +++ b/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift @@ -268,7 +268,7 @@ struct ConnectDesktopView: View { private func ctrlDeviceNameText(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo?) -> Text { var t = Text(rc?.deviceViewName ?? session.ctrlAppInfo?.deviceName ?? "") if (rc == nil) { - t = t + Text(" ") + Text("(new)").italic() + t = t + textSpace + Text("(new)").italic() } return t } @@ -277,7 +277,7 @@ struct ConnectDesktopView: View { let v = session.ctrlAppInfo?.appVersionRange.maxVersion var t = Text("v\(v ?? "")") if v != session.appVersion { - t = t + Text(" ") + Text("(this device v\(session.appVersion))").italic() + t = t + textSpace + Text("(this device v\(session.appVersion))").italic() } return t } diff --git a/apps/ios/Shared/Views/UserSettings/DeveloperView.swift b/apps/ios/Shared/Views/UserSettings/DeveloperView.swift index 4ef05bd998..513a6c2708 100644 --- a/apps/ios/Shared/Views/UserSettings/DeveloperView.swift +++ b/apps/ios/Shared/Views/UserSettings/DeveloperView.swift @@ -45,7 +45,7 @@ struct DeveloperView: View { } header: { Text("") } footer: { - ((developerTools ? Text("Show:") : Text("Hide:")) + Text(" ") + Text("Database IDs and Transport isolation option.")) + ((developerTools ? Text("Show:") : Text("Hide:")) + textSpace + Text("Database IDs and Transport isolation option.")) .foregroundColor(theme.colors.secondary) } diff --git a/apps/ios/Shared/Views/UserSettings/NotificationsView.swift b/apps/ios/Shared/Views/UserSettings/NotificationsView.swift index ee43a24557..4e7f826f4f 100644 --- a/apps/ios/Shared/Views/UserSettings/NotificationsView.swift +++ b/apps/ios/Shared/Views/UserSettings/NotificationsView.swift @@ -237,9 +237,9 @@ struct NotificationsView: View { func ntfModeDescription(_ mode: NotificationsMode) -> LocalizedStringKey { switch mode { - case .off: return "**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." - case .periodic: return "**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." - case .instant: return "**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." + case .off: return "**Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app." + case .periodic: return "**More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata." + case .instant: return "**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from." } } diff --git a/apps/ios/Shared/Views/UserSettings/UserAddressLearnMore.swift b/apps/ios/Shared/Views/UserSettings/UserAddressLearnMore.swift index 414e7efe85..6c1ea8deb2 100644 --- a/apps/ios/Shared/Views/UserSettings/UserAddressLearnMore.swift +++ b/apps/ios/Shared/Views/UserSettings/UserAddressLearnMore.swift @@ -16,20 +16,23 @@ struct UserAddressLearnMore: View { var body: some View { VStack { List { - VStack(alignment: .leading, spacing: 16) { - (Text(Image(systemName: "envelope")).foregroundColor(.secondary) + Text(" ") + Text("Share address publicly").bold().font(.title2)) + VStack(alignment: .leading, spacing: 12) { + (Text(Image(systemName: "envelope")).foregroundColor(.secondary) + textSpace + Text("Share address publicly").bold().font(.title2)) Text("Share SimpleX address on social media.") Text("You won't lose your contacts if you later delete your address.") - (Text(Image(systemName: "link.badge.plus")).foregroundColor(.secondary) + Text(" ") + Text("Share 1-time link with a friend").font(.title2).bold()) + (Text(Image(systemName: "link.badge.plus")).foregroundColor(.secondary) + textSpace + Text("Share 1-time link with a friend").font(.title2).bold()) + .padding(.top) Text("1-time link can be used *with one contact only* - share in person or via any messenger.") Text("You can set connection name, to remember who the link was shared with.") if !showCreateAddressButton { - (Text(Image(systemName: "shield")).foregroundColor(.secondary) + Text(" ") + Text("Connection security").font(.title2).bold()) + (Text(Image(systemName: "shield")).foregroundColor(.secondary) + textSpace + Text("Connection security").font(.title2).bold()) + .padding(.top) Text("SimpleX address and 1-time links are safe to share via any messenger.") Text("To protect against your link being replaced, you can compare contact security codes.") Text("Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses).") + .padding(.top) } } @@ -81,7 +84,7 @@ struct UserAddressLearnMore: View { createOneTimeLinkActive = true } label: { Text("Create 1-time link") - .font(.footnote) + .font(.callout) } NavigationLink(isActive: $createOneTimeLinkActive) { diff --git a/apps/ios/SimpleX Localizations/ar.xcloc/Localized Contents/ar.xliff b/apps/ios/SimpleX Localizations/ar.xcloc/Localized Contents/ar.xliff index 40481d81f1..53707d108f 100644 --- a/apps/ios/SimpleX Localizations/ar.xcloc/Localized Contents/ar.xliff +++ b/apps/ios/SimpleX Localizations/ar.xcloc/Localized Contents/ar.xliff @@ -187,23 +187,18 @@ ) No comment provided by engineer. - - **Add new contact**: to create your one-time QR Code or link for your contact. - ** إضافة جهة اتصال جديدة **: لإنشاء رمز QR لمرة واحدة أو رابط جهة الاتصال الخاصة بكم. - No comment provided by engineer. - **Create link / QR code** for your contact to use. ** أنشئ رابطًا / رمز QR ** لتستخدمه جهة الاتصال الخاصة بك. No comment provided by engineer. - - **More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have. + + **More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata. ** المزيد من الخصوصية **: تحققوا من الرسائل الجديدة كل 20 دقيقة. تتم مشاركة رمز الجهاز مع خادم SimpleX Chat ، ولكن ليس عدد جهات الاتصال أو الرسائل لديكم. No comment provided by engineer. - - **Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app). + + **Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app. ** الأكثر خصوصية **: لا تستخدم خادم إشعارات SimpleX Chat ، وتحقق من الرسائل بشكل دوري في الخلفية (يعتمد على عدد مرات استخدامكم للتطبيق). No comment provided by engineer. @@ -217,8 +212,8 @@ ** يرجى ملاحظة **: لن تتمكنوا من استعادة أو تغيير عبارة المرور إذا فقدتموها. No comment provided by engineer. - - **Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from. + + **Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from. ** موصى به **: يتم إرسال رمز الجهاز والإشعارات إلى خادم إشعارات SimpleX Chat ، ولكن ليس محتوى الرسالة أو حجمها أو مصدرها. No comment provided by engineer. @@ -1528,8 +1523,8 @@ Image will be received when your contact is online, please wait or check later! No comment provided by engineer. - - Immune to spam and abuse + + Immune to spam No comment provided by engineer. @@ -1926,8 +1921,8 @@ We will be adding server redundancy to prevent lost messages. Onion hosts will not be used. No comment provided by engineer. - - Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**. + + Only client devices store user profiles, contacts, groups, and messages. No comment provided by engineer. @@ -1978,8 +1973,8 @@ We will be adding server redundancy to prevent lost messages. Open user profiles authentication reason - - Open-source protocol and code – anybody can run the servers. + + Anybody can host servers. No comment provided by engineer. @@ -2010,8 +2005,8 @@ We will be adding server redundancy to prevent lost messages. Paste the link you received into the box below to connect with your contact. No comment provided by engineer. - - People can connect to you only via the links you share. + + You decide who can connect. No comment provided by engineer. @@ -2590,8 +2585,8 @@ We will be adding server redundancy to prevent lost messages. Thanks to the users – contribute via Weblate! No comment provided by engineer. - - The 1st platform without any user identifiers – private by design. + + No user identifiers. No comment provided by engineer. @@ -2622,8 +2617,8 @@ We will be adding server redundancy to prevent lost messages. The microphone does not work when the app is in the background. No comment provided by engineer. - - The next generation of private messaging + + The future of messaging No comment provided by engineer. @@ -2686,8 +2681,8 @@ We will be adding server redundancy to prevent lost messages. To prevent the call interruption, enable Do Not Disturb mode. No comment provided by engineer. - - To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts. + + To protect your privacy, SimpleX uses separate IDs for each of your contacts. No comment provided by engineer. @@ -2972,10 +2967,6 @@ To connect, please ask your contact to create another connection link and check You can use markdown to format messages: No comment provided by engineer. - - You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them. - No comment provided by engineer. - You could not be verified; please try again. No comment provided by engineer. @@ -3752,8 +3743,8 @@ SimpleX servers cannot see your profile. %u messages skipped. %u تم تخطي الرسائل. - - **Add contact**: to create a new invitation link, or connect via a link you received. + + **Create 1-time link**: to create and share a new invitation link. **إضافة جهة اتصال**: لإنشاء رابط دعوة جديد، أو الاتصال عبر الرابط الذي تلقيتوهم. diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff index 1a40820dce..310b5e8bb3 100644 --- a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff @@ -12,21 +12,6 @@ No comment provided by engineer. - - - - No comment provided by engineer. - - - - - No comment provided by engineer. - - - - - No comment provided by engineer. - ( ( @@ -127,6 +112,14 @@ %@ е потвърдено No comment provided by engineer. + + %@ server + No comment provided by engineer. + + + %@ servers + No comment provided by engineer. + %@ uploaded %@ качено @@ -346,14 +339,9 @@ ) No comment provided by engineer. - - **Add contact**: to create a new invitation link, or connect via a link you received. - **Добави контакт**: за създаване на нов линк или свързване чрез получен линк за връзка. - No comment provided by engineer. - - - **Add new contact**: to create your one-time QR Code or link for your contact. - **Добави нов контакт**: за да създадете своя еднократен QR код или линк за вашия контакт. + + **Create 1-time link**: to create and share a new invitation link. + **Добави контакт**: за създаване на нов линк. No comment provided by engineer. @@ -361,13 +349,13 @@ **Създай група**: за създаване на нова група. No comment provided by engineer. - - **More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have. + + **More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata. **По поверително**: проверявайте новите съобщения на всеки 20 минути. Токенът на устройството се споделя със сървъра за чат SimpleX, но не и колко контакти или съобщения имате. No comment provided by engineer. - - **Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app). + + **Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app. **Най-поверително**: не използвайте сървъра за известия SimpleX Chat, периодично проверявайте съобщенията във фонов режим (зависи от това колко често използвате приложението). No comment provided by engineer. @@ -381,11 +369,15 @@ **Моля, обърнете внимание**: НЯМА да можете да възстановите или промените паролата, ако я загубите. No comment provided by engineer. - - **Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from. + + **Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from. **Препоръчително**: токенът на устройството и известията се изпращат до сървъра за уведомяване на SimpleX Chat, но не и съдържанието, размерът на съобщението или от кого е. No comment provided by engineer. + + **Scan / Paste link**: to connect via a link you received. + No comment provided by engineer. + **Warning**: Instant push notifications require passphrase saved in Keychain. **Внимание**: Незабавните push известия изискват парола, запазена в Keychain. @@ -492,6 +484,14 @@ 1 седмица time interval + + 1-time link + No comment provided by engineer. + + + 1-time link can be used *with one contact only* - share in person or via any messenger. + No comment provided by engineer. + 5 minutes 5 минути @@ -561,21 +561,11 @@ Откажи смяна на адрес? No comment provided by engineer. - - About SimpleX - За SimpleX - No comment provided by engineer. - About SimpleX Chat За SimpleX Chat No comment provided by engineer. - - About SimpleX address - Повече за SimpleX адреса - No comment provided by engineer. - Accent No comment provided by engineer. @@ -587,6 +577,10 @@ accept incoming call via notification swipe action + + Accept conditions + No comment provided by engineer. + Accept connection request? Приемане на заявка за връзка? @@ -603,6 +597,10 @@ accept contact request via notification swipe action + + Accepted conditions + No comment provided by engineer. + Acknowledged No comment provided by engineer. @@ -620,16 +618,6 @@ Добавете адрес към вашия профил, така че вашите контакти да могат да го споделят с други хора. Актуализацията на профила ще бъде изпратена до вашите контакти. No comment provided by engineer. - - Add contact - Добави контакт - No comment provided by engineer. - - - Add preset servers - Добави предварително зададени сървъри - No comment provided by engineer. - Add profile Добави профил @@ -655,6 +643,14 @@ Добави съобщение при посрещане No comment provided by engineer. + + Added media & file servers + No comment provided by engineer. + + + Added message servers + No comment provided by engineer. + Additional accent No comment provided by engineer. @@ -677,6 +673,14 @@ Промяната на адреса ще бъде прекъсната. Ще се използва старият адрес за получаване. No comment provided by engineer. + + Address or 1-time link? + No comment provided by engineer. + + + Address settings + No comment provided by engineer. + Admins can block a member for all. Администраторите могат да блокират член за всички. @@ -720,6 +724,10 @@ Всички членове на групата ще останат свързани. No comment provided by engineer. + + All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages. + No comment provided by engineer. + All messages will be deleted - this cannot be undone! Всички съобщения ще бъдат изтрити - това не може да бъде отменено! @@ -895,6 +903,11 @@ Отговор на повикване No comment provided by engineer. + + Anybody can host servers. + Протокол и код с отворен код – всеки може да оперира собствени сървъри. + No comment provided by engineer. + App build: %@ Компилация на приложението: %@ @@ -1219,7 +1232,8 @@ Cancel Отказ - alert button + alert action + alert button Cancel migration @@ -1300,6 +1314,10 @@ authentication reason set passcode view + + Change user profiles + authentication reason + Chat archive Архив на чата @@ -1380,10 +1398,18 @@ Чатове No comment provided by engineer. + + Check messages every 20 min. + No comment provided by engineer. + + + Check messages when allowed. + No comment provided by engineer. + Check server address and try again. Проверете адреса на сървъра и опитайте отново. - No comment provided by engineer. + alert title Chinese and Spanish interface @@ -1464,15 +1490,47 @@ Completed No comment provided by engineer. + + Conditions accepted on: %@. + No comment provided by engineer. + + + Conditions are accepted for the operator(s): **%@**. + No comment provided by engineer. + + + Conditions are already accepted for following operator(s): **%@**. + No comment provided by engineer. + + + Conditions of use + No comment provided by engineer. + + + Conditions will be accepted for enabled operators after 30 days. + No comment provided by engineer. + + + Conditions will be accepted for operator(s): **%@**. + No comment provided by engineer. + + + Conditions will be accepted for the operator(s): **%@**. + No comment provided by engineer. + + + Conditions will be accepted on: %@. + No comment provided by engineer. + + + Conditions will be automatically accepted for enabled operators on: %@. + No comment provided by engineer. + Configure ICE servers Конфигурирай ICE сървъри No comment provided by engineer. - - Configured %@ servers - No comment provided by engineer. - Confirm Потвърди @@ -1653,6 +1711,10 @@ This is your own one-time link! Заявката за връзка е изпратена! No comment provided by engineer. + + Connection security + No comment provided by engineer. + Connection terminated Връзката е прекратена @@ -1760,6 +1822,10 @@ This is your own one-time link! Създай No comment provided by engineer. + + Create 1-time link + No comment provided by engineer. + Create SimpleX address Създай SimpleX адрес @@ -1770,11 +1836,6 @@ This is your own one-time link! Създай група с автоматично генериран профилл. No comment provided by engineer. - - Create an address to let people connect with you. - Създайте адрес, за да позволите на хората да се свързват с вас. - No comment provided by engineer. - Create file Създай файл @@ -1854,6 +1915,10 @@ This is your own one-time link! Текущ kод за достъп No comment provided by engineer. + + Current conditions text couldn't be loaded, you can review conditions via this link: + No comment provided by engineer. + Current passphrase… Текуща парола… @@ -2005,7 +2070,8 @@ This is your own one-time link! Delete Изтрий - chat item action + alert action + chat item action swipe action @@ -2216,6 +2282,10 @@ This is your own one-time link! Deletion errors No comment provided by engineer. + + Delivered even when Apple drops them. + No comment provided by engineer. + Delivery Доставка @@ -2483,6 +2553,10 @@ This is your own one-time link! Продължителност No comment provided by engineer. + + E2E encrypted notifications. + No comment provided by engineer. + Edit Редактирай @@ -2503,6 +2577,10 @@ This is your own one-time link! Активиране (запазване на промените) No comment provided by engineer. + + Enable Flux + No comment provided by engineer. + Enable SimpleX Lock Активирай SimpleX заключване @@ -2707,6 +2785,10 @@ This is your own one-time link! Грешка при отказване на промяна на адреса No comment provided by engineer. + + Error accepting conditions + alert title + Error accepting contact request Грешка при приемане на заявка за контакт @@ -2722,6 +2804,10 @@ This is your own one-time link! Грешка при добавяне на член(ове) No comment provided by engineer. + + Error adding server + alert title + Error changing address Грешка при промяна на адреса @@ -2858,10 +2944,9 @@ This is your own one-time link! Грешка при присъединяване към група No comment provided by engineer. - - Error loading %@ servers - Грешка при зареждане на %@ сървъри - No comment provided by engineer. + + Error loading servers + alert title Error migrating settings @@ -2894,11 +2979,6 @@ This is your own one-time link! Error resetting statistics No comment provided by engineer. - - Error saving %@ servers - Грешка при запазване на %@ сървъра - No comment provided by engineer. - Error saving ICE servers Грешка при запазване на ICE сървърите @@ -2919,6 +2999,10 @@ This is your own one-time link! Грешка при запазване на парола в Кeychain No comment provided by engineer. + + Error saving servers + alert title + Error saving settings Грешка при запазване на настройките @@ -2988,6 +3072,10 @@ This is your own one-time link! Грешка при актуализиране на съобщението No comment provided by engineer. + + Error updating server + alert title + Error updating settings Грешка при актуализиране на настройките @@ -3032,6 +3120,10 @@ This is your own one-time link! Errors No comment provided by engineer. + + Errors in servers configuration. + servers error + Even when disabled in the conversation. Дори когато е деактивиран в разговора. @@ -3225,11 +3317,27 @@ This is your own one-time link! Поправката не се поддържа от члена на групата No comment provided by engineer. + + For chat profile %@: + servers error + For console За конзолата No comment provided by engineer. + + For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server. + No comment provided by engineer. + + + For private routing + No comment provided by engineer. + + + For social media + No comment provided by engineer. + Forward Препрати @@ -3525,9 +3633,12 @@ Error: %2$@ Как работи SimpleX No comment provided by engineer. - - How it works - Как работи + + How it affects privacy + No comment provided by engineer. + + + How it helps privacy No comment provided by engineer. @@ -3599,8 +3710,8 @@ Error: %2$@ Веднага No comment provided by engineer. - - Immune to spam and abuse + + Immune to spam Защитен от спам и злоупотреби No comment provided by engineer. @@ -3738,6 +3849,11 @@ More improvements are coming soon! Инсталирайте [SimpleX Chat за терминал](https://github.com/simplex-chat/simplex-chat) No comment provided by engineer. + + Instant + Мигновено + No comment provided by engineer. + Instant push notifications will be hidden! @@ -3745,11 +3861,6 @@ More improvements are coming soon! No comment provided by engineer. - - Instantly - Мигновено - No comment provided by engineer. - Interface Интерфейс @@ -3797,7 +3908,7 @@ More improvements are coming soon! Invalid server address! Невалиден адрес на сървъра! - No comment provided by engineer. + alert title Invalid status @@ -3924,7 +4035,7 @@ This is your link for group %@! Keep Запази - No comment provided by engineer. + alert action Keep conversation @@ -3938,7 +4049,7 @@ This is your link for group %@! Keep unused invitation? Запази неизползваната покана за връзка? - No comment provided by engineer. + alert title Keep your connections @@ -4025,11 +4136,6 @@ This is your link for group %@! Съобщения на живо No comment provided by engineer. - - Local - Локално - No comment provided by engineer. - Local name Локално име @@ -4050,11 +4156,6 @@ This is your link for group %@! Режим на заключване No comment provided by engineer. - - Make a private connection - Добави поверителна връзка - No comment provided by engineer. - Make one message disappear Накарайте едно съобщение да изчезне @@ -4065,21 +4166,11 @@ This is your link for group %@! Направи профила поверителен! No comment provided by engineer. - - Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@). - Уверете се, че %@ сървърните адреси са в правилен формат, разделени на редове и не се дублират (%@). - No comment provided by engineer. - Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated. Уверете се, че адресите на WebRTC ICE сървъра са в правилен формат, разделени на редове и не са дублирани. No comment provided by engineer. - - Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?* - Много хора попитаха: *ако SimpleX няма потребителски идентификатори, как може да доставя съобщения?* - No comment provided by engineer. - Mark deleted for everyone Маркирай като изтрито за всички @@ -4344,6 +4435,10 @@ This is your link for group %@! По-надеждна мрежова връзка. No comment provided by engineer. + + More reliable notifications + No comment provided by engineer. + Most likely this connection is deleted. Най-вероятно тази връзка е изтрита. @@ -4379,6 +4474,10 @@ This is your link for group %@! Мрежова връзка No comment provided by engineer. + + Network decentralization + No comment provided by engineer. + Network issues - message expired after many attempts to send it. snd error text @@ -4388,6 +4487,10 @@ This is your link for group %@! Управление на мрежата No comment provided by engineer. + + Network operator + No comment provided by engineer. + Network settings Мрежови настройки @@ -4445,6 +4548,10 @@ This is your link for group %@! Ново име No comment provided by engineer. + + New events + notification + New in %@ Ново в %@ @@ -4469,6 +4576,10 @@ This is your link for group %@! Нова парола… No comment provided by engineer. + + New server + No comment provided by engineer. + No Не @@ -4522,6 +4633,14 @@ This is your link for group %@! No info, try to reload No comment provided by engineer. + + No media & file servers. + servers error + + + No message servers. + servers error + No network connection Няма мрежова връзка @@ -4540,11 +4659,37 @@ This is your link for group %@! Няма разрешение за запис на гласово съобщение No comment provided by engineer. + + No push server + Локално + No comment provided by engineer. + No received or sent files Няма получени или изпратени файлове No comment provided by engineer. + + No servers for private message routing. + servers error + + + No servers to receive files. + servers error + + + No servers to receive messages. + servers error + + + No servers to send files. + servers error + + + No user identifiers. + Първата платформа без никакви потребителски идентификатори – поверителна по дизайн. + No comment provided by engineer. + Not compatible! Несъвместим! @@ -4568,6 +4713,10 @@ This is your link for group %@! Известията са деактивирани! No comment provided by engineer. + + Notifications privacy + No comment provided by engineer. + Now admins can: - delete members' messages. @@ -4626,8 +4775,8 @@ Requires compatible VPN. Няма се използват Onion хостове. No comment provided by engineer. - - Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**. + + Only client devices store user profiles, contacts, groups, and messages. Само потребителските устройства съхраняват потребителски профили, контакти, групи и съобщения, изпратени с **двуслойно криптиране от край до край**. No comment provided by engineer. @@ -4710,6 +4859,10 @@ Requires compatible VPN. Отвори настройки No comment provided by engineer. + + Open changes + No comment provided by engineer. + Open chat Отвори чат @@ -4720,6 +4873,10 @@ Requires compatible VPN. Отвори конзолата authentication reason + + Open conditions + No comment provided by engineer. + Open group Отвори група @@ -4730,25 +4887,19 @@ Requires compatible VPN. Отвори миграцията към друго устройство authentication reason - - Open server settings - No comment provided by engineer. - - - Open user profiles - Отвори потребителските профили - authentication reason - - - Open-source protocol and code – anybody can run the servers. - Протокол и код с отворен код – всеки може да оперира собствени сървъри. - No comment provided by engineer. - Opening app… Приложението се отваря… No comment provided by engineer. + + Operator + No comment provided by engineer. + + + Operator server + alert title + Or paste archive link Или постави архивен линк @@ -4769,15 +4920,15 @@ Requires compatible VPN. Или покажи този код No comment provided by engineer. + + Or to share privately + No comment provided by engineer. + Other Други No comment provided by engineer. - - Other %@ servers - No comment provided by engineer. - Other file errors: %@ @@ -4856,13 +5007,8 @@ Requires compatible VPN. Pending No comment provided by engineer. - - People can connect to you only via the links you share. - Хората могат да се свържат с вас само чрез ликовете, които споделяте. - No comment provided by engineer. - - - Periodically + + Periodic Периодично No comment provided by engineer. @@ -4980,16 +5126,15 @@ Error: %@ Запазете последната чернова на съобщението с прикачени файлове. No comment provided by engineer. - - Preset server - Предварително зададен сървър - No comment provided by engineer. - Preset server address Предварително зададен адрес на сървъра No comment provided by engineer. + + Preset servers + No comment provided by engineer. + Preview Визуализация @@ -5062,7 +5207,7 @@ Error: %@ Profile update will be sent to your contacts. Актуализацията на профила ще бъде изпратена до вашите контакти. - No comment provided by engineer. + alert message Prohibit audio/video calls. @@ -5150,6 +5295,10 @@ Enable in *Network & servers* settings. Proxy requires password No comment provided by engineer. + + Push Notifications + No comment provided by engineer. + Push notifications Push известия @@ -5189,26 +5338,21 @@ Enable in *Network & servers* settings. Прочетете още No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address). - Прочетете повече в [Ръководство за потребителя](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address). - No comment provided by engineer. - Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). Прочетете повече в [Ръководство за потребителя](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). No comment provided by engineer. + + Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). + Прочетете повече в [Ръководство за потребителя](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). + No comment provided by engineer. + Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends). Прочетете повече в [Ръководство на потребителя](https://simplex.chat/docs/guide/readme.html#connect-to-friends). No comment provided by engineer. - - Read more in our GitHub repository. - Прочетете повече в нашето хранилище в GitHub. - No comment provided by engineer. - Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme). Прочетете повече в нашето [GitHub хранилище](https://github.com/simplex-chat/simplex-chat#readme). @@ -5509,6 +5653,14 @@ Enable in *Network & servers* settings. Покажи chat item action + + Review conditions + No comment provided by engineer. + + + Review later + No comment provided by engineer. + Revoke Отзови @@ -5551,6 +5703,14 @@ Enable in *Network & servers* settings. По-безопасни групи No comment provided by engineer. + + Same conditions will apply to operator **%@**. + No comment provided by engineer. + + + Same conditions will apply to operator(s): **%@**. + No comment provided by engineer. + Save Запази @@ -5619,7 +5779,7 @@ Enable in *Network & servers* settings. Save servers? Запази сървърите? - No comment provided by engineer. + alert title Save welcome message? @@ -5818,11 +5978,6 @@ Enable in *Network & servers* settings. Изпращай известия No comment provided by engineer. - - Send notifications: - Изпратени известия: - No comment provided by engineer. - Send questions and ideas Изпращайте въпроси и идеи @@ -5942,6 +6097,10 @@ Enable in *Network & servers* settings. Server No comment provided by engineer. + + Server added to operator %@. + alert message + Server address No comment provided by engineer. @@ -5954,6 +6113,18 @@ Enable in *Network & servers* settings. Server address is incompatible with network settings: %@. No comment provided by engineer. + + Server operator changed. + alert title + + + Server operators + No comment provided by engineer. + + + Server protocol changed. + alert title + Server requires authorization to create queues, check password Сървърът изисква оторизация за създаване на опашки, проверете паролата @@ -6065,22 +6236,35 @@ Enable in *Network & servers* settings. Share Сподели - chat item action + alert action + chat item action Share 1-time link Сподели еднократен линк No comment provided by engineer. + + Share 1-time link with a friend + No comment provided by engineer. + + + Share SimpleX address on social media. + No comment provided by engineer. + Share address Сподели адрес No comment provided by engineer. + + Share address publicly + No comment provided by engineer. + Share address with contacts? Сподели адреса с контактите? - No comment provided by engineer. + alert title Share from other apps. @@ -6190,6 +6374,14 @@ Enable in *Network & servers* settings. SimpleX адрес No comment provided by engineer. + + SimpleX address and 1-time links are safe to share via any messenger. + No comment provided by engineer. + + + SimpleX address or 1-time link? + No comment provided by engineer. + SimpleX contact address SimpleX адрес за контакт @@ -6274,6 +6466,11 @@ Enable in *Network & servers* settings. Some non-fatal errors occurred during import: No comment provided by engineer. + + Some servers failed the test: +%@ + alert message + Somebody Някой @@ -6355,12 +6552,12 @@ Enable in *Network & servers* settings. Stop sharing Спри споделянето - No comment provided by engineer. + alert action Stop sharing address? Спри споделянето на адреса? - No comment provided by engineer. + alert title Stopping chat @@ -6501,7 +6698,7 @@ Enable in *Network & servers* settings. Tests failed! Тестовете са неуспешни! - No comment provided by engineer. + alert title Thank you for installing SimpleX Chat! @@ -6518,11 +6715,6 @@ Enable in *Network & servers* settings. Благодарение на потребителите – допринесете през Weblate! No comment provided by engineer. - - The 1st platform without any user identifiers – private by design. - Първата платформа без никакви потребителски идентификатори – поверителна по дизайн. - No comment provided by engineer. - The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised. @@ -6535,6 +6727,10 @@ It can happen because of some bug or when the connection is compromised.Приложението може да ви уведоми, когато получите съобщения или заявки за контакт - моля, отворете настройките, за да активирате. No comment provided by engineer. + + The app protects your privacy by using different operators in each conversation. + No comment provided by engineer. + The app will ask to confirm downloads from unknown file servers (except .onion). No comment provided by engineer. @@ -6549,6 +6745,10 @@ It can happen because of some bug or when the connection is compromised.QR кодът, който сканирахте, не е SimpleX линк за връзка. No comment provided by engineer. + + The connection reached the limit of undelivered messages, your contact may be offline. + No comment provided by engineer. + The connection you accepted will be cancelled! Връзката, която приехте, ще бъде отказана! @@ -6569,6 +6769,11 @@ It can happen because of some bug or when the connection is compromised.Криптирането работи и новото споразумение за криптиране не е необходимо. Това може да доведе до грешки при свързване! No comment provided by engineer. + + The future of messaging + Ново поколение поверителни съобщения + No comment provided by engineer. + The hash of the previous message is different. Хешът на предишното съобщение е различен. @@ -6592,11 +6797,6 @@ It can happen because of some bug or when the connection is compromised.The messages will be marked as moderated for all members. No comment provided by engineer. - - The next generation of private messaging - Ново поколение поверителни съобщения - No comment provided by engineer. - The old database was not removed during the migration, it can be deleted. Старата база данни не бе премахната по време на миграцията, тя може да бъде изтрита. @@ -6607,6 +6807,10 @@ It can happen because of some bug or when the connection is compromised.Профилът се споделя само с вашите контакти. No comment provided by engineer. + + The second preset operator in the app! + No comment provided by engineer. + The second tick we missed! ✅ Втората отметка, която пропуснахме! ✅ @@ -6622,6 +6826,10 @@ It can happen because of some bug or when the connection is compromised.Сървърите за нови връзки на текущия ви чат профил **%@**. No comment provided by engineer. + + The servers for new files of your current chat profile **%@**. + No comment provided by engineer. + The text you pasted is not a SimpleX link. Текстът, който поставихте, не е SimpleX линк за връзка. @@ -6635,6 +6843,10 @@ It can happen because of some bug or when the connection is compromised.Themes No comment provided by engineer. + + These conditions will also apply for: **%@**. + No comment provided by engineer. + These settings are for your current profile **%@**. Тези настройки са за текущия ви профил **%@**. @@ -6733,9 +6945,8 @@ It can happen because of some bug or when the connection is compromised.За да направите нова връзка No comment provided by engineer. - - To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts. - За да се защити поверителността, вместо потребителски идентификатори, използвани от всички други платформи, SimpleX има идентификатори за опашки от съобщения, отделни за всеки от вашите контакти. + + To protect against your link being replaced, you can compare contact security codes. No comment provided by engineer. @@ -6754,6 +6965,15 @@ You will be prompted to complete authentication before this feature is enabled.< Ще бъдете подканени да извършите идентификация, преди тази функция да бъде активирана. No comment provided by engineer. + + To protect your privacy, SimpleX uses separate IDs for each of your contacts. + За да се защити поверителността, вместо потребителски идентификатори, използвани от всички други платформи, SimpleX има идентификатори за опашки от съобщения, отделни за всеки от вашите контакти. + No comment provided by engineer. + + + To receive + No comment provided by engineer. + To record speech please grant permission to use Microphone. No comment provided by engineer. @@ -6772,11 +6992,19 @@ You will be prompted to complete authentication before this feature is enabled.< За да разкриете своя скрит профил, въведете пълна парола в полето за търсене на страницата **Вашите чат профили**. No comment provided by engineer. + + To send + No comment provided by engineer. + To support instant push notifications the chat database has to be migrated. За поддръжка на незабавни push известия, базата данни за чат трябва да бъде мигрирана. No comment provided by engineer. + + To use the servers of **%@**, accept conditions of use. + No comment provided by engineer. + To verify end-to-end encryption with your contact compare (or scan) the code on your devices. За да проверите криптирането от край до край с вашия контакт, сравнете (или сканирайте) кода на вашите устройства. @@ -6863,6 +7091,10 @@ You will be prompted to complete authentication before this feature is enabled.< Отблокирай член? No comment provided by engineer. + + Undelivered messages + No comment provided by engineer. + Unexpected migration state Неочаквано състояние на миграция @@ -7015,6 +7247,10 @@ To connect, please ask your contact to create another connection link and check Архивът се качва No comment provided by engineer. + + Use %@ + No comment provided by engineer. + Use .onion hosts Използвай .onion хостове @@ -7039,6 +7275,14 @@ To connect, please ask your contact to create another connection link and check Използвай текущия профил No comment provided by engineer. + + Use for files + No comment provided by engineer. + + + Use for messages + No comment provided by engineer. + Use for new connections Използвай за нови връзки @@ -7077,6 +7321,10 @@ To connect, please ask your contact to create another connection link and check Използвай сървър No comment provided by engineer. + + Use servers + No comment provided by engineer. + Use the app while in the call. Използвайте приложението по време на разговора. @@ -7164,11 +7412,19 @@ To connect, please ask your contact to create another connection link and check Видео и файлове до 1gb No comment provided by engineer. + + View conditions + No comment provided by engineer. + View security code Виж кода за сигурност No comment provided by engineer. + + View updated conditions + No comment provided by engineer. + Visible history Видима история @@ -7277,9 +7533,8 @@ To connect, please ask your contact to create another connection link and check При свързване на аудио и видео разговори. No comment provided by engineer. - - When people request to connect, you can accept or reject it. - Когато хората искат да се свържат с вас, можете да ги приемете или отхвърлите. + + When more than one operator is enabled, none of them has metadata to learn who communicates with whom. No comment provided by engineer. @@ -7432,6 +7687,18 @@ Repeat join request? You can change it in Appearance settings. No comment provided by engineer. + + You can configure operators in Network & servers settings. + No comment provided by engineer. + + + You can configure servers via settings. + No comment provided by engineer. + + + You can create it in user picker. + No comment provided by engineer. + You can create it later Можете да го създадете по-късно @@ -7471,6 +7738,10 @@ Repeat join request? You can send messages to %@ from Archived contacts. No comment provided by engineer. + + You can set connection name, to remember who the link was shared with. + No comment provided by engineer. + You can set lock screen notification preview via settings. Можете да зададете визуализация на известията на заключен екран през настройките. @@ -7486,11 +7757,6 @@ Repeat join request? Можете да споделите този адрес с вашите контакти, за да им позволите да се свържат с **%@**. No comment provided by engineer. - - You can share your address as a link or QR code - anybody can connect to you. - Можете да споделите адреса си като линк или QR код - всеки може да се свърже с вас. - No comment provided by engineer. - You can start chat via app Settings / Database or by restarting the app Можете да започнете чат през Настройки на приложението / База данни или като рестартирате приложението @@ -7513,23 +7779,23 @@ Repeat join request? You can view invitation link again in connection details. Можете да видите отново линкът за покана в подробностите за връзката. - No comment provided by engineer. + alert message You can't send messages! Не може да изпращате съобщения! No comment provided by engineer. - - You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them. - Вие контролирате през кой сървър(и) **да получавате** съобщенията, вашите контакти – сървърите, които използвате, за да им изпращате съобщения. - No comment provided by engineer. - You could not be verified; please try again. Не можахте да бъдете потвърдени; Моля, опитайте отново. No comment provided by engineer. + + You decide who can connect. + Хората могат да се свържат с вас само чрез ликовете, които споделяте. + No comment provided by engineer. + You have already requested connection via this address! Вече сте заявили връзка през този адрес! @@ -7649,11 +7915,6 @@ Repeat connection request? Използвате инкогнито профил за тази група - за да се предотврати споделянето на основния ви профил, поканите на контакти не са разрешени No comment provided by engineer. - - Your %@ servers - Вашите %@ сървъри - No comment provided by engineer. - Your ICE servers Вашите ICE сървъри @@ -7669,11 +7930,6 @@ Repeat connection request? Вашият SimpleX адрес No comment provided by engineer. - - Your XFTP servers - Вашите XFTP сървъри - No comment provided by engineer. - Your calls Вашите обаждания @@ -7770,16 +8026,15 @@ Repeat connection request? Вашият автоматично генериран профил No comment provided by engineer. - - Your server - Вашият сървър - No comment provided by engineer. - Your server address Вашият адрес на сървъра No comment provided by engineer. + + Your servers + No comment provided by engineer. + Your settings Вашите настройки @@ -8195,6 +8450,10 @@ Repeat connection request? expired No comment provided by engineer. + + for better metadata privacy. + No comment provided by engineer. + forwarded препратено @@ -8802,6 +9061,33 @@ last received msg: %2$@ + +
+ +
+ + + %d new events + notification body + + + From: %@ + notification body + + + New events + notification + + + New messages + notification + + + New messages in %d chats + notification body + + +

u6|ATCu^_I;OM(rSn!ZCDR@HWjYlc3ZV8`_t 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 z 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 K;{QH={eJ+)h{*r| literal 0 HcmV?d00001 diff --git a/blog/images/20241125-operators-3.png b/blog/images/20241125-operators-3.png new file mode 100644 index 0000000000000000000000000000000000000000..7ae26ef35024a385c4bb9427a6070206e6f9e9b2 GIT binary patch literal 78223 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%0AcxPh*Ie8RJ%&Zruajm?A_`;+5o!|L2^XG9 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

diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/en.lproj/Localizable.strings index cf485752ea..cb83427195 100644 --- a/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/en.lproj/Localizable.strings +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/en.lproj/Localizable.strings @@ -1,9 +1,6 @@ /* No comment provided by engineer. */ "_italic_" = "\\_italic_"; -/* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact."; - /* No comment provided by engineer. */ "*bold*" = "\\*bold*"; @@ -27,4 +24,3 @@ /* No comment provided by engineer. */ "No group!" = "Group not found!"; - diff --git a/apps/ios/SimpleX Localizations/bn.xcloc/Localized Contents/bn.xliff b/apps/ios/SimpleX Localizations/bn.xcloc/Localized Contents/bn.xliff index b92196b78b..d6fb9a40a4 100644 --- a/apps/ios/SimpleX Localizations/bn.xcloc/Localized Contents/bn.xliff +++ b/apps/ios/SimpleX Localizations/bn.xcloc/Localized Contents/bn.xliff @@ -193,20 +193,16 @@ ) No comment provided by engineer. - - **Add new contact**: to create your one-time QR Code or link for your contact. - No comment provided by engineer. - **Create link / QR code** for your contact to use. No comment provided by engineer. - - **More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have. + + **More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata. No comment provided by engineer. - - **Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app). + + **Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app. No comment provided by engineer. @@ -217,8 +213,8 @@ **Please note**: you will NOT be able to recover or change passphrase if you lose it. No comment provided by engineer. - - **Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from. + + **Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from. No comment provided by engineer. @@ -1899,8 +1895,8 @@ Immediately No comment provided by engineer. - - Immune to spam and abuse + + Immune to spam No comment provided by engineer. @@ -2409,8 +2405,8 @@ Onion hosts will not be used. No comment provided by engineer. - - Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**. + + Only client devices store user profiles, contacts, groups, and messages. No comment provided by engineer. @@ -2477,8 +2473,8 @@ Open user profiles authentication reason - - Open-source protocol and code – anybody can run the servers. + + Anybody can host servers. No comment provided by engineer. @@ -2537,8 +2533,8 @@ Paste the link you received into the box below to connect with your contact. No comment provided by engineer. - - People can connect to you only via the links you share. + + You decide who can connect. No comment provided by engineer. @@ -3373,8 +3369,8 @@ Thanks to the users – contribute via Weblate! No comment provided by engineer. - - The 1st platform without any user identifiers – private by design. + + No user identifiers. No comment provided by engineer. @@ -3418,8 +3414,8 @@ It can happen because of some bug or when the connection is compromised.The message will be marked as moderated for all members. No comment provided by engineer. - - The next generation of private messaging + + The future of messaging No comment provided by engineer. @@ -3490,8 +3486,8 @@ It can happen because of some bug or when the connection is compromised.To make a new connection No comment provided by engineer. - - To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts. + + To protect your privacy, SimpleX uses separate IDs for each of your contacts. No comment provided by engineer. @@ -3876,10 +3872,6 @@ To connect, please ask your contact to create another connection link and check You can't send messages! No comment provided by engineer. - - You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them. - No comment provided by engineer. - You could not be verified; please try again. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff index af56b6631f..79cb15d1ae 100644 --- a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff @@ -12,21 +12,6 @@ No comment provided by engineer. - - - - No comment provided by engineer. - - - - - No comment provided by engineer. - - - - - No comment provided by engineer. - ( ( @@ -126,6 +111,14 @@ %@ je ověřený No comment provided by engineer. + + %@ server + No comment provided by engineer. + + + %@ servers + No comment provided by engineer. + %@ uploaded No comment provided by engineer. @@ -336,26 +329,21 @@ ) No comment provided by engineer. - - **Add contact**: to create a new invitation link, or connect via a link you received. - No comment provided by engineer. - - - **Add new contact**: to create your one-time QR Code or link for your contact. - **Přidat nový kontakt**: pro vytvoření jednorázového QR kódu nebo odkazu pro váš kontakt. + + **Create 1-time link**: to create and share a new invitation link. No comment provided by engineer. **Create group**: to create a new group. No comment provided by engineer. - - **More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have. + + **More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata. **Soukromější**: kontrolovat nové zprávy každých 20 minut. Token zařízení je sdílen se serverem SimpleX Chat, ale ne kolik máte kontaktů nebo zpráv. No comment provided by engineer. - - **Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app). + + **Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app. **Nejsoukromější**: nepoužívejte server oznámení SimpleX Chat, pravidelně kontrolujte zprávy na pozadí (závisí na tom, jak často aplikaci používáte). No comment provided by engineer. @@ -368,11 +356,15 @@ **Upozornění**: Pokud heslo ztratíte, NEBUDETE jej moci obnovit ani změnit. No comment provided by engineer. - - **Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from. + + **Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from. **Doporučeno**: Token zařízení a oznámení se odesílají na oznamovací server SimpleX Chat, ale nikoli obsah, velikost nebo od koho jsou zprávy. No comment provided by engineer. + + **Scan / Paste link**: to connect via a link you received. + No comment provided by engineer. + **Warning**: Instant push notifications require passphrase saved in Keychain. **Upozornění**: Okamžitě doručovaná oznámení vyžadují přístupové heslo uložené v Klíčence. @@ -474,6 +466,14 @@ 1 týden time interval + + 1-time link + No comment provided by engineer. + + + 1-time link can be used *with one contact only* - share in person or via any messenger. + No comment provided by engineer. + 5 minutes 5 minut @@ -543,21 +543,11 @@ Přerušit změnu adresy? No comment provided by engineer. - - About SimpleX - O SimpleX - No comment provided by engineer. - About SimpleX Chat O SimpleX chat No comment provided by engineer. - - About SimpleX address - O SimpleX adrese - No comment provided by engineer. - Accent No comment provided by engineer. @@ -569,6 +559,10 @@ accept incoming call via notification swipe action + + Accept conditions + No comment provided by engineer. + Accept connection request? Přijmout kontakt? @@ -585,6 +579,10 @@ accept contact request via notification swipe action + + Accepted conditions + No comment provided by engineer. + Acknowledged No comment provided by engineer. @@ -602,15 +600,6 @@ Přidejte adresu do svého profilu, aby ji vaše kontakty mohly sdílet s dalšími lidmi. Aktualizace profilu bude zaslána vašim kontaktům. No comment provided by engineer. - - Add contact - No comment provided by engineer. - - - Add preset servers - Přidejte přednastavené servery - No comment provided by engineer. - Add profile Přidat profil @@ -636,6 +625,14 @@ Přidat uvítací zprávu No comment provided by engineer. + + Added media & file servers + No comment provided by engineer. + + + Added message servers + No comment provided by engineer. + Additional accent No comment provided by engineer. @@ -658,6 +655,14 @@ Změna adresy bude přerušena. Budou použity staré přijímací adresy. No comment provided by engineer. + + Address or 1-time link? + No comment provided by engineer. + + + Address settings + No comment provided by engineer. + Admins can block a member for all. No comment provided by engineer. @@ -700,6 +705,10 @@ Všichni členové skupiny zůstanou připojeni. No comment provided by engineer. + + All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages. + No comment provided by engineer. + All messages will be deleted - this cannot be undone! No comment provided by engineer. @@ -869,6 +878,11 @@ Přijmout hovor No comment provided by engineer. + + Anybody can host servers. + Servery může provozovat kdokoli. + No comment provided by engineer. + App build: %@ Sestavení aplikace: %@ @@ -1179,7 +1193,8 @@ Cancel Zrušit - alert button + alert action + alert button Cancel migration @@ -1258,6 +1273,10 @@ authentication reason set passcode view + + Change user profiles + authentication reason + Chat archive Chat se archivuje @@ -1336,10 +1355,18 @@ Chaty No comment provided by engineer. + + Check messages every 20 min. + No comment provided by engineer. + + + Check messages when allowed. + No comment provided by engineer. + Check server address and try again. Zkontrolujte adresu serveru a zkuste to znovu. - No comment provided by engineer. + alert title Chinese and Spanish interface @@ -1418,15 +1445,47 @@ Completed No comment provided by engineer. + + Conditions accepted on: %@. + No comment provided by engineer. + + + Conditions are accepted for the operator(s): **%@**. + No comment provided by engineer. + + + Conditions are already accepted for following operator(s): **%@**. + No comment provided by engineer. + + + Conditions of use + No comment provided by engineer. + + + Conditions will be accepted for enabled operators after 30 days. + No comment provided by engineer. + + + Conditions will be accepted for operator(s): **%@**. + No comment provided by engineer. + + + Conditions will be accepted for the operator(s): **%@**. + No comment provided by engineer. + + + Conditions will be accepted on: %@. + No comment provided by engineer. + + + Conditions will be automatically accepted for enabled operators on: %@. + No comment provided by engineer. + Configure ICE servers Konfigurace serverů ICE No comment provided by engineer. - - Configured %@ servers - No comment provided by engineer. - Confirm Potvrdit @@ -1592,6 +1651,10 @@ This is your own one-time link! Požadavek na připojení byl odeslán! No comment provided by engineer. + + Connection security + No comment provided by engineer. + Connection terminated No comment provided by engineer. @@ -1697,6 +1760,10 @@ This is your own one-time link! Vytvořit No comment provided by engineer. + + Create 1-time link + No comment provided by engineer. + Create SimpleX address Vytvořit SimpleX adresu @@ -1706,11 +1773,6 @@ This is your own one-time link! Create a group using a random profile. No comment provided by engineer. - - Create an address to let people connect with you. - Vytvořit adresu, aby se s vámi lidé mohli spojit. - No comment provided by engineer. - Create file Vytvořit soubor @@ -1784,6 +1846,10 @@ This is your own one-time link! Aktuální heslo No comment provided by engineer. + + Current conditions text couldn't be loaded, you can review conditions via this link: + No comment provided by engineer. + Current passphrase… Aktuální přístupová fráze… @@ -1935,7 +2001,8 @@ This is your own one-time link! Delete Smazat - chat item action + alert action + chat item action swipe action @@ -2143,6 +2210,10 @@ This is your own one-time link! Deletion errors No comment provided by engineer. + + Delivered even when Apple drops them. + No comment provided by engineer. + Delivery Doručenka @@ -2400,6 +2471,10 @@ This is your own one-time link! Trvání No comment provided by engineer. + + E2E encrypted notifications. + No comment provided by engineer. + Edit Upravit @@ -2420,6 +2495,10 @@ This is your own one-time link! Povolit (zachovat přepsání) No comment provided by engineer. + + Enable Flux + No comment provided by engineer. + Enable SimpleX Lock Zapnutí zámku SimpleX @@ -2614,6 +2693,10 @@ This is your own one-time link! Chyba přerušení změny adresy No comment provided by engineer. + + Error accepting conditions + alert title + Error accepting contact request Chyba při přijímání žádosti o kontakt @@ -2629,6 +2712,10 @@ This is your own one-time link! Chyba přidávání člena(ů) No comment provided by engineer. + + Error adding server + alert title + Error changing address Chuba změny adresy @@ -2763,10 +2850,9 @@ This is your own one-time link! Chyba při připojování ke skupině No comment provided by engineer. - - Error loading %@ servers - Chyba načítání %@ serverů - No comment provided by engineer. + + Error loading servers + alert title Error migrating settings @@ -2798,11 +2884,6 @@ This is your own one-time link! Error resetting statistics No comment provided by engineer. - - Error saving %@ servers - Chyba při ukládání serverů %@ - No comment provided by engineer. - Error saving ICE servers Chyba při ukládání serverů ICE @@ -2823,6 +2904,10 @@ This is your own one-time link! Při ukládání přístupové fráze do klíčenky došlo k chybě No comment provided by engineer. + + Error saving servers + alert title + Error saving settings when migrating @@ -2890,6 +2975,10 @@ This is your own one-time link! Chyba aktualizace zprávy No comment provided by engineer. + + Error updating server + alert title + Error updating settings Chyba při aktualizaci nastavení @@ -2932,6 +3021,10 @@ This is your own one-time link! Errors No comment provided by engineer. + + Errors in servers configuration. + servers error + Even when disabled in the conversation. I při vypnutí v konverzaci. @@ -3119,11 +3212,27 @@ This is your own one-time link! Opravit nepodporované členem skupiny No comment provided by engineer. + + For chat profile %@: + servers error + For console Pro konzoli No comment provided by engineer. + + For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server. + No comment provided by engineer. + + + For private routing + No comment provided by engineer. + + + For social media + No comment provided by engineer. + Forward chat item action @@ -3409,9 +3518,12 @@ Error: %2$@ Jak SimpleX funguje No comment provided by engineer. - - How it works - Jak to funguje + + How it affects privacy + No comment provided by engineer. + + + How it helps privacy No comment provided by engineer. @@ -3482,8 +3594,8 @@ Error: %2$@ Ihned No comment provided by engineer. - - Immune to spam and abuse + + Immune to spam Odolná vůči spamu a zneužití No comment provided by engineer. @@ -3614,6 +3726,11 @@ More improvements are coming soon! Nainstalujte [SimpleX Chat pro terminál](https://github.com/simplex-chat/simplex-chat) No comment provided by engineer. + + Instant + Okamžitě + No comment provided by engineer. + Instant push notifications will be hidden! @@ -3621,11 +3738,6 @@ More improvements are coming soon! No comment provided by engineer. - - Instantly - Okamžitě - No comment provided by engineer. - Interface Rozhranní @@ -3667,7 +3779,7 @@ More improvements are coming soon! Invalid server address! Neplatná adresa serveru! - No comment provided by engineer. + alert title Invalid status @@ -3788,7 +3900,7 @@ This is your link for group %@! Keep - No comment provided by engineer. + alert action Keep conversation @@ -3800,7 +3912,7 @@ This is your link for group %@! Keep unused invitation? - No comment provided by engineer. + alert title Keep your connections @@ -3884,11 +3996,6 @@ This is your link for group %@! Živé zprávy No comment provided by engineer. - - Local - Místní - No comment provided by engineer. - Local name Místní název @@ -3909,11 +4016,6 @@ This is your link for group %@! Režim zámku No comment provided by engineer. - - Make a private connection - Vytvořte si soukromé připojení - No comment provided by engineer. - Make one message disappear Nechat jednu zprávu zmizet @@ -3924,21 +4026,11 @@ This is your link for group %@! Změnit profil na soukromý! No comment provided by engineer. - - Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@). - Ujistěte se, že adresy %@ serverů jsou ve správném formátu, oddělené řádky a nejsou duplicitní (%@). - No comment provided by engineer. - Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated. Ujistěte se, že adresy serverů WebRTC ICE jsou ve správném formátu, oddělené na řádcích a nejsou duplicitní. No comment provided by engineer. - - Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?* - Mnoho lidí se ptalo: *Pokud SimpleX nemá žádné uživatelské identifikátory, jak může doručovat zprávy?* - No comment provided by engineer. - Mark deleted for everyone Označit jako smazané pro všechny @@ -4190,6 +4282,10 @@ This is your link for group %@! More reliable network connection. No comment provided by engineer. + + More reliable notifications + No comment provided by engineer. + Most likely this connection is deleted. Pravděpodobně je toto spojení smazáno. @@ -4224,6 +4320,10 @@ This is your link for group %@! Network connection No comment provided by engineer. + + Network decentralization + No comment provided by engineer. + Network issues - message expired after many attempts to send it. snd error text @@ -4232,6 +4332,10 @@ This is your link for group %@! Network management No comment provided by engineer. + + Network operator + No comment provided by engineer. + Network settings Nastavení sítě @@ -4288,6 +4392,10 @@ This is your link for group %@! Nově zobrazované jméno No comment provided by engineer. + + New events + notification + New in %@ Nový V %@ @@ -4312,6 +4420,10 @@ This is your link for group %@! Nová přístupová fráze… No comment provided by engineer. + + New server + No comment provided by engineer. + No Ne @@ -4365,6 +4477,14 @@ This is your link for group %@! No info, try to reload No comment provided by engineer. + + No media & file servers. + servers error + + + No message servers. + servers error + No network connection No comment provided by engineer. @@ -4382,11 +4502,37 @@ This is your link for group %@! Nemáte oprávnění nahrávat hlasové zprávy No comment provided by engineer. + + No push server + Místní + No comment provided by engineer. + No received or sent files Žádné přijaté ani odeslané soubory No comment provided by engineer. + + No servers for private message routing. + servers error + + + No servers to receive files. + servers error + + + No servers to receive messages. + servers error + + + No servers to send files. + servers error + + + No user identifiers. + Bez uživatelských identifikátorů + No comment provided by engineer. + Not compatible! No comment provided by engineer. @@ -4409,6 +4555,10 @@ This is your link for group %@! Oznámení jsou zakázána! No comment provided by engineer. + + Notifications privacy + No comment provided by engineer. + Now admins can: - delete members' messages. @@ -4466,8 +4616,8 @@ Vyžaduje povolení sítě VPN. Onion hostitelé nebudou použiti. No comment provided by engineer. - - Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**. + + Only client devices store user profiles, contacts, groups, and messages. Pouze klientská zařízení ukládají uživatelské profily, kontakty, skupiny a zprávy odeslané s **2vrstvým šifrováním typu end-to-end**. No comment provided by engineer. @@ -4550,6 +4700,10 @@ Vyžaduje povolení sítě VPN. Otevřít nastavení No comment provided by engineer. + + Open changes + No comment provided by engineer. + Open chat Otevřete chat @@ -4560,6 +4714,10 @@ Vyžaduje povolení sítě VPN. Otevřete konzolu chatu authentication reason + + Open conditions + No comment provided by engineer. + Open group No comment provided by engineer. @@ -4568,24 +4726,18 @@ Vyžaduje povolení sítě VPN. Open migration to another device authentication reason - - Open server settings - No comment provided by engineer. - - - Open user profiles - Otevřít uživatelské profily - authentication reason - - - Open-source protocol and code – anybody can run the servers. - Protokol a kód s otevřeným zdrojovým kódem - servery může provozovat kdokoli. - No comment provided by engineer. - Opening app… No comment provided by engineer. + + Operator + No comment provided by engineer. + + + Operator server + alert title + Or paste archive link No comment provided by engineer. @@ -4602,12 +4754,12 @@ Vyžaduje povolení sítě VPN. Or show this code No comment provided by engineer. - - Other + + Or to share privately No comment provided by engineer. - - Other %@ servers + + Other No comment provided by engineer. @@ -4684,13 +4836,8 @@ Vyžaduje povolení sítě VPN. Pending No comment provided by engineer. - - People can connect to you only via the links you share. - Lidé se s vámi mohou spojit pouze prostřednictvím odkazů, které sdílíte. - No comment provided by engineer. - - - Periodically + + Periodic Pravidelně No comment provided by engineer. @@ -4804,16 +4951,15 @@ Error: %@ Zachování posledního návrhu zprávy s přílohami. No comment provided by engineer. - - Preset server - Přednastavený server - No comment provided by engineer. - Preset server address Přednastavená adresa serveru No comment provided by engineer. + + Preset servers + No comment provided by engineer. + Preview Náhled @@ -4884,7 +5030,7 @@ Error: %@ Profile update will be sent to your contacts. Aktualizace profilu bude zaslána vašim kontaktům. - No comment provided by engineer. + alert message Prohibit audio/video calls. @@ -4971,6 +5117,10 @@ Enable in *Network & servers* settings. Proxy requires password No comment provided by engineer. + + Push Notifications + No comment provided by engineer. + Push notifications Nabízená oznámení @@ -5008,25 +5158,20 @@ Enable in *Network & servers* settings. Přečíst více No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address). - Další informace naleznete v [Uživatelské příručce](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address). - No comment provided by engineer. - Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). No comment provided by engineer. + + Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). + Další informace naleznete v [Uživatelské příručce](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). + No comment provided by engineer. + Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends). Přečtěte si více v [Uživatelské příručce](https://simplex.chat/docs/guide/readme.html#connect-to-friends). No comment provided by engineer. - - Read more in our GitHub repository. - Další informace najdete v našem repozitáři GitHub. - No comment provided by engineer. - Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme). Přečtěte si více v našem [GitHub repozitáři](https://github.com/simplex-chat/simplex-chat#readme). @@ -5319,6 +5464,14 @@ Enable in *Network & servers* settings. Odhalit chat item action + + Review conditions + No comment provided by engineer. + + + Review later + No comment provided by engineer. + Revoke Odvolat @@ -5360,6 +5513,14 @@ Enable in *Network & servers* settings. Safer groups No comment provided by engineer. + + Same conditions will apply to operator **%@**. + No comment provided by engineer. + + + Same conditions will apply to operator(s): **%@**. + No comment provided by engineer. + Save Uložit @@ -5428,7 +5589,7 @@ Enable in *Network & servers* settings. Save servers? Uložit servery? - No comment provided by engineer. + alert title Save welcome message? @@ -5621,11 +5782,6 @@ Enable in *Network & servers* settings. Odeslat oznámení No comment provided by engineer. - - Send notifications: - Odeslat oznámení: - No comment provided by engineer. - Send questions and ideas Zasílání otázek a nápadů @@ -5744,6 +5900,10 @@ Enable in *Network & servers* settings. Server No comment provided by engineer. + + Server added to operator %@. + alert message + Server address No comment provided by engineer. @@ -5756,6 +5916,18 @@ Enable in *Network & servers* settings. Server address is incompatible with network settings: %@. No comment provided by engineer. + + Server operator changed. + alert title + + + Server operators + No comment provided by engineer. + + + Server protocol changed. + alert title + Server requires authorization to create queues, check password Server vyžaduje autorizaci pro vytváření front, zkontrolujte heslo @@ -5864,22 +6036,35 @@ Enable in *Network & servers* settings. Share Sdílet - chat item action + alert action + chat item action Share 1-time link Sdílet jednorázovou pozvánku No comment provided by engineer. + + Share 1-time link with a friend + No comment provided by engineer. + + + Share SimpleX address on social media. + No comment provided by engineer. + Share address Sdílet adresu No comment provided by engineer. + + Share address publicly + No comment provided by engineer. + Share address with contacts? Sdílet adresu s kontakty? - No comment provided by engineer. + alert title Share from other apps. @@ -5987,6 +6172,14 @@ Enable in *Network & servers* settings. Adresa SimpleX No comment provided by engineer. + + SimpleX address and 1-time links are safe to share via any messenger. + No comment provided by engineer. + + + SimpleX address or 1-time link? + No comment provided by engineer. + SimpleX contact address SimpleX kontaktní adresa @@ -6069,6 +6262,11 @@ Enable in *Network & servers* settings. Some non-fatal errors occurred during import: No comment provided by engineer. + + Some servers failed the test: +%@ + alert message + Somebody Někdo @@ -6147,12 +6345,12 @@ Enable in *Network & servers* settings. Stop sharing Přestat sdílet - No comment provided by engineer. + alert action Stop sharing address? Přestat sdílet adresu? - No comment provided by engineer. + alert title Stopping chat @@ -6289,7 +6487,7 @@ Enable in *Network & servers* settings. Tests failed! Testy selhaly! - No comment provided by engineer. + alert title Thank you for installing SimpleX Chat! @@ -6306,11 +6504,6 @@ Enable in *Network & servers* settings. Díky uživatelům - přispívejte prostřednictvím Weblate! No comment provided by engineer. - - The 1st platform without any user identifiers – private by design. - 1. Platforma bez identifikátorů uživatelů - soukromá už od záměru. - No comment provided by engineer. - The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised. @@ -6323,6 +6516,10 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován Aplikace vás může upozornit na přijaté zprávy nebo žádosti o kontakt - povolte to v nastavení. No comment provided by engineer. + + The app protects your privacy by using different operators in each conversation. + No comment provided by engineer. + The app will ask to confirm downloads from unknown file servers (except .onion). No comment provided by engineer. @@ -6336,6 +6533,10 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován The code you scanned is not a SimpleX link QR code. No comment provided by engineer. + + The connection reached the limit of undelivered messages, your contact may be offline. + No comment provided by engineer. + The connection you accepted will be cancelled! Připojení, které jste přijali, bude zrušeno! @@ -6356,6 +6557,11 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován Šifrování funguje a nové povolení šifrování není vyžadováno. To může vyvolat chybu v připojení! No comment provided by engineer. + + The future of messaging + Nová generace soukromých zpráv + No comment provided by engineer. + The hash of the previous message is different. Hash předchozí zprávy se liší. @@ -6379,11 +6585,6 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován The messages will be marked as moderated for all members. No comment provided by engineer. - - The next generation of private messaging - Nová generace soukromých zpráv - No comment provided by engineer. - The old database was not removed during the migration, it can be deleted. Stará databáze nebyla během přenášení odstraněna, lze ji smazat. @@ -6394,6 +6595,10 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován Profil je sdílen pouze s vašimi kontakty. No comment provided by engineer. + + The second preset operator in the app! + No comment provided by engineer. + The second tick we missed! ✅ Druhé zaškrtnutí jsme přehlédli! ✅ @@ -6409,6 +6614,10 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován Servery pro nová připojení vašeho aktuálního chat profilu **%@**. No comment provided by engineer. + + The servers for new files of your current chat profile **%@**. + No comment provided by engineer. + The text you pasted is not a SimpleX link. No comment provided by engineer. @@ -6421,6 +6630,10 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován Themes No comment provided by engineer. + + These conditions will also apply for: **%@**. + No comment provided by engineer. + These settings are for your current profile **%@**. Toto nastavení je pro váš aktuální profil **%@**. @@ -6512,9 +6725,8 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován Vytvoření nového připojení No comment provided by engineer. - - To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts. - Pro ochranu soukromí namísto ID uživatelů používaných všemi ostatními platformami má SimpleX identifikátory pro fronty zpráv, oddělené pro každý z vašich kontaktů. + + To protect against your link being replaced, you can compare contact security codes. No comment provided by engineer. @@ -6533,6 +6745,15 @@ You will be prompted to complete authentication before this feature is enabled.< Před zapnutím této funkce budete vyzváni k dokončení ověření. No comment provided by engineer. + + To protect your privacy, SimpleX uses separate IDs for each of your contacts. + Pro ochranu soukromí namísto ID uživatelů používaných všemi ostatními platformami má SimpleX identifikátory pro fronty zpráv, oddělené pro každý z vašich kontaktů. + No comment provided by engineer. + + + To receive + No comment provided by engineer. + To record speech please grant permission to use Microphone. No comment provided by engineer. @@ -6551,11 +6772,19 @@ Před zapnutím této funkce budete vyzváni k dokončení ověření. Chcete-li odhalit svůj skrytý profil, zadejte celé heslo do vyhledávacího pole na stránce **Chat profily**. No comment provided by engineer. + + To send + No comment provided by engineer. + To support instant push notifications the chat database has to be migrated. Pro podporu doručování okamžitých upozornění musí být přenesena chat databáze. No comment provided by engineer. + + To use the servers of **%@**, accept conditions of use. + No comment provided by engineer. + To verify end-to-end encryption with your contact compare (or scan) the code on your devices. Chcete-li ověřit koncové šifrování u svého kontaktu, porovnejte (nebo naskenujte) kód na svých zařízeních. @@ -6636,6 +6865,10 @@ Před zapnutím této funkce budete vyzváni k dokončení ověření. Unblock member? No comment provided by engineer. + + Undelivered messages + No comment provided by engineer. + Unexpected migration state Neočekávaný stav přenášení @@ -6783,6 +7016,10 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Uploading archive No comment provided by engineer. + + Use %@ + No comment provided by engineer. + Use .onion hosts Použít hostitele .onion @@ -6807,6 +7044,14 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Použít aktuální profil No comment provided by engineer. + + Use for files + No comment provided by engineer. + + + Use for messages + No comment provided by engineer. + Use for new connections Použít pro nová připojení @@ -6843,6 +7088,10 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Použít server No comment provided by engineer. + + Use servers + No comment provided by engineer. + Use the app while in the call. No comment provided by engineer. @@ -6923,11 +7172,19 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Videa a soubory až do velikosti 1 gb No comment provided by engineer. + + View conditions + No comment provided by engineer. + View security code Zobrazení bezpečnostního kódu No comment provided by engineer. + + View updated conditions + No comment provided by engineer. + Visible history chat feature @@ -7030,9 +7287,8 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu When connecting audio and video calls. No comment provided by engineer. - - When people request to connect, you can accept or reject it. - Když někdo požádá o připojení, můžete žádost přijmout nebo odmítnout. + + When more than one operator is enabled, none of them has metadata to learn who communicates with whom. No comment provided by engineer. @@ -7171,6 +7427,18 @@ Repeat join request? You can change it in Appearance settings. No comment provided by engineer. + + You can configure operators in Network & servers settings. + No comment provided by engineer. + + + You can configure servers via settings. + No comment provided by engineer. + + + You can create it in user picker. + No comment provided by engineer. + You can create it later Můžete vytvořit později @@ -7208,6 +7476,10 @@ Repeat join request? You can send messages to %@ from Archived contacts. No comment provided by engineer. + + You can set connection name, to remember who the link was shared with. + No comment provided by engineer. + You can set lock screen notification preview via settings. Náhled oznámení na zamykací obrazovce můžete změnit v nastavení. @@ -7223,11 +7495,6 @@ Repeat join request? Tuto adresu můžete sdílet s vašimi kontakty, abyse se mohli spojit s **%@**. No comment provided by engineer. - - You can share your address as a link or QR code - anybody can connect to you. - Můžete sdílet svou adresu jako odkaz nebo jako QR kód - kdokoli se k vám bude moci připojit. - No comment provided by engineer. - You can start chat via app Settings / Database or by restarting the app Chat můžete zahájit prostřednictvím aplikace Nastavení / Databáze nebo restartováním aplikace @@ -7249,23 +7516,23 @@ Repeat join request? You can view invitation link again in connection details. - No comment provided by engineer. + alert message You can't send messages! Nemůžete posílat zprávy! No comment provided by engineer. - - You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them. - Sami řídíte, přes který server(y) **přijímat** zprávy, své kontakty – servery, které používáte k odesílání zpráv. - No comment provided by engineer. - You could not be verified; please try again. Nemohli jste být ověřeni; Zkuste to prosím znovu. No comment provided by engineer. + + You decide who can connect. + Lidé se s vámi mohou spojit pouze prostřednictvím odkazu, který sdílíte. + No comment provided by engineer. + You have already requested connection via this address! No comment provided by engineer. @@ -7380,11 +7647,6 @@ Repeat connection request? Pro tuto skupinu používáte inkognito profil - abyste zabránili sdílení svého hlavního profilu, není pozvání kontaktů povoleno No comment provided by engineer. - - Your %@ servers - Vaše servery %@ - No comment provided by engineer. - Your ICE servers Vaše servery ICE @@ -7400,11 +7662,6 @@ Repeat connection request? Vaše SimpleX adresa No comment provided by engineer. - - Your XFTP servers - Vaše XFTP servery - No comment provided by engineer. - Your calls Vaše hovory @@ -7500,16 +7757,15 @@ Repeat connection request? Váš náhodný profil No comment provided by engineer. - - Your server - Váš server - No comment provided by engineer. - Your server address Adresa vašeho serveru No comment provided by engineer. + + Your servers + No comment provided by engineer. + Your settings Vaše nastavení @@ -7915,6 +8171,10 @@ Repeat connection request? expired No comment provided by engineer. + + for better metadata privacy. + No comment provided by engineer. + forwarded No comment provided by engineer. @@ -8503,6 +8763,33 @@ last received msg: %2$@ + +
+ +
+ + + %d new events + notification body + + + From: %@ + notification body + + + New events + notification + + + New messages + notification + + + New messages in %d chats + notification body + + +
diff --git a/apps/ios/SimpleX Localizations/cs.xcloc/Source Contents/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/cs.xcloc/Source Contents/en.lproj/Localizable.strings index cf485752ea..cb83427195 100644 --- a/apps/ios/SimpleX Localizations/cs.xcloc/Source Contents/en.lproj/Localizable.strings +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Source Contents/en.lproj/Localizable.strings @@ -1,9 +1,6 @@ /* No comment provided by engineer. */ "_italic_" = "\\_italic_"; -/* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact."; - /* No comment provided by engineer. */ "*bold*" = "\\*bold*"; @@ -27,4 +24,3 @@ /* No comment provided by engineer. */ "No group!" = "Group not found!"; - diff --git a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff index 7ab1e3a588..743c08ed00 100644 --- a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff +++ b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff @@ -12,21 +12,6 @@ No comment provided by engineer. - - - - No comment provided by engineer. - - - - - No comment provided by engineer. - - - - - No comment provided by engineer. - ( ( @@ -127,6 +112,14 @@ %@ wurde erfolgreich überprüft No comment provided by engineer. + + %@ server + No comment provided by engineer. + + + %@ servers + No comment provided by engineer. + %@ uploaded %@ hochgeladen @@ -352,14 +345,9 @@ ) No comment provided by engineer. - - **Add contact**: to create a new invitation link, or connect via a link you received. - **Kontakt hinzufügen**: Um einen neuen Einladungslink zu erstellen oder eine Verbindung über einen Link herzustellen, den Sie erhalten haben. - No comment provided by engineer. - - - **Add new contact**: to create your one-time QR Code or link for your contact. - **Neuen Kontakt hinzufügen**: Um einen Einmal-QR-Code oder -Link für Ihren Kontakt zu erzeugen. + + **Create 1-time link**: to create and share a new invitation link. + **Kontakt hinzufügen**: Um einen neuen Einladungslink zu erstellen. No comment provided by engineer. @@ -367,13 +355,13 @@ **Gruppe erstellen**: Um eine neue Gruppe zu erstellen. No comment provided by engineer. - - **More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have. + + **More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata. **Mehr Privatsphäre**: Es wird alle 20 Minuten auf neue Nachrichten geprüft. Nur Ihr Geräte-Token wird dem SimpleX-Chat-Server mitgeteilt, aber nicht wie viele Kontakte Sie haben oder welche Nachrichten Sie empfangen. No comment provided by engineer. - - **Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app). + + **Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app. **Beste Privatsphäre**: Es wird kein SimpleX-Chat-Benachrichtigungs-Server genutzt, Nachrichten werden in periodischen Abständen im Hintergrund geprüft (dies hängt davon ab, wie häufig Sie die App nutzen). No comment provided by engineer. @@ -387,11 +375,15 @@ **Bitte beachten Sie**: Das Passwort kann NICHT wiederhergestellt oder geändert werden, wenn Sie es vergessen haben oder verlieren. No comment provided by engineer. - - **Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from. + + **Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from. **Empfohlen**: Nur Ihr Geräte-Token und ihre Benachrichtigungen werden an den SimpleX-Chat-Benachrichtigungs-Server gesendet, aber weder der Nachrichteninhalt noch deren Größe oder von wem sie gesendet wurde. No comment provided by engineer. + + **Scan / Paste link**: to connect via a link you received. + No comment provided by engineer. + **Warning**: Instant push notifications require passphrase saved in Keychain. **Warnung**: Sofortige Push-Benachrichtigungen erfordern die Eingabe eines Passworts, welches in Ihrem Schlüsselbund gespeichert ist. @@ -498,6 +490,14 @@ wöchentlich time interval + + 1-time link + No comment provided by engineer. + + + 1-time link can be used *with one contact only* - share in person or via any messenger. + No comment provided by engineer. + 5 minutes 5 Minuten @@ -567,21 +567,11 @@ Wechsel der Empfängeradresse beenden? No comment provided by engineer. - - About SimpleX - Über SimpleX - No comment provided by engineer. - About SimpleX Chat Über SimpleX Chat No comment provided by engineer. - - About SimpleX address - Über die SimpleX-Adresse - No comment provided by engineer. - Accent Akzent @@ -594,6 +584,10 @@ accept incoming call via notification swipe action + + Accept conditions + No comment provided by engineer. + Accept connection request? Kontaktanfrage annehmen? @@ -610,6 +604,10 @@ accept contact request via notification swipe action + + Accepted conditions + No comment provided by engineer. + Acknowledged Bestätigt @@ -630,16 +628,6 @@ Fügen Sie die Adresse Ihrem Profil hinzu, damit Ihre Kontakte sie mit anderen Personen teilen können. Es wird eine Profilaktualisierung an Ihre Kontakte gesendet. No comment provided by engineer. - - Add contact - Kontakt hinzufügen - No comment provided by engineer. - - - Add preset servers - Füge voreingestellte Server hinzu - No comment provided by engineer. - Add profile Profil hinzufügen @@ -665,6 +653,14 @@ Begrüßungsmeldung hinzufügen No comment provided by engineer. + + Added media & file servers + No comment provided by engineer. + + + Added message servers + No comment provided by engineer. + Additional accent Erste Akzentfarbe @@ -690,6 +686,14 @@ Der Wechsel der Empfängeradresse wird beendet. Die bisherige Adresse wird weiter verwendet. No comment provided by engineer. + + Address or 1-time link? + No comment provided by engineer. + + + Address settings + No comment provided by engineer. + Admins can block a member for all. Administratoren können ein Gruppenmitglied für Alle blockieren. @@ -735,6 +739,10 @@ Alle Gruppenmitglieder bleiben verbunden. No comment provided by engineer. + + All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages. + No comment provided by engineer. + All messages will be deleted - this cannot be undone! Es werden alle Nachrichten gelöscht. Dies kann nicht rückgängig gemacht werden! @@ -915,6 +923,11 @@ Anruf annehmen No comment provided by engineer. + + Anybody can host servers. + Jeder kann seine eigenen Server aufsetzen. + No comment provided by engineer. + App build: %@ App Build: %@ @@ -1258,7 +1271,8 @@ Cancel Abbrechen - alert button + alert action + alert button Cancel migration @@ -1341,6 +1355,10 @@ authentication reason set passcode view + + Change user profiles + authentication reason + Chat archive Datenbank Archiv @@ -1426,10 +1444,18 @@ Chats No comment provided by engineer. + + Check messages every 20 min. + No comment provided by engineer. + + + Check messages when allowed. + No comment provided by engineer. + Check server address and try again. Überprüfen Sie die Serveradresse und versuchen Sie es nochmal. - No comment provided by engineer. + alert title Chinese and Spanish interface @@ -1516,16 +1542,47 @@ Abgeschlossen No comment provided by engineer. + + Conditions accepted on: %@. + No comment provided by engineer. + + + Conditions are accepted for the operator(s): **%@**. + No comment provided by engineer. + + + Conditions are already accepted for following operator(s): **%@**. + No comment provided by engineer. + + + Conditions of use + No comment provided by engineer. + + + Conditions will be accepted for enabled operators after 30 days. + No comment provided by engineer. + + + Conditions will be accepted for operator(s): **%@**. + No comment provided by engineer. + + + Conditions will be accepted for the operator(s): **%@**. + No comment provided by engineer. + + + Conditions will be accepted on: %@. + No comment provided by engineer. + + + Conditions will be automatically accepted for enabled operators on: %@. + No comment provided by engineer. + Configure ICE servers ICE-Server konfigurieren No comment provided by engineer. - - Configured %@ servers - Konfigurierte %@ Server - No comment provided by engineer. - Confirm Bestätigen @@ -1715,6 +1772,10 @@ Das ist Ihr eigener Einmal-Link! Verbindungsanfrage wurde gesendet! No comment provided by engineer. + + Connection security + No comment provided by engineer. + Connection terminated Verbindung beendet @@ -1830,6 +1891,10 @@ Das ist Ihr eigener Einmal-Link! Erstellen No comment provided by engineer. + + Create 1-time link + No comment provided by engineer. + Create SimpleX address SimpleX-Adresse erstellen @@ -1840,11 +1905,6 @@ Das ist Ihr eigener Einmal-Link! Erstellen Sie eine Gruppe mit einem zufälligen Profil. No comment provided by engineer. - - Create an address to let people connect with you. - Erstellen Sie eine Adresse, damit sich Personen mit Ihnen verbinden können. - No comment provided by engineer. - Create file Datei erstellen @@ -1925,6 +1985,10 @@ Das ist Ihr eigener Einmal-Link! Aktueller Zugangscode No comment provided by engineer. + + Current conditions text couldn't be loaded, you can review conditions via this link: + No comment provided by engineer. + Current passphrase… Aktuelles Passwort… @@ -2081,7 +2145,8 @@ Das ist Ihr eigener Einmal-Link! Delete Löschen - chat item action + alert action + chat item action swipe action @@ -2299,6 +2364,10 @@ Das ist Ihr eigener Einmal-Link! Fehler beim Löschen No comment provided by engineer. + + Delivered even when Apple drops them. + No comment provided by engineer. + Delivery Zustellung @@ -2580,6 +2649,10 @@ Das ist Ihr eigener Einmal-Link! Dauer No comment provided by engineer. + + E2E encrypted notifications. + No comment provided by engineer. + Edit Bearbeiten @@ -2600,6 +2673,10 @@ Das ist Ihr eigener Einmal-Link! Aktivieren (vorgenommene Einstellungen bleiben erhalten) No comment provided by engineer. + + Enable Flux + No comment provided by engineer. + Enable SimpleX Lock SimpleX-Sperre aktivieren @@ -2805,6 +2882,10 @@ Das ist Ihr eigener Einmal-Link! Fehler beim Beenden des Adresswechsels No comment provided by engineer. + + Error accepting conditions + alert title + Error accepting contact request Fehler beim Annehmen der Kontaktanfrage @@ -2820,6 +2901,10 @@ Das ist Ihr eigener Einmal-Link! Fehler beim Hinzufügen von Mitgliedern No comment provided by engineer. + + Error adding server + alert title + Error changing address Fehler beim Wechseln der Empfängeradresse @@ -2960,10 +3045,9 @@ Das ist Ihr eigener Einmal-Link! Fehler beim Beitritt zur Gruppe No comment provided by engineer. - - Error loading %@ servers - Fehler beim Laden von %@ Servern - No comment provided by engineer. + + Error loading servers + alert title Error migrating settings @@ -3000,11 +3084,6 @@ Das ist Ihr eigener Einmal-Link! Fehler beim Zurücksetzen der Statistiken No comment provided by engineer. - - Error saving %@ servers - Fehler beim Speichern der %@-Server - No comment provided by engineer. - Error saving ICE servers Fehler beim Speichern der ICE-Server @@ -3025,6 +3104,10 @@ Das ist Ihr eigener Einmal-Link! Fehler beim Speichern des Passworts in den Schlüsselbund No comment provided by engineer. + + Error saving servers + alert title + Error saving settings Fehler beim Abspeichern der Einstellungen @@ -3095,6 +3178,10 @@ Das ist Ihr eigener Einmal-Link! Fehler beim Aktualisieren der Nachricht No comment provided by engineer. + + Error updating server + alert title + Error updating settings Fehler beim Aktualisieren der Einstellungen @@ -3140,6 +3227,10 @@ Das ist Ihr eigener Einmal-Link! Fehler No comment provided by engineer. + + Errors in servers configuration. + servers error + Even when disabled in the conversation. Auch wenn sie im Chat deaktiviert sind. @@ -3342,11 +3433,27 @@ Das ist Ihr eigener Einmal-Link! Reparatur wird vom Gruppenmitglied nicht unterstützt No comment provided by engineer. + + For chat profile %@: + servers error + For console Für Konsole No comment provided by engineer. + + For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server. + No comment provided by engineer. + + + For private routing + No comment provided by engineer. + + + For social media + No comment provided by engineer. + Forward Weiterleiten @@ -3656,9 +3763,12 @@ Fehler: %2$@ Wie SimpleX funktioniert No comment provided by engineer. - - How it works - Wie es funktioniert + + How it affects privacy + No comment provided by engineer. + + + How it helps privacy No comment provided by engineer. @@ -3731,8 +3841,8 @@ Fehler: %2$@ Sofort No comment provided by engineer. - - Immune to spam and abuse + + Immune to spam Immun gegen Spam und Missbrauch No comment provided by engineer. @@ -3873,6 +3983,11 @@ Weitere Verbesserungen sind bald verfügbar! Installieren Sie [SimpleX Chat als Terminalanwendung](https://github.com/simplex-chat/simplex-chat) No comment provided by engineer. + + Instant + Sofort + No comment provided by engineer. + Instant push notifications will be hidden! @@ -3880,11 +3995,6 @@ Weitere Verbesserungen sind bald verfügbar! No comment provided by engineer. - - Instantly - Sofort - No comment provided by engineer. - Interface Schnittstelle @@ -3933,7 +4043,7 @@ Weitere Verbesserungen sind bald verfügbar! Invalid server address! Ungültige Serveradresse! - No comment provided by engineer. + alert title Invalid status @@ -4061,7 +4171,7 @@ Das ist Ihr Link für die Gruppe %@! Keep Behalten - No comment provided by engineer. + alert action Keep conversation @@ -4076,7 +4186,7 @@ Das ist Ihr Link für die Gruppe %@! Keep unused invitation? Nicht genutzte Einladung behalten? - No comment provided by engineer. + alert title Keep your connections @@ -4163,11 +4273,6 @@ Das ist Ihr Link für die Gruppe %@! Live Nachrichten No comment provided by engineer. - - Local - Lokal - No comment provided by engineer. - Local name Lokaler Name @@ -4188,11 +4293,6 @@ Das ist Ihr Link für die Gruppe %@! Sperr-Modus No comment provided by engineer. - - Make a private connection - Stellen Sie eine private Verbindung her - No comment provided by engineer. - Make one message disappear Eine verschwindende Nachricht verfassen @@ -4203,21 +4303,11 @@ Das ist Ihr Link für die Gruppe %@! Privates Profil erzeugen! No comment provided by engineer. - - Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@). - Stellen Sie sicher, dass die %@-Server-Adressen das richtige Format haben, zeilenweise getrennt und nicht doppelt vorhanden sind (%@). - No comment provided by engineer. - Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated. Stellen Sie sicher, dass die WebRTC ICE-Server Adressen das richtige Format haben, zeilenweise getrennt und nicht doppelt vorhanden sind. No comment provided by engineer. - - Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?* - Viele Menschen haben gefragt: *Wie kann SimpleX Nachrichten zustellen, wenn es keine Benutzerkennungen gibt?* - No comment provided by engineer. - Mark deleted for everyone Für Alle als gelöscht markieren @@ -4498,6 +4588,10 @@ Das ist Ihr Link für die Gruppe %@! Zuverlässigere Netzwerkverbindung. No comment provided by engineer. + + More reliable notifications + No comment provided by engineer. + Most likely this connection is deleted. Wahrscheinlich ist diese Verbindung gelöscht worden. @@ -4533,6 +4627,10 @@ Das ist Ihr Link für die Gruppe %@! Netzwerkverbindung No comment provided by engineer. + + Network decentralization + No comment provided by engineer. + Network issues - message expired after many attempts to send it. Netzwerk-Fehler - die Nachricht ist nach vielen Sende-Versuchen abgelaufen. @@ -4543,6 +4641,10 @@ Das ist Ihr Link für die Gruppe %@! Netzwerk-Verwaltung No comment provided by engineer. + + Network operator + No comment provided by engineer. + Network settings Netzwerkeinstellungen @@ -4603,6 +4705,10 @@ Das ist Ihr Link für die Gruppe %@! Neuer Anzeigename No comment provided by engineer. + + New events + notification + New in %@ Neu in %@ @@ -4628,6 +4734,10 @@ Das ist Ihr Link für die Gruppe %@! Neues Passwort… No comment provided by engineer. + + New server + No comment provided by engineer. + No Nein @@ -4683,6 +4793,14 @@ Das ist Ihr Link für die Gruppe %@! Keine Information - es wird versucht neu zu laden No comment provided by engineer. + + No media & file servers. + servers error + + + No message servers. + servers error + No network connection Keine Netzwerkverbindung @@ -4703,11 +4821,37 @@ Das ist Ihr Link für die Gruppe %@! Keine Berechtigung für das Aufnehmen von Sprachnachrichten No comment provided by engineer. + + No push server + Lokal + No comment provided by engineer. + No received or sent files Keine empfangenen oder gesendeten Dateien No comment provided by engineer. + + No servers for private message routing. + servers error + + + No servers to receive files. + servers error + + + No servers to receive messages. + servers error + + + No servers to send files. + servers error + + + No user identifiers. + Keine Benutzerkennungen. + No comment provided by engineer. + Not compatible! Nicht kompatibel! @@ -4733,6 +4877,10 @@ Das ist Ihr Link für die Gruppe %@! Benachrichtigungen sind deaktiviert! No comment provided by engineer. + + Notifications privacy + No comment provided by engineer. + Now admins can: - delete members' messages. @@ -4791,8 +4939,8 @@ Dies erfordert die Aktivierung eines VPNs. Onion-Hosts werden nicht verwendet. No comment provided by engineer. - - Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**. + + Only client devices store user profiles, contacts, groups, and messages. Nur die Endgeräte speichern die Benutzerprofile, Kontakte, Gruppen und Nachrichten, welche über eine **2-Schichten Ende-zu-Ende-Verschlüsselung** gesendet werden. No comment provided by engineer. @@ -4876,6 +5024,10 @@ Dies erfordert die Aktivierung eines VPNs. Geräte-Einstellungen öffnen No comment provided by engineer. + + Open changes + No comment provided by engineer. + Open chat Chat öffnen @@ -4886,6 +5038,10 @@ Dies erfordert die Aktivierung eines VPNs. Chat-Konsole öffnen authentication reason + + Open conditions + No comment provided by engineer. + Open group Gruppe öffnen @@ -4896,26 +5052,19 @@ Dies erfordert die Aktivierung eines VPNs. Migration auf ein anderes Gerät öffnen authentication reason - - Open server settings - Server-Einstellungen öffnen - No comment provided by engineer. - - - Open user profiles - Benutzerprofile öffnen - authentication reason - - - Open-source protocol and code – anybody can run the servers. - Open-Source-Protokoll und -Code – Jede Person kann ihre eigenen Server aufsetzen und nutzen. - No comment provided by engineer. - Opening app… App wird geöffnet… No comment provided by engineer. + + Operator + No comment provided by engineer. + + + Operator server + alert title + Or paste archive link Oder fügen Sie den Archiv-Link ein @@ -4936,16 +5085,15 @@ Dies erfordert die Aktivierung eines VPNs. Oder diesen QR-Code anzeigen No comment provided by engineer. + + Or to share privately + No comment provided by engineer. + Other Andere No comment provided by engineer. - - Other %@ servers - Andere %@ Server - No comment provided by engineer. - Other file errors: %@ @@ -5028,13 +5176,8 @@ Dies erfordert die Aktivierung eines VPNs. Ausstehend No comment provided by engineer. - - People can connect to you only via the links you share. - Verbindungen mit Kontakten sind nur über Links möglich, die Sie oder Ihre Kontakte untereinander teilen. - No comment provided by engineer. - - - Periodically + + Periodic Periodisch No comment provided by engineer. @@ -5157,16 +5300,15 @@ Fehler: %@ Den letzten Nachrichtenentwurf, auch mit seinen Anhängen, aufbewahren. No comment provided by engineer. - - Preset server - Voreingestellter Server - No comment provided by engineer. - Preset server address Voreingestellte Serveradresse No comment provided by engineer. + + Preset servers + No comment provided by engineer. + Preview Vorschau @@ -5245,7 +5387,7 @@ Fehler: %@ Profile update will be sent to your contacts. Profil-Aktualisierung wird an Ihre Kontakte gesendet. - No comment provided by engineer. + alert message Prohibit audio/video calls. @@ -5339,6 +5481,10 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Der Proxy benötigt ein Passwort No comment provided by engineer. + + Push Notifications + No comment provided by engineer. + Push notifications Push-Benachrichtigungen @@ -5379,26 +5525,21 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Mehr erfahren No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address). - Mehr dazu in der [Benutzeranleitung](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address) lesen. - No comment provided by engineer. - Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). Lesen Sie mehr dazu im [Benutzerhandbuch](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). No comment provided by engineer. + + Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). + Mehr dazu in der [Benutzeranleitung](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses) lesen. + No comment provided by engineer. + Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends). Mehr dazu in der [Benutzeranleitung](https://simplex.chat/docs/guide/readme.html#connect-to-friends) lesen. No comment provided by engineer. - - Read more in our GitHub repository. - Erfahren Sie in unserem GitHub-Repository mehr dazu. - No comment provided by engineer. - Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme). Erfahren Sie in unserem [GitHub-Repository](https://github.com/simplex-chat/simplex-chat#readme) mehr dazu. @@ -5715,6 +5856,14 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Aufdecken chat item action + + Review conditions + No comment provided by engineer. + + + Review later + No comment provided by engineer. + Revoke Widerrufen @@ -5760,6 +5909,14 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Sicherere Gruppen No comment provided by engineer. + + Same conditions will apply to operator **%@**. + No comment provided by engineer. + + + Same conditions will apply to operator(s): **%@**. + No comment provided by engineer. + Save Speichern @@ -5829,7 +5986,7 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Save servers? Alle Server speichern? - No comment provided by engineer. + alert title Save welcome message? @@ -6041,11 +6198,6 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Benachrichtigungen senden No comment provided by engineer. - - Send notifications: - Benachrichtigungen senden: - No comment provided by engineer. - Send questions and ideas Senden Sie Fragen und Ideen @@ -6171,6 +6323,10 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Server No comment provided by engineer. + + Server added to operator %@. + alert message + Server address Server-Adresse @@ -6186,6 +6342,18 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Die Server-Adresse ist nicht mit den Netzwerkeinstellungen kompatibel: %@. No comment provided by engineer. + + Server operator changed. + alert title + + + Server operators + No comment provided by engineer. + + + Server protocol changed. + alert title + Server requires authorization to create queues, check password Um Warteschlangen zu erzeugen benötigt der Server eine Authentifizierung. Bitte überprüfen Sie das Passwort @@ -6304,22 +6472,35 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Share Teilen - chat item action + alert action + chat item action Share 1-time link Einmal-Link teilen No comment provided by engineer. + + Share 1-time link with a friend + No comment provided by engineer. + + + Share SimpleX address on social media. + No comment provided by engineer. + Share address Adresse teilen No comment provided by engineer. + + Share address publicly + No comment provided by engineer. + Share address with contacts? Die Adresse mit Kontakten teilen? - No comment provided by engineer. + alert title Share from other apps. @@ -6436,6 +6617,14 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. SimpleX-Adresse No comment provided by engineer. + + SimpleX address and 1-time links are safe to share via any messenger. + No comment provided by engineer. + + + SimpleX address or 1-time link? + No comment provided by engineer. + SimpleX contact address SimpleX-Kontaktadressen-Link @@ -6526,6 +6715,11 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Während des Imports traten ein paar nicht schwerwiegende Fehler auf: No comment provided by engineer. + + Some servers failed the test: +%@ + alert message + Somebody Jemand @@ -6609,12 +6803,12 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Stop sharing Teilen beenden - No comment provided by engineer. + alert action Stop sharing address? Das Teilen der Adresse beenden? - No comment provided by engineer. + alert title Stopping chat @@ -6764,7 +6958,7 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Tests failed! Tests sind fehlgeschlagen! - No comment provided by engineer. + alert title Thank you for installing SimpleX Chat! @@ -6781,11 +6975,6 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Dank der Nutzer - Tragen Sie per Weblate bei! No comment provided by engineer. - - The 1st platform without any user identifiers – private by design. - Die erste Plattform ohne Benutzerkennungen – Privat per Design. - No comment provided by engineer. - The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised. @@ -6798,6 +6987,10 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro Wenn sie Nachrichten oder Kontaktanfragen empfangen, kann Sie die App benachrichtigen - Um dies zu aktivieren, öffnen Sie bitte die Einstellungen. No comment provided by engineer. + + The app protects your privacy by using different operators in each conversation. + No comment provided by engineer. + The app will ask to confirm downloads from unknown file servers (except .onion). Die App wird eine Bestätigung bei Downloads von unbekannten Datei-Servern anfordern (außer bei .onion). @@ -6813,6 +7006,10 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro Der von Ihnen gescannte Code ist kein SimpleX-Link-QR-Code. No comment provided by engineer. + + The connection reached the limit of undelivered messages, your contact may be offline. + No comment provided by engineer. + The connection you accepted will be cancelled! Die von Ihnen akzeptierte Verbindung wird abgebrochen! @@ -6833,6 +7030,11 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro Die Verschlüsselung funktioniert und ein neues Verschlüsselungsabkommen ist nicht erforderlich. Es kann zu Verbindungsfehlern kommen! No comment provided by engineer. + + The future of messaging + Die nächste Generation von privatem Messaging + No comment provided by engineer. + The hash of the previous message is different. Der Hash der vorherigen Nachricht unterscheidet sich. @@ -6858,11 +7060,6 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro Die Nachrichten werden für alle Mitglieder als moderiert gekennzeichnet werden. No comment provided by engineer. - - The next generation of private messaging - Die nächste Generation von privatem Messaging - No comment provided by engineer. - The old database was not removed during the migration, it can be deleted. Die alte Datenbank wurde während der Migration nicht entfernt. Sie kann gelöscht werden. @@ -6873,6 +7070,10 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro Das Profil wird nur mit Ihren Kontakten geteilt. No comment provided by engineer. + + The second preset operator in the app! + No comment provided by engineer. + The second tick we missed! ✅ Wir haben das zweite Häkchen vermisst! ✅ @@ -6888,6 +7089,10 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro Mögliche Server für neue Verbindungen von Ihrem aktuellen Chat-Profil **%@**. No comment provided by engineer. + + The servers for new files of your current chat profile **%@**. + No comment provided by engineer. + The text you pasted is not a SimpleX link. Der von Ihnen eingefügte Text ist kein SimpleX-Link. @@ -6903,6 +7108,10 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro Design No comment provided by engineer. + + These conditions will also apply for: **%@**. + No comment provided by engineer. + These settings are for your current profile **%@**. Diese Einstellungen betreffen Ihr aktuelles Profil **%@**. @@ -7003,9 +7212,8 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro Um eine Verbindung mit einem neuen Kontakt zu erstellen No comment provided by engineer. - - To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts. - Zum Schutz Ihrer Privatsphäre verwendet SimpleX an Stelle von Benutzerkennungen, die von allen anderen Plattformen verwendet werden, Kennungen für Nachrichtenwarteschlangen, die für jeden Ihrer Kontakte individuell sind. + + To protect against your link being replaced, you can compare contact security codes. No comment provided by engineer. @@ -7025,6 +7233,15 @@ You will be prompted to complete authentication before this feature is enabled.< Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funktion aktiviert wird. No comment provided by engineer. + + To protect your privacy, SimpleX uses separate IDs for each of your contacts. + Zum Schutz Ihrer Privatsphäre verwendet SimpleX an Stelle von Benutzerkennungen, die von allen anderen Plattformen verwendet werden, Kennungen für Nachrichtenwarteschlangen, die für jeden Ihrer Kontakte individuell sind. + No comment provided by engineer. + + + To receive + No comment provided by engineer. + To record speech please grant permission to use Microphone. Bitte erteilen Sie für Sprach-Aufnahmen die Genehmigung das Mikrofon zu nutzen. @@ -7045,11 +7262,19 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt Geben Sie ein vollständiges Passwort in das Suchfeld auf der Seite **Ihre Chat-Profile** ein, um Ihr verborgenes Profil zu sehen. No comment provided by engineer. + + To send + No comment provided by engineer. + To support instant push notifications the chat database has to be migrated. Um sofortige Push-Benachrichtigungen zu unterstützen, muss die Chat-Datenbank migriert werden. No comment provided by engineer. + + To use the servers of **%@**, accept conditions of use. + No comment provided by engineer. + To verify end-to-end encryption with your contact compare (or scan) the code on your devices. Um die Ende-zu-Ende-Verschlüsselung mit Ihrem Kontakt zu überprüfen, müssen Sie den Sicherheitscode in Ihren Apps vergleichen oder scannen. @@ -7140,6 +7365,10 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt Mitglied freigeben? No comment provided by engineer. + + Undelivered messages + No comment provided by engineer. + Unexpected migration state Unerwarteter Migrationsstatus @@ -7297,6 +7526,10 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Archiv wird hochgeladen No comment provided by engineer. + + Use %@ + No comment provided by engineer. + Use .onion hosts Verwende .onion-Hosts @@ -7322,6 +7555,14 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Aktuelles Profil nutzen No comment provided by engineer. + + Use for files + No comment provided by engineer. + + + Use for messages + No comment provided by engineer. + Use for new connections Für neue Verbindungen nutzen @@ -7362,6 +7603,10 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Server nutzen No comment provided by engineer. + + Use servers + No comment provided by engineer. + Use the app while in the call. Die App kann während eines Anrufs genutzt werden. @@ -7452,11 +7697,19 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Videos und Dateien bis zu 1GB No comment provided by engineer. + + View conditions + No comment provided by engineer. + View security code Schauen Sie sich den Sicherheitscode an No comment provided by engineer. + + View updated conditions + No comment provided by engineer. + Visible history Sichtbarer Nachrichtenverlauf @@ -7567,9 +7820,8 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Bei der Verbindung über Audio- und Video-Anrufe. No comment provided by engineer. - - When people request to connect, you can accept or reject it. - Wenn Personen eine Verbindung anfordern, können Sie diese annehmen oder ablehnen. + + When more than one operator is enabled, none of them has metadata to learn who communicates with whom. No comment provided by engineer. @@ -7729,6 +7981,18 @@ Verbindungsanfrage wiederholen? Kann von Ihnen in den Erscheinungsbild-Einstellungen geändert werden. No comment provided by engineer. + + You can configure operators in Network & servers settings. + No comment provided by engineer. + + + You can configure servers via settings. + No comment provided by engineer. + + + You can create it in user picker. + No comment provided by engineer. + You can create it later Sie können dies später erstellen @@ -7769,6 +8033,10 @@ Verbindungsanfrage wiederholen? Sie können aus den archivierten Kontakten heraus Nachrichten an %@ versenden. No comment provided by engineer. + + You can set connection name, to remember who the link was shared with. + No comment provided by engineer. + You can set lock screen notification preview via settings. Über die Geräte-Einstellungen können Sie die Benachrichtigungsvorschau im Sperrbildschirm erlauben. @@ -7784,11 +8052,6 @@ Verbindungsanfrage wiederholen? Sie können diese Adresse mit Ihren Kontakten teilen, um sie mit **%@** verbinden zu lassen. No comment provided by engineer. - - You can share your address as a link or QR code - anybody can connect to you. - Sie können Ihre Adresse als Link oder als QR-Code teilen – Jede Person kann sich darüber mit Ihnen verbinden. - No comment provided by engineer. - You can start chat via app Settings / Database or by restarting the app Sie können den Chat über die App-Einstellungen / Datenbank oder durch Neustart der App starten @@ -7812,23 +8075,23 @@ Verbindungsanfrage wiederholen? You can view invitation link again in connection details. Den Einladungslink können Sie in den Details der Verbindung nochmals sehen. - No comment provided by engineer. + alert message You can't send messages! Sie können keine Nachrichten versenden! No comment provided by engineer. - - You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them. - Sie können selbst festlegen, über welche Server Sie Ihre Nachrichten **empfangen** und an Ihre Kontakte **senden** wollen. - No comment provided by engineer. - You could not be verified; please try again. Sie konnten nicht überprüft werden; bitte versuchen Sie es erneut. No comment provided by engineer. + + You decide who can connect. + Sie entscheiden, wer sich mit Ihnen verbinden kann. + No comment provided by engineer. + You have already requested connection via this address! Sie haben über diese Adresse bereits eine Verbindung beantragt! @@ -7951,11 +8214,6 @@ Verbindungsanfrage wiederholen? Sie verwenden ein Inkognito-Profil für diese Gruppe. Um zu verhindern, dass Sie Ihr Hauptprofil teilen, ist in diesem Fall das Einladen von Kontakten nicht erlaubt No comment provided by engineer. - - Your %@ servers - Ihre %@-Server - No comment provided by engineer. - Your ICE servers Ihre ICE-Server @@ -7971,11 +8229,6 @@ Verbindungsanfrage wiederholen? Ihre SimpleX-Adresse No comment provided by engineer. - - Your XFTP servers - Ihre XFTP-Server - No comment provided by engineer. - Your calls Anrufe @@ -8076,16 +8329,15 @@ Verbindungsanfrage wiederholen? Ihr Zufallsprofil No comment provided by engineer. - - Your server - Ihr Server - No comment provided by engineer. - Your server address Ihre Serveradresse No comment provided by engineer. + + Your servers + No comment provided by engineer. + Your settings Einstellungen @@ -8506,6 +8758,10 @@ Verbindungsanfrage wiederholen? Abgelaufen No comment provided by engineer. + + for better metadata privacy. + No comment provided by engineer. + forwarded weitergeleitet @@ -9128,6 +9384,33 @@ Zuletzt empfangene Nachricht: %2$@ + +
+ +
+ + + %d new events + notification body + + + From: %@ + notification body + + + New events + notification + + + New messages + notification + + + New messages in %d chats + notification body + + +
diff --git a/apps/ios/SimpleX Localizations/de.xcloc/Source Contents/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/de.xcloc/Source Contents/en.lproj/Localizable.strings index cf485752ea..cb83427195 100644 --- a/apps/ios/SimpleX Localizations/de.xcloc/Source Contents/en.lproj/Localizable.strings +++ b/apps/ios/SimpleX Localizations/de.xcloc/Source Contents/en.lproj/Localizable.strings @@ -1,9 +1,6 @@ /* No comment provided by engineer. */ "_italic_" = "\\_italic_"; -/* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact."; - /* No comment provided by engineer. */ "*bold*" = "\\*bold*"; @@ -27,4 +24,3 @@ /* No comment provided by engineer. */ "No group!" = "Group not found!"; - diff --git a/apps/ios/SimpleX Localizations/el.xcloc/Localized Contents/el.xliff b/apps/ios/SimpleX Localizations/el.xcloc/Localized Contents/el.xliff index 799c61b448..d18eb4483c 100644 --- a/apps/ios/SimpleX Localizations/el.xcloc/Localized Contents/el.xliff +++ b/apps/ios/SimpleX Localizations/el.xcloc/Localized Contents/el.xliff @@ -186,20 +186,16 @@ Available in v5.1 ) No comment provided by engineer. - - **Add new contact**: to create your one-time QR Code or link for your contact. - No comment provided by engineer. - **Create link / QR code** for your contact to use. No comment provided by engineer. - - **More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have. + + **More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata. No comment provided by engineer. - - **Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app). + + **Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app. No comment provided by engineer. @@ -210,8 +206,8 @@ Available in v5.1 **Please note**: you will NOT be able to recover or change passphrase if you lose it. No comment provided by engineer. - - **Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from. + + **Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from. No comment provided by engineer. @@ -1708,8 +1704,8 @@ Available in v5.1 Immediately No comment provided by engineer. - - Immune to spam and abuse + + Immune to spam No comment provided by engineer. @@ -2174,8 +2170,8 @@ Available in v5.1 Onion hosts will not be used. No comment provided by engineer. - - Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**. + + Only client devices store user profiles, contacts, groups, and messages. No comment provided by engineer. @@ -2234,8 +2230,8 @@ Available in v5.1 Open user profiles authentication reason - - Open-source protocol and code – anybody can run the servers. + + Anybody can host servers. No comment provided by engineer. @@ -2290,8 +2286,8 @@ Available in v5.1 Paste the link you received into the box below to connect with your contact. No comment provided by engineer. - - People can connect to you only via the links you share. + + You decide who can connect. No comment provided by engineer. @@ -2994,8 +2990,8 @@ Available in v5.1 Thanks to the users – contribute via Weblate! No comment provided by engineer. - - The 1st platform without any user identifiers – private by design. + + No user identifiers. No comment provided by engineer. @@ -3039,8 +3035,8 @@ It can happen because of some bug or when the connection is compromised.The message will be marked as moderated for all members. No comment provided by engineer. - - The next generation of private messaging + + The future of messaging No comment provided by engineer. @@ -3111,8 +3107,8 @@ It can happen because of some bug or when the connection is compromised.To make a new connection No comment provided by engineer. - - To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts. + + To protect your privacy, SimpleX uses separate IDs for each of your contacts. No comment provided by engineer. @@ -3478,10 +3474,6 @@ SimpleX Lock must be enabled. You can't send messages! No comment provided by engineer. - - You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them. - No comment provided by engineer. - You could not be verified; please try again. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff index 321b430a3f..5a2c41379b 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff +++ b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff @@ -12,21 +12,6 @@ No comment provided by engineer. - - - - No comment provided by engineer. - - - - - No comment provided by engineer. - - - - - No comment provided by engineer. - ( ( @@ -127,6 +112,16 @@ %@ is verified No comment provided by engineer. + + %@ server + %@ server + No comment provided by engineer. + + + %@ servers + %@ servers + No comment provided by engineer. + %@ uploaded %@ uploaded @@ -352,14 +347,9 @@ ) No comment provided by engineer. - - **Add contact**: to create a new invitation link, or connect via a link you received. - **Add contact**: to create a new invitation link, or connect via a link you received. - No comment provided by engineer. - - - **Add new contact**: to create your one-time QR Code or link for your contact. - **Add new contact**: to create your one-time QR Code or link for your contact. + + **Create 1-time link**: to create and share a new invitation link. + **Create 1-time link**: to create and share a new invitation link. No comment provided by engineer. @@ -367,14 +357,14 @@ **Create group**: to create a new group. No comment provided by engineer. - - **More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have. - **More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have. + + **More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata. + **More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata. No comment provided by engineer. - - **Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app). - **Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app). + + **Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app. + **Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app. No comment provided by engineer. @@ -387,9 +377,14 @@ **Please note**: you will NOT be able to recover or change passphrase if you lose it. No comment provided by engineer. - - **Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from. - **Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from. + + **Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from. + **Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from. + No comment provided by engineer. + + + **Scan / Paste link**: to connect via a link you received. + **Scan / Paste link**: to connect via a link you received. No comment provided by engineer. @@ -498,6 +493,16 @@ 1 week time interval + + 1-time link + 1-time link + No comment provided by engineer. + + + 1-time link can be used *with one contact only* - share in person or via any messenger. + 1-time link can be used *with one contact only* - share in person or via any messenger. + No comment provided by engineer. + 5 minutes 5 minutes @@ -567,21 +572,11 @@ Abort changing address? No comment provided by engineer. - - About SimpleX - About SimpleX - No comment provided by engineer. - About SimpleX Chat About SimpleX Chat No comment provided by engineer. - - About SimpleX address - About SimpleX address - No comment provided by engineer. - Accent Accent @@ -594,6 +589,11 @@ accept incoming call via notification swipe action + + Accept conditions + Accept conditions + No comment provided by engineer. + Accept connection request? Accept connection request? @@ -610,6 +610,11 @@ accept contact request via notification swipe action + + Accepted conditions + Accepted conditions + No comment provided by engineer. + Acknowledged Acknowledged @@ -630,16 +635,6 @@ Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts. No comment provided by engineer. - - Add contact - Add contact - No comment provided by engineer. - - - Add preset servers - Add preset servers - No comment provided by engineer. - Add profile Add profile @@ -665,6 +660,16 @@ Add welcome message No comment provided by engineer. + + Added media & file servers + Added media & file servers + No comment provided by engineer. + + + Added message servers + Added message servers + No comment provided by engineer. + Additional accent Additional accent @@ -690,6 +695,16 @@ Address change will be aborted. Old receiving address will be used. No comment provided by engineer. + + Address or 1-time link? + Address or 1-time link? + No comment provided by engineer. + + + Address settings + Address settings + No comment provided by engineer. + Admins can block a member for all. Admins can block a member for all. @@ -735,6 +750,11 @@ All group members will remain connected. No comment provided by engineer. + + All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages. + All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages. + No comment provided by engineer. + All messages will be deleted - this cannot be undone! All messages will be deleted - this cannot be undone! @@ -915,6 +935,11 @@ Answer call No comment provided by engineer. + + Anybody can host servers. + Anybody can host servers. + No comment provided by engineer. + App build: %@ App build: %@ @@ -1258,7 +1283,8 @@ Cancel Cancel - alert button + alert action + alert button Cancel migration @@ -1341,6 +1367,11 @@ authentication reason set passcode view + + Change user profiles + Change user profiles + authentication reason + Chat archive Chat archive @@ -1426,10 +1457,20 @@ Chats No comment provided by engineer. + + Check messages every 20 min. + Check messages every 20 min. + No comment provided by engineer. + + + Check messages when allowed. + Check messages when allowed. + No comment provided by engineer. + Check server address and try again. Check server address and try again. - No comment provided by engineer. + alert title Chinese and Spanish interface @@ -1516,16 +1557,56 @@ Completed No comment provided by engineer. + + Conditions accepted on: %@. + Conditions accepted on: %@. + No comment provided by engineer. + + + Conditions are accepted for the operator(s): **%@**. + Conditions are accepted for the operator(s): **%@**. + No comment provided by engineer. + + + Conditions are already accepted for following operator(s): **%@**. + Conditions are already accepted for following operator(s): **%@**. + No comment provided by engineer. + + + Conditions of use + Conditions of use + No comment provided by engineer. + + + Conditions will be accepted for enabled operators after 30 days. + Conditions will be accepted for enabled operators after 30 days. + No comment provided by engineer. + + + Conditions will be accepted for operator(s): **%@**. + Conditions will be accepted for operator(s): **%@**. + No comment provided by engineer. + + + Conditions will be accepted for the operator(s): **%@**. + Conditions will be accepted for the operator(s): **%@**. + No comment provided by engineer. + + + Conditions will be accepted on: %@. + Conditions will be accepted on: %@. + No comment provided by engineer. + + + Conditions will be automatically accepted for enabled operators on: %@. + Conditions will be automatically accepted for enabled operators on: %@. + No comment provided by engineer. + Configure ICE servers Configure ICE servers No comment provided by engineer. - - Configured %@ servers - Configured %@ servers - No comment provided by engineer. - Confirm Confirm @@ -1715,6 +1796,11 @@ This is your own one-time link! Connection request sent! No comment provided by engineer. + + Connection security + Connection security + No comment provided by engineer. + Connection terminated Connection terminated @@ -1830,6 +1916,11 @@ This is your own one-time link! Create No comment provided by engineer. + + Create 1-time link + Create 1-time link + No comment provided by engineer. + Create SimpleX address Create SimpleX address @@ -1840,11 +1931,6 @@ This is your own one-time link! Create a group using a random profile. No comment provided by engineer. - - Create an address to let people connect with you. - Create an address to let people connect with you. - No comment provided by engineer. - Create file Create file @@ -1925,6 +2011,11 @@ This is your own one-time link! Current Passcode No comment provided by engineer. + + Current conditions text couldn't be loaded, you can review conditions via this link: + Current conditions text couldn't be loaded, you can review conditions via this link: + No comment provided by engineer. + Current passphrase… Current passphrase… @@ -2081,7 +2172,8 @@ This is your own one-time link! Delete Delete - chat item action + alert action + chat item action swipe action @@ -2299,6 +2391,11 @@ This is your own one-time link! Deletion errors No comment provided by engineer. + + Delivered even when Apple drops them. + Delivered even when Apple drops them. + No comment provided by engineer. + Delivery Delivery @@ -2580,6 +2677,11 @@ This is your own one-time link! Duration No comment provided by engineer. + + E2E encrypted notifications. + E2E encrypted notifications. + No comment provided by engineer. + Edit Edit @@ -2600,6 +2702,11 @@ This is your own one-time link! Enable (keep overrides) No comment provided by engineer. + + Enable Flux + Enable Flux + No comment provided by engineer. + Enable SimpleX Lock Enable SimpleX Lock @@ -2805,6 +2912,11 @@ This is your own one-time link! Error aborting address change No comment provided by engineer. + + Error accepting conditions + Error accepting conditions + alert title + Error accepting contact request Error accepting contact request @@ -2820,6 +2932,11 @@ This is your own one-time link! Error adding member(s) No comment provided by engineer. + + Error adding server + Error adding server + alert title + Error changing address Error changing address @@ -2960,10 +3077,10 @@ This is your own one-time link! Error joining group No comment provided by engineer. - - Error loading %@ servers - Error loading %@ servers - No comment provided by engineer. + + Error loading servers + Error loading servers + alert title Error migrating settings @@ -3000,11 +3117,6 @@ This is your own one-time link! Error resetting statistics No comment provided by engineer. - - Error saving %@ servers - Error saving %@ servers - No comment provided by engineer. - Error saving ICE servers Error saving ICE servers @@ -3025,6 +3137,11 @@ This is your own one-time link! Error saving passphrase to keychain No comment provided by engineer. + + Error saving servers + Error saving servers + alert title + Error saving settings Error saving settings @@ -3095,6 +3212,11 @@ This is your own one-time link! Error updating message No comment provided by engineer. + + Error updating server + Error updating server + alert title + Error updating settings Error updating settings @@ -3140,6 +3262,11 @@ This is your own one-time link! Errors No comment provided by engineer. + + Errors in servers configuration. + Errors in servers configuration. + servers error + Even when disabled in the conversation. Even when disabled in the conversation. @@ -3342,11 +3469,31 @@ This is your own one-time link! Fix not supported by group member No comment provided by engineer. + + For chat profile %@: + For chat profile %@: + servers error + For console For console No comment provided by engineer. + + For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server. + For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server. + No comment provided by engineer. + + + For private routing + For private routing + No comment provided by engineer. + + + For social media + For social media + No comment provided by engineer. + Forward Forward @@ -3656,9 +3803,14 @@ Error: %2$@ How SimpleX works No comment provided by engineer. - - How it works - How it works + + How it affects privacy + How it affects privacy + No comment provided by engineer. + + + How it helps privacy + How it helps privacy No comment provided by engineer. @@ -3731,9 +3883,9 @@ Error: %2$@ Immediately No comment provided by engineer. - - Immune to spam and abuse - Immune to spam and abuse + + Immune to spam + Immune to spam No comment provided by engineer. @@ -3873,6 +4025,11 @@ More improvements are coming soon! Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat) No comment provided by engineer. + + Instant + Instant + No comment provided by engineer. + Instant push notifications will be hidden! @@ -3880,11 +4037,6 @@ More improvements are coming soon! No comment provided by engineer. - - Instantly - Instantly - No comment provided by engineer. - Interface Interface @@ -3933,7 +4085,7 @@ More improvements are coming soon! Invalid server address! Invalid server address! - No comment provided by engineer. + alert title Invalid status @@ -4061,7 +4213,7 @@ This is your link for group %@! Keep Keep - No comment provided by engineer. + alert action Keep conversation @@ -4076,7 +4228,7 @@ This is your link for group %@! Keep unused invitation? Keep unused invitation? - No comment provided by engineer. + alert title Keep your connections @@ -4163,11 +4315,6 @@ This is your link for group %@! Live messages No comment provided by engineer. - - Local - Local - No comment provided by engineer. - Local name Local name @@ -4188,11 +4335,6 @@ This is your link for group %@! Lock mode No comment provided by engineer. - - Make a private connection - Make a private connection - No comment provided by engineer. - Make one message disappear Make one message disappear @@ -4203,21 +4345,11 @@ This is your link for group %@! Make profile private! No comment provided by engineer. - - Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@). - Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@). - No comment provided by engineer. - Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated. Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated. No comment provided by engineer. - - Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?* - Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?* - No comment provided by engineer. - Mark deleted for everyone Mark deleted for everyone @@ -4498,6 +4630,11 @@ This is your link for group %@! More reliable network connection. No comment provided by engineer. + + More reliable notifications + More reliable notifications + No comment provided by engineer. + Most likely this connection is deleted. Most likely this connection is deleted. @@ -4533,6 +4670,11 @@ This is your link for group %@! Network connection No comment provided by engineer. + + Network decentralization + Network decentralization + No comment provided by engineer. + Network issues - message expired after many attempts to send it. Network issues - message expired after many attempts to send it. @@ -4543,6 +4685,11 @@ This is your link for group %@! Network management No comment provided by engineer. + + Network operator + Network operator + No comment provided by engineer. + Network settings Network settings @@ -4603,6 +4750,11 @@ This is your link for group %@! New display name No comment provided by engineer. + + New events + New events + notification + New in %@ New in %@ @@ -4628,6 +4780,11 @@ This is your link for group %@! New passphrase… No comment provided by engineer. + + New server + New server + No comment provided by engineer. + No No @@ -4683,6 +4840,16 @@ This is your link for group %@! No info, try to reload No comment provided by engineer. + + No media & file servers. + No media & file servers. + servers error + + + No message servers. + No message servers. + servers error + No network connection No network connection @@ -4703,11 +4870,41 @@ This is your link for group %@! No permission to record voice message No comment provided by engineer. + + No push server + No push server + No comment provided by engineer. + No received or sent files No received or sent files No comment provided by engineer. + + No servers for private message routing. + No servers for private message routing. + servers error + + + No servers to receive files. + No servers to receive files. + servers error + + + No servers to receive messages. + No servers to receive messages. + servers error + + + No servers to send files. + No servers to send files. + servers error + + + No user identifiers. + No user identifiers. + No comment provided by engineer. + Not compatible! Not compatible! @@ -4733,6 +4930,11 @@ This is your link for group %@! Notifications are disabled! No comment provided by engineer. + + Notifications privacy + Notifications privacy + No comment provided by engineer. + Now admins can: - delete members' messages. @@ -4791,9 +4993,9 @@ Requires compatible VPN. Onion hosts will not be used. No comment provided by engineer. - - Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**. - Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**. + + Only client devices store user profiles, contacts, groups, and messages. + Only client devices store user profiles, contacts, groups, and messages. No comment provided by engineer. @@ -4876,6 +5078,11 @@ Requires compatible VPN. Open Settings No comment provided by engineer. + + Open changes + Open changes + No comment provided by engineer. + Open chat Open chat @@ -4886,6 +5093,11 @@ Requires compatible VPN. Open chat console authentication reason + + Open conditions + Open conditions + No comment provided by engineer. + Open group Open group @@ -4896,26 +5108,21 @@ Requires compatible VPN. Open migration to another device authentication reason - - Open server settings - Open server settings - No comment provided by engineer. - - - Open user profiles - Open user profiles - authentication reason - - - Open-source protocol and code – anybody can run the servers. - Open-source protocol and code – anybody can run the servers. - No comment provided by engineer. - Opening app… Opening app… No comment provided by engineer. + + Operator + Operator + No comment provided by engineer. + + + Operator server + Operator server + alert title + Or paste archive link Or paste archive link @@ -4936,16 +5143,16 @@ Requires compatible VPN. Or show this code No comment provided by engineer. + + Or to share privately + Or to share privately + No comment provided by engineer. + Other Other No comment provided by engineer. - - Other %@ servers - Other %@ servers - No comment provided by engineer. - Other file errors: %@ @@ -5028,14 +5235,9 @@ Requires compatible VPN. Pending No comment provided by engineer. - - People can connect to you only via the links you share. - People can connect to you only via the links you share. - No comment provided by engineer. - - - Periodically - Periodically + + Periodic + Periodic No comment provided by engineer. @@ -5157,16 +5359,16 @@ Error: %@ Preserve the last message draft, with attachments. No comment provided by engineer. - - Preset server - Preset server - No comment provided by engineer. - Preset server address Preset server address No comment provided by engineer. + + Preset servers + Preset servers + No comment provided by engineer. + Preview Preview @@ -5245,7 +5447,7 @@ Error: %@ Profile update will be sent to your contacts. Profile update will be sent to your contacts. - No comment provided by engineer. + alert message Prohibit audio/video calls. @@ -5339,6 +5541,11 @@ Enable in *Network & servers* settings. Proxy requires password No comment provided by engineer. + + Push Notifications + Push Notifications + No comment provided by engineer. + Push notifications Push notifications @@ -5379,26 +5586,21 @@ Enable in *Network & servers* settings. Read more No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address). - Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address). - No comment provided by engineer. - Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). No comment provided by engineer. + + Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). + Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). + No comment provided by engineer. + Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends). Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends). No comment provided by engineer. - - Read more in our GitHub repository. - Read more in our GitHub repository. - No comment provided by engineer. - Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme). Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme). @@ -5715,6 +5917,16 @@ Enable in *Network & servers* settings. Reveal chat item action + + Review conditions + Review conditions + No comment provided by engineer. + + + Review later + Review later + No comment provided by engineer. + Revoke Revoke @@ -5760,6 +5972,16 @@ Enable in *Network & servers* settings. Safer groups No comment provided by engineer. + + Same conditions will apply to operator **%@**. + Same conditions will apply to operator **%@**. + No comment provided by engineer. + + + Same conditions will apply to operator(s): **%@**. + Same conditions will apply to operator(s): **%@**. + No comment provided by engineer. + Save Save @@ -5829,7 +6051,7 @@ Enable in *Network & servers* settings. Save servers? Save servers? - No comment provided by engineer. + alert title Save welcome message? @@ -6041,11 +6263,6 @@ Enable in *Network & servers* settings. Send notifications No comment provided by engineer. - - Send notifications: - Send notifications: - No comment provided by engineer. - Send questions and ideas Send questions and ideas @@ -6171,6 +6388,11 @@ Enable in *Network & servers* settings. Server No comment provided by engineer. + + Server added to operator %@. + Server added to operator %@. + alert message + Server address Server address @@ -6186,6 +6408,21 @@ Enable in *Network & servers* settings. Server address is incompatible with network settings: %@. No comment provided by engineer. + + Server operator changed. + Server operator changed. + alert title + + + Server operators + Server operators + No comment provided by engineer. + + + Server protocol changed. + Server protocol changed. + alert title + Server requires authorization to create queues, check password Server requires authorization to create queues, check password @@ -6304,22 +6541,38 @@ Enable in *Network & servers* settings. Share Share - chat item action + alert action + chat item action Share 1-time link Share 1-time link No comment provided by engineer. + + Share 1-time link with a friend + Share 1-time link with a friend + No comment provided by engineer. + + + Share SimpleX address on social media. + Share SimpleX address on social media. + No comment provided by engineer. + Share address Share address No comment provided by engineer. + + Share address publicly + Share address publicly + No comment provided by engineer. + Share address with contacts? Share address with contacts? - No comment provided by engineer. + alert title Share from other apps. @@ -6436,6 +6689,16 @@ Enable in *Network & servers* settings. SimpleX address No comment provided by engineer. + + SimpleX address and 1-time links are safe to share via any messenger. + SimpleX address and 1-time links are safe to share via any messenger. + No comment provided by engineer. + + + SimpleX address or 1-time link? + SimpleX address or 1-time link? + No comment provided by engineer. + SimpleX contact address SimpleX contact address @@ -6526,6 +6789,13 @@ Enable in *Network & servers* settings. Some non-fatal errors occurred during import: No comment provided by engineer. + + Some servers failed the test: +%@ + Some servers failed the test: +%@ + alert message + Somebody Somebody @@ -6609,12 +6879,12 @@ Enable in *Network & servers* settings. Stop sharing Stop sharing - No comment provided by engineer. + alert action Stop sharing address? Stop sharing address? - No comment provided by engineer. + alert title Stopping chat @@ -6764,7 +7034,7 @@ Enable in *Network & servers* settings. Tests failed! Tests failed! - No comment provided by engineer. + alert title Thank you for installing SimpleX Chat! @@ -6781,11 +7051,6 @@ Enable in *Network & servers* settings. Thanks to the users – contribute via Weblate! No comment provided by engineer. - - The 1st platform without any user identifiers – private by design. - The 1st platform without any user identifiers – private by design. - No comment provided by engineer. - The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised. @@ -6798,6 +7063,11 @@ It can happen because of some bug or when the connection is compromised.The app can notify you when you receive messages or contact requests - please open settings to enable. No comment provided by engineer. + + The app protects your privacy by using different operators in each conversation. + The app protects your privacy by using different operators in each conversation. + No comment provided by engineer. + The app will ask to confirm downloads from unknown file servers (except .onion). The app will ask to confirm downloads from unknown file servers (except .onion). @@ -6813,6 +7083,11 @@ It can happen because of some bug or when the connection is compromised.The code you scanned is not a SimpleX link QR code. No comment provided by engineer. + + The connection reached the limit of undelivered messages, your contact may be offline. + The connection reached the limit of undelivered messages, your contact may be offline. + No comment provided by engineer. + The connection you accepted will be cancelled! The connection you accepted will be cancelled! @@ -6833,6 +7108,11 @@ It can happen because of some bug or when the connection is compromised.The encryption is working and the new encryption agreement is not required. It may result in connection errors! No comment provided by engineer. + + The future of messaging + The future of messaging + No comment provided by engineer. + The hash of the previous message is different. The hash of the previous message is different. @@ -6858,11 +7138,6 @@ It can happen because of some bug or when the connection is compromised.The messages will be marked as moderated for all members. No comment provided by engineer. - - The next generation of private messaging - The next generation of private messaging - No comment provided by engineer. - The old database was not removed during the migration, it can be deleted. The old database was not removed during the migration, it can be deleted. @@ -6873,6 +7148,11 @@ It can happen because of some bug or when the connection is compromised.The profile is only shared with your contacts. No comment provided by engineer. + + The second preset operator in the app! + The second preset operator in the app! + No comment provided by engineer. + The second tick we missed! ✅ The second tick we missed! ✅ @@ -6888,6 +7168,11 @@ It can happen because of some bug or when the connection is compromised.The servers for new connections of your current chat profile **%@**. No comment provided by engineer. + + The servers for new files of your current chat profile **%@**. + The servers for new files of your current chat profile **%@**. + No comment provided by engineer. + The text you pasted is not a SimpleX link. The text you pasted is not a SimpleX link. @@ -6903,6 +7188,11 @@ It can happen because of some bug or when the connection is compromised.Themes No comment provided by engineer. + + These conditions will also apply for: **%@**. + These conditions will also apply for: **%@**. + No comment provided by engineer. + These settings are for your current profile **%@**. These settings are for your current profile **%@**. @@ -7003,9 +7293,9 @@ It can happen because of some bug or when the connection is compromised.To make a new connection No comment provided by engineer. - - To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts. - To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts. + + To protect against your link being replaced, you can compare contact security codes. + To protect against your link being replaced, you can compare contact security codes. No comment provided by engineer. @@ -7025,6 +7315,16 @@ You will be prompted to complete authentication before this feature is enabled.< You will be prompted to complete authentication before this feature is enabled. No comment provided by engineer. + + To protect your privacy, SimpleX uses separate IDs for each of your contacts. + To protect your privacy, SimpleX uses separate IDs for each of your contacts. + No comment provided by engineer. + + + To receive + To receive + No comment provided by engineer. + To record speech please grant permission to use Microphone. To record speech please grant permission to use Microphone. @@ -7045,11 +7345,21 @@ You will be prompted to complete authentication before this feature is enabled.< To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page. No comment provided by engineer. + + To send + To send + No comment provided by engineer. + To support instant push notifications the chat database has to be migrated. To support instant push notifications the chat database has to be migrated. No comment provided by engineer. + + To use the servers of **%@**, accept conditions of use. + To use the servers of **%@**, accept conditions of use. + No comment provided by engineer. + To verify end-to-end encryption with your contact compare (or scan) the code on your devices. To verify end-to-end encryption with your contact compare (or scan) the code on your devices. @@ -7140,6 +7450,11 @@ You will be prompted to complete authentication before this feature is enabled.< Unblock member? No comment provided by engineer. + + Undelivered messages + Undelivered messages + No comment provided by engineer. + Unexpected migration state Unexpected migration state @@ -7297,6 +7612,11 @@ To connect, please ask your contact to create another connection link and check Uploading archive No comment provided by engineer. + + Use %@ + Use %@ + No comment provided by engineer. + Use .onion hosts Use .onion hosts @@ -7322,6 +7642,16 @@ To connect, please ask your contact to create another connection link and check Use current profile No comment provided by engineer. + + Use for files + Use for files + No comment provided by engineer. + + + Use for messages + Use for messages + No comment provided by engineer. + Use for new connections Use for new connections @@ -7362,6 +7692,11 @@ To connect, please ask your contact to create another connection link and check Use server No comment provided by engineer. + + Use servers + Use servers + No comment provided by engineer. + Use the app while in the call. Use the app while in the call. @@ -7452,11 +7787,21 @@ To connect, please ask your contact to create another connection link and check Videos and files up to 1gb No comment provided by engineer. + + View conditions + View conditions + No comment provided by engineer. + View security code View security code No comment provided by engineer. + + View updated conditions + View updated conditions + No comment provided by engineer. + Visible history Visible history @@ -7567,9 +7912,9 @@ To connect, please ask your contact to create another connection link and check When connecting audio and video calls. No comment provided by engineer. - - When people request to connect, you can accept or reject it. - When people request to connect, you can accept or reject it. + + When more than one operator is enabled, none of them has metadata to learn who communicates with whom. + When more than one operator is enabled, none of them has metadata to learn who communicates with whom. No comment provided by engineer. @@ -7729,6 +8074,21 @@ Repeat join request? You can change it in Appearance settings. No comment provided by engineer. + + You can configure operators in Network & servers settings. + You can configure operators in Network & servers settings. + No comment provided by engineer. + + + You can configure servers via settings. + You can configure servers via settings. + No comment provided by engineer. + + + You can create it in user picker. + You can create it in user picker. + No comment provided by engineer. + You can create it later You can create it later @@ -7769,6 +8129,11 @@ Repeat join request? You can send messages to %@ from Archived contacts. No comment provided by engineer. + + You can set connection name, to remember who the link was shared with. + You can set connection name, to remember who the link was shared with. + No comment provided by engineer. + You can set lock screen notification preview via settings. You can set lock screen notification preview via settings. @@ -7784,11 +8149,6 @@ Repeat join request? You can share this address with your contacts to let them connect with **%@**. No comment provided by engineer. - - You can share your address as a link or QR code - anybody can connect to you. - You can share your address as a link or QR code - anybody can connect to you. - No comment provided by engineer. - You can start chat via app Settings / Database or by restarting the app You can start chat via app Settings / Database or by restarting the app @@ -7812,23 +8172,23 @@ Repeat join request? You can view invitation link again in connection details. You can view invitation link again in connection details. - No comment provided by engineer. + alert message You can't send messages! You can't send messages! No comment provided by engineer. - - You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them. - You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them. - No comment provided by engineer. - You could not be verified; please try again. You could not be verified; please try again. No comment provided by engineer. + + You decide who can connect. + You decide who can connect. + No comment provided by engineer. + You have already requested connection via this address! You have already requested connection via this address! @@ -7951,11 +8311,6 @@ Repeat connection request? You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed No comment provided by engineer. - - Your %@ servers - Your %@ servers - No comment provided by engineer. - Your ICE servers Your ICE servers @@ -7971,11 +8326,6 @@ Repeat connection request? Your SimpleX address No comment provided by engineer. - - Your XFTP servers - Your XFTP servers - No comment provided by engineer. - Your calls Your calls @@ -8076,16 +8426,16 @@ Repeat connection request? Your random profile No comment provided by engineer. - - Your server - Your server - No comment provided by engineer. - Your server address Your server address No comment provided by engineer. + + Your servers + Your servers + No comment provided by engineer. + Your settings Your settings @@ -8506,6 +8856,11 @@ Repeat connection request? expired No comment provided by engineer. + + for better metadata privacy. + for better metadata privacy. + No comment provided by engineer. + forwarded forwarded @@ -9128,6 +9483,38 @@ last received msg: %2$@ + +
+ +
+ + + %d new events + %d new events + notification body + + + From: %@ + From: %@ + notification body + + + New events + New events + notification + + + New messages + New messages + notification + + + New messages in %d chats + New messages in %d chats + notification body + + +
diff --git a/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/en.lproj/Localizable.strings index cf485752ea..cb83427195 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/en.lproj/Localizable.strings +++ b/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/en.lproj/Localizable.strings @@ -1,9 +1,6 @@ /* No comment provided by engineer. */ "_italic_" = "\\_italic_"; -/* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact."; - /* No comment provided by engineer. */ "*bold*" = "\\*bold*"; @@ -27,4 +24,3 @@ /* No comment provided by engineer. */ "No group!" = "Group not found!"; - diff --git a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff index 21cd9919db..59c4bd167f 100644 --- a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff +++ b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff @@ -12,21 +12,6 @@ No comment provided by engineer. - - - - No comment provided by engineer. - - - - - No comment provided by engineer. - - - - - No comment provided by engineer. - ( ( @@ -127,6 +112,14 @@ %@ está verificado No comment provided by engineer. + + %@ server + No comment provided by engineer. + + + %@ servers + No comment provided by engineer. + %@ uploaded %@ subido @@ -352,14 +345,9 @@ ) No comment provided by engineer. - - **Add contact**: to create a new invitation link, or connect via a link you received. - **Añadir contacto**: crea un enlace de invitación nuevo o usa un enlace recibido. - No comment provided by engineer. - - - **Add new contact**: to create your one-time QR Code or link for your contact. - **Añadir nuevo contacto**: para crear tu código QR o enlace de un uso para tu contacto. + + **Create 1-time link**: to create and share a new invitation link. + **Añadir contacto**: crea un enlace de invitación nuevo. No comment provided by engineer. @@ -367,13 +355,13 @@ **Crear grupo**: crea un grupo nuevo. No comment provided by engineer. - - **More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have. + + **More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata. **Más privado**: comprueba los mensajes nuevos cada 20 minutos. El token del dispositivo se comparte con el servidor de SimpleX Chat, pero no cuántos contactos o mensajes tienes. No comment provided by engineer. - - **Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app). + + **Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app. **Más privado**: no se usa el servidor de notificaciones de SimpleX Chat, los mensajes se comprueban periódicamente en segundo plano (dependiendo de la frecuencia con la que utilices la aplicación). No comment provided by engineer. @@ -387,11 +375,15 @@ **Atención**: NO podrás recuperar o cambiar la contraseña si la pierdes. No comment provided by engineer. - - **Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from. + + **Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from. **Recomendado**: el token del dispositivo y las notificaciones se envían al servidor de notificaciones de SimpleX Chat, pero no el contenido del mensaje, su tamaño o su procedencia. No comment provided by engineer. + + **Scan / Paste link**: to connect via a link you received. + No comment provided by engineer. + **Warning**: Instant push notifications require passphrase saved in Keychain. **Advertencia**: Las notificaciones automáticas instantáneas requieren una contraseña guardada en Keychain. @@ -498,6 +490,14 @@ una semana time interval + + 1-time link + No comment provided by engineer. + + + 1-time link can be used *with one contact only* - share in person or via any messenger. + No comment provided by engineer. + 5 minutes 5 minutos @@ -567,21 +567,11 @@ ¿Cancelar el cambio de servidor? No comment provided by engineer. - - About SimpleX - Acerca de SimpleX - No comment provided by engineer. - About SimpleX Chat Sobre SimpleX Chat No comment provided by engineer. - - About SimpleX address - Acerca de la dirección SimpleX - No comment provided by engineer. - Accent Color @@ -594,6 +584,10 @@ accept incoming call via notification swipe action + + Accept conditions + No comment provided by engineer. + Accept connection request? ¿Aceptar solicitud de conexión? @@ -610,6 +604,10 @@ accept contact request via notification swipe action + + Accepted conditions + No comment provided by engineer. + Acknowledged Confirmaciones @@ -630,16 +628,6 @@ Añade la dirección a tu perfil para que tus contactos puedan compartirla con otros. La actualización del perfil se enviará a tus contactos. No comment provided by engineer. - - Add contact - Añadir contacto - No comment provided by engineer. - - - Add preset servers - Añadir servidores predefinidos - No comment provided by engineer. - Add profile Añadir perfil @@ -665,6 +653,14 @@ Añadir mensaje de bienvenida No comment provided by engineer. + + Added media & file servers + No comment provided by engineer. + + + Added message servers + No comment provided by engineer. + Additional accent Acento adicional @@ -690,6 +686,14 @@ El cambio de dirección se cancelará. Se usará la antigua dirección de recepción. No comment provided by engineer. + + Address or 1-time link? + No comment provided by engineer. + + + Address settings + No comment provided by engineer. + Admins can block a member for all. Los administradores pueden bloquear a un miembro para los demás. @@ -735,6 +739,10 @@ Todos los miembros del grupo permanecerán conectados. No comment provided by engineer. + + All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages. + No comment provided by engineer. + All messages will be deleted - this cannot be undone! Todos los mensajes serán borrados. ¡No podrá deshacerse! @@ -915,6 +923,11 @@ Responder llamada No comment provided by engineer. + + Anybody can host servers. + Cualquiera puede alojar servidores. + No comment provided by engineer. + App build: %@ Compilación app: %@ @@ -1258,7 +1271,8 @@ Cancel Cancelar - alert button + alert action + alert button Cancel migration @@ -1341,6 +1355,10 @@ authentication reason set passcode view + + Change user profiles + authentication reason + Chat archive Archivo del chat @@ -1426,10 +1444,18 @@ Chats No comment provided by engineer. + + Check messages every 20 min. + No comment provided by engineer. + + + Check messages when allowed. + No comment provided by engineer. + Check server address and try again. Comprueba la dirección del servidor e inténtalo de nuevo. - No comment provided by engineer. + alert title Chinese and Spanish interface @@ -1516,16 +1542,47 @@ Completadas No comment provided by engineer. + + Conditions accepted on: %@. + No comment provided by engineer. + + + Conditions are accepted for the operator(s): **%@**. + No comment provided by engineer. + + + Conditions are already accepted for following operator(s): **%@**. + No comment provided by engineer. + + + Conditions of use + No comment provided by engineer. + + + Conditions will be accepted for enabled operators after 30 days. + No comment provided by engineer. + + + Conditions will be accepted for operator(s): **%@**. + No comment provided by engineer. + + + Conditions will be accepted for the operator(s): **%@**. + No comment provided by engineer. + + + Conditions will be accepted on: %@. + No comment provided by engineer. + + + Conditions will be automatically accepted for enabled operators on: %@. + No comment provided by engineer. + Configure ICE servers Configure servidores ICE No comment provided by engineer. - - Configured %@ servers - %@ servidores configurados - No comment provided by engineer. - Confirm Confirmar @@ -1715,6 +1772,10 @@ This is your own one-time link! ¡Solicitud de conexión enviada! No comment provided by engineer. + + Connection security + No comment provided by engineer. + Connection terminated Conexión finalizada @@ -1830,6 +1891,10 @@ This is your own one-time link! Crear No comment provided by engineer. + + Create 1-time link + No comment provided by engineer. + Create SimpleX address Crear dirección SimpleX @@ -1840,11 +1905,6 @@ This is your own one-time link! Crear grupo usando perfil aleatorio. No comment provided by engineer. - - Create an address to let people connect with you. - Crea una dirección para que otras personas puedan conectar contigo. - No comment provided by engineer. - Create file Crear archivo @@ -1925,6 +1985,10 @@ This is your own one-time link! Código de Acceso No comment provided by engineer. + + Current conditions text couldn't be loaded, you can review conditions via this link: + No comment provided by engineer. + Current passphrase… Contraseña actual… @@ -2081,7 +2145,8 @@ This is your own one-time link! Delete Eliminar - chat item action + alert action + chat item action swipe action @@ -2299,6 +2364,10 @@ This is your own one-time link! Errores de eliminación No comment provided by engineer. + + Delivered even when Apple drops them. + No comment provided by engineer. + Delivery Entrega @@ -2580,6 +2649,10 @@ This is your own one-time link! Duración No comment provided by engineer. + + E2E encrypted notifications. + No comment provided by engineer. + Edit Editar @@ -2600,6 +2673,10 @@ This is your own one-time link! Activar (conservar anulaciones) No comment provided by engineer. + + Enable Flux + No comment provided by engineer. + Enable SimpleX Lock Activar Bloqueo SimpleX @@ -2805,6 +2882,10 @@ This is your own one-time link! Error al cancelar cambio de dirección No comment provided by engineer. + + Error accepting conditions + alert title + Error accepting contact request Error al aceptar solicitud del contacto @@ -2820,6 +2901,10 @@ This is your own one-time link! Error al añadir miembro(s) No comment provided by engineer. + + Error adding server + alert title + Error changing address Error al cambiar servidor @@ -2960,10 +3045,9 @@ This is your own one-time link! Error al unirte al grupo No comment provided by engineer. - - Error loading %@ servers - Error al cargar servidores %@ - No comment provided by engineer. + + Error loading servers + alert title Error migrating settings @@ -3000,11 +3084,6 @@ This is your own one-time link! Error al restablecer las estadísticas No comment provided by engineer. - - Error saving %@ servers - Error al guardar servidores %@ - No comment provided by engineer. - Error saving ICE servers Error al guardar servidores ICE @@ -3025,6 +3104,10 @@ This is your own one-time link! Error al guardar contraseña en Keychain No comment provided by engineer. + + Error saving servers + alert title + Error saving settings Error al guardar ajustes @@ -3095,6 +3178,10 @@ This is your own one-time link! Error al actualizar mensaje No comment provided by engineer. + + Error updating server + alert title + Error updating settings Error al actualizar configuración @@ -3140,6 +3227,10 @@ This is your own one-time link! Errores No comment provided by engineer. + + Errors in servers configuration. + servers error + Even when disabled in the conversation. Incluso si está desactivado para la conversación. @@ -3342,11 +3433,27 @@ This is your own one-time link! Corrección no compatible con miembro del grupo No comment provided by engineer. + + For chat profile %@: + servers error + For console Para consola No comment provided by engineer. + + For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server. + No comment provided by engineer. + + + For private routing + No comment provided by engineer. + + + For social media + No comment provided by engineer. + Forward Reenviar @@ -3656,9 +3763,12 @@ Error: %2$@ Cómo funciona SimpleX No comment provided by engineer. - - How it works - Cómo funciona + + How it affects privacy + No comment provided by engineer. + + + How it helps privacy No comment provided by engineer. @@ -3731,8 +3841,8 @@ Error: %2$@ Inmediatamente No comment provided by engineer. - - Immune to spam and abuse + + Immune to spam Inmune a spam y abuso No comment provided by engineer. @@ -3873,6 +3983,11 @@ More improvements are coming soon! Instalar terminal para [SimpleX Chat](https://github.com/simplex-chat/simplex-chat) No comment provided by engineer. + + Instant + Al instante + No comment provided by engineer. + Instant push notifications will be hidden! @@ -3880,11 +3995,6 @@ More improvements are coming soon! No comment provided by engineer. - - Instantly - Al instante - No comment provided by engineer. - Interface Interfaz @@ -3933,7 +4043,7 @@ More improvements are coming soon! Invalid server address! ¡Dirección de servidor no válida! - No comment provided by engineer. + alert title Invalid status @@ -4061,7 +4171,7 @@ This is your link for group %@! Keep Guardar - No comment provided by engineer. + alert action Keep conversation @@ -4076,7 +4186,7 @@ This is your link for group %@! Keep unused invitation? ¿Guardar invitación no usada? - No comment provided by engineer. + alert title Keep your connections @@ -4163,11 +4273,6 @@ This is your link for group %@! Mensajes en vivo No comment provided by engineer. - - Local - Local - No comment provided by engineer. - Local name Nombre local @@ -4188,11 +4293,6 @@ This is your link for group %@! Modo bloqueo No comment provided by engineer. - - Make a private connection - Establecer una conexión privada - No comment provided by engineer. - Make one message disappear Escribir un mensaje temporal @@ -4203,21 +4303,11 @@ This is your link for group %@! ¡Hacer perfil privado! No comment provided by engineer. - - Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@). - Asegúrate de que las direcciones del servidor %@ tienen el formato correcto, están separadas por líneas y no duplicadas (%@). - No comment provided by engineer. - Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated. Asegúrate de que las direcciones del servidor WebRTC ICE tienen el formato correcto, están separadas por líneas y no duplicadas. No comment provided by engineer. - - Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?* - Muchos se preguntarán: *si SimpleX no tiene identificadores de usuario, ¿cómo puede entregar los mensajes?* - No comment provided by engineer. - Mark deleted for everyone Marcar como eliminado para todos @@ -4498,6 +4588,10 @@ This is your link for group %@! Conexión de red más fiable. No comment provided by engineer. + + More reliable notifications + No comment provided by engineer. + Most likely this connection is deleted. Probablemente la conexión ha sido eliminada. @@ -4533,6 +4627,10 @@ This is your link for group %@! Conexión de red No comment provided by engineer. + + Network decentralization + No comment provided by engineer. + Network issues - message expired after many attempts to send it. Problema en la red - el mensaje ha expirado tras muchos intentos de envío. @@ -4543,6 +4641,10 @@ This is your link for group %@! Gestión de la red No comment provided by engineer. + + Network operator + No comment provided by engineer. + Network settings Configuración de red @@ -4603,6 +4705,10 @@ This is your link for group %@! Nuevo nombre mostrado No comment provided by engineer. + + New events + notification + New in %@ Nuevo en %@ @@ -4628,6 +4734,10 @@ This is your link for group %@! Contraseña nueva… No comment provided by engineer. + + New server + No comment provided by engineer. + No No @@ -4683,6 +4793,14 @@ This is your link for group %@! No hay información, intenta recargar No comment provided by engineer. + + No media & file servers. + servers error + + + No message servers. + servers error + No network connection Sin conexión de red @@ -4703,11 +4821,37 @@ This is your link for group %@! Sin permiso para grabar mensajes de voz No comment provided by engineer. + + No push server + No push server + No comment provided by engineer. + No received or sent files Sin archivos recibidos o enviados No comment provided by engineer. + + No servers for private message routing. + servers error + + + No servers to receive files. + servers error + + + No servers to receive messages. + servers error + + + No servers to send files. + servers error + + + No user identifiers. + Sin identificadores de usuario. + No comment provided by engineer. + Not compatible! ¡No compatible! @@ -4733,6 +4877,10 @@ This is your link for group %@! ¡Las notificaciones están desactivadas! No comment provided by engineer. + + Notifications privacy + No comment provided by engineer. + Now admins can: - delete members' messages. @@ -4791,8 +4939,8 @@ Requiere activación de la VPN. No se usarán hosts .onion. No comment provided by engineer. - - Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**. + + Only client devices store user profiles, contacts, groups, and messages. Sólo los dispositivos cliente almacenan perfiles de usuario, contactos, grupos y mensajes enviados con **cifrado de extremo a extremo de 2 capas**. No comment provided by engineer. @@ -4876,6 +5024,10 @@ Requiere activación de la VPN. Abrir Configuración No comment provided by engineer. + + Open changes + No comment provided by engineer. + Open chat Abrir chat @@ -4886,6 +5038,10 @@ Requiere activación de la VPN. Abrir consola de Chat authentication reason + + Open conditions + No comment provided by engineer. + Open group Grupo abierto @@ -4896,26 +5052,19 @@ Requiere activación de la VPN. Abrir menú migración a otro dispositivo authentication reason - - Open server settings - Abrir configuración del servidor - No comment provided by engineer. - - - Open user profiles - Abrir perfil de usuario - authentication reason - - - Open-source protocol and code – anybody can run the servers. - Protocolo y código abiertos: cualquiera puede usar los servidores. - No comment provided by engineer. - Opening app… Iniciando aplicación… No comment provided by engineer. + + Operator + No comment provided by engineer. + + + Operator server + alert title + Or paste archive link O pegar enlace del archivo @@ -4936,16 +5085,15 @@ Requiere activación de la VPN. O muestra este código QR No comment provided by engineer. + + Or to share privately + No comment provided by engineer. + Other Otro No comment provided by engineer. - - Other %@ servers - Otros servidores %@ - No comment provided by engineer. - Other file errors: %@ @@ -5028,13 +5176,8 @@ Requiere activación de la VPN. Pendientes No comment provided by engineer. - - People can connect to you only via the links you share. - Las personas pueden conectarse contigo solo mediante los enlaces que compartes. - No comment provided by engineer. - - - Periodically + + Periodic Periódicamente No comment provided by engineer. @@ -5157,16 +5300,15 @@ Error: %@ Conserva el último borrador del mensaje con los datos adjuntos. No comment provided by engineer. - - Preset server - Servidor predefinido - No comment provided by engineer. - Preset server address Dirección del servidor predefinida No comment provided by engineer. + + Preset servers + No comment provided by engineer. + Preview Vista previa @@ -5245,7 +5387,7 @@ Error: %@ Profile update will be sent to your contacts. La actualización del perfil se enviará a tus contactos. - No comment provided by engineer. + alert message Prohibit audio/video calls. @@ -5339,6 +5481,10 @@ Actívalo en ajustes de *Servidores y Redes*. El proxy requiere contraseña No comment provided by engineer. + + Push Notifications + No comment provided by engineer. + Push notifications Notificaciones automáticas @@ -5379,26 +5525,21 @@ Actívalo en ajustes de *Servidores y Redes*. Conoce más No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address). - Conoce más en el [Manual del Usuario](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address). - No comment provided by engineer. - Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). Conoce más en la [Guía del Usuario](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). No comment provided by engineer. + + Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). + Conoce más en el [Manual del Usuario](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). + No comment provided by engineer. + Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends). Conoce más en el [Manual del Usuario](https://simplex.chat/docs/guide/readme.html#connect-to-friends). No comment provided by engineer. - - Read more in our GitHub repository. - Conoce más en nuestro repositorio GitHub. - No comment provided by engineer. - Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme). Conoce más en nuestro [repositorio GitHub](https://github.com/simplex-chat/simplex-chat#readme). @@ -5715,6 +5856,14 @@ Actívalo en ajustes de *Servidores y Redes*. Revelar chat item action + + Review conditions + No comment provided by engineer. + + + Review later + No comment provided by engineer. + Revoke Revocar @@ -5760,6 +5909,14 @@ Actívalo en ajustes de *Servidores y Redes*. Grupos más seguros No comment provided by engineer. + + Same conditions will apply to operator **%@**. + No comment provided by engineer. + + + Same conditions will apply to operator(s): **%@**. + No comment provided by engineer. + Save Guardar @@ -5829,7 +5986,7 @@ Actívalo en ajustes de *Servidores y Redes*. Save servers? ¿Guardar servidores? - No comment provided by engineer. + alert title Save welcome message? @@ -6041,11 +6198,6 @@ Actívalo en ajustes de *Servidores y Redes*. Enviar notificaciones No comment provided by engineer. - - Send notifications: - Enviar notificaciones: - No comment provided by engineer. - Send questions and ideas Consultas y sugerencias @@ -6171,6 +6323,10 @@ Actívalo en ajustes de *Servidores y Redes*. Servidor No comment provided by engineer. + + Server added to operator %@. + alert message + Server address Dirección del servidor @@ -6186,6 +6342,18 @@ Actívalo en ajustes de *Servidores y Redes*. La dirección del servidor es incompatible con la configuración de la red: %@. No comment provided by engineer. + + Server operator changed. + alert title + + + Server operators + No comment provided by engineer. + + + Server protocol changed. + alert title + Server requires authorization to create queues, check password El servidor requiere autorización para crear colas, comprueba la contraseña @@ -6304,22 +6472,35 @@ Actívalo en ajustes de *Servidores y Redes*. Share Compartir - chat item action + alert action + chat item action Share 1-time link Compartir enlace de un uso No comment provided by engineer. + + Share 1-time link with a friend + No comment provided by engineer. + + + Share SimpleX address on social media. + No comment provided by engineer. + Share address Compartir dirección No comment provided by engineer. + + Share address publicly + No comment provided by engineer. + Share address with contacts? ¿Compartir la dirección con los contactos? - No comment provided by engineer. + alert title Share from other apps. @@ -6436,6 +6617,14 @@ Actívalo en ajustes de *Servidores y Redes*. Dirección SimpleX No comment provided by engineer. + + SimpleX address and 1-time links are safe to share via any messenger. + No comment provided by engineer. + + + SimpleX address or 1-time link? + No comment provided by engineer. + SimpleX contact address Dirección de contacto SimpleX @@ -6526,6 +6715,11 @@ Actívalo en ajustes de *Servidores y Redes*. Han ocurrido algunos errores no críticos durante la importación: No comment provided by engineer. + + Some servers failed the test: +%@ + alert message + Somebody Alguien @@ -6609,12 +6803,12 @@ Actívalo en ajustes de *Servidores y Redes*. Stop sharing Dejar de compartir - No comment provided by engineer. + alert action Stop sharing address? ¿Dejar de compartir la dirección? - No comment provided by engineer. + alert title Stopping chat @@ -6764,7 +6958,7 @@ Actívalo en ajustes de *Servidores y Redes*. Tests failed! ¡Pruebas no superadas! - No comment provided by engineer. + alert title Thank you for installing SimpleX Chat! @@ -6781,11 +6975,6 @@ Actívalo en ajustes de *Servidores y Redes*. ¡Nuestro agradecimiento a todos los colaboradores! Puedes contribuir a través de Weblate No comment provided by engineer. - - The 1st platform without any user identifiers – private by design. - La primera plataforma sin identificadores de usuario: diseñada para la privacidad. - No comment provided by engineer. - The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised. @@ -6798,6 +6987,10 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. La aplicación puede notificarte cuando recibas mensajes o solicitudes de contacto: por favor, abre la configuración para activarlo. No comment provided by engineer. + + The app protects your privacy by using different operators in each conversation. + No comment provided by engineer. + The app will ask to confirm downloads from unknown file servers (except .onion). La aplicación pedirá que confirmes las descargas desde servidores de archivos desconocidos (excepto si son .onion). @@ -6813,6 +7006,10 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. El código QR escaneado no es un enlace SimpleX. No comment provided by engineer. + + The connection reached the limit of undelivered messages, your contact may be offline. + No comment provided by engineer. + The connection you accepted will be cancelled! ¡La conexión que has aceptado se cancelará! @@ -6833,6 +7030,11 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. El cifrado funciona y un cifrado nuevo no es necesario. ¡Podría dar lugar a errores de conexión! No comment provided by engineer. + + The future of messaging + La nueva generación de mensajería privada + No comment provided by engineer. + The hash of the previous message is different. El hash del mensaje anterior es diferente. @@ -6858,11 +7060,6 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. Los mensajes serán marcados como moderados para todos los miembros. No comment provided by engineer. - - The next generation of private messaging - La nueva generación de mensajería privada - No comment provided by engineer. - The old database was not removed during the migration, it can be deleted. La base de datos antigua no se eliminó durante la migración, puede eliminarse. @@ -6873,6 +7070,10 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. El perfil sólo se comparte con tus contactos. No comment provided by engineer. + + The second preset operator in the app! + No comment provided by engineer. + The second tick we missed! ✅ ¡El doble check que nos faltaba! ✅ @@ -6888,6 +7089,10 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. Lista de servidores para las conexiones nuevas de tu perfil actual **%@**. No comment provided by engineer. + + The servers for new files of your current chat profile **%@**. + No comment provided by engineer. + The text you pasted is not a SimpleX link. El texto pegado no es un enlace SimpleX. @@ -6903,6 +7108,10 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. Temas No comment provided by engineer. + + These conditions will also apply for: **%@**. + No comment provided by engineer. + These settings are for your current profile **%@**. Esta configuración afecta a tu perfil actual **%@**. @@ -7003,9 +7212,8 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. Para hacer una conexión nueva No comment provided by engineer. - - To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts. - Para proteger tu privacidad, en lugar de los identificadores de usuario que usan el resto de plataformas, SimpleX dispone de identificadores para las colas de mensajes, independientes para cada uno de tus contactos. + + To protect against your link being replaced, you can compare contact security codes. No comment provided by engineer. @@ -7025,6 +7233,15 @@ You will be prompted to complete authentication before this feature is enabled.< Se te pedirá que completes la autenticación antes de activar esta función. No comment provided by engineer. + + To protect your privacy, SimpleX uses separate IDs for each of your contacts. + Para proteger tu privacidad, en lugar de los identificadores de usuario que usan el resto de plataformas, SimpleX dispone de identificadores para las colas de mensajes, independientes para cada uno de tus contactos. + No comment provided by engineer. + + + To receive + No comment provided by engineer. + To record speech please grant permission to use Microphone. Para grabación de voz, por favor concede el permiso para usar el micrófono. @@ -7045,11 +7262,19 @@ Se te pedirá que completes la autenticación antes de activar esta función.Para hacer visible tu perfil oculto, introduce la contraseña en el campo de búsqueda del menú **Mis perfiles**. No comment provided by engineer. + + To send + No comment provided by engineer. + To support instant push notifications the chat database has to be migrated. Para permitir las notificaciones automáticas instantáneas, la base de datos se debe migrar. No comment provided by engineer. + + To use the servers of **%@**, accept conditions of use. + No comment provided by engineer. + To verify end-to-end encryption with your contact compare (or scan) the code on your devices. Para verificar el cifrado de extremo a extremo con tu contacto, compara (o escanea) el código en ambos dispositivos. @@ -7140,6 +7365,10 @@ Se te pedirá que completes la autenticación antes de activar esta función.¿Desbloquear miembro? No comment provided by engineer. + + Undelivered messages + No comment provided by engineer. + Unexpected migration state Estado de migración inesperado @@ -7297,6 +7526,10 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión Subiendo archivo No comment provided by engineer. + + Use %@ + No comment provided by engineer. + Use .onion hosts Usar hosts .onion @@ -7322,6 +7555,14 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión Usar perfil actual No comment provided by engineer. + + Use for files + No comment provided by engineer. + + + Use for messages + No comment provided by engineer. + Use for new connections Usar para conexiones nuevas @@ -7362,6 +7603,10 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión Usar servidor No comment provided by engineer. + + Use servers + No comment provided by engineer. + Use the app while in the call. Usar la aplicación durante la llamada. @@ -7452,11 +7697,19 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión Vídeos y archivos de hasta 1Gb No comment provided by engineer. + + View conditions + No comment provided by engineer. + View security code Mostrar código de seguridad No comment provided by engineer. + + View updated conditions + No comment provided by engineer. + Visible history Historial visible @@ -7567,9 +7820,8 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión Al iniciar llamadas de audio y vídeo. No comment provided by engineer. - - When people request to connect, you can accept or reject it. - Cuando alguien solicite conectarse podrás aceptar o rechazar la solicitud. + + When more than one operator is enabled, none of them has metadata to learn who communicates with whom. No comment provided by engineer. @@ -7729,6 +7981,18 @@ Repeat join request? Puedes cambiar la posición de la barra desde el menú Apariencia. No comment provided by engineer. + + You can configure operators in Network & servers settings. + No comment provided by engineer. + + + You can configure servers via settings. + No comment provided by engineer. + + + You can create it in user picker. + No comment provided by engineer. + You can create it later Puedes crearla más tarde @@ -7769,6 +8033,10 @@ Repeat join request? Puedes enviar mensajes a %@ desde Contactos archivados. No comment provided by engineer. + + You can set connection name, to remember who the link was shared with. + No comment provided by engineer. + You can set lock screen notification preview via settings. Puedes configurar las notificaciones de la pantalla de bloqueo desde Configuración. @@ -7784,11 +8052,6 @@ Repeat join request? Puedes compartir esta dirección con tus contactos para que puedan conectar con **%@**. No comment provided by engineer. - - You can share your address as a link or QR code - anybody can connect to you. - Puedes compartir tu dirección como enlace o código QR para que cualquiera pueda conectarse contigo. - No comment provided by engineer. - You can start chat via app Settings / Database or by restarting the app Puede iniciar Chat a través de la Configuración / Base de datos de la aplicación o reiniciando la aplicación @@ -7812,23 +8075,23 @@ Repeat join request? You can view invitation link again in connection details. Podrás ver el enlace de invitación en detalles de conexión. - No comment provided by engineer. + alert message You can't send messages! ¡No puedes enviar mensajes! No comment provided by engineer. - - You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them. - Tú controlas a través de qué servidor(es) **recibes** los mensajes. Tus contactos controlan a través de qué servidor(es) **envías** tus mensajes. - No comment provided by engineer. - You could not be verified; please try again. No has podido ser autenticado. Inténtalo de nuevo. No comment provided by engineer. + + You decide who can connect. + Tu decides quién se conecta. + No comment provided by engineer. + You have already requested connection via this address! ¡Ya has solicitado la conexión mediante esta dirección! @@ -7951,11 +8214,6 @@ Repeat connection request? Estás usando un perfil incógnito en este grupo. Para evitar descubrir tu perfil principal no se permite invitar contactos No comment provided by engineer. - - Your %@ servers - Mis servidores %@ - No comment provided by engineer. - Your ICE servers Servidores ICE @@ -7971,11 +8229,6 @@ Repeat connection request? Mi dirección SimpleX No comment provided by engineer. - - Your XFTP servers - Servidores XFTP - No comment provided by engineer. - Your calls Llamadas @@ -8076,16 +8329,15 @@ Repeat connection request? Tu perfil aleatorio No comment provided by engineer. - - Your server - Tu servidor - No comment provided by engineer. - Your server address Dirección del servidor No comment provided by engineer. + + Your servers + No comment provided by engineer. + Your settings Configuración @@ -8506,6 +8758,10 @@ Repeat connection request? expirados No comment provided by engineer. + + for better metadata privacy. + No comment provided by engineer. + forwarded reenviado @@ -9128,6 +9384,33 @@ last received msg: %2$@ + +
+ +
+ + + %d new events + notification body + + + From: %@ + notification body + + + New events + notification + + + New messages + notification + + + New messages in %d chats + notification body + + +
diff --git a/apps/ios/SimpleX Localizations/es.xcloc/Source Contents/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/es.xcloc/Source Contents/en.lproj/Localizable.strings index cf485752ea..cb83427195 100644 --- a/apps/ios/SimpleX Localizations/es.xcloc/Source Contents/en.lproj/Localizable.strings +++ b/apps/ios/SimpleX Localizations/es.xcloc/Source Contents/en.lproj/Localizable.strings @@ -1,9 +1,6 @@ /* No comment provided by engineer. */ "_italic_" = "\\_italic_"; -/* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact."; - /* No comment provided by engineer. */ "*bold*" = "\\*bold*"; @@ -27,4 +24,3 @@ /* No comment provided by engineer. */ "No group!" = "Group not found!"; - diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff index 4b384842b6..c41190e0f1 100644 --- a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff @@ -12,21 +12,6 @@ No comment provided by engineer. - - - - No comment provided by engineer. - - - - - No comment provided by engineer. - - - - - No comment provided by engineer. - ( ( @@ -124,6 +109,14 @@ %@ on vahvistettu No comment provided by engineer. + + %@ server + No comment provided by engineer. + + + %@ servers + No comment provided by engineer. + %@ uploaded No comment provided by engineer. @@ -334,26 +327,21 @@ ) No comment provided by engineer. - - **Add contact**: to create a new invitation link, or connect via a link you received. - No comment provided by engineer. - - - **Add new contact**: to create your one-time QR Code or link for your contact. - **Lisää uusi kontakti**: luo kertakäyttöinen QR-koodi tai linkki kontaktille. + + **Create 1-time link**: to create and share a new invitation link. No comment provided by engineer. **Create group**: to create a new group. No comment provided by engineer. - - **More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have. + + **More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata. **Yksityisempi**: tarkista uudet viestit 20 minuutin välein. Laitetunnus jaetaan SimpleX Chat -palvelimen kanssa, mutta ei sitä, kuinka monta yhteystietoa tai viestiä sinulla on. No comment provided by engineer. - - **Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app). + + **Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app. **Yksityisin**: älä käytä SimpleX Chat -ilmoituspalvelinta, tarkista viestit ajoittain taustalla (riippuu siitä, kuinka usein käytät sovellusta). No comment provided by engineer. @@ -366,11 +354,15 @@ **Huomaa**: et voi palauttaa tai muuttaa tunnuslausetta, jos kadotat sen. No comment provided by engineer. - - **Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from. + + **Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from. **Suositus**: laitetunnus ja ilmoitukset lähetetään SimpleX Chat -ilmoituspalvelimelle, mutta ei viestin sisältöä, kokoa tai sitä, keneltä se on peräisin. No comment provided by engineer. + + **Scan / Paste link**: to connect via a link you received. + No comment provided by engineer. + **Warning**: Instant push notifications require passphrase saved in Keychain. **Varoitus**: Välittömät push-ilmoitukset vaativat tunnuslauseen, joka on tallennettu Keychainiin. @@ -469,6 +461,14 @@ 1 viikko time interval + + 1-time link + No comment provided by engineer. + + + 1-time link can be used *with one contact only* - share in person or via any messenger. + No comment provided by engineer. + 5 minutes 5 minuuttia @@ -538,21 +538,11 @@ Keskeytä osoitteenvaihto? No comment provided by engineer. - - About SimpleX - Tietoja SimpleX:stä - No comment provided by engineer. - About SimpleX Chat Tietoja SimpleX Chatistä No comment provided by engineer. - - About SimpleX address - Tietoja SimpleX osoitteesta - No comment provided by engineer. - Accent No comment provided by engineer. @@ -564,6 +554,10 @@ accept incoming call via notification swipe action + + Accept conditions + No comment provided by engineer. + Accept connection request? Hyväksy yhteyspyyntö? @@ -580,6 +574,10 @@ accept contact request via notification swipe action + + Accepted conditions + No comment provided by engineer. + Acknowledged No comment provided by engineer. @@ -597,15 +595,6 @@ Lisää osoite profiiliisi, jotta kontaktisi voivat jakaa sen muiden kanssa. Profiilipäivitys lähetetään kontakteillesi. No comment provided by engineer. - - Add contact - No comment provided by engineer. - - - Add preset servers - Lisää esiasetettuja palvelimia - No comment provided by engineer. - Add profile Lisää profiili @@ -631,6 +620,14 @@ Lisää tervetuloviesti No comment provided by engineer. + + Added media & file servers + No comment provided by engineer. + + + Added message servers + No comment provided by engineer. + Additional accent No comment provided by engineer. @@ -653,6 +650,14 @@ Osoitteenmuutos keskeytetään. Käytetään vanhaa vastaanotto-osoitetta. No comment provided by engineer. + + Address or 1-time link? + No comment provided by engineer. + + + Address settings + No comment provided by engineer. + Admins can block a member for all. No comment provided by engineer. @@ -695,6 +700,10 @@ Kaikki ryhmän jäsenet pysyvät yhteydessä. No comment provided by engineer. + + All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages. + No comment provided by engineer. + All messages will be deleted - this cannot be undone! No comment provided by engineer. @@ -864,6 +873,11 @@ Vastaa puheluun No comment provided by engineer. + + Anybody can host servers. + Avoimen lähdekoodin protokolla ja koodi - kuka tahansa voi käyttää palvelimia. + No comment provided by engineer. + App build: %@ Sovellusversio: %@ @@ -1172,7 +1186,8 @@ Cancel Peruuta - alert button + alert action + alert button Cancel migration @@ -1251,6 +1266,10 @@ authentication reason set passcode view + + Change user profiles + authentication reason + Chat archive Chat-arkisto @@ -1329,10 +1348,18 @@ Keskustelut No comment provided by engineer. + + Check messages every 20 min. + No comment provided by engineer. + + + Check messages when allowed. + No comment provided by engineer. + Check server address and try again. Tarkista palvelimen osoite ja yritä uudelleen. - No comment provided by engineer. + alert title Chinese and Spanish interface @@ -1411,15 +1438,47 @@ Completed No comment provided by engineer. + + Conditions accepted on: %@. + No comment provided by engineer. + + + Conditions are accepted for the operator(s): **%@**. + No comment provided by engineer. + + + Conditions are already accepted for following operator(s): **%@**. + No comment provided by engineer. + + + Conditions of use + No comment provided by engineer. + + + Conditions will be accepted for enabled operators after 30 days. + No comment provided by engineer. + + + Conditions will be accepted for operator(s): **%@**. + No comment provided by engineer. + + + Conditions will be accepted for the operator(s): **%@**. + No comment provided by engineer. + + + Conditions will be accepted on: %@. + No comment provided by engineer. + + + Conditions will be automatically accepted for enabled operators on: %@. + No comment provided by engineer. + Configure ICE servers Määritä ICE-palvelimet No comment provided by engineer. - - Configured %@ servers - No comment provided by engineer. - Confirm Vahvista @@ -1585,6 +1644,10 @@ This is your own one-time link! Yhteyspyyntö lähetetty! No comment provided by engineer. + + Connection security + No comment provided by engineer. + Connection terminated No comment provided by engineer. @@ -1690,6 +1753,10 @@ This is your own one-time link! Luo No comment provided by engineer. + + Create 1-time link + No comment provided by engineer. + Create SimpleX address Luo SimpleX-osoite @@ -1699,11 +1766,6 @@ This is your own one-time link! Create a group using a random profile. No comment provided by engineer. - - Create an address to let people connect with you. - Luo osoite, jolla ihmiset voivat ottaa sinuun yhteyttä. - No comment provided by engineer. - Create file Luo tiedosto @@ -1777,6 +1839,10 @@ This is your own one-time link! Nykyinen pääsykoodi No comment provided by engineer. + + Current conditions text couldn't be loaded, you can review conditions via this link: + No comment provided by engineer. + Current passphrase… Nykyinen tunnuslause… @@ -1928,7 +1994,8 @@ This is your own one-time link! Delete Poista - chat item action + alert action + chat item action swipe action @@ -2136,6 +2203,10 @@ This is your own one-time link! Deletion errors No comment provided by engineer. + + Delivered even when Apple drops them. + No comment provided by engineer. + Delivery Toimitus @@ -2393,6 +2464,10 @@ This is your own one-time link! Kesto No comment provided by engineer. + + E2E encrypted notifications. + No comment provided by engineer. + Edit Muokkaa @@ -2413,6 +2488,10 @@ This is your own one-time link! Salli (pidä ohitukset) No comment provided by engineer. + + Enable Flux + No comment provided by engineer. + Enable SimpleX Lock Ota SimpleX Lock käyttöön @@ -2606,6 +2685,10 @@ This is your own one-time link! Virhe osoitteenmuutoksen keskeytyksessä No comment provided by engineer. + + Error accepting conditions + alert title + Error accepting contact request Virhe kontaktipyynnön hyväksymisessä @@ -2621,6 +2704,10 @@ This is your own one-time link! Virhe lisättäessä jäseniä No comment provided by engineer. + + Error adding server + alert title + Error changing address Virhe osoitteenvaihdossa @@ -2754,10 +2841,9 @@ This is your own one-time link! Virhe ryhmään liittymisessä No comment provided by engineer. - - Error loading %@ servers - Virhe %@-palvelimien lataamisessa - No comment provided by engineer. + + Error loading servers + alert title Error migrating settings @@ -2789,11 +2875,6 @@ This is your own one-time link! Error resetting statistics No comment provided by engineer. - - Error saving %@ servers - Virhe %@ palvelimien tallentamisessa - No comment provided by engineer. - Error saving ICE servers Virhe ICE-palvelimien tallentamisessa @@ -2814,6 +2895,10 @@ This is your own one-time link! Virhe tunnuslauseen tallentamisessa avainnippuun No comment provided by engineer. + + Error saving servers + alert title + Error saving settings when migrating @@ -2880,6 +2965,10 @@ This is your own one-time link! Virhe viestin päivityksessä No comment provided by engineer. + + Error updating server + alert title + Error updating settings Virhe asetusten päivittämisessä @@ -2922,6 +3011,10 @@ This is your own one-time link! Errors No comment provided by engineer. + + Errors in servers configuration. + servers error + Even when disabled in the conversation. Jopa kun ei käytössä keskustelussa. @@ -3109,11 +3202,27 @@ This is your own one-time link! Ryhmän jäsen ei tue korjausta No comment provided by engineer. + + For chat profile %@: + servers error + For console Konsoliin No comment provided by engineer. + + For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server. + No comment provided by engineer. + + + For private routing + No comment provided by engineer. + + + For social media + No comment provided by engineer. + Forward chat item action @@ -3399,9 +3508,12 @@ Error: %2$@ Miten SimpleX toimii No comment provided by engineer. - - How it works - Kuinka se toimii + + How it affects privacy + No comment provided by engineer. + + + How it helps privacy No comment provided by engineer. @@ -3472,8 +3584,8 @@ Error: %2$@ Heti No comment provided by engineer. - - Immune to spam and abuse + + Immune to spam Immuuni roskapostille ja väärinkäytöksille No comment provided by engineer. @@ -3604,6 +3716,11 @@ More improvements are coming soon! Asenna [SimpleX Chat terminaalille](https://github.com/simplex-chat/simplex-chat) No comment provided by engineer. + + Instant + Heti + No comment provided by engineer. + Instant push notifications will be hidden! @@ -3611,11 +3728,6 @@ More improvements are coming soon! No comment provided by engineer. - - Instantly - Heti - No comment provided by engineer. - Interface Käyttöliittymä @@ -3657,7 +3769,7 @@ More improvements are coming soon! Invalid server address! Virheellinen palvelinosoite! - No comment provided by engineer. + alert title Invalid status @@ -3778,7 +3890,7 @@ This is your link for group %@! Keep - No comment provided by engineer. + alert action Keep conversation @@ -3790,7 +3902,7 @@ This is your link for group %@! Keep unused invitation? - No comment provided by engineer. + alert title Keep your connections @@ -3874,11 +3986,6 @@ This is your link for group %@! Live-viestit No comment provided by engineer. - - Local - Paikallinen - No comment provided by engineer. - Local name Paikallinen nimi @@ -3899,11 +4006,6 @@ This is your link for group %@! Lukitustila No comment provided by engineer. - - Make a private connection - Luo yksityinen yhteys - No comment provided by engineer. - Make one message disappear Hävitä yksi viesti @@ -3914,21 +4016,11 @@ This is your link for group %@! Tee profiilista yksityinen! No comment provided by engineer. - - Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@). - Varmista, että %@-palvelinosoitteet ovat oikeassa muodossa, että ne on erotettu toisistaan riveittäin ja että ne eivät ole päällekkäisiä (%@). - No comment provided by engineer. - Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated. Varmista, että WebRTC ICE -palvelinosoitteet ovat oikeassa muodossa, rivieroteltuina ja että ne eivät ole päällekkäisiä. No comment provided by engineer. - - Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?* - Monet ihmiset kysyivät: *Jos SimpleX:llä ei ole käyttäjätunnuksia, miten se voi toimittaa viestejä?* - No comment provided by engineer. - Mark deleted for everyone Merkitse poistetuksi kaikilta @@ -4180,6 +4272,10 @@ This is your link for group %@! More reliable network connection. No comment provided by engineer. + + More reliable notifications + No comment provided by engineer. + Most likely this connection is deleted. Todennäköisesti tämä yhteys on poistettu. @@ -4214,6 +4310,10 @@ This is your link for group %@! Network connection No comment provided by engineer. + + Network decentralization + No comment provided by engineer. + Network issues - message expired after many attempts to send it. snd error text @@ -4222,6 +4322,10 @@ This is your link for group %@! Network management No comment provided by engineer. + + Network operator + No comment provided by engineer. + Network settings Verkkoasetukset @@ -4277,6 +4381,10 @@ This is your link for group %@! Uusi näyttönimi No comment provided by engineer. + + New events + notification + New in %@ Uutta %@ @@ -4301,6 +4409,10 @@ This is your link for group %@! Uusi tunnuslause… No comment provided by engineer. + + New server + No comment provided by engineer. + No Ei @@ -4354,6 +4466,14 @@ This is your link for group %@! No info, try to reload No comment provided by engineer. + + No media & file servers. + servers error + + + No message servers. + servers error + No network connection No comment provided by engineer. @@ -4371,11 +4491,37 @@ This is your link for group %@! Ei lupaa ääniviestin tallentamiseen No comment provided by engineer. + + No push server + Paikallinen + No comment provided by engineer. + No received or sent files Ei vastaanotettuja tai lähetettyjä tiedostoja No comment provided by engineer. + + No servers for private message routing. + servers error + + + No servers to receive files. + servers error + + + No servers to receive messages. + servers error + + + No servers to send files. + servers error + + + No user identifiers. + Ensimmäinen alusta ilman käyttäjätunnisteita – suunniteltu yksityiseksi. + No comment provided by engineer. + Not compatible! No comment provided by engineer. @@ -4398,6 +4544,10 @@ This is your link for group %@! Ilmoitukset on poistettu käytöstä! No comment provided by engineer. + + Notifications privacy + No comment provided by engineer. + Now admins can: - delete members' messages. @@ -4455,8 +4605,8 @@ Edellyttää VPN:n sallimista. Onion-isäntiä ei käytetä. No comment provided by engineer. - - Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**. + + Only client devices store user profiles, contacts, groups, and messages. Vain asiakaslaitteet tallentavat käyttäjäprofiileja, yhteystietoja, ryhmiä ja viestejä, jotka on lähetetty **kaksinkertaisella päästä päähän -salauksella**. No comment provided by engineer. @@ -4538,6 +4688,10 @@ Edellyttää VPN:n sallimista. Avaa Asetukset No comment provided by engineer. + + Open changes + No comment provided by engineer. + Open chat Avaa keskustelu @@ -4548,6 +4702,10 @@ Edellyttää VPN:n sallimista. Avaa keskustelukonsoli authentication reason + + Open conditions + No comment provided by engineer. + Open group No comment provided by engineer. @@ -4556,24 +4714,18 @@ Edellyttää VPN:n sallimista. Open migration to another device authentication reason - - Open server settings - No comment provided by engineer. - - - Open user profiles - Avaa käyttäjäprofiilit - authentication reason - - - Open-source protocol and code – anybody can run the servers. - Avoimen lähdekoodin protokolla ja koodi - kuka tahansa voi käyttää palvelimia. - No comment provided by engineer. - Opening app… No comment provided by engineer. + + Operator + No comment provided by engineer. + + + Operator server + alert title + Or paste archive link No comment provided by engineer. @@ -4590,12 +4742,12 @@ Edellyttää VPN:n sallimista. Or show this code No comment provided by engineer. - - Other + + Or to share privately No comment provided by engineer. - - Other %@ servers + + Other No comment provided by engineer. @@ -4672,13 +4824,8 @@ Edellyttää VPN:n sallimista. Pending No comment provided by engineer. - - People can connect to you only via the links you share. - Ihmiset voivat ottaa sinuun yhteyttä vain jakamiesi linkkien kautta. - No comment provided by engineer. - - - Periodically + + Periodic Ajoittain No comment provided by engineer. @@ -4792,16 +4939,15 @@ Error: %@ Säilytä viimeinen viestiluonnos liitteineen. No comment provided by engineer. - - Preset server - Esiasetettu palvelin - No comment provided by engineer. - Preset server address Esiasetettu palvelimen osoite No comment provided by engineer. + + Preset servers + No comment provided by engineer. + Preview Esikatselu @@ -4872,7 +5018,7 @@ Error: %@ Profile update will be sent to your contacts. Profiilipäivitys lähetetään kontakteillesi. - No comment provided by engineer. + alert message Prohibit audio/video calls. @@ -4959,6 +5105,10 @@ Enable in *Network & servers* settings. Proxy requires password No comment provided by engineer. + + Push Notifications + No comment provided by engineer. + Push notifications Push-ilmoitukset @@ -4996,25 +5146,20 @@ Enable in *Network & servers* settings. Lue lisää No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address). - Lue lisää [Käyttöoppaasta](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address). - No comment provided by engineer. - Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). No comment provided by engineer. + + Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). + Lue lisää [Käyttöoppaasta](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). + No comment provided by engineer. + Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends). Lue lisää [Käyttöoppaasta](https://simplex.chat/docs/guide/readme.html#connect-to-friends). No comment provided by engineer. - - Read more in our GitHub repository. - Lue lisää GitHub-tietovarastostamme. - No comment provided by engineer. - Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme). Lue lisää [GitHub-arkistosta](https://github.com/simplex-chat/simplex-chat#readme). @@ -5307,6 +5452,14 @@ Enable in *Network & servers* settings. Paljasta chat item action + + Review conditions + No comment provided by engineer. + + + Review later + No comment provided by engineer. + Revoke Peruuta @@ -5348,6 +5501,14 @@ Enable in *Network & servers* settings. Safer groups No comment provided by engineer. + + Same conditions will apply to operator **%@**. + No comment provided by engineer. + + + Same conditions will apply to operator(s): **%@**. + No comment provided by engineer. + Save Tallenna @@ -5416,7 +5577,7 @@ Enable in *Network & servers* settings. Save servers? Tallenna palvelimet? - No comment provided by engineer. + alert title Save welcome message? @@ -5608,11 +5769,6 @@ Enable in *Network & servers* settings. Lähetys ilmoitukset No comment provided by engineer. - - Send notifications: - Lähetys ilmoitukset: - No comment provided by engineer. - Send questions and ideas Lähetä kysymyksiä ja ideoita @@ -5731,6 +5887,10 @@ Enable in *Network & servers* settings. Server No comment provided by engineer. + + Server added to operator %@. + alert message + Server address No comment provided by engineer. @@ -5743,6 +5903,18 @@ Enable in *Network & servers* settings. Server address is incompatible with network settings: %@. No comment provided by engineer. + + Server operator changed. + alert title + + + Server operators + No comment provided by engineer. + + + Server protocol changed. + alert title + Server requires authorization to create queues, check password Palvelin vaatii valtuutuksen jonojen luomiseen, tarkista salasana @@ -5851,22 +6023,35 @@ Enable in *Network & servers* settings. Share Jaa - chat item action + alert action + chat item action Share 1-time link Jaa kertakäyttölinkki No comment provided by engineer. + + Share 1-time link with a friend + No comment provided by engineer. + + + Share SimpleX address on social media. + No comment provided by engineer. + Share address Jaa osoite No comment provided by engineer. + + Share address publicly + No comment provided by engineer. + Share address with contacts? Jaa osoite kontakteille? - No comment provided by engineer. + alert title Share from other apps. @@ -5974,6 +6159,14 @@ Enable in *Network & servers* settings. SimpleX-osoite No comment provided by engineer. + + SimpleX address and 1-time links are safe to share via any messenger. + No comment provided by engineer. + + + SimpleX address or 1-time link? + No comment provided by engineer. + SimpleX contact address SimpleX-yhteystiedot @@ -6055,6 +6248,11 @@ Enable in *Network & servers* settings. Some non-fatal errors occurred during import: No comment provided by engineer. + + Some servers failed the test: +%@ + alert message + Somebody Joku @@ -6133,12 +6331,12 @@ Enable in *Network & servers* settings. Stop sharing Lopeta jakaminen - No comment provided by engineer. + alert action Stop sharing address? Lopeta osoitteen jakaminen? - No comment provided by engineer. + alert title Stopping chat @@ -6275,7 +6473,7 @@ Enable in *Network & servers* settings. Tests failed! Testit epäonnistuivat! - No comment provided by engineer. + alert title Thank you for installing SimpleX Chat! @@ -6292,11 +6490,6 @@ Enable in *Network & servers* settings. Kiitokset käyttäjille – osallistu Weblaten kautta! No comment provided by engineer. - - The 1st platform without any user identifiers – private by design. - Ensimmäinen alusta ilman käyttäjätunnisteita – suunniteltu yksityiseksi. - No comment provided by engineer. - The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised. @@ -6309,6 +6502,10 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.Sovellus voi ilmoittaa sinulle, kun saat viestejä tai yhteydenottopyyntöjä - avaa asetukset ottaaksesi ne käyttöön. No comment provided by engineer. + + The app protects your privacy by using different operators in each conversation. + No comment provided by engineer. + The app will ask to confirm downloads from unknown file servers (except .onion). No comment provided by engineer. @@ -6322,6 +6519,10 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.The code you scanned is not a SimpleX link QR code. No comment provided by engineer. + + The connection reached the limit of undelivered messages, your contact may be offline. + No comment provided by engineer. + The connection you accepted will be cancelled! Hyväksymäsi yhteys peruuntuu! @@ -6342,6 +6543,11 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.Salaus toimii ja uutta salaussopimusta ei tarvita. Tämä voi johtaa yhteysvirheisiin! No comment provided by engineer. + + The future of messaging + Seuraavan sukupolven yksityisviestit + No comment provided by engineer. + The hash of the previous message is different. Edellisen viestin tarkiste on erilainen. @@ -6365,11 +6571,6 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.The messages will be marked as moderated for all members. No comment provided by engineer. - - The next generation of private messaging - Seuraavan sukupolven yksityisviestit - No comment provided by engineer. - The old database was not removed during the migration, it can be deleted. Vanhaa tietokantaa ei poistettu siirron aikana, se voidaan kuitenkin poistaa. @@ -6380,6 +6581,10 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.Profiili jaetaan vain kontaktiesi kanssa. No comment provided by engineer. + + The second preset operator in the app! + No comment provided by engineer. + The second tick we missed! ✅ Toinen kuittaus, joka uupui! ✅ @@ -6395,6 +6600,10 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.Palvelimet nykyisen keskusteluprofiilisi uusille yhteyksille **%@**. No comment provided by engineer. + + The servers for new files of your current chat profile **%@**. + No comment provided by engineer. + The text you pasted is not a SimpleX link. No comment provided by engineer. @@ -6407,6 +6616,10 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.Themes No comment provided by engineer. + + These conditions will also apply for: **%@**. + No comment provided by engineer. + These settings are for your current profile **%@**. Nämä asetukset koskevat nykyistä profiiliasi **%@**. @@ -6498,9 +6711,8 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.Uuden yhteyden luominen No comment provided by engineer. - - To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts. - Yksityisyyden suojaamiseksi kaikkien muiden alustojen käyttämien käyttäjätunnusten sijaan SimpleX käyttää viestijonojen tunnisteita, jotka ovat kaikille kontakteille erillisiä. + + To protect against your link being replaced, you can compare contact security codes. No comment provided by engineer. @@ -6519,6 +6731,15 @@ You will be prompted to complete authentication before this feature is enabled.< Sinua kehotetaan suorittamaan todennus loppuun, ennen kuin tämä ominaisuus otetaan käyttöön. No comment provided by engineer. + + To protect your privacy, SimpleX uses separate IDs for each of your contacts. + Yksityisyyden suojaamiseksi kaikkien muiden alustojen käyttämien käyttäjätunnusten sijaan SimpleX käyttää viestijonojen tunnisteita, jotka ovat kaikille kontakteille erillisiä. + No comment provided by engineer. + + + To receive + No comment provided by engineer. + To record speech please grant permission to use Microphone. No comment provided by engineer. @@ -6537,11 +6758,19 @@ Sinua kehotetaan suorittamaan todennus loppuun, ennen kuin tämä ominaisuus ote Voit paljastaa piilotetun profiilisi syöttämällä koko salasanan hakukenttään **Keskusteluprofiilisi** -sivulla. No comment provided by engineer. + + To send + No comment provided by engineer. + To support instant push notifications the chat database has to be migrated. Keskustelujen-tietokanta on siirrettävä välittömien push-ilmoitusten tukemiseksi. No comment provided by engineer. + + To use the servers of **%@**, accept conditions of use. + No comment provided by engineer. + To verify end-to-end encryption with your contact compare (or scan) the code on your devices. Voit tarkistaa päästä päähän -salauksen kontaktisi kanssa vertaamalla (tai skannaamalla) laitteidenne koodia. @@ -6621,6 +6850,10 @@ Sinua kehotetaan suorittamaan todennus loppuun, ennen kuin tämä ominaisuus ote Unblock member? No comment provided by engineer. + + Undelivered messages + No comment provided by engineer. + Unexpected migration state Odottamaton siirtotila @@ -6768,6 +7001,10 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Uploading archive No comment provided by engineer. + + Use %@ + No comment provided by engineer. + Use .onion hosts Käytä .onion-isäntiä @@ -6792,6 +7029,14 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Käytä nykyistä profiilia No comment provided by engineer. + + Use for files + No comment provided by engineer. + + + Use for messages + No comment provided by engineer. + Use for new connections Käytä uusiin yhteyksiin @@ -6828,6 +7073,10 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Käytä palvelinta No comment provided by engineer. + + Use servers + No comment provided by engineer. + Use the app while in the call. No comment provided by engineer. @@ -6908,11 +7157,19 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Videot ja tiedostot 1 Gt asti No comment provided by engineer. + + View conditions + No comment provided by engineer. + View security code Näytä turvakoodi No comment provided by engineer. + + View updated conditions + No comment provided by engineer. + Visible history chat feature @@ -7015,9 +7272,8 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja When connecting audio and video calls. No comment provided by engineer. - - When people request to connect, you can accept or reject it. - Kun ihmiset pyytävät yhteyden muodostamista, voit hyväksyä tai hylätä sen. + + When more than one operator is enabled, none of them has metadata to learn who communicates with whom. No comment provided by engineer. @@ -7156,6 +7412,18 @@ Repeat join request? You can change it in Appearance settings. No comment provided by engineer. + + You can configure operators in Network & servers settings. + No comment provided by engineer. + + + You can configure servers via settings. + No comment provided by engineer. + + + You can create it in user picker. + No comment provided by engineer. + You can create it later Voit luoda sen myöhemmin @@ -7193,6 +7461,10 @@ Repeat join request? You can send messages to %@ from Archived contacts. No comment provided by engineer. + + You can set connection name, to remember who the link was shared with. + No comment provided by engineer. + You can set lock screen notification preview via settings. Voit määrittää lukitusnäytön ilmoituksen esikatselun asetuksista. @@ -7208,11 +7480,6 @@ Repeat join request? Voit jakaa tämän osoitteen kontaktiesi kanssa, jotta ne voivat muodostaa yhteyden **%@** kanssa. No comment provided by engineer. - - You can share your address as a link or QR code - anybody can connect to you. - Voit jakaa osoitteesi linkkinä tai QR-koodina - kuka tahansa voi muodostaa yhteyden sinuun. - No comment provided by engineer. - You can start chat via app Settings / Database or by restarting the app Voit aloittaa keskustelun sovelluksen Asetukset / Tietokanta kautta tai käynnistämällä sovelluksen uudelleen @@ -7234,23 +7501,23 @@ Repeat join request? You can view invitation link again in connection details. - No comment provided by engineer. + alert message You can't send messages! Et voi lähettää viestejä! No comment provided by engineer. - - You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them. - Sinä hallitset, minkä palvelim(i)en kautta **viestit vastaanotetaan**, kontaktisi - palvelimet, joita käytät viestien lähettämiseen niille. - No comment provided by engineer. - You could not be verified; please try again. Sinua ei voitu todentaa; yritä uudelleen. No comment provided by engineer. + + You decide who can connect. + Kimin bağlanabileceğine siz karar verirsiniz. + No comment provided by engineer. + You have already requested connection via this address! No comment provided by engineer. @@ -7365,11 +7632,6 @@ Repeat connection request? Käytät tässä ryhmässä incognito-profiilia. Kontaktien kutsuminen ei ole sallittua, jotta pääprofiilisi ei tule jaetuksi No comment provided by engineer. - - Your %@ servers - %@-palvelimesi - No comment provided by engineer. - Your ICE servers ICE-palvelimesi @@ -7385,11 +7647,6 @@ Repeat connection request? SimpleX-osoitteesi No comment provided by engineer. - - Your XFTP servers - XFTP-palvelimesi - No comment provided by engineer. - Your calls Puhelusi @@ -7485,16 +7742,15 @@ Repeat connection request? Satunnainen profiilisi No comment provided by engineer. - - Your server - Palvelimesi - No comment provided by engineer. - Your server address Palvelimesi osoite No comment provided by engineer. + + Your servers + No comment provided by engineer. + Your settings Asetuksesi @@ -7900,6 +8156,10 @@ Repeat connection request? expired No comment provided by engineer. + + for better metadata privacy. + No comment provided by engineer. + forwarded No comment provided by engineer. @@ -8487,6 +8747,33 @@ last received msg: %2$@ + +
+ +
+ + + %d new events + notification body + + + From: %@ + notification body + + + New events + notification + + + New messages + notification + + + New messages in %d chats + notification body + + +
diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/en.lproj/Localizable.strings index cf485752ea..cb83427195 100644 --- a/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/en.lproj/Localizable.strings +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/en.lproj/Localizable.strings @@ -1,9 +1,6 @@ /* No comment provided by engineer. */ "_italic_" = "\\_italic_"; -/* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact."; - /* No comment provided by engineer. */ "*bold*" = "\\*bold*"; @@ -27,4 +24,3 @@ /* No comment provided by engineer. */ "No group!" = "Group not found!"; - diff --git a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff index b672bebc8c..3ed363cb10 100644 --- a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff @@ -12,21 +12,6 @@ No comment provided by engineer. - - - - No comment provided by engineer. - - - - - No comment provided by engineer. - - - - - No comment provided by engineer. - ( ( @@ -127,6 +112,14 @@ %@ est vérifié·e No comment provided by engineer. + + %@ server + No comment provided by engineer. + + + %@ servers + No comment provided by engineer. + %@ uploaded %@ envoyé @@ -346,14 +339,9 @@ ) No comment provided by engineer. - - **Add contact**: to create a new invitation link, or connect via a link you received. - **Ajouter un contact** : pour créer un nouveau lien d'invitation ou vous connecter via un lien que vous avez reçu. - No comment provided by engineer. - - - **Add new contact**: to create your one-time QR Code or link for your contact. - **Ajouter un nouveau contact** : pour créer un lien ou code QR unique pour votre contact. + + **Create 1-time link**: to create and share a new invitation link. + **Ajouter un contact** : pour créer un nouveau lien d'invitation. No comment provided by engineer. @@ -361,13 +349,13 @@ **Créer un groupe** : pour créer un nouveau groupe. No comment provided by engineer. - - **More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have. + + **More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata. **Vie privée** : vérification de nouveaux messages toute les 20 minutes. Le token de l'appareil est partagé avec le serveur SimpleX, mais pas le nombre de messages ou de contacts. No comment provided by engineer. - - **Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app). + + **Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app. **Confidentiel** : ne pas utiliser le serveur de notifications SimpleX, vérification de nouveaux messages periodiquement en arrière plan (dépend de l'utilisation de l'app). No comment provided by engineer. @@ -381,11 +369,15 @@ **Veuillez noter** : vous NE pourrez PAS récupérer ou modifier votre phrase secrète si vous la perdez. No comment provided by engineer. - - **Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from. + + **Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from. **Recommandé** : le token de l'appareil et les notifications sont envoyés au serveur de notifications SimpleX, mais pas le contenu du message, sa taille ou son auteur. No comment provided by engineer. + + **Scan / Paste link**: to connect via a link you received. + No comment provided by engineer. + **Warning**: Instant push notifications require passphrase saved in Keychain. **Avertissement** : les notifications push instantanées nécessitent une phrase secrète enregistrée dans la keychain. @@ -492,6 +484,14 @@ 1 semaine time interval + + 1-time link + No comment provided by engineer. + + + 1-time link can be used *with one contact only* - share in person or via any messenger. + No comment provided by engineer. + 5 minutes 5 minutes @@ -561,21 +561,11 @@ Abandonner le changement d'adresse ? No comment provided by engineer. - - About SimpleX - À propos de SimpleX - No comment provided by engineer. - About SimpleX Chat À propos de SimpleX Chat No comment provided by engineer. - - About SimpleX address - À propos de l'adresse SimpleX - No comment provided by engineer. - Accent Principale @@ -588,6 +578,10 @@ accept incoming call via notification swipe action + + Accept conditions + No comment provided by engineer. + Accept connection request? Accepter la demande de connexion ? @@ -604,6 +598,10 @@ accept contact request via notification swipe action + + Accepted conditions + No comment provided by engineer. + Acknowledged Reçu avec accusé de réception @@ -624,16 +622,6 @@ Ajoutez une adresse à votre profil, afin que vos contacts puissent la partager avec d'autres personnes. La mise à jour du profil sera envoyée à vos contacts. No comment provided by engineer. - - Add contact - Ajouter le contact - No comment provided by engineer. - - - Add preset servers - Ajouter des serveurs prédéfinis - No comment provided by engineer. - Add profile Ajouter un profil @@ -659,6 +647,14 @@ Ajouter un message d'accueil No comment provided by engineer. + + Added media & file servers + No comment provided by engineer. + + + Added message servers + No comment provided by engineer. + Additional accent Accent additionnel @@ -684,6 +680,14 @@ Le changement d'adresse sera annulé. L'ancienne adresse de réception sera utilisée. No comment provided by engineer. + + Address or 1-time link? + No comment provided by engineer. + + + Address settings + No comment provided by engineer. + Admins can block a member for all. Les admins peuvent bloquer un membre pour tous. @@ -729,6 +733,10 @@ Tous les membres du groupe resteront connectés. No comment provided by engineer. + + All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages. + No comment provided by engineer. + All messages will be deleted - this cannot be undone! Tous les messages seront supprimés - il n'est pas possible de revenir en arrière ! @@ -909,6 +917,11 @@ Répondre à l'appel No comment provided by engineer. + + Anybody can host servers. + N'importe qui peut heberger un serveur. + No comment provided by engineer. + App build: %@ Build de l'app : %@ @@ -1245,7 +1258,8 @@ Cancel Annuler - alert button + alert action + alert button Cancel migration @@ -1328,6 +1342,10 @@ authentication reason set passcode view + + Change user profiles + authentication reason + Chat archive Archives du chat @@ -1412,10 +1430,18 @@ Discussions No comment provided by engineer. + + Check messages every 20 min. + No comment provided by engineer. + + + Check messages when allowed. + No comment provided by engineer. + Check server address and try again. Vérifiez l'adresse du serveur et réessayez. - No comment provided by engineer. + alert title Chinese and Spanish interface @@ -1502,16 +1528,47 @@ Complétées No comment provided by engineer. + + Conditions accepted on: %@. + No comment provided by engineer. + + + Conditions are accepted for the operator(s): **%@**. + No comment provided by engineer. + + + Conditions are already accepted for following operator(s): **%@**. + No comment provided by engineer. + + + Conditions of use + No comment provided by engineer. + + + Conditions will be accepted for enabled operators after 30 days. + No comment provided by engineer. + + + Conditions will be accepted for operator(s): **%@**. + No comment provided by engineer. + + + Conditions will be accepted for the operator(s): **%@**. + No comment provided by engineer. + + + Conditions will be accepted on: %@. + No comment provided by engineer. + + + Conditions will be automatically accepted for enabled operators on: %@. + No comment provided by engineer. + Configure ICE servers Configurer les serveurs ICE No comment provided by engineer. - - Configured %@ servers - %@ serveurs configurés - No comment provided by engineer. - Confirm Confirmer @@ -1701,6 +1758,10 @@ Il s'agit de votre propre lien unique ! Demande de connexion envoyée ! No comment provided by engineer. + + Connection security + No comment provided by engineer. + Connection terminated Connexion terminée @@ -1815,6 +1876,10 @@ Il s'agit de votre propre lien unique ! Créer No comment provided by engineer. + + Create 1-time link + No comment provided by engineer. + Create SimpleX address Créer une adresse SimpleX @@ -1825,11 +1890,6 @@ Il s'agit de votre propre lien unique ! Création de groupes via un profil aléatoire. No comment provided by engineer. - - Create an address to let people connect with you. - Vous pouvez créer une adresse pour permettre aux autres utilisateurs de vous contacter. - No comment provided by engineer. - Create file Créer un fichier @@ -1910,6 +1970,10 @@ Il s'agit de votre propre lien unique ! Code d'accès actuel No comment provided by engineer. + + Current conditions text couldn't be loaded, you can review conditions via this link: + No comment provided by engineer. + Current passphrase… Phrase secrète actuelle… @@ -2065,7 +2129,8 @@ Il s'agit de votre propre lien unique ! Delete Supprimer - chat item action + alert action + chat item action swipe action @@ -2282,6 +2347,10 @@ Il s'agit de votre propre lien unique ! Erreurs de suppression No comment provided by engineer. + + Delivered even when Apple drops them. + No comment provided by engineer. + Delivery Distribution @@ -2561,6 +2630,10 @@ Il s'agit de votre propre lien unique ! Durée No comment provided by engineer. + + E2E encrypted notifications. + No comment provided by engineer. + Edit Modifier @@ -2581,6 +2654,10 @@ Il s'agit de votre propre lien unique ! Activer (conserver les remplacements) No comment provided by engineer. + + Enable Flux + No comment provided by engineer. + Enable SimpleX Lock Activer SimpleX Lock @@ -2786,6 +2863,10 @@ Il s'agit de votre propre lien unique ! Erreur lors de l'annulation du changement d'adresse No comment provided by engineer. + + Error accepting conditions + alert title + Error accepting contact request Erreur de validation de la demande de contact @@ -2801,6 +2882,10 @@ Il s'agit de votre propre lien unique ! Erreur lors de l'ajout de membre·s No comment provided by engineer. + + Error adding server + alert title + Error changing address Erreur de changement d'adresse @@ -2939,10 +3024,9 @@ Il s'agit de votre propre lien unique ! Erreur lors de la liaison avec le groupe No comment provided by engineer. - - Error loading %@ servers - Erreur lors du chargement des serveurs %@ - No comment provided by engineer. + + Error loading servers + alert title Error migrating settings @@ -2978,11 +3062,6 @@ Il s'agit de votre propre lien unique ! Erreur de réinitialisation des statistiques No comment provided by engineer. - - Error saving %@ servers - Erreur lors de la sauvegarde des serveurs %@ - No comment provided by engineer. - Error saving ICE servers Erreur lors de la sauvegarde des serveurs ICE @@ -3003,6 +3082,10 @@ Il s'agit de votre propre lien unique ! Erreur lors de l'enregistrement de la phrase de passe dans la keychain No comment provided by engineer. + + Error saving servers + alert title + Error saving settings Erreur lors de l'enregistrement des paramètres @@ -3072,6 +3155,10 @@ Il s'agit de votre propre lien unique ! Erreur lors de la mise à jour du message No comment provided by engineer. + + Error updating server + alert title + Error updating settings Erreur lors de la mise à jour des paramètres @@ -3117,6 +3204,10 @@ Il s'agit de votre propre lien unique ! Erreurs No comment provided by engineer. + + Errors in servers configuration. + servers error + Even when disabled in the conversation. Même s'il est désactivé dans la conversation. @@ -3317,11 +3408,27 @@ Il s'agit de votre propre lien unique ! Correction non prise en charge par un membre du groupe No comment provided by engineer. + + For chat profile %@: + servers error + For console Pour la console No comment provided by engineer. + + For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server. + No comment provided by engineer. + + + For private routing + No comment provided by engineer. + + + For social media + No comment provided by engineer. + Forward Transférer @@ -3626,9 +3733,12 @@ Erreur : %2$@ Comment SimpleX fonctionne No comment provided by engineer. - - How it works - Comment ça fonctionne + + How it affects privacy + No comment provided by engineer. + + + How it helps privacy No comment provided by engineer. @@ -3700,8 +3810,8 @@ Erreur : %2$@ Immédiatement No comment provided by engineer. - - Immune to spam and abuse + + Immune to spam Protégé du spam et des abus No comment provided by engineer. @@ -3840,6 +3950,11 @@ More improvements are coming soon! Installer [SimpleX Chat pour terminal](https://github.com/simplex-chat/simplex-chat) No comment provided by engineer. + + Instant + Instantané + No comment provided by engineer. + Instant push notifications will be hidden! @@ -3847,11 +3962,6 @@ More improvements are coming soon! No comment provided by engineer. - - Instantly - Instantané - No comment provided by engineer. - Interface Interface @@ -3900,7 +4010,7 @@ More improvements are coming soon! Invalid server address! Adresse de serveur invalide ! - No comment provided by engineer. + alert title Invalid status @@ -4028,7 +4138,7 @@ Voici votre lien pour le groupe %@ ! Keep Conserver - No comment provided by engineer. + alert action Keep conversation @@ -4043,7 +4153,7 @@ Voici votre lien pour le groupe %@ ! Keep unused invitation? Conserver l'invitation inutilisée ? - No comment provided by engineer. + alert title Keep your connections @@ -4130,11 +4240,6 @@ Voici votre lien pour le groupe %@ ! Messages dynamiques No comment provided by engineer. - - Local - Local - No comment provided by engineer. - Local name Nom local @@ -4155,11 +4260,6 @@ Voici votre lien pour le groupe %@ ! Mode de verrouillage No comment provided by engineer. - - Make a private connection - Établir une connexion privée - No comment provided by engineer. - Make one message disappear Rendre un message éphémère @@ -4170,21 +4270,11 @@ Voici votre lien pour le groupe %@ ! Rendre un profil privé ! No comment provided by engineer. - - Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@). - Assurez-vous que les adresses des serveurs %@ sont au bon format et ne sont pas dupliquées, un par ligne (%@). - No comment provided by engineer. - Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated. Assurez-vous que les adresses des serveurs WebRTC ICE sont au bon format et ne sont pas dupliquées, un par ligne. No comment provided by engineer. - - Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?* - Beaucoup se demandent : *si SimpleX n'a pas d'identifiant d'utilisateur, comment peut-il délivrer des messages ?* - No comment provided by engineer. - Mark deleted for everyone Marquer comme supprimé pour tout le monde @@ -4463,6 +4553,10 @@ Voici votre lien pour le groupe %@ ! Connexion réseau plus fiable. No comment provided by engineer. + + More reliable notifications + No comment provided by engineer. + Most likely this connection is deleted. Connexion probablement supprimée. @@ -4498,6 +4592,10 @@ Voici votre lien pour le groupe %@ ! Connexion au réseau No comment provided by engineer. + + Network decentralization + No comment provided by engineer. + Network issues - message expired after many attempts to send it. Problèmes de réseau - le message a expiré après plusieurs tentatives d'envoi. @@ -4508,6 +4606,10 @@ Voici votre lien pour le groupe %@ ! Gestion du réseau No comment provided by engineer. + + Network operator + No comment provided by engineer. + Network settings Paramètres réseau @@ -4566,6 +4668,10 @@ Voici votre lien pour le groupe %@ ! Nouveau nom d'affichage No comment provided by engineer. + + New events + notification + New in %@ Nouveautés de la %@ @@ -4591,6 +4697,10 @@ Voici votre lien pour le groupe %@ ! Nouvelle phrase secrète… No comment provided by engineer. + + New server + No comment provided by engineer. + No Non @@ -4646,6 +4756,14 @@ Voici votre lien pour le groupe %@ ! Pas d'info, essayez de recharger No comment provided by engineer. + + No media & file servers. + servers error + + + No message servers. + servers error + No network connection Pas de connexion au réseau @@ -4664,11 +4782,37 @@ Voici votre lien pour le groupe %@ ! Pas l'autorisation d'enregistrer un message vocal No comment provided by engineer. + + No push server + No push server + No comment provided by engineer. + No received or sent files Aucun fichier reçu ou envoyé No comment provided by engineer. + + No servers for private message routing. + servers error + + + No servers to receive files. + servers error + + + No servers to receive messages. + servers error + + + No servers to send files. + servers error + + + No user identifiers. + Aucun identifiant d'utilisateur. + No comment provided by engineer. + Not compatible! Non compatible ! @@ -4693,6 +4837,10 @@ Voici votre lien pour le groupe %@ ! Les notifications sont désactivées ! No comment provided by engineer. + + Notifications privacy + No comment provided by engineer. + Now admins can: - delete members' messages. @@ -4751,8 +4899,8 @@ Nécessite l'activation d'un VPN. Les hôtes .onion ne seront pas utilisés. No comment provided by engineer. - - Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**. + + Only client devices store user profiles, contacts, groups, and messages. Seuls les appareils clients stockent les profils des utilisateurs, les contacts, les groupes et les messages envoyés avec un **chiffrement de bout en bout à deux couches**. No comment provided by engineer. @@ -4836,6 +4984,10 @@ Nécessite l'activation d'un VPN. Ouvrir les Paramètres No comment provided by engineer. + + Open changes + No comment provided by engineer. + Open chat Ouvrir le chat @@ -4846,6 +4998,10 @@ Nécessite l'activation d'un VPN. Ouvrir la console du chat authentication reason + + Open conditions + No comment provided by engineer. + Open group Ouvrir le groupe @@ -4856,26 +5012,19 @@ Nécessite l'activation d'un VPN. Ouvrir le transfert vers un autre appareil authentication reason - - Open server settings - Ouvrir les paramètres du serveur - No comment provided by engineer. - - - Open user profiles - Ouvrir les profils d'utilisateurs - authentication reason - - - Open-source protocol and code – anybody can run the servers. - Protocole et code open-source – n'importe qui peut heberger un serveur. - No comment provided by engineer. - Opening app… Ouverture de l'app… No comment provided by engineer. + + Operator + No comment provided by engineer. + + + Operator server + alert title + Or paste archive link Ou coller le lien de l'archive @@ -4896,16 +5045,15 @@ Nécessite l'activation d'un VPN. Ou présenter ce code No comment provided by engineer. + + Or to share privately + No comment provided by engineer. + Other Autres No comment provided by engineer. - - Other %@ servers - Autres serveurs %@ - No comment provided by engineer. - Other file errors: %@ @@ -4985,13 +5133,8 @@ Nécessite l'activation d'un VPN. En attente No comment provided by engineer. - - People can connect to you only via the links you share. - On ne peut se connecter à vous qu’avec les liens que vous partagez. - No comment provided by engineer. - - - Periodically + + Periodic Périodique No comment provided by engineer. @@ -5113,16 +5256,15 @@ Erreur : %@ Conserver le brouillon du dernier message, avec les pièces jointes. No comment provided by engineer. - - Preset server - Serveur prédéfini - No comment provided by engineer. - Preset server address Adresse du serveur prédéfinie No comment provided by engineer. + + Preset servers + No comment provided by engineer. + Preview Aperçu @@ -5201,7 +5343,7 @@ Erreur : %@ Profile update will be sent to your contacts. La mise à jour du profil sera envoyée à vos contacts. - No comment provided by engineer. + alert message Prohibit audio/video calls. @@ -5294,6 +5436,10 @@ Activez-le dans les paramètres *Réseau et serveurs*. Proxy requires password No comment provided by engineer. + + Push Notifications + No comment provided by engineer. + Push notifications Notifications push @@ -5334,26 +5480,21 @@ Activez-le dans les paramètres *Réseau et serveurs*. En savoir plus No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address). - Pour en savoir plus, consultez le [Guide de l'utilisateur](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address). - No comment provided by engineer. - Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). Pour en savoir plus, consultez le [Guide de l'utilisateur](https ://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). No comment provided by engineer. + + Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). + Pour en savoir plus, consultez le [Guide de l'utilisateur](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). + No comment provided by engineer. + Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends). Pour en savoir plus, consultez le [Guide de l'utilisateur](https://simplex.chat/docs/guide/readme.html#connect-to-friends). No comment provided by engineer. - - Read more in our GitHub repository. - Plus d'informations sur notre GitHub. - No comment provided by engineer. - Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme). Pour en savoir plus, consultez notre [dépôt GitHub](https://github.com/simplex-chat/simplex-chat#readme). @@ -5669,6 +5810,14 @@ Activez-le dans les paramètres *Réseau et serveurs*. Révéler chat item action + + Review conditions + No comment provided by engineer. + + + Review later + No comment provided by engineer. + Revoke Révoquer @@ -5713,6 +5862,14 @@ Activez-le dans les paramètres *Réseau et serveurs*. Groupes plus sûrs No comment provided by engineer. + + Same conditions will apply to operator **%@**. + No comment provided by engineer. + + + Same conditions will apply to operator(s): **%@**. + No comment provided by engineer. + Save Enregistrer @@ -5782,7 +5939,7 @@ Activez-le dans les paramètres *Réseau et serveurs*. Save servers? Enregistrer les serveurs ? - No comment provided by engineer. + alert title Save welcome message? @@ -5991,11 +6148,6 @@ Activez-le dans les paramètres *Réseau et serveurs*. Envoi de notifications No comment provided by engineer. - - Send notifications: - Envoi de notifications : - No comment provided by engineer. - Send questions and ideas Envoyez vos questions et idées @@ -6120,6 +6272,10 @@ Activez-le dans les paramètres *Réseau et serveurs*. Server No comment provided by engineer. + + Server added to operator %@. + alert message + Server address Adresse du serveur @@ -6135,6 +6291,18 @@ Activez-le dans les paramètres *Réseau et serveurs*. L'adresse du serveur est incompatible avec les paramètres réseau : %@. No comment provided by engineer. + + Server operator changed. + alert title + + + Server operators + No comment provided by engineer. + + + Server protocol changed. + alert title + Server requires authorization to create queues, check password Le serveur requiert une autorisation pour créer des files d'attente, vérifiez le mot de passe @@ -6252,22 +6420,35 @@ Activez-le dans les paramètres *Réseau et serveurs*. Share Partager - chat item action + alert action + chat item action Share 1-time link Partager un lien unique No comment provided by engineer. + + Share 1-time link with a friend + No comment provided by engineer. + + + Share SimpleX address on social media. + No comment provided by engineer. + Share address Partager l'adresse No comment provided by engineer. + + Share address publicly + No comment provided by engineer. + Share address with contacts? Partager l'adresse avec vos contacts ? - No comment provided by engineer. + alert title Share from other apps. @@ -6383,6 +6564,14 @@ Activez-le dans les paramètres *Réseau et serveurs*. Adresse SimpleX No comment provided by engineer. + + SimpleX address and 1-time links are safe to share via any messenger. + No comment provided by engineer. + + + SimpleX address or 1-time link? + No comment provided by engineer. + SimpleX contact address Adresse de contact SimpleX @@ -6471,6 +6660,11 @@ Activez-le dans les paramètres *Réseau et serveurs*. L'importation a entraîné des erreurs non fatales : No comment provided by engineer. + + Some servers failed the test: +%@ + alert message + Somebody Quelqu'un @@ -6554,12 +6748,12 @@ Activez-le dans les paramètres *Réseau et serveurs*. Stop sharing Cesser le partage - No comment provided by engineer. + alert action Stop sharing address? Cesser le partage d'adresse ? - No comment provided by engineer. + alert title Stopping chat @@ -6706,7 +6900,7 @@ Activez-le dans les paramètres *Réseau et serveurs*. Tests failed! Échec des tests ! - No comment provided by engineer. + alert title Thank you for installing SimpleX Chat! @@ -6723,11 +6917,6 @@ Activez-le dans les paramètres *Réseau et serveurs*. Merci aux utilisateurs - contribuez via Weblate ! No comment provided by engineer. - - The 1st platform without any user identifiers – private by design. - La 1ère plateforme sans aucun identifiant d'utilisateur – privée par design. - No comment provided by engineer. - The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised. @@ -6740,6 +6929,10 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. L'application peut vous avertir lorsque vous recevez des messages ou des demandes de contact - veuillez ouvrir les paramètres pour les activer. No comment provided by engineer. + + The app protects your privacy by using different operators in each conversation. + No comment provided by engineer. + The app will ask to confirm downloads from unknown file servers (except .onion). L'application demandera de confirmer les téléchargements à partir de serveurs de fichiers inconnus (sauf .onion). @@ -6755,6 +6948,10 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. Le code scanné n'est pas un code QR de lien SimpleX. No comment provided by engineer. + + The connection reached the limit of undelivered messages, your contact may be offline. + No comment provided by engineer. + The connection you accepted will be cancelled! La connexion que vous avez acceptée sera annulée ! @@ -6775,6 +6972,11 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. Le chiffrement fonctionne et le nouvel accord de chiffrement n'est pas nécessaire. Cela peut provoquer des erreurs de connexion ! No comment provided by engineer. + + The future of messaging + La nouvelle génération de messagerie privée + No comment provided by engineer. + The hash of the previous message is different. Le hash du message précédent est différent. @@ -6800,11 +7002,6 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. Les messages seront marqués comme modérés pour tous les membres. No comment provided by engineer. - - The next generation of private messaging - La nouvelle génération de messagerie privée - No comment provided by engineer. - The old database was not removed during the migration, it can be deleted. L'ancienne base de données n'a pas été supprimée lors de la migration, elle peut être supprimée. @@ -6815,6 +7012,10 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. Le profil n'est partagé qu'avec vos contacts. No comment provided by engineer. + + The second preset operator in the app! + No comment provided by engineer. + The second tick we missed! ✅ Le deuxième coche que nous avons manqué ! ✅ @@ -6830,6 +7031,10 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. Les serveurs pour les nouvelles connexions de votre profil de chat actuel **%@**. No comment provided by engineer. + + The servers for new files of your current chat profile **%@**. + No comment provided by engineer. + The text you pasted is not a SimpleX link. Le texte collé n'est pas un lien SimpleX. @@ -6844,6 +7049,10 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. Thèmes No comment provided by engineer. + + These conditions will also apply for: **%@**. + No comment provided by engineer. + These settings are for your current profile **%@**. Ces paramètres s'appliquent à votre profil actuel **%@**. @@ -6944,9 +7153,8 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. Pour établir une nouvelle connexion No comment provided by engineer. - - To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts. - Pour protéger votre vie privée, au lieu d’IDs utilisés par toutes les autres plateformes, SimpleX a des IDs pour les queues de messages, distinctes pour chacun de vos contacts. + + To protect against your link being replaced, you can compare contact security codes. No comment provided by engineer. @@ -6966,6 +7174,15 @@ You will be prompted to complete authentication before this feature is enabled.< Vous serez invité à confirmer l'authentification avant que cette fonction ne soit activée. No comment provided by engineer. + + To protect your privacy, SimpleX uses separate IDs for each of your contacts. + Pour protéger votre vie privée, au lieu d’IDs utilisés par toutes les autres plateformes, SimpleX a des IDs pour les queues de messages, distinctes pour chacun de vos contacts. + No comment provided by engineer. + + + To receive + No comment provided by engineer. + To record speech please grant permission to use Microphone. No comment provided by engineer. @@ -6984,11 +7201,19 @@ Vous serez invité à confirmer l'authentification avant que cette fonction ne s Pour révéler votre profil caché, entrez le mot de passe dans le champ de recherche de la page **Vos profils de chat**. No comment provided by engineer. + + To send + No comment provided by engineer. + To support instant push notifications the chat database has to be migrated. Pour prendre en charge les notifications push instantanées, la base de données du chat doit être migrée. No comment provided by engineer. + + To use the servers of **%@**, accept conditions of use. + No comment provided by engineer. + To verify end-to-end encryption with your contact compare (or scan) the code on your devices. Pour vérifier le chiffrement de bout en bout avec votre contact, comparez (ou scannez) le code sur vos appareils. @@ -7079,6 +7304,10 @@ Vous serez invité à confirmer l'authentification avant que cette fonction ne s Débloquer ce membre ? No comment provided by engineer. + + Undelivered messages + No comment provided by engineer. + Unexpected migration state État de la migration inattendu @@ -7236,6 +7465,10 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Envoi de l'archive No comment provided by engineer. + + Use %@ + No comment provided by engineer. + Use .onion hosts Utiliser les hôtes .onions @@ -7260,6 +7493,14 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Utiliser le profil actuel No comment provided by engineer. + + Use for files + No comment provided by engineer. + + + Use for messages + No comment provided by engineer. + Use for new connections Utiliser pour les nouvelles connexions @@ -7300,6 +7541,10 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Utiliser ce serveur No comment provided by engineer. + + Use servers + No comment provided by engineer. + Use the app while in the call. Utiliser l'application pendant l'appel. @@ -7389,11 +7634,19 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Vidéos et fichiers jusqu'à 1Go No comment provided by engineer. + + View conditions + No comment provided by engineer. + View security code Afficher le code de sécurité No comment provided by engineer. + + View updated conditions + No comment provided by engineer. + Visible history Historique visible @@ -7504,9 +7757,8 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Lors des appels audio et vidéo. No comment provided by engineer. - - When people request to connect, you can accept or reject it. - Vous pouvez accepter ou refuser les demandes de contacts. + + When more than one operator is enabled, none of them has metadata to learn who communicates with whom. No comment provided by engineer. @@ -7666,6 +7918,18 @@ Répéter la demande d'adhésion ? Vous pouvez choisir de le modifier dans les paramètres d'apparence. No comment provided by engineer. + + You can configure operators in Network & servers settings. + No comment provided by engineer. + + + You can configure servers via settings. + No comment provided by engineer. + + + You can create it in user picker. + No comment provided by engineer. + You can create it later Vous pouvez la créer plus tard @@ -7706,6 +7970,10 @@ Répéter la demande d'adhésion ? Vous pouvez envoyer des messages à %@ à partir des contacts archivés. No comment provided by engineer. + + You can set connection name, to remember who the link was shared with. + No comment provided by engineer. + You can set lock screen notification preview via settings. Vous pouvez configurer l'aperçu des notifications sur l'écran de verrouillage via les paramètres. @@ -7721,11 +7989,6 @@ Répéter la demande d'adhésion ? Vous pouvez partager cette adresse avec vos contacts pour leur permettre de se connecter avec **%@**. No comment provided by engineer. - - You can share your address as a link or QR code - anybody can connect to you. - Vous pouvez partager votre adresse sous la forme d'un lien ou d'un code QR - tout le monde peut l'utiliser pour vous contacter. - No comment provided by engineer. - You can start chat via app Settings / Database or by restarting the app Vous pouvez lancer le chat via Paramètres / Base de données ou en redémarrant l'app @@ -7749,23 +8012,23 @@ Répéter la demande d'adhésion ? You can view invitation link again in connection details. Vous pouvez à nouveau consulter le lien d'invitation dans les détails de la connexion. - No comment provided by engineer. + alert message You can't send messages! Vous ne pouvez pas envoyer de messages ! No comment provided by engineer. - - You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them. - Vous contrôlez par quel·s serveur·s vous pouvez **transmettre** ainsi que par quel·s serveur·s vous pouvez **recevoir** les messages de vos contacts. - No comment provided by engineer. - You could not be verified; please try again. Vous n'avez pas pu être vérifié·e ; veuillez réessayer. No comment provided by engineer. + + You decide who can connect. + Vous choisissez qui peut se connecter. + No comment provided by engineer. + You have already requested connection via this address! Vous avez déjà demandé une connexion via cette adresse ! @@ -7888,11 +8151,6 @@ Répéter la demande de connexion ? Vous utilisez un profil incognito pour ce groupe - pour éviter de partager votre profil principal ; inviter des contacts n'est pas possible No comment provided by engineer. - - Your %@ servers - Vos serveurs %@ - No comment provided by engineer. - Your ICE servers Vos serveurs ICE @@ -7908,11 +8166,6 @@ Répéter la demande de connexion ? Votre adresse SimpleX No comment provided by engineer. - - Your XFTP servers - Vos serveurs XFTP - No comment provided by engineer. - Your calls Vos appels @@ -8009,16 +8262,15 @@ Répéter la demande de connexion ? Votre profil aléatoire No comment provided by engineer. - - Your server - Votre serveur - No comment provided by engineer. - Your server address Votre adresse de serveur No comment provided by engineer. + + Your servers + No comment provided by engineer. + Your settings Vos paramètres @@ -8439,6 +8691,10 @@ Répéter la demande de connexion ? expiré No comment provided by engineer. + + for better metadata privacy. + No comment provided by engineer. + forwarded transféré @@ -9061,6 +9317,33 @@ dernier message reçu : %2$@ + +
+ +
+ + + %d new events + notification body + + + From: %@ + notification body + + + New events + notification + + + New messages + notification + + + New messages in %d chats + notification body + + +
diff --git a/apps/ios/SimpleX Localizations/fr.xcloc/Source Contents/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/fr.xcloc/Source Contents/en.lproj/Localizable.strings index cf485752ea..cb83427195 100644 --- a/apps/ios/SimpleX Localizations/fr.xcloc/Source Contents/en.lproj/Localizable.strings +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Source Contents/en.lproj/Localizable.strings @@ -1,9 +1,6 @@ /* No comment provided by engineer. */ "_italic_" = "\\_italic_"; -/* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact."; - /* No comment provided by engineer. */ "*bold*" = "\\*bold*"; @@ -27,4 +24,3 @@ /* No comment provided by engineer. */ "No group!" = "Group not found!"; - diff --git a/apps/ios/SimpleX Localizations/he.xcloc/Localized Contents/he.xliff b/apps/ios/SimpleX Localizations/he.xcloc/Localized Contents/he.xliff index 928a01dead..219812651a 100644 --- a/apps/ios/SimpleX Localizations/he.xcloc/Localized Contents/he.xliff +++ b/apps/ios/SimpleX Localizations/he.xcloc/Localized Contents/he.xliff @@ -217,23 +217,18 @@ Available in v5.1 ) No comment provided by engineer. - - **Add new contact**: to create your one-time QR Code or link for your contact. - **הוסיפו איש קשר חדש**: ליצירת קוד QR או קישור חד־פעמיים עבור איש הקשר שלכם. - No comment provided by engineer. - **Create link / QR code** for your contact to use. **צור קישור / קוד QR** לשימוש איש הקשר שלך. No comment provided by engineer. - - **More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have. + + **More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata. **יותר פרטי**: בדוק הודעות חדשות כל 20 דקות. אסימון המכשיר משותף עם שרת SimpleX Chat, אך לא כמה אנשי קשר או הודעות יש לך. No comment provided by engineer. - - **Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app). + + **Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app. **הכי פרטי**: אל תשתמש בשרת ההתראות של SimpleX Chat, בדוק הודעות מעת לעת ברקע (תלוי בתדירות השימוש באפליקציה). No comment provided by engineer. @@ -247,8 +242,8 @@ Available in v5.1 **שימו לב**: לא ניתן יהיה לשחזר או לשנות את הסיסמה אם תאבדו אותה. No comment provided by engineer. - - **Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from. + + **Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from. **מומלץ**: אסימון מכשיר והתראות נשלחים לשרת ההתראות של SimpleX Chat, אך לא תוכן ההודעה, גודלה או ממי היא. No comment provided by engineer. @@ -2115,8 +2110,8 @@ Available in v5.1 מיד No comment provided by engineer. - - Immune to spam and abuse + + Immune to spam חסין מפני ספאם ושימוש לרעה No comment provided by engineer. @@ -2701,8 +2696,8 @@ Available in v5.1 לא ייעשה שימוש במארחי Onion. No comment provided by engineer. - - Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**. + + Only client devices store user profiles, contacts, groups, and messages. No comment provided by engineer. @@ -2761,8 +2756,8 @@ Available in v5.1 Open user profiles authentication reason - - Open-source protocol and code – anybody can run the servers. + + Anybody can host servers. No comment provided by engineer. @@ -2817,8 +2812,8 @@ Available in v5.1 Paste the link you received into the box below to connect with your contact. No comment provided by engineer. - - People can connect to you only via the links you share. + + You decide who can connect. No comment provided by engineer. @@ -3521,8 +3516,8 @@ Available in v5.1 Thanks to the users – contribute via Weblate! No comment provided by engineer. - - The 1st platform without any user identifiers – private by design. + + No user identifiers. No comment provided by engineer. @@ -3566,8 +3561,8 @@ It can happen because of some bug or when the connection is compromised.The message will be marked as moderated for all members. No comment provided by engineer. - - The next generation of private messaging + + The future of messaging No comment provided by engineer. @@ -3638,8 +3633,8 @@ It can happen because of some bug or when the connection is compromised.To make a new connection No comment provided by engineer. - - To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts. + + To protect your privacy, SimpleX uses separate IDs for each of your contacts. No comment provided by engineer. @@ -4005,10 +4000,6 @@ SimpleX Lock must be enabled. You can't send messages! No comment provided by engineer. - - You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them. - No comment provided by engineer. - You could not be verified; please try again. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/hr.xcloc/Localized Contents/hr.xliff b/apps/ios/SimpleX Localizations/hr.xcloc/Localized Contents/hr.xliff index 50f5536e5e..7ae670185c 100644 --- a/apps/ios/SimpleX Localizations/hr.xcloc/Localized Contents/hr.xliff +++ b/apps/ios/SimpleX Localizations/hr.xcloc/Localized Contents/hr.xliff @@ -181,23 +181,18 @@ ) No comment provided by engineer. - - **Add new contact**: to create your one-time QR Code or link for your contact. - **Dodajte novi kontakt**: da biste stvorili svoj jednokratni QR kôd ili vezu za svoj kontakt. - No comment provided by engineer. - **Create link / QR code** for your contact to use. **Stvorite vezu / QR kôd** za vaš kontakt. No comment provided by engineer. - - **More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have. + + **More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata. **Privatnije**: provjeravajte nove poruke svakih 20 minuta. Token uređaja dijeli se s SimpleX Chat poslužiteljem, ali ne i s brojem kontakata ili poruka koje imate. No comment provided by engineer. - - **Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app). + + **Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app. **Najprivatniji**: nemojte koristiti SimpleX Chat poslužitelj obavijesti, povremeno provjeravajte poruke u pozadini (ovisi o tome koliko često koristite aplikaciju). No comment provided by engineer. @@ -211,8 +206,8 @@ **Imajte na umu**: NEĆETE moći oporaviti ili promijeniti pristupni izraz ako ga izgubite. No comment provided by engineer. - - **Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from. + + **Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from. **Preporučeno**: token uređaja i obavijesti šalju se na poslužitelj obavijesti SimpleX Chata, ali ne i sadržaj poruke, veličinu ili od koga je. No comment provided by engineer. @@ -1519,8 +1514,8 @@ Image will be received when your contact is online, please wait or check later! No comment provided by engineer. - - Immune to spam and abuse + + Immune to spam No comment provided by engineer. @@ -1917,8 +1912,8 @@ We will be adding server redundancy to prevent lost messages. Onion hosts will not be used. No comment provided by engineer. - - Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**. + + Only client devices store user profiles, contacts, groups, and messages. No comment provided by engineer. @@ -1965,8 +1960,8 @@ We will be adding server redundancy to prevent lost messages. Open chat console authentication reason - - Open-source protocol and code – anybody can run the servers. + + Anybody can host servers. No comment provided by engineer. @@ -1997,8 +1992,8 @@ We will be adding server redundancy to prevent lost messages. Paste the link you received into the box below to connect with your contact. No comment provided by engineer. - - People can connect to you only via the links you share. + + You decide who can connect. No comment provided by engineer. @@ -2577,8 +2572,8 @@ We will be adding server redundancy to prevent lost messages. Thanks to the users – contribute via Weblate! No comment provided by engineer. - - The 1st platform without any user identifiers – private by design. + + No user identifiers. No comment provided by engineer. @@ -2609,8 +2604,8 @@ We will be adding server redundancy to prevent lost messages. The microphone does not work when the app is in the background. No comment provided by engineer. - - The next generation of private messaging + + The future of messaging No comment provided by engineer. @@ -2673,8 +2668,8 @@ We will be adding server redundancy to prevent lost messages. To prevent the call interruption, enable Do Not Disturb mode. No comment provided by engineer. - - To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts. + + To protect your privacy, SimpleX uses separate IDs for each of your contacts. No comment provided by engineer. @@ -2959,10 +2954,6 @@ To connect, please ask your contact to create another connection link and check You can use markdown to format messages: No comment provided by engineer. - - You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them. - No comment provided by engineer. - You could not be verified; please try again. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff index 0c8c1635a5..81cece7794 100644 --- a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff @@ -12,21 +12,6 @@ No comment provided by engineer. - - - - No comment provided by engineer. - - - - - No comment provided by engineer. - - - - - No comment provided by engineer. - ( ( @@ -127,6 +112,14 @@ %@ hitelesítve No comment provided by engineer. + + %@ server + No comment provided by engineer. + + + %@ servers + No comment provided by engineer. + %@ uploaded %@ feltöltve @@ -352,28 +345,23 @@ ) No comment provided by engineer. - - **Add contact**: to create a new invitation link, or connect via a link you received. + + **Create 1-time link**: to create and share a new invitation link. **Ismerős hozzáadása:** új meghívó-hivatkozás létrehozásához, vagy egy kapott hivatkozáson keresztül történő kapcsolódáshoz. No comment provided by engineer. - - **Add new contact**: to create your one-time QR Code or link for your contact. - **Új ismerős hozzáadása:** egyszer használható QR-kód vagy hivatkozás létrehozása az ismerőse számára. - No comment provided by engineer. - **Create group**: to create a new group. **Csoport létrehozása:** új csoport létrehozásához. No comment provided by engineer. - - **More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have. + + **More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata. **Privátabb:** 20 percenként ellenőrzi az új üzeneteket. Az eszköztoken megosztásra kerül a SimpleX Chat-kiszolgálóval, de az nem, hogy hány ismerőse vagy üzenete van. No comment provided by engineer. - - **Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app). + + **Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app. **Legprivátabb:** ne használja a SimpleX Chat értesítési kiszolgálót, rendszeresen ellenőrizze az üzeneteket a háttérben (attól függően, hogy milyen gyakran használja az alkalmazást). No comment provided by engineer. @@ -387,11 +375,15 @@ **Megjegyzés:** NEM tudja visszaállítani vagy megváltoztatni jelmondatát, ha elveszíti azt. No comment provided by engineer. - - **Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from. + + **Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from. **Megjegyzés:** az eszköztoken és az értesítések elküldésre kerülnek a SimpleX Chat értesítési kiszolgálóra, kivéve az üzenet tartalma, mérete vagy az, hogy kitől származik. No comment provided by engineer. + + **Scan / Paste link**: to connect via a link you received. + No comment provided by engineer. + **Warning**: Instant push notifications require passphrase saved in Keychain. **Figyelmeztetés:** Az azonnali push-értesítésekhez a kulcstartóban tárolt jelmondat megadása szükséges. @@ -498,6 +490,14 @@ 1 hét time interval + + 1-time link + No comment provided by engineer. + + + 1-time link can be used *with one contact only* - share in person or via any messenger. + No comment provided by engineer. + 5 minutes 5 perc @@ -567,21 +567,11 @@ Címváltoztatás megszakítása?? No comment provided by engineer. - - About SimpleX - A SimpleXről - No comment provided by engineer. - About SimpleX Chat A SimpleX Chatről No comment provided by engineer. - - About SimpleX address - A SimpleX-címről - No comment provided by engineer. - Accent Kiemelés @@ -594,6 +584,10 @@ accept incoming call via notification swipe action + + Accept conditions + No comment provided by engineer. + Accept connection request? Kapcsolatkérés elfogadása? @@ -610,6 +604,10 @@ accept contact request via notification swipe action + + Accepted conditions + No comment provided by engineer. + Acknowledged Nyugtázva @@ -630,16 +628,6 @@ Cím hozzáadása a profilhoz, hogy az ismerősei megoszthassák másokkal. A profilfrissítés elküldésre kerül az ismerősei számára. No comment provided by engineer. - - Add contact - Ismerős hozzáadása - No comment provided by engineer. - - - Add preset servers - Előre beállított kiszolgálók hozzáadása - No comment provided by engineer. - Add profile Profil hozzáadása @@ -665,6 +653,14 @@ Üdvözlőüzenet hozzáadása No comment provided by engineer. + + Added media & file servers + No comment provided by engineer. + + + Added message servers + No comment provided by engineer. + Additional accent További kiemelés @@ -690,6 +686,14 @@ A cím módosítása megszakad. A régi fogadási cím kerül felhasználásra. No comment provided by engineer. + + Address or 1-time link? + No comment provided by engineer. + + + Address settings + No comment provided by engineer. + Admins can block a member for all. Az adminok egy tagot mindenki számára letilthatnak. @@ -735,6 +739,10 @@ Minden csoporttag kapcsolódva marad. No comment provided by engineer. + + All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages. + No comment provided by engineer. + All messages will be deleted - this cannot be undone! Minden üzenet törlésre kerül – ez a művelet nem vonható vissza! @@ -915,6 +923,11 @@ Hívás fogadása No comment provided by engineer. + + Anybody can host servers. + Bárki üzemeltethet kiszolgálókat. + No comment provided by engineer. + App build: %@ Az alkalmazás build száma: %@ @@ -1258,7 +1271,8 @@ Cancel Mégse - alert button + alert action + alert button Cancel migration @@ -1341,6 +1355,10 @@ authentication reason set passcode view + + Change user profiles + authentication reason + Chat archive Csevegési archívum @@ -1426,10 +1444,18 @@ Csevegések No comment provided by engineer. + + Check messages every 20 min. + No comment provided by engineer. + + + Check messages when allowed. + No comment provided by engineer. + Check server address and try again. Kiszolgáló címének ellenőrzése és újrapróbálkozás. - No comment provided by engineer. + alert title Chinese and Spanish interface @@ -1516,16 +1542,47 @@ Elkészült No comment provided by engineer. + + Conditions accepted on: %@. + No comment provided by engineer. + + + Conditions are accepted for the operator(s): **%@**. + No comment provided by engineer. + + + Conditions are already accepted for following operator(s): **%@**. + No comment provided by engineer. + + + Conditions of use + No comment provided by engineer. + + + Conditions will be accepted for enabled operators after 30 days. + No comment provided by engineer. + + + Conditions will be accepted for operator(s): **%@**. + No comment provided by engineer. + + + Conditions will be accepted for the operator(s): **%@**. + No comment provided by engineer. + + + Conditions will be accepted on: %@. + No comment provided by engineer. + + + Conditions will be automatically accepted for enabled operators on: %@. + No comment provided by engineer. + Configure ICE servers ICE-kiszolgálók beállítása No comment provided by engineer. - - Configured %@ servers - Beállított %@ kiszolgálók - No comment provided by engineer. - Confirm Megerősítés @@ -1715,6 +1772,10 @@ Ez az Ön egyszer használható hivatkozása! Kapcsolatkérés elküldve! No comment provided by engineer. + + Connection security + No comment provided by engineer. + Connection terminated Kapcsolat megszakítva @@ -1830,6 +1891,10 @@ Ez az Ön egyszer használható hivatkozása! Létrehozás No comment provided by engineer. + + Create 1-time link + No comment provided by engineer. + Create SimpleX address SimpleX-cím létrehozása @@ -1840,11 +1905,6 @@ Ez az Ön egyszer használható hivatkozása! Csoport létrehozása véletlenszerűen létrehozott profillal. No comment provided by engineer. - - Create an address to let people connect with you. - Cím létrehozása, hogy az emberek kapcsolatba léphessenek Önnel. - No comment provided by engineer. - Create file Fájl létrehozása @@ -1925,6 +1985,10 @@ Ez az Ön egyszer használható hivatkozása! Jelenlegi jelkód No comment provided by engineer. + + Current conditions text couldn't be loaded, you can review conditions via this link: + No comment provided by engineer. + Current passphrase… Jelenlegi jelmondat… @@ -2081,7 +2145,8 @@ Ez az Ön egyszer használható hivatkozása! Delete Törlés - chat item action + alert action + chat item action swipe action @@ -2299,6 +2364,10 @@ Ez az Ön egyszer használható hivatkozása! Törlési hibák No comment provided by engineer. + + Delivered even when Apple drops them. + No comment provided by engineer. + Delivery Kézbesítés @@ -2580,6 +2649,10 @@ Ez az Ön egyszer használható hivatkozása! Időtartam No comment provided by engineer. + + E2E encrypted notifications. + No comment provided by engineer. + Edit Szerkesztés @@ -2600,6 +2673,10 @@ Ez az Ön egyszer használható hivatkozása! Engedélyezés (felülírások megtartásával) No comment provided by engineer. + + Enable Flux + No comment provided by engineer. + Enable SimpleX Lock SimpleX-zár bekapcsolása @@ -2805,6 +2882,10 @@ Ez az Ön egyszer használható hivatkozása! Hiba a cím megváltoztatásának megszakításakor No comment provided by engineer. + + Error accepting conditions + alert title + Error accepting contact request Hiba történt a kapcsolatkérés elfogadásakor @@ -2820,6 +2901,10 @@ Ez az Ön egyszer használható hivatkozása! Hiba a tag(ok) hozzáadásakor No comment provided by engineer. + + Error adding server + alert title + Error changing address Hiba a cím megváltoztatásakor @@ -2960,10 +3045,9 @@ Ez az Ön egyszer használható hivatkozása! Hiba a csoporthoz való csatlakozáskor No comment provided by engineer. - - Error loading %@ servers - Hiba a(z) %@ -kiszolgálók betöltésekor - No comment provided by engineer. + + Error loading servers + alert title Error migrating settings @@ -3000,11 +3084,6 @@ Ez az Ön egyszer használható hivatkozása! Hiba a statisztikák visszaállításakor No comment provided by engineer. - - Error saving %@ servers - Hiba történt a(z) %@ -kiszolgálók mentésekor - No comment provided by engineer. - Error saving ICE servers Hiba az ICE-kiszolgálók mentésekor @@ -3025,6 +3104,10 @@ Ez az Ön egyszer használható hivatkozása! Hiba a jelmondat kulcstartóba történő mentésekor No comment provided by engineer. + + Error saving servers + alert title + Error saving settings Hiba a beállítások mentésekor @@ -3095,6 +3178,10 @@ Ez az Ön egyszer használható hivatkozása! Hiba az üzenet frissítésekor No comment provided by engineer. + + Error updating server + alert title + Error updating settings Hiba történt a beállítások frissítésekor @@ -3140,6 +3227,10 @@ Ez az Ön egyszer használható hivatkozása! Hibák No comment provided by engineer. + + Errors in servers configuration. + servers error + Even when disabled in the conversation. Akkor is, ha le van tiltva a beszélgetésben. @@ -3342,11 +3433,27 @@ Ez az Ön egyszer használható hivatkozása! Csoporttag általi javítás nem támogatott No comment provided by engineer. + + For chat profile %@: + servers error + For console Konzolhoz No comment provided by engineer. + + For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server. + No comment provided by engineer. + + + For private routing + No comment provided by engineer. + + + For social media + No comment provided by engineer. + Forward Továbbítás @@ -3656,9 +3763,12 @@ Hiba: %2$@ Hogyan működik a SimpleX No comment provided by engineer. - - How it works - Hogyan működik + + How it affects privacy + No comment provided by engineer. + + + How it helps privacy No comment provided by engineer. @@ -3731,8 +3841,8 @@ Hiba: %2$@ Azonnal No comment provided by engineer. - - Immune to spam and abuse + + Immune to spam Spam és visszaélések elleni védelem No comment provided by engineer. @@ -3873,6 +3983,11 @@ További fejlesztések hamarosan! A [SimpleX Chat terminálhoz] telepítése (https://github.com/simplex-chat/simplex-chat) No comment provided by engineer. + + Instant + Azonnal + No comment provided by engineer. + Instant push notifications will be hidden! @@ -3880,11 +3995,6 @@ További fejlesztések hamarosan! No comment provided by engineer. - - Instantly - Azonnal - No comment provided by engineer. - Interface Felület @@ -3933,7 +4043,7 @@ További fejlesztések hamarosan! Invalid server address! Érvénytelen kiszolgálócím! - No comment provided by engineer. + alert title Invalid status @@ -4061,7 +4171,7 @@ Ez az Ön hivatkozása a(z) %@ csoporthoz! Keep Megtart - No comment provided by engineer. + alert action Keep conversation @@ -4076,7 +4186,7 @@ Ez az Ön hivatkozása a(z) %@ csoporthoz! Keep unused invitation? Fel nem használt meghívó megtartása? - No comment provided by engineer. + alert title Keep your connections @@ -4163,11 +4273,6 @@ Ez az Ön hivatkozása a(z) %@ csoporthoz! Élő üzenetek No comment provided by engineer. - - Local - Helyi - No comment provided by engineer. - Local name Helyi név @@ -4188,11 +4293,6 @@ Ez az Ön hivatkozása a(z) %@ csoporthoz! Zárolási mód No comment provided by engineer. - - Make a private connection - Privát kapcsolat létrehozása - No comment provided by engineer. - Make one message disappear Egy üzenet eltüntetése @@ -4203,21 +4303,11 @@ Ez az Ön hivatkozása a(z) %@ csoporthoz! Tegye priváttá a profilját! No comment provided by engineer. - - Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@). - Győződjön meg arról, hogy a(z) %@ kiszolgálócímek megfelelő formátumúak, sorszeparáltak és nincsenek duplikálva (%@). - No comment provided by engineer. - Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated. Győződjön meg arról, hogy a WebRTC ICE-kiszolgáló címei megfelelő formátumúak, sorszeparáltak és nincsenek duplikálva. No comment provided by engineer. - - Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?* - Sokan kérdezték: *ha a SimpleX Chatnek nincsenek felhasználó-azonosítói, akkor hogyan tud üzeneteket kézbesíteni?* - No comment provided by engineer. - Mark deleted for everyone Jelölje meg mindenki számára töröltként @@ -4498,6 +4588,10 @@ Ez az Ön hivatkozása a(z) %@ csoporthoz! Megbízhatóbb hálózati kapcsolat. No comment provided by engineer. + + More reliable notifications + No comment provided by engineer. + Most likely this connection is deleted. Valószínűleg ez a kapcsolat törlésre került. @@ -4533,6 +4627,10 @@ Ez az Ön hivatkozása a(z) %@ csoporthoz! Internetkapcsolat No comment provided by engineer. + + Network decentralization + No comment provided by engineer. + Network issues - message expired after many attempts to send it. Hálózati problémák - az üzenet többszöri elküldési kísérlet után lejárt. @@ -4543,6 +4641,10 @@ Ez az Ön hivatkozása a(z) %@ csoporthoz! Hálózatkezelés No comment provided by engineer. + + Network operator + No comment provided by engineer. + Network settings Hálózati beállítások @@ -4603,6 +4705,10 @@ Ez az Ön hivatkozása a(z) %@ csoporthoz! Új megjelenítési név No comment provided by engineer. + + New events + notification + New in %@ Újdonságok a(z) %@ verzióban @@ -4628,6 +4734,10 @@ Ez az Ön hivatkozása a(z) %@ csoporthoz! Új jelmondat… No comment provided by engineer. + + New server + No comment provided by engineer. + No Nem @@ -4683,6 +4793,14 @@ Ez az Ön hivatkozása a(z) %@ csoporthoz! Nincs információ, próbálja meg újratölteni No comment provided by engineer. + + No media & file servers. + servers error + + + No message servers. + servers error + No network connection Nincs hálózati kapcsolat @@ -4703,11 +4821,37 @@ Ez az Ön hivatkozása a(z) %@ csoporthoz! Nincs engedély a hangüzenet rögzítésére No comment provided by engineer. + + No push server + Helyi + No comment provided by engineer. + No received or sent files Nincsenek fogadott vagy küldött fájlok No comment provided by engineer. + + No servers for private message routing. + servers error + + + No servers to receive files. + servers error + + + No servers to receive messages. + servers error + + + No servers to send files. + servers error + + + No user identifiers. + Nincsenek felhasználó-azonosítók. + No comment provided by engineer. + Not compatible! Nem kompatibilis! @@ -4733,6 +4877,10 @@ Ez az Ön hivatkozása a(z) %@ csoporthoz! Az értesítések le vannak tiltva! No comment provided by engineer. + + Notifications privacy + No comment provided by engineer. + Now admins can: - delete members' messages. @@ -4791,8 +4939,8 @@ VPN engedélyezése szükséges. Onion-kiszolgálók nem lesznek használva. No comment provided by engineer. - - Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**. + + Only client devices store user profiles, contacts, groups, and messages. Csak az eszközök alkalmazásai tárolják a felhasználó-profilokat, névjegyeket, csoportokat és a **2 rétegű végpontok közötti titkosítással** küldött üzeneteket. No comment provided by engineer. @@ -4876,6 +5024,10 @@ VPN engedélyezése szükséges. Beállítások megnyitása No comment provided by engineer. + + Open changes + No comment provided by engineer. + Open chat Csevegés megnyitása @@ -4886,6 +5038,10 @@ VPN engedélyezése szükséges. Csevegés konzol megnyitása authentication reason + + Open conditions + No comment provided by engineer. + Open group Csoport megnyitása @@ -4896,26 +5052,19 @@ VPN engedélyezése szükséges. Átköltöztetés megkezdése egy másik eszközre authentication reason - - Open server settings - Kiszolgáló-beállítások megnyitása - No comment provided by engineer. - - - Open user profiles - Felhasználó-profilok megnyitása - authentication reason - - - Open-source protocol and code – anybody can run the servers. - Nyílt forráskódú protokoll és forráskód – bárki üzemeltethet kiszolgálókat. - No comment provided by engineer. - Opening app… Az alkalmazás megnyitása… No comment provided by engineer. + + Operator + No comment provided by engineer. + + + Operator server + alert title + Or paste archive link Vagy az archívum hivatkozásának beillesztése @@ -4936,16 +5085,15 @@ VPN engedélyezése szükséges. Vagy mutassa meg ezt a kódot No comment provided by engineer. + + Or to share privately + No comment provided by engineer. + Other További No comment provided by engineer. - - Other %@ servers - További %@ kiszolgálók - No comment provided by engineer. - Other file errors: %@ @@ -5028,13 +5176,8 @@ VPN engedélyezése szükséges. Függőben No comment provided by engineer. - - People can connect to you only via the links you share. - Az emberek csak az Ön által megosztott hivatkozáson keresztül kapcsolódhatnak. - No comment provided by engineer. - - - Periodically + + Periodic Rendszeresen No comment provided by engineer. @@ -5157,16 +5300,15 @@ Hiba: %@ Az utolsó üzenet tervezetének megőrzése a mellékletekkel együtt. No comment provided by engineer. - - Preset server - Előre beállított kiszolgáló - No comment provided by engineer. - Preset server address Előre beállított kiszolgáló címe No comment provided by engineer. + + Preset servers + No comment provided by engineer. + Preview Előnézet @@ -5245,7 +5387,7 @@ Hiba: %@ Profile update will be sent to your contacts. A profilfrissítés elküldésre került az ismerősök számára. - No comment provided by engineer. + alert message Prohibit audio/video calls. @@ -5339,6 +5481,10 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. A proxy jelszót igényel No comment provided by engineer. + + Push Notifications + No comment provided by engineer. + Push notifications Push-értesítések @@ -5379,26 +5525,21 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Tudjon meg többet No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address). - További információ a [Használati útmutatóban](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address). - No comment provided by engineer. - Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). További információ a [Használati útmutatóban](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). No comment provided by engineer. + + Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). + További információ a [Használati útmutatóban](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). + No comment provided by engineer. + Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends). További információ a [Használati útmutatóban](https://simplex.chat/docs/guide/readme.html#connect-to-friends). No comment provided by engineer. - - Read more in our GitHub repository. - További információ a GitHub tárolónkban. - No comment provided by engineer. - Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme). További információ a [GitHub tárolóban](https://github.com/simplex-chat/simplex-chat#readme). @@ -5715,6 +5856,14 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Felfedés chat item action + + Review conditions + No comment provided by engineer. + + + Review later + No comment provided by engineer. + Revoke Visszavonás @@ -5760,6 +5909,14 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Biztonságosabb csoportok No comment provided by engineer. + + Same conditions will apply to operator **%@**. + No comment provided by engineer. + + + Same conditions will apply to operator(s): **%@**. + No comment provided by engineer. + Save Mentés @@ -5829,7 +5986,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Save servers? Kiszolgálók mentése? - No comment provided by engineer. + alert title Save welcome message? @@ -6041,11 +6198,6 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Értesítések küldése No comment provided by engineer. - - Send notifications: - Értesítések küldése: - No comment provided by engineer. - Send questions and ideas Ötletek és kérdések beküldése @@ -6171,6 +6323,10 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Kiszolgáló No comment provided by engineer. + + Server added to operator %@. + alert message + Server address Kiszolgáló címe @@ -6186,6 +6342,18 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. A kiszolgáló címe nem kompatibilis a hálózati beállításokkal: %@. No comment provided by engineer. + + Server operator changed. + alert title + + + Server operators + No comment provided by engineer. + + + Server protocol changed. + alert title + Server requires authorization to create queues, check password A kiszolgálónak engedélyre van szüksége a sorbaállítás létrehozásához, ellenőrizze jelszavát @@ -6304,22 +6472,35 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Share Megosztás - chat item action + alert action + chat item action Share 1-time link Egyszer használható hivatkozás megosztása No comment provided by engineer. + + Share 1-time link with a friend + No comment provided by engineer. + + + Share SimpleX address on social media. + No comment provided by engineer. + Share address Cím megosztása No comment provided by engineer. + + Share address publicly + No comment provided by engineer. + Share address with contacts? Megosztja a címet az ismerőseivel? - No comment provided by engineer. + alert title Share from other apps. @@ -6436,6 +6617,14 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. SimpleX-cím No comment provided by engineer. + + SimpleX address and 1-time links are safe to share via any messenger. + No comment provided by engineer. + + + SimpleX address or 1-time link? + No comment provided by engineer. + SimpleX contact address SimpleX kapcsolattartási cím @@ -6526,6 +6715,11 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Néhány nem végzetes hiba történt az importáláskor: No comment provided by engineer. + + Some servers failed the test: +%@ + alert message + Somebody Valaki @@ -6609,12 +6803,12 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Stop sharing Megosztás megállítása - No comment provided by engineer. + alert action Stop sharing address? Címmegosztás megállítása? - No comment provided by engineer. + alert title Stopping chat @@ -6764,7 +6958,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Tests failed! Sikertelen tesztek! - No comment provided by engineer. + alert title Thank you for installing SimpleX Chat! @@ -6781,11 +6975,6 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Köszönet a felhasználóknak - hozzájárulás a Weblate-en! No comment provided by engineer. - - The 1st platform without any user identifiers – private by design. - Az első csevegési rendszer bármiféle felhasználó-azonosító nélkül - privátra lett tervezre. - No comment provided by engineer. - The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised. @@ -6798,6 +6987,10 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. Az alkalmazás értesíteni fogja, amikor üzeneteket vagy kapcsolatkéréseket kap – beállítások megnyitása az engedélyezéshez. No comment provided by engineer. + + The app protects your privacy by using different operators in each conversation. + No comment provided by engineer. + The app will ask to confirm downloads from unknown file servers (except .onion). Az alkalmazás kérni fogja az ismeretlen fájlkiszolgálókról (kivéve .onion) történő letöltések megerősítését. @@ -6813,6 +7006,10 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. A beolvasott QR-kód nem egy SimpleX QR-kód hivatkozás. No comment provided by engineer. + + The connection reached the limit of undelivered messages, your contact may be offline. + No comment provided by engineer. + The connection you accepted will be cancelled! Az Ön által elfogadott kérelem vissza lesz vonva! @@ -6833,6 +7030,11 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. 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 future of messaging + A privát üzenetküldés következő generációja + No comment provided by engineer. + The hash of the previous message is different. Az előző üzenet hasító értéke különbözik. @@ -6858,11 +7060,6 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. Az üzenetek moderáltként lesznek megjelölve minden tag számára. No comment provided by engineer. - - The next generation of private messaging - A privát üzenetküldés következő generációja - No comment provided by engineer. - The old database was not removed during the migration, it can be deleted. A régi adatbázis nem került eltávolításra az átköltöztetéskor, így törölhető. @@ -6873,6 +7070,10 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. A profilja csak az ismerőseivel kerül megosztásra. No comment provided by engineer. + + The second preset operator in the app! + No comment provided by engineer. + The second tick we missed! ✅ A második jelölés, amit kihagytunk! ✅ @@ -6888,6 +7089,10 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. A jelenlegi csevegési profilhoz tartozó új kapcsolatok kiszolgálói **%@**. No comment provided by engineer. + + The servers for new files of your current chat profile **%@**. + No comment provided by engineer. + The text you pasted is not a SimpleX link. A beillesztett szöveg nem egy SimpleX-hivatkozás. @@ -6903,6 +7108,10 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. Témák No comment provided by engineer. + + These conditions will also apply for: **%@**. + No comment provided by engineer. + These settings are for your current profile **%@**. Ezek a beállítások a jelenlegi **%@** profiljára vonatkoznak. @@ -7003,9 +7212,8 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. Új kapcsolat létrehozásához No comment provided by engineer. - - To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts. - Az adatvédelem érdekében, a más csevegési platformokon megszokott felhasználó-azonosítók helyett, a SimpleX csak az üzenetek sorbaállításához használ azonosítókat, minden egyes ismerőshöz egy-egy különbözőt. + + To protect against your link being replaced, you can compare contact security codes. No comment provided by engineer. @@ -7025,6 +7233,15 @@ You will be prompted to complete authentication before this feature is enabled.< A funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beállítására az eszközén. No comment provided by engineer. + + To protect your privacy, SimpleX uses separate IDs for each of your contacts. + Az adatvédelem érdekében, a más csevegési platformokon megszokott felhasználó-azonosítók helyett, a SimpleX csak az üzenetek sorbaállításához használ azonosítókat, minden egyes ismerőshöz egy-egy különbözőt. + No comment provided by engineer. + + + To receive + No comment provided by engineer. + To record speech please grant permission to use Microphone. A beszéd rögzítéséhez adjon engedélyt a Mikrofon használatára. @@ -7045,11 +7262,19 @@ A funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beáll Rejtett profilja megjelenítéséhez írja be a teljes jelszavát a keresőmezőbe a **Csevegési profilok** menüben. No comment provided by engineer. + + To send + No comment provided by engineer. + To support instant push notifications the chat database has to be migrated. Az azonnali push-értesítések támogatásához a csevegési adatbázis átköltöztetése szükséges. No comment provided by engineer. + + To use the servers of **%@**, accept conditions of use. + No comment provided by engineer. + To verify end-to-end encryption with your contact compare (or scan) the code on your devices. A végpontok közötti titkosítás hitelesítéséhez hasonlítsa össze (vagy olvassa be a QR-kódot) az ismerőse eszközén lévő kóddal. @@ -7140,6 +7365,10 @@ A funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beáll Tag feloldása? No comment provided by engineer. + + Undelivered messages + No comment provided by engineer. + Unexpected migration state Váratlan átköltöztetési állapot @@ -7297,6 +7526,10 @@ A kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapc Archívum feltöltése No comment provided by engineer. + + Use %@ + No comment provided by engineer. + Use .onion hosts Onion-kiszolgálók használata @@ -7322,6 +7555,14 @@ A kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapc Jelenlegi profil használata No comment provided by engineer. + + Use for files + No comment provided by engineer. + + + Use for messages + No comment provided by engineer. + Use for new connections Alkalmazás új kapcsolatokhoz @@ -7362,6 +7603,10 @@ A kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapc Kiszolgáló használata No comment provided by engineer. + + Use servers + No comment provided by engineer. + Use the app while in the call. Használja az alkalmazást hívás közben. @@ -7452,11 +7697,19 @@ A kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapc Videók és fájlok 1Gb méretig No comment provided by engineer. + + View conditions + No comment provided by engineer. + View security code Biztonsági kód megtekintése No comment provided by engineer. + + View updated conditions + No comment provided by engineer. + Visible history Látható előzmények @@ -7567,9 +7820,8 @@ A kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapc Amikor egy bejövő hang- vagy videóhívás érkezik. No comment provided by engineer. - - When people request to connect, you can accept or reject it. - Amikor az emberek kapcsolatot kérnek, Ön elfogadhatja vagy elutasíthatja azokat. + + When more than one operator is enabled, none of them has metadata to learn who communicates with whom. No comment provided by engineer. @@ -7729,6 +7981,18 @@ Csatlakozáskérés megismétlése? Ezt a „Megjelenés” menüben módosíthatja. No comment provided by engineer. + + You can configure operators in Network & servers settings. + No comment provided by engineer. + + + You can configure servers via settings. + No comment provided by engineer. + + + You can create it in user picker. + No comment provided by engineer. + You can create it later Létrehozás később @@ -7769,6 +8033,10 @@ Csatlakozáskérés megismétlése? Az „Archivált ismerősökből” továbbra is küldhet üzeneteket neki: %@. No comment provided by engineer. + + You can set connection name, to remember who the link was shared with. + No comment provided by engineer. + You can set lock screen notification preview via settings. A beállításokon keresztül beállíthatja a lezárási képernyő értesítési előnézetét. @@ -7784,11 +8052,6 @@ Csatlakozáskérés megismétlése? Megoszthatja ezt a címet az ismerőseivel, hogy kapcsolatba léphessenek Önnel a(z) **%@** nevű profilján keresztül. No comment provided by engineer. - - You can share your address as a link or QR code - anybody can connect to you. - Megoszthatja a címét egy hivatkozásként vagy QR-kódként – így bárki kapcsolódhat Önhöz. - No comment provided by engineer. - You can start chat via app Settings / Database or by restarting the app A csevegést az alkalmazás „Beállítások / Adatbázis” menüben vagy az alkalmazás újraindításával indíthatja el @@ -7812,23 +8075,23 @@ Csatlakozáskérés megismétlése? You can view invitation link again in connection details. A meghívó-hivatkozást újra megtekintheti a kapcsolat részleteinél. - No comment provided by engineer. + alert message You can't send messages! Nem lehet üzeneteket küldeni! No comment provided by engineer. - - You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them. - Ön szabályozhatja, hogy mely kiszogál(ók)ón keresztül **kapja** az üzeneteket, az ismerősöket - az üzenetküldéshez használt kiszolgálókon. - No comment provided by engineer. - You could not be verified; please try again. Nem sikerült hitelesíteni; próbálja meg újra. No comment provided by engineer. + + You decide who can connect. + Ön dönti el, hogy kivel beszélget. + No comment provided by engineer. + You have already requested connection via this address! Már küldött egy kapcsolatkérést ezen a címen keresztül! @@ -7951,11 +8214,6 @@ Kapcsolatkérés megismétlése? Inkognitóprofilt használ ehhez a csoporthoz - fő profilja megosztásának elkerülése érdekében a meghívók küldése le van tiltva No comment provided by engineer. - - Your %@ servers - %@ nevű profiljához tartozó kiszolgálók - No comment provided by engineer. - Your ICE servers Saját ICE-kiszolgálók @@ -7971,11 +8229,6 @@ Kapcsolatkérés megismétlése? Profil SimpleX-címe No comment provided by engineer. - - Your XFTP servers - Saját XFTP-kiszolgálók - No comment provided by engineer. - Your calls Hívások @@ -8076,16 +8329,15 @@ Kapcsolatkérés megismétlése? Véletlenszerű profil No comment provided by engineer. - - Your server - Saját SMP-kiszolgáló - No comment provided by engineer. - Your server address Saját SMP-kiszolgálójának címe No comment provided by engineer. + + Your servers + No comment provided by engineer. + Your settings Beállítások @@ -8506,6 +8758,10 @@ Kapcsolatkérés megismétlése? lejárt No comment provided by engineer. + + for better metadata privacy. + No comment provided by engineer. + forwarded továbbított @@ -9128,6 +9384,33 @@ utoljára fogadott üzenet: %2$@ + +
+ +
+ + + %d new events + notification body + + + From: %@ + notification body + + + New events + notification + + + New messages + notification + + + New messages in %d chats + notification body + + +
diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/en.lproj/Localizable.strings index cf485752ea..cb83427195 100644 --- a/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/en.lproj/Localizable.strings +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/en.lproj/Localizable.strings @@ -1,9 +1,6 @@ /* No comment provided by engineer. */ "_italic_" = "\\_italic_"; -/* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact."; - /* No comment provided by engineer. */ "*bold*" = "\\*bold*"; @@ -27,4 +24,3 @@ /* No comment provided by engineer. */ "No group!" = "Group not found!"; - diff --git a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff index 488d51d225..3b75e36a86 100644 --- a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff +++ b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff @@ -12,21 +12,6 @@ No comment provided by engineer. - - - - No comment provided by engineer. - - - - - No comment provided by engineer. - - - - - No comment provided by engineer. - ( ( @@ -127,6 +112,14 @@ %@ è verificato/a No comment provided by engineer. + + %@ server + No comment provided by engineer. + + + %@ servers + No comment provided by engineer. + %@ uploaded %@ caricati @@ -352,14 +345,9 @@ ) No comment provided by engineer. - - **Add contact**: to create a new invitation link, or connect via a link you received. - **Aggiungi contatto**: per creare un nuovo link di invito o connetterti tramite un link che hai ricevuto. - No comment provided by engineer. - - - **Add new contact**: to create your one-time QR Code or link for your contact. - **Aggiungi un contatto**: per creare il tuo codice QR o link una tantum per il tuo contatto. + + **Create 1-time link**: to create and share a new invitation link. + **Aggiungi contatto**: per creare un nuovo link di invito. No comment provided by engineer. @@ -367,13 +355,13 @@ **Crea gruppo**: per creare un nuovo gruppo. No comment provided by engineer. - - **More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have. + + **More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata. **Più privato**: controlla messaggi nuovi ogni 20 minuti. Viene condiviso il token del dispositivo con il server di SimpleX Chat, ma non quanti contatti o messaggi hai. No comment provided by engineer. - - **Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app). + + **Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app. **Il più privato**: non usare il server di notifica di SimpleX Chat, controlla i messaggi periodicamente in secondo piano (dipende da quanto spesso usi l'app). No comment provided by engineer. @@ -387,11 +375,15 @@ **Nota bene**: NON potrai recuperare o cambiare la password se la perdi. No comment provided by engineer. - - **Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from. + + **Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from. **Consigliato**: vengono inviati il token del dispositivo e le notifiche al server di notifica di SimpleX Chat, ma non il contenuto del messaggio,la sua dimensione o il suo mittente. No comment provided by engineer. + + **Scan / Paste link**: to connect via a link you received. + No comment provided by engineer. + **Warning**: Instant push notifications require passphrase saved in Keychain. **Attenzione**: le notifiche push istantanee richiedono una password salvata nel portachiavi. @@ -498,6 +490,14 @@ 1 settimana time interval + + 1-time link + No comment provided by engineer. + + + 1-time link can be used *with one contact only* - share in person or via any messenger. + No comment provided by engineer. + 5 minutes 5 minuti @@ -567,21 +567,11 @@ Interrompere il cambio di indirizzo? No comment provided by engineer. - - About SimpleX - Riguardo SimpleX - No comment provided by engineer. - About SimpleX Chat Riguardo SimpleX Chat No comment provided by engineer. - - About SimpleX address - Info sull'indirizzo SimpleX - No comment provided by engineer. - Accent Principale @@ -594,6 +584,10 @@ accept incoming call via notification swipe action + + Accept conditions + No comment provided by engineer. + Accept connection request? Accettare la richiesta di connessione? @@ -610,6 +604,10 @@ accept contact request via notification swipe action + + Accepted conditions + No comment provided by engineer. + Acknowledged Riconosciuto @@ -630,16 +628,6 @@ Aggiungi l'indirizzo al tuo profilo, in modo che i tuoi contatti possano condividerlo con altre persone. L'aggiornamento del profilo verrà inviato ai tuoi contatti. No comment provided by engineer. - - Add contact - Aggiungi contatto - No comment provided by engineer. - - - Add preset servers - Aggiungi server preimpostati - No comment provided by engineer. - Add profile Aggiungi profilo @@ -665,6 +653,14 @@ Aggiungi messaggio di benvenuto No comment provided by engineer. + + Added media & file servers + No comment provided by engineer. + + + Added message servers + No comment provided by engineer. + Additional accent Principale aggiuntivo @@ -690,6 +686,14 @@ Il cambio di indirizzo verrà interrotto. Verrà usato il vecchio indirizzo di ricezione. No comment provided by engineer. + + Address or 1-time link? + No comment provided by engineer. + + + Address settings + No comment provided by engineer. + Admins can block a member for all. Gli amministratori possono bloccare un membro per tutti. @@ -735,6 +739,10 @@ Tutti i membri del gruppo resteranno connessi. No comment provided by engineer. + + All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages. + No comment provided by engineer. + All messages will be deleted - this cannot be undone! Tutti i messaggi verranno eliminati, non è reversibile! @@ -915,6 +923,11 @@ Rispondi alla chiamata No comment provided by engineer. + + Anybody can host servers. + Chiunque può installare i server. + No comment provided by engineer. + App build: %@ Build dell'app: %@ @@ -1258,7 +1271,8 @@ Cancel Annulla - alert button + alert action + alert button Cancel migration @@ -1341,6 +1355,10 @@ authentication reason set passcode view + + Change user profiles + authentication reason + Chat archive Archivio chat @@ -1426,10 +1444,18 @@ Chat No comment provided by engineer. + + Check messages every 20 min. + No comment provided by engineer. + + + Check messages when allowed. + No comment provided by engineer. + Check server address and try again. Controlla l'indirizzo del server e riprova. - No comment provided by engineer. + alert title Chinese and Spanish interface @@ -1516,16 +1542,47 @@ Completato No comment provided by engineer. + + Conditions accepted on: %@. + No comment provided by engineer. + + + Conditions are accepted for the operator(s): **%@**. + No comment provided by engineer. + + + Conditions are already accepted for following operator(s): **%@**. + No comment provided by engineer. + + + Conditions of use + No comment provided by engineer. + + + Conditions will be accepted for enabled operators after 30 days. + No comment provided by engineer. + + + Conditions will be accepted for operator(s): **%@**. + No comment provided by engineer. + + + Conditions will be accepted for the operator(s): **%@**. + No comment provided by engineer. + + + Conditions will be accepted on: %@. + No comment provided by engineer. + + + Conditions will be automatically accepted for enabled operators on: %@. + No comment provided by engineer. + Configure ICE servers Configura server ICE No comment provided by engineer. - - Configured %@ servers - Configurati %@ server - No comment provided by engineer. - Confirm Conferma @@ -1715,6 +1772,10 @@ Questo è il tuo link una tantum! Richiesta di connessione inviata! No comment provided by engineer. + + Connection security + No comment provided by engineer. + Connection terminated Connessione terminata @@ -1830,6 +1891,10 @@ Questo è il tuo link una tantum! Crea No comment provided by engineer. + + Create 1-time link + No comment provided by engineer. + Create SimpleX address Crea indirizzo SimpleX @@ -1840,11 +1905,6 @@ Questo è il tuo link una tantum! Crea un gruppo usando un profilo casuale. No comment provided by engineer. - - Create an address to let people connect with you. - Crea un indirizzo per consentire alle persone di connettersi con te. - No comment provided by engineer. - Create file Crea file @@ -1925,6 +1985,10 @@ Questo è il tuo link una tantum! Codice di accesso attuale No comment provided by engineer. + + Current conditions text couldn't be loaded, you can review conditions via this link: + No comment provided by engineer. + Current passphrase… Password attuale… @@ -2081,7 +2145,8 @@ Questo è il tuo link una tantum! Delete Elimina - chat item action + alert action + chat item action swipe action @@ -2299,6 +2364,10 @@ Questo è il tuo link una tantum! Errori di eliminazione No comment provided by engineer. + + Delivered even when Apple drops them. + No comment provided by engineer. + Delivery Consegna @@ -2580,6 +2649,10 @@ Questo è il tuo link una tantum! Durata No comment provided by engineer. + + E2E encrypted notifications. + No comment provided by engineer. + Edit Modifica @@ -2600,6 +2673,10 @@ Questo è il tuo link una tantum! Attiva (mantieni sostituzioni) No comment provided by engineer. + + Enable Flux + No comment provided by engineer. + Enable SimpleX Lock Attiva SimpleX Lock @@ -2805,6 +2882,10 @@ Questo è il tuo link una tantum! Errore nell'interruzione del cambio di indirizzo No comment provided by engineer. + + Error accepting conditions + alert title + Error accepting contact request Errore nell'accettazione della richiesta di contatto @@ -2820,6 +2901,10 @@ Questo è il tuo link una tantum! Errore di aggiunta membro/i No comment provided by engineer. + + Error adding server + alert title + Error changing address Errore nella modifica dell'indirizzo @@ -2960,10 +3045,9 @@ Questo è il tuo link una tantum! Errore di ingresso nel gruppo No comment provided by engineer. - - Error loading %@ servers - Errore nel caricamento dei server %@ - No comment provided by engineer. + + Error loading servers + alert title Error migrating settings @@ -3000,11 +3084,6 @@ Questo è il tuo link una tantum! Errore di azzeramento statistiche No comment provided by engineer. - - Error saving %@ servers - Errore nel salvataggio dei server %@ - No comment provided by engineer. - Error saving ICE servers Errore nel salvataggio dei server ICE @@ -3025,6 +3104,10 @@ Questo è il tuo link una tantum! Errore nel salvataggio della password nel portachiavi No comment provided by engineer. + + Error saving servers + alert title + Error saving settings Errore di salvataggio delle impostazioni @@ -3095,6 +3178,10 @@ Questo è il tuo link una tantum! Errore nell'aggiornamento del messaggio No comment provided by engineer. + + Error updating server + alert title + Error updating settings Errore nell'aggiornamento delle impostazioni @@ -3140,6 +3227,10 @@ Questo è il tuo link una tantum! Errori No comment provided by engineer. + + Errors in servers configuration. + servers error + Even when disabled in the conversation. Anche quando disattivato nella conversazione. @@ -3342,11 +3433,27 @@ Questo è il tuo link una tantum! Correzione non supportata dal membro del gruppo No comment provided by engineer. + + For chat profile %@: + servers error + For console Per console No comment provided by engineer. + + For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server. + No comment provided by engineer. + + + For private routing + No comment provided by engineer. + + + For social media + No comment provided by engineer. + Forward Inoltra @@ -3656,9 +3763,12 @@ Errore: %2$@ Come funziona SimpleX No comment provided by engineer. - - How it works - Come funziona + + How it affects privacy + No comment provided by engineer. + + + How it helps privacy No comment provided by engineer. @@ -3731,8 +3841,8 @@ Errore: %2$@ Immediatamente No comment provided by engineer. - - Immune to spam and abuse + + Immune to spam Immune a spam e abusi No comment provided by engineer. @@ -3873,6 +3983,11 @@ Altri miglioramenti sono in arrivo! Installa [Simplex Chat per terminale](https://github.com/simplex-chat/simplex-chat) No comment provided by engineer. + + Instant + Istantaneamente + No comment provided by engineer. + Instant push notifications will be hidden! @@ -3880,11 +3995,6 @@ Altri miglioramenti sono in arrivo! No comment provided by engineer. - - Instantly - Istantaneamente - No comment provided by engineer. - Interface Interfaccia @@ -3933,7 +4043,7 @@ Altri miglioramenti sono in arrivo! Invalid server address! Indirizzo del server non valido! - No comment provided by engineer. + alert title Invalid status @@ -4061,7 +4171,7 @@ Questo è il tuo link per il gruppo %@! Keep Tieni - No comment provided by engineer. + alert action Keep conversation @@ -4076,7 +4186,7 @@ Questo è il tuo link per il gruppo %@! Keep unused invitation? Tenere l'invito inutilizzato? - No comment provided by engineer. + alert title Keep your connections @@ -4163,11 +4273,6 @@ Questo è il tuo link per il gruppo %@! Messaggi in diretta No comment provided by engineer. - - Local - Locale - No comment provided by engineer. - Local name Nome locale @@ -4188,11 +4293,6 @@ Questo è il tuo link per il gruppo %@! Modalità di blocco No comment provided by engineer. - - Make a private connection - Crea una connessione privata - No comment provided by engineer. - Make one message disappear Fai sparire un messaggio @@ -4203,21 +4303,11 @@ Questo è il tuo link per il gruppo %@! Rendi privato il profilo! No comment provided by engineer. - - Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@). - Assicurati che gli indirizzi dei server %@ siano nel formato corretto, uno per riga e non doppi (%@). - No comment provided by engineer. - Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated. Assicurati che gli indirizzi dei server WebRTC ICE siano nel formato corretto, uno per riga e non doppi. No comment provided by engineer. - - Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?* - Molte persone hanno chiesto: *se SimpleX non ha identificatori utente, come può recapitare i messaggi?* - No comment provided by engineer. - Mark deleted for everyone Contrassegna eliminato per tutti @@ -4498,6 +4588,10 @@ Questo è il tuo link per il gruppo %@! Connessione di rete più affidabile. No comment provided by engineer. + + More reliable notifications + No comment provided by engineer. + Most likely this connection is deleted. Probabilmente questa connessione è stata eliminata. @@ -4533,6 +4627,10 @@ Questo è il tuo link per il gruppo %@! Connessione di rete No comment provided by engineer. + + Network decentralization + No comment provided by engineer. + Network issues - message expired after many attempts to send it. Problemi di rete - messaggio scaduto dopo molti tentativi di inviarlo. @@ -4543,6 +4641,10 @@ Questo è il tuo link per il gruppo %@! Gestione della rete No comment provided by engineer. + + Network operator + No comment provided by engineer. + Network settings Impostazioni di rete @@ -4603,6 +4705,10 @@ Questo è il tuo link per il gruppo %@! Nuovo nome da mostrare No comment provided by engineer. + + New events + notification + New in %@ Novità nella %@ @@ -4628,6 +4734,10 @@ Questo è il tuo link per il gruppo %@! Nuova password… No comment provided by engineer. + + New server + No comment provided by engineer. + No No @@ -4683,6 +4793,14 @@ Questo è il tuo link per il gruppo %@! Nessuna informazione, prova a ricaricare No comment provided by engineer. + + No media & file servers. + servers error + + + No message servers. + servers error + No network connection Nessuna connessione di rete @@ -4703,11 +4821,37 @@ Questo è il tuo link per il gruppo %@! Nessuna autorizzazione per registrare messaggi vocali No comment provided by engineer. + + No push server + Locale + No comment provided by engineer. + No received or sent files Nessun file ricevuto o inviato No comment provided by engineer. + + No servers for private message routing. + servers error + + + No servers to receive files. + servers error + + + No servers to receive messages. + servers error + + + No servers to send files. + servers error + + + No user identifiers. + Nessun identificatore utente. + No comment provided by engineer. + Not compatible! Non compatibile! @@ -4733,6 +4877,10 @@ Questo è il tuo link per il gruppo %@! Le notifiche sono disattivate! No comment provided by engineer. + + Notifications privacy + No comment provided by engineer. + Now admins can: - delete members' messages. @@ -4791,8 +4939,8 @@ Richiede l'attivazione della VPN. Gli host Onion non verranno usati. No comment provided by engineer. - - Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**. + + Only client devices store user profiles, contacts, groups, and messages. Solo i dispositivi client archiviano profili utente, i contatti, i gruppi e i messaggi inviati con la **crittografia end-to-end a 2 livelli**. No comment provided by engineer. @@ -4876,6 +5024,10 @@ Richiede l'attivazione della VPN. Apri le impostazioni No comment provided by engineer. + + Open changes + No comment provided by engineer. + Open chat Apri chat @@ -4886,6 +5038,10 @@ Richiede l'attivazione della VPN. Apri la console della chat authentication reason + + Open conditions + No comment provided by engineer. + Open group Apri gruppo @@ -4896,26 +5052,19 @@ Richiede l'attivazione della VPN. Apri migrazione ad un altro dispositivo authentication reason - - Open server settings - Apri impostazioni server - No comment provided by engineer. - - - Open user profiles - Apri i profili utente - authentication reason - - - Open-source protocol and code – anybody can run the servers. - Protocollo e codice open source: chiunque può gestire i server. - No comment provided by engineer. - Opening app… Apertura dell'app… No comment provided by engineer. + + Operator + No comment provided by engineer. + + + Operator server + alert title + Or paste archive link O incolla il link dell'archivio @@ -4936,16 +5085,15 @@ Richiede l'attivazione della VPN. O mostra questo codice No comment provided by engineer. + + Or to share privately + No comment provided by engineer. + Other Altro No comment provided by engineer. - - Other %@ servers - Altri %@ server - No comment provided by engineer. - Other file errors: %@ @@ -5028,13 +5176,8 @@ Richiede l'attivazione della VPN. In attesa No comment provided by engineer. - - People can connect to you only via the links you share. - Le persone possono connettersi a te solo tramite i link che condividi. - No comment provided by engineer. - - - Periodically + + Periodic Periodicamente No comment provided by engineer. @@ -5157,16 +5300,15 @@ Errore: %@ Conserva la bozza dell'ultimo messaggio, con gli allegati. No comment provided by engineer. - - Preset server - Server preimpostato - No comment provided by engineer. - Preset server address Indirizzo server preimpostato No comment provided by engineer. + + Preset servers + No comment provided by engineer. + Preview Anteprima @@ -5245,7 +5387,7 @@ Errore: %@ Profile update will be sent to your contacts. L'aggiornamento del profilo verrà inviato ai tuoi contatti. - No comment provided by engineer. + alert message Prohibit audio/video calls. @@ -5339,6 +5481,10 @@ Attivalo nelle impostazioni *Rete e server*. Il proxy richiede una password No comment provided by engineer. + + Push Notifications + No comment provided by engineer. + Push notifications Notifiche push @@ -5379,26 +5525,21 @@ Attivalo nelle impostazioni *Rete e server*. Leggi tutto No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address). - Maggiori informazioni nella [Guida per l'utente](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address). - No comment provided by engineer. - Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). Leggi di più nella [Guida utente](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). No comment provided by engineer. + + Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). + Maggiori informazioni nella [Guida per l'utente](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). + No comment provided by engineer. + Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends). Maggiori informazioni nella [Guida per l'utente](https://simplex.chat/docs/guide/readme.html#connect-to-friends). No comment provided by engineer. - - Read more in our GitHub repository. - Maggiori informazioni nel nostro repository GitHub. - No comment provided by engineer. - Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme). Maggiori informazioni nel nostro [repository GitHub](https://github.com/simplex-chat/simplex-chat#readme). @@ -5715,6 +5856,14 @@ Attivalo nelle impostazioni *Rete e server*. Rivela chat item action + + Review conditions + No comment provided by engineer. + + + Review later + No comment provided by engineer. + Revoke Revoca @@ -5760,6 +5909,14 @@ Attivalo nelle impostazioni *Rete e server*. Gruppi più sicuri No comment provided by engineer. + + Same conditions will apply to operator **%@**. + No comment provided by engineer. + + + Same conditions will apply to operator(s): **%@**. + No comment provided by engineer. + Save Salva @@ -5829,7 +5986,7 @@ Attivalo nelle impostazioni *Rete e server*. Save servers? Salvare i server? - No comment provided by engineer. + alert title Save welcome message? @@ -6041,11 +6198,6 @@ Attivalo nelle impostazioni *Rete e server*. Invia notifiche No comment provided by engineer. - - Send notifications: - Invia notifiche: - No comment provided by engineer. - Send questions and ideas Invia domande e idee @@ -6171,6 +6323,10 @@ Attivalo nelle impostazioni *Rete e server*. Server No comment provided by engineer. + + Server added to operator %@. + alert message + Server address Indirizzo server @@ -6186,6 +6342,18 @@ Attivalo nelle impostazioni *Rete e server*. L'indirizzo del server è incompatibile con le impostazioni di rete: %@. No comment provided by engineer. + + Server operator changed. + alert title + + + Server operators + No comment provided by engineer. + + + Server protocol changed. + alert title + Server requires authorization to create queues, check password Il server richiede l'autorizzazione di creare code, controlla la password @@ -6304,22 +6472,35 @@ Attivalo nelle impostazioni *Rete e server*. Share Condividi - chat item action + alert action + chat item action Share 1-time link Condividi link una tantum No comment provided by engineer. + + Share 1-time link with a friend + No comment provided by engineer. + + + Share SimpleX address on social media. + No comment provided by engineer. + Share address Condividi indirizzo No comment provided by engineer. + + Share address publicly + No comment provided by engineer. + Share address with contacts? Condividere l'indirizzo con i contatti? - No comment provided by engineer. + alert title Share from other apps. @@ -6436,6 +6617,14 @@ Attivalo nelle impostazioni *Rete e server*. Indirizzo SimpleX No comment provided by engineer. + + SimpleX address and 1-time links are safe to share via any messenger. + No comment provided by engineer. + + + SimpleX address or 1-time link? + No comment provided by engineer. + SimpleX contact address Indirizzo di contatto SimpleX @@ -6526,6 +6715,11 @@ Attivalo nelle impostazioni *Rete e server*. Si sono verificati alcuni errori non fatali durante l'importazione: No comment provided by engineer. + + Some servers failed the test: +%@ + alert message + Somebody Qualcuno @@ -6609,12 +6803,12 @@ Attivalo nelle impostazioni *Rete e server*. Stop sharing Smetti di condividere - No comment provided by engineer. + alert action Stop sharing address? Smettere di condividere l'indirizzo? - No comment provided by engineer. + alert title Stopping chat @@ -6764,7 +6958,7 @@ Attivalo nelle impostazioni *Rete e server*. Tests failed! Test falliti! - No comment provided by engineer. + alert title Thank you for installing SimpleX Chat! @@ -6781,11 +6975,6 @@ Attivalo nelle impostazioni *Rete e server*. Grazie agli utenti – contribuite via Weblate! No comment provided by engineer. - - The 1st platform without any user identifiers – private by design. - La prima piattaforma senza alcun identificatore utente – privata by design. - No comment provided by engineer. - The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised. @@ -6798,6 +6987,10 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa.L'app può avvisarti quando ricevi messaggi o richieste di contatto: apri le impostazioni per attivare. No comment provided by engineer. + + The app protects your privacy by using different operators in each conversation. + No comment provided by engineer. + The app will ask to confirm downloads from unknown file servers (except .onion). L'app chiederà di confermare i download da server di file sconosciuti (eccetto .onion). @@ -6813,6 +7006,10 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa.Il codice che hai scansionato non è un codice QR di link SimpleX. No comment provided by engineer. + + The connection reached the limit of undelivered messages, your contact may be offline. + No comment provided by engineer. + The connection you accepted will be cancelled! La connessione che hai accettato verrà annullata! @@ -6833,6 +7030,11 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa.La crittografia funziona e il nuovo accordo sulla crittografia non è richiesto. Potrebbero verificarsi errori di connessione! No comment provided by engineer. + + The future of messaging + La nuova generazione di messaggistica privata + No comment provided by engineer. + The hash of the previous message is different. L'hash del messaggio precedente è diverso. @@ -6858,11 +7060,6 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa.I messaggi verranno contrassegnati come moderati per tutti i membri. No comment provided by engineer. - - The next generation of private messaging - La nuova generazione di messaggistica privata - No comment provided by engineer. - The old database was not removed during the migration, it can be deleted. Il database vecchio non è stato rimosso durante la migrazione, può essere eliminato. @@ -6873,6 +7070,10 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa.Il profilo è condiviso solo con i tuoi contatti. No comment provided by engineer. + + The second preset operator in the app! + No comment provided by engineer. + The second tick we missed! ✅ Il secondo segno di spunta che ci mancava! ✅ @@ -6888,6 +7089,10 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa.I server per le nuove connessioni del profilo di chat attuale **%@**. No comment provided by engineer. + + The servers for new files of your current chat profile **%@**. + No comment provided by engineer. + The text you pasted is not a SimpleX link. Il testo che hai incollato non è un link SimpleX. @@ -6903,6 +7108,10 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa.Temi No comment provided by engineer. + + These conditions will also apply for: **%@**. + No comment provided by engineer. + These settings are for your current profile **%@**. Queste impostazioni sono per il tuo profilo attuale **%@**. @@ -7003,9 +7212,8 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa.Per creare una nuova connessione No comment provided by engineer. - - To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts. - Per proteggere la privacy, invece degli ID utente utilizzati da tutte le altre piattaforme, SimpleX ha identificatori per le code di messaggi, separati per ciascuno dei tuoi contatti. + + To protect against your link being replaced, you can compare contact security codes. No comment provided by engineer. @@ -7025,6 +7233,15 @@ You will be prompted to complete authentication before this feature is enabled.< Ti verrà chiesto di completare l'autenticazione prima di attivare questa funzionalità. No comment provided by engineer. + + To protect your privacy, SimpleX uses separate IDs for each of your contacts. + Per proteggere la privacy, invece degli ID utente utilizzati da tutte le altre piattaforme, SimpleX ha identificatori per le code di messaggi, separati per ciascuno dei tuoi contatti. + No comment provided by engineer. + + + To receive + No comment provided by engineer. + To record speech please grant permission to use Microphone. Per registrare l'audio, concedi l'autorizzazione di usare il microfono. @@ -7045,11 +7262,19 @@ Ti verrà chiesto di completare l'autenticazione prima di attivare questa funzio Per rivelare il tuo profilo nascosto, inserisci una password completa in un campo di ricerca nella pagina **I tuoi profili di chat**. No comment provided by engineer. + + To send + No comment provided by engineer. + To support instant push notifications the chat database has to be migrated. Per supportare le notifiche push istantanee, il database della chat deve essere migrato. No comment provided by engineer. + + To use the servers of **%@**, accept conditions of use. + No comment provided by engineer. + To verify end-to-end encryption with your contact compare (or scan) the code on your devices. Per verificare la crittografia end-to-end con il tuo contatto, confrontate (o scansionate) il codice sui vostri dispositivi. @@ -7140,6 +7365,10 @@ Ti verrà chiesto di completare l'autenticazione prima di attivare questa funzio Sbloccare il membro? No comment provided by engineer. + + Undelivered messages + No comment provided by engineer. + Unexpected migration state Stato di migrazione imprevisto @@ -7297,6 +7526,10 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Invio dell'archivio No comment provided by engineer. + + Use %@ + No comment provided by engineer. + Use .onion hosts Usa gli host .onion @@ -7322,6 +7555,14 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Usa il profilo attuale No comment provided by engineer. + + Use for files + No comment provided by engineer. + + + Use for messages + No comment provided by engineer. + Use for new connections Usa per connessioni nuove @@ -7362,6 +7603,10 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Usa il server No comment provided by engineer. + + Use servers + No comment provided by engineer. + Use the app while in the call. Usa l'app mentre sei in chiamata. @@ -7452,11 +7697,19 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Video e file fino a 1 GB No comment provided by engineer. + + View conditions + No comment provided by engineer. + View security code Vedi codice di sicurezza No comment provided by engineer. + + View updated conditions + No comment provided by engineer. + Visible history Cronologia visibile @@ -7567,9 +7820,8 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Quando si connettono le chiamate audio e video. No comment provided by engineer. - - When people request to connect, you can accept or reject it. - Quando le persone chiedono di connettersi, puoi accettare o rifiutare. + + When more than one operator is enabled, none of them has metadata to learn who communicates with whom. No comment provided by engineer. @@ -7729,6 +7981,18 @@ Ripetere la richiesta di ingresso? Puoi cambiarlo nelle impostazioni dell'aspetto. No comment provided by engineer. + + You can configure operators in Network & servers settings. + No comment provided by engineer. + + + You can configure servers via settings. + No comment provided by engineer. + + + You can create it in user picker. + No comment provided by engineer. + You can create it later Puoi crearlo più tardi @@ -7769,6 +8033,10 @@ Ripetere la richiesta di ingresso? Puoi inviare messaggi a %@ dai contatti archiviati. No comment provided by engineer. + + You can set connection name, to remember who the link was shared with. + No comment provided by engineer. + You can set lock screen notification preview via settings. Puoi impostare l'anteprima della notifica nella schermata di blocco tramite le impostazioni. @@ -7784,11 +8052,6 @@ Ripetere la richiesta di ingresso? Puoi condividere questo indirizzo con i tuoi contatti per consentire loro di connettersi con **%@**. No comment provided by engineer. - - You can share your address as a link or QR code - anybody can connect to you. - Puoi condividere il tuo indirizzo come link o come codice QR: chiunque potrà connettersi a te. - No comment provided by engineer. - You can start chat via app Settings / Database or by restarting the app Puoi avviare la chat via Impostazioni / Database o riavviando l'app @@ -7812,23 +8075,23 @@ Ripetere la richiesta di ingresso? You can view invitation link again in connection details. Puoi vedere di nuovo il link di invito nei dettagli di connessione. - No comment provided by engineer. + alert message You can't send messages! Non puoi inviare messaggi! No comment provided by engineer. - - You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them. - Tu decidi attraverso quale/i server **ricevere** i messaggi, i tuoi contatti quali server usi per inviare loro i messaggi. - No comment provided by engineer. - You could not be verified; please try again. Non è stato possibile verificarti, riprova. No comment provided by engineer. + + You decide who can connect. + Sei tu a decidere chi può connettersi. + No comment provided by engineer. + You have already requested connection via this address! Hai già richiesto la connessione tramite questo indirizzo! @@ -7951,11 +8214,6 @@ Ripetere la richiesta di connessione? Stai usando un profilo in incognito per questo gruppo: per impedire la condivisione del tuo profilo principale non è consentito invitare contatti No comment provided by engineer. - - Your %@ servers - I tuoi server %@ - No comment provided by engineer. - Your ICE servers I tuoi server ICE @@ -7971,11 +8229,6 @@ Ripetere la richiesta di connessione? Il tuo indirizzo SimpleX No comment provided by engineer. - - Your XFTP servers - I tuoi server XFTP - No comment provided by engineer. - Your calls Le tue chiamate @@ -8076,16 +8329,15 @@ Ripetere la richiesta di connessione? Il tuo profilo casuale No comment provided by engineer. - - Your server - Il tuo server - No comment provided by engineer. - Your server address L'indirizzo del tuo server No comment provided by engineer. + + Your servers + No comment provided by engineer. + Your settings Le tue impostazioni @@ -8506,6 +8758,10 @@ Ripetere la richiesta di connessione? scaduto No comment provided by engineer. + + for better metadata privacy. + No comment provided by engineer. + forwarded inoltrato @@ -9128,6 +9384,33 @@ ultimo msg ricevuto: %2$@ + +
+ +
+ + + %d new events + notification body + + + From: %@ + notification body + + + New events + notification + + + New messages + notification + + + New messages in %d chats + notification body + + +
diff --git a/apps/ios/SimpleX Localizations/it.xcloc/Source Contents/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/it.xcloc/Source Contents/en.lproj/Localizable.strings index cf485752ea..cb83427195 100644 --- a/apps/ios/SimpleX Localizations/it.xcloc/Source Contents/en.lproj/Localizable.strings +++ b/apps/ios/SimpleX Localizations/it.xcloc/Source Contents/en.lproj/Localizable.strings @@ -1,9 +1,6 @@ /* No comment provided by engineer. */ "_italic_" = "\\_italic_"; -/* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact."; - /* No comment provided by engineer. */ "*bold*" = "\\*bold*"; @@ -27,4 +24,3 @@ /* No comment provided by engineer. */ "No group!" = "Group not found!"; - diff --git a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff index 4fa8144d91..7f97220bc5 100644 --- a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff +++ b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff @@ -12,21 +12,6 @@ No comment provided by engineer. - - - - No comment provided by engineer. - - - - - No comment provided by engineer. - - - - - No comment provided by engineer. - ( ( @@ -127,6 +112,14 @@ %@ は検証されています No comment provided by engineer. + + %@ server + No comment provided by engineer. + + + %@ servers + No comment provided by engineer. + %@ uploaded %@ アップロード済 @@ -346,28 +339,23 @@ ) No comment provided by engineer. - - **Add contact**: to create a new invitation link, or connect via a link you received. + + **Create 1-time link**: to create and share a new invitation link. **コンタクトの追加**: 新しい招待リンクを作成するか、受け取ったリンクから接続します。 No comment provided by engineer. - - **Add new contact**: to create your one-time QR Code or link for your contact. - **新しい連絡先を追加**: 連絡先のワンタイム QR コードまたはリンクを作成します。 - No comment provided by engineer. - **Create group**: to create a new group. **グループ作成**: 新しいグループを作成する。 No comment provided by engineer. - - **More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have. + + **More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata. **よりプライベート**: 20 分ごとに新しいメッセージを確認します。 デバイス トークンは SimpleX Chat サーバーと共有されますが、連絡先やメッセージの数は共有されません。 No comment provided by engineer. - - **Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app). + + **Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app. **最もプライベート**: SimpleX Chat 通知サーバーを使用せず、バックグラウンドで定期的にメッセージをチェックします (アプリの使用頻度によって異なります)。 No comment provided by engineer. @@ -381,11 +369,15 @@ **注意**: パスフレーズを紛失すると、パスフレーズを復元または変更できなくなります。 No comment provided by engineer. - - **Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from. + + **Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from. **推奨**: デバイス トークンと通知は SimpleX Chat 通知サーバーに送信されますが、メッセージの内容、サイズ、送信者は送信されません。 No comment provided by engineer. + + **Scan / Paste link**: to connect via a link you received. + No comment provided by engineer. + **Warning**: Instant push notifications require passphrase saved in Keychain. **警告**: 即時の プッシュ通知には、キーチェーンに保存されたパスフレーズが必要です。 @@ -486,6 +478,14 @@ 1週間 time interval + + 1-time link + No comment provided by engineer. + + + 1-time link can be used *with one contact only* - share in person or via any messenger. + No comment provided by engineer. + 5 minutes 5分 @@ -555,21 +555,11 @@ アドレス変更を中止しますか? No comment provided by engineer. - - About SimpleX - SimpleXについて - No comment provided by engineer. - About SimpleX Chat SimpleX Chat について No comment provided by engineer. - - About SimpleX address - SimpleXアドレスについて - No comment provided by engineer. - Accent No comment provided by engineer. @@ -581,6 +571,10 @@ accept incoming call via notification swipe action + + Accept conditions + No comment provided by engineer. + Accept connection request? 接続要求を承認? @@ -597,6 +591,10 @@ accept contact request via notification swipe action + + Accepted conditions + No comment provided by engineer. + Acknowledged No comment provided by engineer. @@ -614,15 +612,6 @@ プロフィールにアドレスを追加し、連絡先があなたのアドレスを他の人と共有できるようにします。プロフィールの更新は連絡先に送信されます。 No comment provided by engineer. - - Add contact - No comment provided by engineer. - - - Add preset servers - 既存サーバを追加 - No comment provided by engineer. - Add profile プロフィールを追加 @@ -648,6 +637,14 @@ ウェルカムメッセージを追加 No comment provided by engineer. + + Added media & file servers + No comment provided by engineer. + + + Added message servers + No comment provided by engineer. + Additional accent No comment provided by engineer. @@ -670,6 +667,14 @@ アドレス変更は中止されます。古い受信アドレスが使用されます。 No comment provided by engineer. + + Address or 1-time link? + No comment provided by engineer. + + + Address settings + No comment provided by engineer. + Admins can block a member for all. No comment provided by engineer. @@ -712,6 +717,10 @@ グループ全員の接続が継続します。 No comment provided by engineer. + + All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages. + No comment provided by engineer. + All messages will be deleted - this cannot be undone! No comment provided by engineer. @@ -885,6 +894,11 @@ 通話に応答 No comment provided by engineer. + + Anybody can host servers. + プロトコル技術とコードはオープンソースで、どなたでもご自分のサーバを運用できます。 + No comment provided by engineer. + App build: %@ アプリのビルド: %@ @@ -1196,7 +1210,8 @@ Cancel 中止 - alert button + alert action + alert button Cancel migration @@ -1275,6 +1290,10 @@ authentication reason set passcode view + + Change user profiles + authentication reason + Chat archive チャットのアーカイブ @@ -1353,10 +1372,18 @@ チャット No comment provided by engineer. + + Check messages every 20 min. + No comment provided by engineer. + + + Check messages when allowed. + No comment provided by engineer. + Check server address and try again. サーバのアドレスを確認してから再度試してください。 - No comment provided by engineer. + alert title Chinese and Spanish interface @@ -1435,15 +1462,47 @@ Completed No comment provided by engineer. + + Conditions accepted on: %@. + No comment provided by engineer. + + + Conditions are accepted for the operator(s): **%@**. + No comment provided by engineer. + + + Conditions are already accepted for following operator(s): **%@**. + No comment provided by engineer. + + + Conditions of use + No comment provided by engineer. + + + Conditions will be accepted for enabled operators after 30 days. + No comment provided by engineer. + + + Conditions will be accepted for operator(s): **%@**. + No comment provided by engineer. + + + Conditions will be accepted for the operator(s): **%@**. + No comment provided by engineer. + + + Conditions will be accepted on: %@. + No comment provided by engineer. + + + Conditions will be automatically accepted for enabled operators on: %@. + No comment provided by engineer. + Configure ICE servers ICEサーバを設定 No comment provided by engineer. - - Configured %@ servers - No comment provided by engineer. - Confirm 確認 @@ -1609,6 +1668,10 @@ This is your own one-time link! 接続リクエストを送信しました! No comment provided by engineer. + + Connection security + No comment provided by engineer. + Connection terminated No comment provided by engineer. @@ -1714,6 +1777,10 @@ This is your own one-time link! 作成 No comment provided by engineer. + + Create 1-time link + No comment provided by engineer. + Create SimpleX address SimpleXアドレスの作成 @@ -1723,11 +1790,6 @@ This is your own one-time link! Create a group using a random profile. No comment provided by engineer. - - Create an address to let people connect with you. - 人とつながるためのアドレスを作成する。 - No comment provided by engineer. - Create file ファイルを作成 @@ -1801,6 +1863,10 @@ This is your own one-time link! 現在のパスコード No comment provided by engineer. + + Current conditions text couldn't be loaded, you can review conditions via this link: + No comment provided by engineer. + Current passphrase… 現在の暗証フレーズ… @@ -1952,7 +2018,8 @@ This is your own one-time link! Delete 削除 - chat item action + alert action + chat item action swipe action @@ -2160,6 +2227,10 @@ This is your own one-time link! Deletion errors No comment provided by engineer. + + Delivered even when Apple drops them. + No comment provided by engineer. + Delivery 配信 @@ -2417,6 +2488,10 @@ This is your own one-time link! 間隔 No comment provided by engineer. + + E2E encrypted notifications. + No comment provided by engineer. + Edit 編集する @@ -2437,6 +2512,10 @@ This is your own one-time link! 有効にする(設定の優先を維持) No comment provided by engineer. + + Enable Flux + No comment provided by engineer. + Enable SimpleX Lock SimpleXロックを有効にする @@ -2631,6 +2710,10 @@ This is your own one-time link! アドレス変更中止エラー No comment provided by engineer. + + Error accepting conditions + alert title + Error accepting contact request 連絡先リクエストの承諾にエラー発生 @@ -2646,6 +2729,10 @@ This is your own one-time link! メンバー追加にエラー発生 No comment provided by engineer. + + Error adding server + alert title + Error changing address アドレス変更にエラー発生 @@ -2779,10 +2866,9 @@ This is your own one-time link! グループ参加にエラー発生 No comment provided by engineer. - - Error loading %@ servers - %@ サーバーのロード中にエラーが発生 - No comment provided by engineer. + + Error loading servers + alert title Error migrating settings @@ -2814,11 +2900,6 @@ This is your own one-time link! Error resetting statistics No comment provided by engineer. - - Error saving %@ servers - %@ サーバの保存エラー - No comment provided by engineer. - Error saving ICE servers ICEサーバ保存にエラー発生 @@ -2839,6 +2920,10 @@ This is your own one-time link! キーチェーンにパスフレーズを保存にエラー発生 No comment provided by engineer. + + Error saving servers + alert title + Error saving settings when migrating @@ -2905,6 +2990,10 @@ This is your own one-time link! メッセージの更新にエラー発生 No comment provided by engineer. + + Error updating server + alert title + Error updating settings 設定の更新にエラー発生 @@ -2947,6 +3036,10 @@ This is your own one-time link! Errors No comment provided by engineer. + + Errors in servers configuration. + servers error + Even when disabled in the conversation. 会話中に無効になっている場合でも。 @@ -3134,11 +3227,27 @@ This is your own one-time link! グループメンバーによる修正はサポートされていません No comment provided by engineer. + + For chat profile %@: + servers error + For console コンソール No comment provided by engineer. + + For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server. + No comment provided by engineer. + + + For private routing + No comment provided by engineer. + + + For social media + No comment provided by engineer. + Forward chat item action @@ -3424,9 +3533,12 @@ Error: %2$@ SimpleX の仕組み No comment provided by engineer. - - How it works - 技術の説明 + + How it affects privacy + No comment provided by engineer. + + + How it helps privacy No comment provided by engineer. @@ -3497,8 +3609,8 @@ Error: %2$@ 即座に No comment provided by engineer. - - Immune to spam and abuse + + Immune to spam スパムや悪質送信を防止 No comment provided by engineer. @@ -3629,6 +3741,11 @@ More improvements are coming soon! インストール [ターミナル用SimpleX Chat](https://github.com/simplex-chat/simplex-chat) No comment provided by engineer. + + Instant + すぐに + No comment provided by engineer. + Instant push notifications will be hidden! @@ -3636,11 +3753,6 @@ More improvements are coming soon! No comment provided by engineer. - - Instantly - すぐに - No comment provided by engineer. - Interface インターフェース @@ -3682,7 +3794,7 @@ More improvements are coming soon! Invalid server address! 無効なサーバアドレス! - No comment provided by engineer. + alert title Invalid status @@ -3803,7 +3915,7 @@ This is your link for group %@! Keep - No comment provided by engineer. + alert action Keep conversation @@ -3815,7 +3927,7 @@ This is your link for group %@! Keep unused invitation? - No comment provided by engineer. + alert title Keep your connections @@ -3899,11 +4011,6 @@ This is your link for group %@! ライブメッセージ No comment provided by engineer. - - Local - 自分のみ - No comment provided by engineer. - Local name ローカルネーム @@ -3924,11 +4031,6 @@ This is your link for group %@! ロックモード No comment provided by engineer. - - Make a private connection - プライベートな接続をする - No comment provided by engineer. - Make one message disappear メッセージを1つ消す @@ -3939,21 +4041,11 @@ This is your link for group %@! プロフィールを非表示にできます! No comment provided by engineer. - - Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@). - %@ サーバー アドレスが正しい形式で、行が区切られており、重複していないことを確認してください (%@)。 - No comment provided by engineer. - Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated. WebRTC ICEサーバのアドレスを正しく1行ずつに分けて、重複しないように、形式もご確認ください。 No comment provided by engineer. - - Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?* - 多くの人が次のような質問をしました: *SimpleX にユーザー識別子がない場合、どうやってメッセージを配信できるのですか?* - No comment provided by engineer. - Mark deleted for everyone 全員に対して削除済みマークを付ける @@ -4206,6 +4298,10 @@ This is your link for group %@! More reliable network connection. No comment provided by engineer. + + More reliable notifications + No comment provided by engineer. + Most likely this connection is deleted. おそらく、この接続は削除されています。 @@ -4240,6 +4336,10 @@ This is your link for group %@! Network connection No comment provided by engineer. + + Network decentralization + No comment provided by engineer. + Network issues - message expired after many attempts to send it. snd error text @@ -4248,6 +4348,10 @@ This is your link for group %@! Network management No comment provided by engineer. + + Network operator + No comment provided by engineer. + Network settings ネットワーク設定 @@ -4304,6 +4408,10 @@ This is your link for group %@! 新たな表示名 No comment provided by engineer. + + New events + notification + New in %@ %@ の新機能 @@ -4328,6 +4436,10 @@ This is your link for group %@! 新しいパスフレーズ… No comment provided by engineer. + + New server + No comment provided by engineer. + No いいえ @@ -4381,6 +4493,14 @@ This is your link for group %@! No info, try to reload No comment provided by engineer. + + No media & file servers. + servers error + + + No message servers. + servers error + No network connection No comment provided by engineer. @@ -4398,11 +4518,37 @@ This is your link for group %@! 音声メッセージを録音する権限がありません No comment provided by engineer. + + No push server + 自分のみ + No comment provided by engineer. + No received or sent files 送受信済みのファイルがありません No comment provided by engineer. + + No servers for private message routing. + servers error + + + No servers to receive files. + servers error + + + No servers to receive messages. + servers error + + + No servers to send files. + servers error + + + No user identifiers. + 世界初のユーザーIDのないプラットフォーム|設計も元からプライベート。 + No comment provided by engineer. + Not compatible! No comment provided by engineer. @@ -4425,6 +4571,10 @@ This is your link for group %@! 通知が無効になっています! No comment provided by engineer. + + Notifications privacy + No comment provided by engineer. + Now admins can: - delete members' messages. @@ -4482,8 +4632,8 @@ VPN を有効にする必要があります。 オニオンのホストが使われません。 No comment provided by engineer. - - Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**. + + Only client devices store user profiles, contacts, groups, and messages. **2 レイヤーのエンドツーエンド暗号化**を使用して送信されたユーザー プロファイル、連絡先、グループ、メッセージを保存できるのはクライアント デバイスのみです。 No comment provided by engineer. @@ -4566,6 +4716,10 @@ VPN を有効にする必要があります。 設定を開く No comment provided by engineer. + + Open changes + No comment provided by engineer. + Open chat チャットを開く @@ -4576,6 +4730,10 @@ VPN を有効にする必要があります。 チャットのコンソールを開く authentication reason + + Open conditions + No comment provided by engineer. + Open group No comment provided by engineer. @@ -4584,24 +4742,18 @@ VPN を有効にする必要があります。 Open migration to another device authentication reason - - Open server settings - No comment provided by engineer. - - - Open user profiles - ユーザープロフィールを開く - authentication reason - - - Open-source protocol and code – anybody can run the servers. - プロトコル技術とコードはオープンソースで、どなたでもご自分のサーバを運用できます。 - No comment provided by engineer. - Opening app… No comment provided by engineer. + + Operator + No comment provided by engineer. + + + Operator server + alert title + Or paste archive link No comment provided by engineer. @@ -4618,12 +4770,12 @@ VPN を有効にする必要があります。 Or show this code No comment provided by engineer. - - Other + + Or to share privately No comment provided by engineer. - - Other %@ servers + + Other No comment provided by engineer. @@ -4700,13 +4852,8 @@ VPN を有効にする必要があります。 Pending No comment provided by engineer. - - People can connect to you only via the links you share. - あなたと繋がることができるのは、あなたからリンクを頂いた方のみです。 - No comment provided by engineer. - - - Periodically + + Periodic 定期的に No comment provided by engineer. @@ -4820,16 +4967,15 @@ Error: %@ 添付を含めて、下書きを保存する。 No comment provided by engineer. - - Preset server - プレセットサーバ - No comment provided by engineer. - Preset server address プレセットサーバのアドレス No comment provided by engineer. + + Preset servers + No comment provided by engineer. + Preview プレビュー @@ -4900,7 +5046,7 @@ Error: %@ Profile update will be sent to your contacts. 連絡先にプロフィール更新のお知らせが届きます。 - No comment provided by engineer. + alert message Prohibit audio/video calls. @@ -4987,6 +5133,10 @@ Enable in *Network & servers* settings. Proxy requires password No comment provided by engineer. + + Push Notifications + No comment provided by engineer. + Push notifications プッシュ通知 @@ -5024,28 +5174,23 @@ Enable in *Network & servers* settings. 続きを読む No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address). - 詳しくは[ユーザーガイド](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)をご覧ください。 - No comment provided by engineer. - Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). No comment provided by engineer. + + Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). + 詳しくは[ユーザーガイド](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)をご覧ください。 + No comment provided by engineer. + Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends). 詳しくは[ユーザーガイド](https://simplex.chat/docs/guide/readme.html#connect-to-friends)をご覧ください。 No comment provided by engineer. - - Read more in our GitHub repository. - GitHubリポジトリで詳細をご確認ください。 - No comment provided by engineer. - Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme). - 詳しくは[GitHubリポジトリ](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)をご覧ください。 + 詳しくは[GitHubリポジトリ](https://github.com/simplex-chat/simplex-chat#readme)をご覧ください。 No comment provided by engineer. @@ -5334,6 +5479,14 @@ Enable in *Network & servers* settings. 開示する chat item action + + Review conditions + No comment provided by engineer. + + + Review later + No comment provided by engineer. + Revoke 取り消す @@ -5375,6 +5528,14 @@ Enable in *Network & servers* settings. Safer groups No comment provided by engineer. + + Same conditions will apply to operator **%@**. + No comment provided by engineer. + + + Same conditions will apply to operator(s): **%@**. + No comment provided by engineer. + Save 保存 @@ -5443,7 +5604,7 @@ Enable in *Network & servers* settings. Save servers? サーバを保存しますか? - No comment provided by engineer. + alert title Save welcome message? @@ -5635,11 +5796,6 @@ Enable in *Network & servers* settings. 通知を送信する No comment provided by engineer. - - Send notifications: - 通知を送信する: - No comment provided by engineer. - Send questions and ideas 質問やアイデアを送る @@ -5751,6 +5907,10 @@ Enable in *Network & servers* settings. Server No comment provided by engineer. + + Server added to operator %@. + alert message + Server address No comment provided by engineer. @@ -5763,6 +5923,18 @@ Enable in *Network & servers* settings. Server address is incompatible with network settings: %@. No comment provided by engineer. + + Server operator changed. + alert title + + + Server operators + No comment provided by engineer. + + + Server protocol changed. + alert title + Server requires authorization to create queues, check password キューを作成するにはサーバーの認証が必要です。パスワードを確認してください @@ -5871,22 +6043,35 @@ Enable in *Network & servers* settings. Share 共有する - chat item action + alert action + chat item action Share 1-time link 使い捨てのリンクを共有 No comment provided by engineer. + + Share 1-time link with a friend + No comment provided by engineer. + + + Share SimpleX address on social media. + No comment provided by engineer. + Share address アドレスを共有する No comment provided by engineer. + + Share address publicly + No comment provided by engineer. + Share address with contacts? アドレスを連絡先と共有しますか? - No comment provided by engineer. + alert title Share from other apps. @@ -5994,6 +6179,14 @@ Enable in *Network & servers* settings. SimpleXアドレス No comment provided by engineer. + + SimpleX address and 1-time links are safe to share via any messenger. + No comment provided by engineer. + + + SimpleX address or 1-time link? + No comment provided by engineer. + SimpleX contact address SimpleX連絡先アドレス @@ -6076,6 +6269,11 @@ Enable in *Network & servers* settings. Some non-fatal errors occurred during import: No comment provided by engineer. + + Some servers failed the test: +%@ + alert message + Somebody 誰か @@ -6154,12 +6352,12 @@ Enable in *Network & servers* settings. Stop sharing 共有を停止 - No comment provided by engineer. + alert action Stop sharing address? アドレスの共有を停止しますか? - No comment provided by engineer. + alert title Stopping chat @@ -6296,7 +6494,7 @@ Enable in *Network & servers* settings. Tests failed! テストは失敗しました! - No comment provided by engineer. + alert title Thank you for installing SimpleX Chat! @@ -6313,11 +6511,6 @@ Enable in *Network & servers* settings. ユーザーに感謝します – Weblate 経由で貢献してください! No comment provided by engineer. - - The 1st platform without any user identifiers – private by design. - 世界初のユーザーIDのないプラットフォーム|設計も元からプライベート。 - No comment provided by engineer. - The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised. @@ -6330,6 +6523,10 @@ It can happen because of some bug or when the connection is compromised.アプリは、メッセージや連絡先のリクエストを受信したときに通知することができます - 設定を開いて有効にしてください。 No comment provided by engineer. + + The app protects your privacy by using different operators in each conversation. + No comment provided by engineer. + The app will ask to confirm downloads from unknown file servers (except .onion). No comment provided by engineer. @@ -6343,6 +6540,10 @@ It can happen because of some bug or when the connection is compromised.The code you scanned is not a SimpleX link QR code. No comment provided by engineer. + + The connection reached the limit of undelivered messages, your contact may be offline. + No comment provided by engineer. + The connection you accepted will be cancelled! 承認済の接続がキャンセルされます! @@ -6363,6 +6564,11 @@ It can happen because of some bug or when the connection is compromised.暗号化は機能しており、新しい暗号化への同意は必要ありません。接続エラーが発生する可能性があります! No comment provided by engineer. + + The future of messaging + 次世代のプライバシー・メッセンジャー + No comment provided by engineer. + The hash of the previous message is different. 以前のメッセージとハッシュ値が異なります。 @@ -6386,11 +6592,6 @@ It can happen because of some bug or when the connection is compromised.The messages will be marked as moderated for all members. No comment provided by engineer. - - The next generation of private messaging - 次世代のプライバシー・メッセンジャー - No comment provided by engineer. - The old database was not removed during the migration, it can be deleted. 古いデータベースは移行時に削除されなかったので、削除することができます。 @@ -6401,6 +6602,10 @@ It can happen because of some bug or when the connection is compromised.プロフィールは連絡先にしか共有されません。 No comment provided by engineer. + + The second preset operator in the app! + No comment provided by engineer. + The second tick we missed! ✅ 長らくお待たせしました! ✅ @@ -6416,6 +6621,10 @@ It can happen because of some bug or when the connection is compromised.現在のチャットプロフィールの新しい接続のサーバ **%@**。 No comment provided by engineer. + + The servers for new files of your current chat profile **%@**. + No comment provided by engineer. + The text you pasted is not a SimpleX link. No comment provided by engineer. @@ -6428,6 +6637,10 @@ It can happen because of some bug or when the connection is compromised.Themes No comment provided by engineer. + + These conditions will also apply for: **%@**. + No comment provided by engineer. + These settings are for your current profile **%@**. これらの設定は現在のプロファイル **%@** 用です。 @@ -6518,9 +6731,8 @@ It can happen because of some bug or when the connection is compromised.新規に接続する場合 No comment provided by engineer. - - To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts. - プライバシーを保護するために、SimpleX には、他のすべてのプラットフォームで使用されるユーザー ID の代わりに、連絡先ごとに個別のメッセージ キューの識別子があります。 + + To protect against your link being replaced, you can compare contact security codes. No comment provided by engineer. @@ -6539,6 +6751,15 @@ You will be prompted to complete authentication before this feature is enabled.< オンにするには、認証ステップが行われます。 No comment provided by engineer. + + To protect your privacy, SimpleX uses separate IDs for each of your contacts. + プライバシーを保護するために、SimpleX には、他のすべてのプラットフォームで使用されるユーザー ID の代わりに、連絡先ごとに個別のメッセージ キューの識別子があります。 + No comment provided by engineer. + + + To receive + No comment provided by engineer. + To record speech please grant permission to use Microphone. No comment provided by engineer. @@ -6557,11 +6778,19 @@ You will be prompted to complete authentication before this feature is enabled.< 非表示のプロフィールを表示するには、**チャット プロフィール** ページの検索フィールドに完全なパスワードを入力します。 No comment provided by engineer. + + To send + No comment provided by engineer. + To support instant push notifications the chat database has to be migrated. インスタント プッシュ通知をサポートするには、チャット データベースを移行する必要があります。 No comment provided by engineer. + + To use the servers of **%@**, accept conditions of use. + No comment provided by engineer. + To verify end-to-end encryption with your contact compare (or scan) the code on your devices. エンドツーエンド暗号化を確認するには、ご自分の端末と連絡先の端末のコードを比べます (スキャンします)。 @@ -6641,6 +6870,10 @@ You will be prompted to complete authentication before this feature is enabled.< Unblock member? No comment provided by engineer. + + Undelivered messages + No comment provided by engineer. + Unexpected migration state 予期しない移行状態 @@ -6788,6 +7021,10 @@ To connect, please ask your contact to create another connection link and check Uploading archive No comment provided by engineer. + + Use %@ + No comment provided by engineer. + Use .onion hosts .onionホストを使う @@ -6812,6 +7049,14 @@ To connect, please ask your contact to create another connection link and check 現在のプロファイルを使用する No comment provided by engineer. + + Use for files + No comment provided by engineer. + + + Use for messages + No comment provided by engineer. + Use for new connections 新しい接続に使う @@ -6848,6 +7093,10 @@ To connect, please ask your contact to create another connection link and check サーバを使う No comment provided by engineer. + + Use servers + No comment provided by engineer. + Use the app while in the call. No comment provided by engineer. @@ -6928,11 +7177,19 @@ To connect, please ask your contact to create another connection link and check 1GBまでのビデオとファイル No comment provided by engineer. + + View conditions + No comment provided by engineer. + View security code セキュリティコードを確認 No comment provided by engineer. + + View updated conditions + No comment provided by engineer. + Visible history chat feature @@ -7035,9 +7292,8 @@ To connect, please ask your contact to create another connection link and check When connecting audio and video calls. No comment provided by engineer. - - When people request to connect, you can accept or reject it. - 接続が要求されたら、それを受け入れるか拒否するかを選択できます。 + + When more than one operator is enabled, none of them has metadata to learn who communicates with whom. No comment provided by engineer. @@ -7176,6 +7432,18 @@ Repeat join request? You can change it in Appearance settings. No comment provided by engineer. + + You can configure operators in Network & servers settings. + No comment provided by engineer. + + + You can configure servers via settings. + No comment provided by engineer. + + + You can create it in user picker. + No comment provided by engineer. + You can create it later 後からでも作成できます @@ -7213,6 +7481,10 @@ Repeat join request? You can send messages to %@ from Archived contacts. No comment provided by engineer. + + You can set connection name, to remember who the link was shared with. + No comment provided by engineer. + You can set lock screen notification preview via settings. 設定からロック画面の通知プレビューを設定できます。 @@ -7228,11 +7500,6 @@ Repeat join request? このアドレスを連絡先と共有して、**%@** に接続できるようにすることができます。 No comment provided by engineer. - - You can share your address as a link or QR code - anybody can connect to you. - アドレスをリンクやQRコードとして共有することで、誰でも接続することができます。 - No comment provided by engineer. - You can start chat via app Settings / Database or by restarting the app アプリの設定/データベースから、またはアプリを再起動することでチャットを開始できます @@ -7254,23 +7521,23 @@ Repeat join request? You can view invitation link again in connection details. - No comment provided by engineer. + alert message You can't send messages! メッセージを送信できませんでした! No comment provided by engineer. - - You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them. - あなたはメッセージの受信に使用するサーバーを制御し、連絡先はあなたがメッセージの送信に使用するサーバーを使用することができます。 - No comment provided by engineer. - You could not be verified; please try again. 確認できませんでした。 もう一度お試しください。 No comment provided by engineer. + + You decide who can connect. + あなたと繋がることができるのは、あなたからリンクを頂いた方のみです。 + No comment provided by engineer. + You have already requested connection via this address! No comment provided by engineer. @@ -7385,11 +7652,6 @@ Repeat connection request? シークレットモードのプロフィールでこのグループに参加しています。メインのプロフィールを守るために、招待することができません No comment provided by engineer. - - Your %@ servers - あなたの %@ サーバー - No comment provided by engineer. - Your ICE servers あなたのICEサーバ @@ -7405,11 +7667,6 @@ Repeat connection request? あなたのSimpleXアドレス No comment provided by engineer. - - Your XFTP servers - あなたのXFTPサーバ - No comment provided by engineer. - Your calls あなたの通話 @@ -7505,16 +7762,15 @@ Repeat connection request? あなたのランダム・プロフィール No comment provided by engineer. - - Your server - あなたのサーバ - No comment provided by engineer. - Your server address あなたのサーバアドレス No comment provided by engineer. + + Your servers + No comment provided by engineer. + Your settings あなたの設定 @@ -7920,6 +8176,10 @@ Repeat connection request? expired No comment provided by engineer. + + for better metadata privacy. + No comment provided by engineer. + forwarded No comment provided by engineer. @@ -8507,6 +8767,33 @@ last received msg: %2$@ + +
+ +
+ + + %d new events + notification body + + + From: %@ + notification body + + + New events + notification + + + New messages + notification + + + New messages in %d chats + notification body + + +
diff --git a/apps/ios/SimpleX Localizations/ja.xcloc/Source Contents/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/ja.xcloc/Source Contents/en.lproj/Localizable.strings index cf485752ea..cb83427195 100644 --- a/apps/ios/SimpleX Localizations/ja.xcloc/Source Contents/en.lproj/Localizable.strings +++ b/apps/ios/SimpleX Localizations/ja.xcloc/Source Contents/en.lproj/Localizable.strings @@ -1,9 +1,6 @@ /* No comment provided by engineer. */ "_italic_" = "\\_italic_"; -/* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact."; - /* No comment provided by engineer. */ "*bold*" = "\\*bold*"; @@ -27,4 +24,3 @@ /* No comment provided by engineer. */ "No group!" = "Group not found!"; - diff --git a/apps/ios/SimpleX Localizations/ko.xcloc/Localized Contents/ko.xliff b/apps/ios/SimpleX Localizations/ko.xcloc/Localized Contents/ko.xliff index 511536427d..ac9d83e24b 100644 --- a/apps/ios/SimpleX Localizations/ko.xcloc/Localized Contents/ko.xliff +++ b/apps/ios/SimpleX Localizations/ko.xcloc/Localized Contents/ko.xliff @@ -152,20 +152,16 @@ ) No comment provided by engineer. - - **Add new contact**: to create your one-time QR Code or link for your contact. - No comment provided by engineer. - **Create link / QR code** for your contact to use. No comment provided by engineer. - - **More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have. + + **More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata. No comment provided by engineer. - - **Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app). + + **Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app. No comment provided by engineer. @@ -176,8 +172,8 @@ **Please note**: you will NOT be able to recover or change passphrase if you lose it. No comment provided by engineer. - - **Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from. + + **Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from. No comment provided by engineer. @@ -1537,8 +1533,8 @@ Image will be received when your contact is online, please wait or check later! No comment provided by engineer. - - Immune to spam and abuse + + Immune to spam No comment provided by engineer. @@ -1961,8 +1957,8 @@ We will be adding server redundancy to prevent lost messages. Onion hosts will not be used. No comment provided by engineer. - - Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**. + + Only client devices store user profiles, contacts, groups, and messages. No comment provided by engineer. @@ -2013,8 +2009,8 @@ We will be adding server redundancy to prevent lost messages. Open user profiles authentication reason - - Open-source protocol and code – anybody can run the servers. + + Anybody can host servers. No comment provided by engineer. @@ -2049,8 +2045,8 @@ We will be adding server redundancy to prevent lost messages. Paste the link you received into the box below to connect with your contact. No comment provided by engineer. - - People can connect to you only via the links you share. + + You decide who can connect. No comment provided by engineer. @@ -2670,8 +2666,8 @@ We will be adding server redundancy to prevent lost messages. Thanks to the users – contribute via Weblate! No comment provided by engineer. - - The 1st platform without any user identifiers – private by design. + + No user identifiers. No comment provided by engineer. @@ -2706,8 +2702,8 @@ We will be adding server redundancy to prevent lost messages. The message will be marked as moderated for all members. No comment provided by engineer. - - The next generation of private messaging + + The future of messaging No comment provided by engineer. @@ -2774,8 +2770,8 @@ We will be adding server redundancy to prevent lost messages. To make a new connection No comment provided by engineer. - - To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts. + + To protect your privacy, SimpleX uses separate IDs for each of your contacts. No comment provided by engineer. @@ -3093,10 +3089,6 @@ SimpleX Lock must be enabled. You can't send messages! No comment provided by engineer. - - You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them. - No comment provided by engineer. - You could not be verified; please try again. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/lt.xcloc/Localized Contents/lt.xliff b/apps/ios/SimpleX Localizations/lt.xcloc/Localized Contents/lt.xliff index 6df24149e9..e16f585da8 100644 --- a/apps/ios/SimpleX Localizations/lt.xcloc/Localized Contents/lt.xliff +++ b/apps/ios/SimpleX Localizations/lt.xcloc/Localized Contents/lt.xliff @@ -162,20 +162,16 @@ ) No comment provided by engineer. - - **Add new contact**: to create your one-time QR Code or link for your contact. - No comment provided by engineer. - **Create link / QR code** for your contact to use. No comment provided by engineer. - - **More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have. + + **More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata. No comment provided by engineer. - - **Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app). + + **Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app. No comment provided by engineer. @@ -187,8 +183,8 @@ **Turėkite omenyje**: jeigu prarasite slaptafrazę, NEBEGALĖSITE jos atkurti ar pakeisti. No comment provided by engineer. - - **Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from. + + **Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from. No comment provided by engineer. @@ -1513,8 +1509,8 @@ Image will be received when your contact is online, please wait or check later! No comment provided by engineer. - - Immune to spam and abuse + + Immune to spam No comment provided by engineer. @@ -1919,8 +1915,8 @@ We will be adding server redundancy to prevent lost messages. Onion hosts will not be used. No comment provided by engineer. - - Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**. + + Only client devices store user profiles, contacts, groups, and messages. No comment provided by engineer. @@ -1971,8 +1967,8 @@ We will be adding server redundancy to prevent lost messages. Open user profiles authentication reason - - Open-source protocol and code – anybody can run the servers. + + Anybody can host servers. No comment provided by engineer. @@ -2003,8 +1999,8 @@ We will be adding server redundancy to prevent lost messages. Paste the link you received into the box below to connect with your contact. No comment provided by engineer. - - People can connect to you only via the links you share. + + You decide who can connect. No comment provided by engineer. @@ -2591,8 +2587,8 @@ We will be adding server redundancy to prevent lost messages. Thanks to the users – contribute via Weblate! No comment provided by engineer. - - The 1st platform without any user identifiers – private by design. + + No user identifiers. No comment provided by engineer. @@ -2627,8 +2623,8 @@ We will be adding server redundancy to prevent lost messages. The message will be marked as moderated for all members. No comment provided by engineer. - - The next generation of private messaging + + The future of messaging No comment provided by engineer. @@ -2687,8 +2683,8 @@ We will be adding server redundancy to prevent lost messages. To make a new connection No comment provided by engineer. - - To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts. + + To protect your privacy, SimpleX uses separate IDs for each of your contacts. No comment provided by engineer. @@ -2993,10 +2989,6 @@ To connect, please ask your contact to create another connection link and check You can't send messages! No comment provided by engineer. - - You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them. - No comment provided by engineer. - You could not be verified; please try again. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff index ce6faeccca..06ab82cf2a 100644 --- a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff @@ -12,21 +12,6 @@ No comment provided by engineer. - - - - No comment provided by engineer. - - - - - No comment provided by engineer. - - - - - No comment provided by engineer. - ( ( @@ -127,6 +112,14 @@ %@ is geverifieerd No comment provided by engineer. + + %@ server + No comment provided by engineer. + + + %@ servers + No comment provided by engineer. + %@ uploaded %@ geüpload @@ -352,28 +345,23 @@ ) No comment provided by engineer. - - **Add contact**: to create a new invitation link, or connect via a link you received. + + **Create 1-time link**: to create and share a new invitation link. **Contact toevoegen**: om een nieuwe uitnodigingslink aan te maken, of verbinding te maken via een link die u heeft ontvangen. No comment provided by engineer. - - **Add new contact**: to create your one-time QR Code or link for your contact. - **Nieuw contact toevoegen**: om uw eenmalige QR-code of link voor uw contact te maken. - No comment provided by engineer. - **Create group**: to create a new group. **Groep aanmaken**: om een nieuwe groep aan te maken. No comment provided by engineer. - - **More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have. + + **More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata. **Meer privé**: bekijk elke 20 minuten nieuwe berichten. Apparaattoken wordt gedeeld met de SimpleX Chat-server, maar niet hoeveel contacten of berichten u heeft. No comment provided by engineer. - - **Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app). + + **Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app. **Meest privé**: gebruik geen SimpleX Chat-notificatie server, controleer berichten regelmatig op de achtergrond (afhankelijk van hoe vaak u de app gebruikt). No comment provided by engineer. @@ -387,11 +375,15 @@ **Let op**: u kunt het wachtwoord NIET herstellen of wijzigen als u het kwijtraakt. No comment provided by engineer. - - **Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from. + + **Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from. **Aanbevolen**: apparaattoken en meldingen worden naar de SimpleX Chat-meldingsserver gestuurd, maar niet de berichtinhoud, -grootte of van wie het afkomstig is. No comment provided by engineer. + + **Scan / Paste link**: to connect via a link you received. + No comment provided by engineer. + **Warning**: Instant push notifications require passphrase saved in Keychain. **Waarschuwing**: voor directe push meldingen is een wachtwoord vereist dat is opgeslagen in de Keychain. @@ -498,6 +490,14 @@ 1 week time interval + + 1-time link + No comment provided by engineer. + + + 1-time link can be used *with one contact only* - share in person or via any messenger. + No comment provided by engineer. + 5 minutes 5 minuten @@ -567,21 +567,11 @@ Adres wijziging afbreken? No comment provided by engineer. - - About SimpleX - Over SimpleX - No comment provided by engineer. - About SimpleX Chat Over SimpleX Chat No comment provided by engineer. - - About SimpleX address - Over SimpleX adres - No comment provided by engineer. - Accent Accent @@ -594,6 +584,10 @@ accept incoming call via notification swipe action + + Accept conditions + No comment provided by engineer. + Accept connection request? Accepteer contact @@ -610,6 +604,10 @@ accept contact request via notification swipe action + + Accepted conditions + No comment provided by engineer. + Acknowledged Erkend @@ -630,16 +628,6 @@ Voeg een adres toe aan uw profiel, zodat uw contacten het met andere mensen kunnen delen. Profiel update wordt naar uw contacten verzonden. No comment provided by engineer. - - Add contact - Contact toevoegen - No comment provided by engineer. - - - Add preset servers - Vooraf ingestelde servers toevoegen - No comment provided by engineer. - Add profile Profiel toevoegen @@ -665,6 +653,14 @@ Welkom bericht toevoegen No comment provided by engineer. + + Added media & file servers + No comment provided by engineer. + + + Added message servers + No comment provided by engineer. + Additional accent Extra accent @@ -690,6 +686,14 @@ Adres wijziging wordt afgebroken. Het oude ontvangstadres wordt gebruikt. No comment provided by engineer. + + Address or 1-time link? + No comment provided by engineer. + + + Address settings + No comment provided by engineer. + Admins can block a member for all. Beheerders kunnen een lid voor iedereen blokkeren. @@ -735,6 +739,10 @@ Alle groepsleden blijven verbonden. No comment provided by engineer. + + All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages. + No comment provided by engineer. + All messages will be deleted - this cannot be undone! Alle berichten worden verwijderd. Dit kan niet ongedaan worden gemaakt! @@ -915,6 +923,11 @@ Beantwoord oproep No comment provided by engineer. + + Anybody can host servers. + Iedereen kan servers hosten. + No comment provided by engineer. + App build: %@ App build: %@ @@ -1258,7 +1271,8 @@ Cancel Annuleren - alert button + alert action + alert button Cancel migration @@ -1341,6 +1355,10 @@ authentication reason set passcode view + + Change user profiles + authentication reason + Chat archive Gesprek archief @@ -1426,10 +1444,18 @@ Chats No comment provided by engineer. + + Check messages every 20 min. + No comment provided by engineer. + + + Check messages when allowed. + No comment provided by engineer. + Check server address and try again. Controleer het server adres en probeer het opnieuw. - No comment provided by engineer. + alert title Chinese and Spanish interface @@ -1516,16 +1542,47 @@ Voltooid No comment provided by engineer. + + Conditions accepted on: %@. + No comment provided by engineer. + + + Conditions are accepted for the operator(s): **%@**. + No comment provided by engineer. + + + Conditions are already accepted for following operator(s): **%@**. + No comment provided by engineer. + + + Conditions of use + No comment provided by engineer. + + + Conditions will be accepted for enabled operators after 30 days. + No comment provided by engineer. + + + Conditions will be accepted for operator(s): **%@**. + No comment provided by engineer. + + + Conditions will be accepted for the operator(s): **%@**. + No comment provided by engineer. + + + Conditions will be accepted on: %@. + No comment provided by engineer. + + + Conditions will be automatically accepted for enabled operators on: %@. + No comment provided by engineer. + Configure ICE servers ICE servers configureren No comment provided by engineer. - - Configured %@ servers - %@ servers geconfigureerd - No comment provided by engineer. - Confirm Bevestigen @@ -1715,6 +1772,10 @@ Dit is uw eigen eenmalige link! Verbindingsverzoek verzonden! No comment provided by engineer. + + Connection security + No comment provided by engineer. + Connection terminated Verbinding beëindigd @@ -1830,6 +1891,10 @@ Dit is uw eigen eenmalige link! Maak No comment provided by engineer. + + Create 1-time link + No comment provided by engineer. + Create SimpleX address Maak een SimpleX adres aan @@ -1840,11 +1905,6 @@ Dit is uw eigen eenmalige link! Maak een groep met een willekeurig profiel. No comment provided by engineer. - - Create an address to let people connect with you. - Maak een adres aan zodat mensen contact met je kunnen opnemen. - No comment provided by engineer. - Create file Bestand maken @@ -1925,6 +1985,10 @@ Dit is uw eigen eenmalige link! Huidige toegangscode No comment provided by engineer. + + Current conditions text couldn't be loaded, you can review conditions via this link: + No comment provided by engineer. + Current passphrase… Huidige wachtwoord… @@ -2081,7 +2145,8 @@ Dit is uw eigen eenmalige link! Delete Verwijderen - chat item action + alert action + chat item action swipe action @@ -2299,6 +2364,10 @@ Dit is uw eigen eenmalige link! Verwijderingsfouten No comment provided by engineer. + + Delivered even when Apple drops them. + No comment provided by engineer. + Delivery Bezorging @@ -2580,6 +2649,10 @@ Dit is uw eigen eenmalige link! Duur No comment provided by engineer. + + E2E encrypted notifications. + No comment provided by engineer. + Edit Bewerk @@ -2600,6 +2673,10 @@ Dit is uw eigen eenmalige link! Inschakelen (overschrijvingen behouden) No comment provided by engineer. + + Enable Flux + No comment provided by engineer. + Enable SimpleX Lock SimpleX Vergrendelen inschakelen @@ -2805,6 +2882,10 @@ Dit is uw eigen eenmalige link! Fout bij het afbreken van adres wijziging No comment provided by engineer. + + Error accepting conditions + alert title + Error accepting contact request Fout bij het accepteren van een contactverzoek @@ -2820,6 +2901,10 @@ Dit is uw eigen eenmalige link! Fout bij het toevoegen van leden No comment provided by engineer. + + Error adding server + alert title + Error changing address Fout bij wijzigen van adres @@ -2960,10 +3045,9 @@ Dit is uw eigen eenmalige link! Fout bij lid worden van groep No comment provided by engineer. - - Error loading %@ servers - Fout bij het laden van %@ servers - No comment provided by engineer. + + Error loading servers + alert title Error migrating settings @@ -3000,11 +3084,6 @@ Dit is uw eigen eenmalige link! Fout bij het resetten van statistieken No comment provided by engineer. - - Error saving %@ servers - Fout bij opslaan van %@ servers - No comment provided by engineer. - Error saving ICE servers Fout bij opslaan van ICE servers @@ -3025,6 +3104,10 @@ Dit is uw eigen eenmalige link! Fout bij opslaan van wachtwoord in de keychain No comment provided by engineer. + + Error saving servers + alert title + Error saving settings Fout bij opslaan van instellingen @@ -3095,6 +3178,10 @@ Dit is uw eigen eenmalige link! Fout bij updaten van bericht No comment provided by engineer. + + Error updating server + alert title + Error updating settings Fout bij bijwerken van instellingen @@ -3140,6 +3227,10 @@ Dit is uw eigen eenmalige link! Fouten No comment provided by engineer. + + Errors in servers configuration. + servers error + Even when disabled in the conversation. Zelfs wanneer uitgeschakeld in het gesprek. @@ -3342,11 +3433,27 @@ Dit is uw eigen eenmalige link! Herstel wordt niet ondersteund door groepslid No comment provided by engineer. + + For chat profile %@: + servers error + For console Voor console No comment provided by engineer. + + For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server. + No comment provided by engineer. + + + For private routing + No comment provided by engineer. + + + For social media + No comment provided by engineer. + Forward Doorsturen @@ -3656,9 +3763,12 @@ Fout: %2$@ Hoe SimpleX werkt No comment provided by engineer. - - How it works - Hoe het werkt + + How it affects privacy + No comment provided by engineer. + + + How it helps privacy No comment provided by engineer. @@ -3731,8 +3841,8 @@ Fout: %2$@ Onmiddellijk No comment provided by engineer. - - Immune to spam and abuse + + Immune to spam Immuun voor spam en misbruik No comment provided by engineer. @@ -3873,6 +3983,11 @@ Binnenkort meer verbeteringen! Installeer [SimpleX Chat voor terminal](https://github.com/simplex-chat/simplex-chat) No comment provided by engineer. + + Instant + Direct + No comment provided by engineer. + Instant push notifications will be hidden! @@ -3880,11 +3995,6 @@ Binnenkort meer verbeteringen! No comment provided by engineer. - - Instantly - Direct - No comment provided by engineer. - Interface Interface @@ -3933,7 +4043,7 @@ Binnenkort meer verbeteringen! Invalid server address! Ongeldig server adres! - No comment provided by engineer. + alert title Invalid status @@ -4061,7 +4171,7 @@ Dit is jouw link voor groep %@! Keep Bewaar - No comment provided by engineer. + alert action Keep conversation @@ -4076,7 +4186,7 @@ Dit is jouw link voor groep %@! Keep unused invitation? Ongebruikte uitnodiging bewaren? - No comment provided by engineer. + alert title Keep your connections @@ -4163,11 +4273,6 @@ Dit is jouw link voor groep %@! Live berichten No comment provided by engineer. - - Local - Lokaal - No comment provided by engineer. - Local name Lokale naam @@ -4188,11 +4293,6 @@ Dit is jouw link voor groep %@! Vergrendeling modus No comment provided by engineer. - - Make a private connection - Maak een privéverbinding - No comment provided by engineer. - Make one message disappear Eén bericht laten verdwijnen @@ -4203,21 +4303,11 @@ Dit is jouw link voor groep %@! Profiel privé maken! No comment provided by engineer. - - Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@). - Zorg ervoor dat %@ server adressen de juiste indeling hebben, regel gescheiden zijn en niet gedupliceerd zijn (%@). - No comment provided by engineer. - Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated. Zorg ervoor dat WebRTC ICE server adressen de juiste indeling hebben, regel gescheiden zijn en niet gedupliceerd zijn. No comment provided by engineer. - - Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?* - Veel mensen vroegen: *als SimpleX geen gebruikers-ID's heeft, hoe kan het dan berichten bezorgen?* - No comment provided by engineer. - Mark deleted for everyone Markeer verwijderd voor iedereen @@ -4498,6 +4588,10 @@ Dit is jouw link voor groep %@! Betrouwbaardere netwerkverbinding. No comment provided by engineer. + + More reliable notifications + No comment provided by engineer. + Most likely this connection is deleted. Hoogstwaarschijnlijk is deze verbinding verwijderd. @@ -4533,6 +4627,10 @@ Dit is jouw link voor groep %@! Netwerkverbinding No comment provided by engineer. + + Network decentralization + No comment provided by engineer. + Network issues - message expired after many attempts to send it. Netwerkproblemen - bericht is verlopen na vele pogingen om het te verzenden. @@ -4543,6 +4641,10 @@ Dit is jouw link voor groep %@! Netwerkbeheer No comment provided by engineer. + + Network operator + No comment provided by engineer. + Network settings Netwerk instellingen @@ -4603,6 +4705,10 @@ Dit is jouw link voor groep %@! Nieuwe weergavenaam No comment provided by engineer. + + New events + notification + New in %@ Nieuw in %@ @@ -4628,6 +4734,10 @@ Dit is jouw link voor groep %@! Nieuw wachtwoord… No comment provided by engineer. + + New server + No comment provided by engineer. + No Nee @@ -4683,6 +4793,14 @@ Dit is jouw link voor groep %@! Geen info, probeer opnieuw te laden No comment provided by engineer. + + No media & file servers. + servers error + + + No message servers. + servers error + No network connection Geen netwerkverbinding @@ -4703,11 +4821,37 @@ Dit is jouw link voor groep %@! Geen toestemming om spraakbericht op te nemen No comment provided by engineer. + + No push server + Lokaal + No comment provided by engineer. + No received or sent files Geen ontvangen of verzonden bestanden No comment provided by engineer. + + No servers for private message routing. + servers error + + + No servers to receive files. + servers error + + + No servers to receive messages. + servers error + + + No servers to send files. + servers error + + + No user identifiers. + Geen gebruikers-ID's. + No comment provided by engineer. + Not compatible! Niet compatibel! @@ -4733,6 +4877,10 @@ Dit is jouw link voor groep %@! Meldingen zijn uitgeschakeld! No comment provided by engineer. + + Notifications privacy + No comment provided by engineer. + Now admins can: - delete members' messages. @@ -4791,8 +4939,8 @@ Vereist het inschakelen van VPN. Onion hosts worden niet gebruikt. No comment provided by engineer. - - Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**. + + Only client devices store user profiles, contacts, groups, and messages. Alleen client apparaten slaan gebruikers profielen, contacten, groepen en berichten op die zijn verzonden met **2-laags end-to-end-codering**. No comment provided by engineer. @@ -4876,6 +5024,10 @@ Vereist het inschakelen van VPN. Open instellingen No comment provided by engineer. + + Open changes + No comment provided by engineer. + Open chat Chat openen @@ -4886,6 +5038,10 @@ Vereist het inschakelen van VPN. Chat console openen authentication reason + + Open conditions + No comment provided by engineer. + Open group Open groep @@ -4896,26 +5052,19 @@ Vereist het inschakelen van VPN. Open de migratie naar een ander apparaat authentication reason - - Open server settings - Server instellingen openen - No comment provided by engineer. - - - Open user profiles - Gebruikers profielen openen - authentication reason - - - Open-source protocol and code – anybody can run the servers. - Open-source protocol en code. Iedereen kan de servers draaien. - No comment provided by engineer. - Opening app… App openen… No comment provided by engineer. + + Operator + No comment provided by engineer. + + + Operator server + alert title + Or paste archive link Of plak de archief link @@ -4936,16 +5085,15 @@ Vereist het inschakelen van VPN. Of laat deze code zien No comment provided by engineer. + + Or to share privately + No comment provided by engineer. + Other Ander No comment provided by engineer. - - Other %@ servers - Andere %@ servers - No comment provided by engineer. - Other file errors: %@ @@ -5028,13 +5176,8 @@ Vereist het inschakelen van VPN. in behandeling No comment provided by engineer. - - People can connect to you only via the links you share. - Mensen kunnen alleen verbinding met u maken via de links die u deelt. - No comment provided by engineer. - - - Periodically + + Periodic Periodiek No comment provided by engineer. @@ -5157,16 +5300,15 @@ Fout: %@ Bewaar het laatste berichtconcept, met bijlagen. No comment provided by engineer. - - Preset server - Vooraf ingestelde server - No comment provided by engineer. - Preset server address Vooraf ingesteld server adres No comment provided by engineer. + + Preset servers + No comment provided by engineer. + Preview Voorbeeld @@ -5245,7 +5387,7 @@ Fout: %@ Profile update will be sent to your contacts. Profiel update wordt naar uw contacten verzonden. - No comment provided by engineer. + alert message Prohibit audio/video calls. @@ -5339,6 +5481,10 @@ Schakel dit in in *Netwerk en servers*-instellingen. Proxy vereist wachtwoord No comment provided by engineer. + + Push Notifications + No comment provided by engineer. + Push notifications Push meldingen @@ -5379,26 +5525,21 @@ Schakel dit in in *Netwerk en servers*-instellingen. Lees meer No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address). - Lees meer in de [Gebruikershandleiding](https://simplex.chat/docs/guide/app-settings.html#uw-simplex-contactadres). - No comment provided by engineer. - Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). Lees meer in de [Gebruikershandleiding](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). No comment provided by engineer. + + Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). + Lees meer in de [Gebruikershandleiding](https://simplex.chat/docs/guide/app-settings.html#uw-simplex-contactadres). + No comment provided by engineer. + Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends). Lees meer in de [Gebruikershandleiding](https://simplex.chat/docs/guide/readme.html#connect-to-friends). No comment provided by engineer. - - Read more in our GitHub repository. - Lees meer in onze GitHub repository. - No comment provided by engineer. - Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme). Lees meer in onze [GitHub-repository](https://github.com/simplex-chat/simplex-chat#readme). @@ -5715,6 +5856,14 @@ Schakel dit in in *Netwerk en servers*-instellingen. Onthullen chat item action + + Review conditions + No comment provided by engineer. + + + Review later + No comment provided by engineer. + Revoke Intrekken @@ -5760,6 +5909,14 @@ Schakel dit in in *Netwerk en servers*-instellingen. Veiligere groepen No comment provided by engineer. + + Same conditions will apply to operator **%@**. + No comment provided by engineer. + + + Same conditions will apply to operator(s): **%@**. + No comment provided by engineer. + Save Opslaan @@ -5829,7 +5986,7 @@ Schakel dit in in *Netwerk en servers*-instellingen. Save servers? Servers opslaan? - No comment provided by engineer. + alert title Save welcome message? @@ -6041,11 +6198,6 @@ Schakel dit in in *Netwerk en servers*-instellingen. Meldingen verzenden No comment provided by engineer. - - Send notifications: - Meldingen verzenden: - No comment provided by engineer. - Send questions and ideas Stuur vragen en ideeën @@ -6171,6 +6323,10 @@ Schakel dit in in *Netwerk en servers*-instellingen. Server No comment provided by engineer. + + Server added to operator %@. + alert message + Server address Server adres @@ -6186,6 +6342,18 @@ Schakel dit in in *Netwerk en servers*-instellingen. Serveradres is incompatibel met netwerkinstellingen: %@. No comment provided by engineer. + + Server operator changed. + alert title + + + Server operators + No comment provided by engineer. + + + Server protocol changed. + alert title + Server requires authorization to create queues, check password Server vereist autorisatie om wachtrijen te maken, controleer wachtwoord @@ -6304,22 +6472,35 @@ Schakel dit in in *Netwerk en servers*-instellingen. Share Deel - chat item action + alert action + chat item action Share 1-time link Eenmalige link delen No comment provided by engineer. + + Share 1-time link with a friend + No comment provided by engineer. + + + Share SimpleX address on social media. + No comment provided by engineer. + Share address Adres delen No comment provided by engineer. + + Share address publicly + No comment provided by engineer. + Share address with contacts? Adres delen met contacten? - No comment provided by engineer. + alert title Share from other apps. @@ -6436,6 +6617,14 @@ Schakel dit in in *Netwerk en servers*-instellingen. SimpleX adres No comment provided by engineer. + + SimpleX address and 1-time links are safe to share via any messenger. + No comment provided by engineer. + + + SimpleX address or 1-time link? + No comment provided by engineer. + SimpleX contact address SimpleX contact adres @@ -6526,6 +6715,11 @@ Schakel dit in in *Netwerk en servers*-instellingen. Er zijn enkele niet-fatale fouten opgetreden tijdens het importeren: No comment provided by engineer. + + Some servers failed the test: +%@ + alert message + Somebody Iemand @@ -6609,12 +6803,12 @@ Schakel dit in in *Netwerk en servers*-instellingen. Stop sharing Stop met delen - No comment provided by engineer. + alert action Stop sharing address? Stop met het delen van adres? - No comment provided by engineer. + alert title Stopping chat @@ -6764,7 +6958,7 @@ Schakel dit in in *Netwerk en servers*-instellingen. Tests failed! Testen mislukt! - No comment provided by engineer. + alert title Thank you for installing SimpleX Chat! @@ -6781,11 +6975,6 @@ Schakel dit in in *Netwerk en servers*-instellingen. Dank aan de gebruikers – draag bij via Weblate! No comment provided by engineer. - - The 1st platform without any user identifiers – private by design. - Het eerste platform zonder gebruikers-ID's, privé door ontwerp. - No comment provided by engineer. - The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised. @@ -6798,6 +6987,10 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast. De app kan u op de hoogte stellen wanneer u berichten of contact verzoeken ontvangt - open de instellingen om dit in te schakelen. No comment provided by engineer. + + The app protects your privacy by using different operators in each conversation. + No comment provided by engineer. + The app will ask to confirm downloads from unknown file servers (except .onion). De app vraagt om downloads van onbekende bestandsservers (behalve .onion) te bevestigen. @@ -6813,6 +7006,10 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast. De code die u heeft gescand is geen SimpleX link QR-code. No comment provided by engineer. + + The connection reached the limit of undelivered messages, your contact may be offline. + No comment provided by engineer. + The connection you accepted will be cancelled! De door u geaccepteerde verbinding wordt geannuleerd! @@ -6833,6 +7030,11 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast. De versleuteling werkt en de nieuwe versleutelingsovereenkomst is niet vereist. Dit kan leiden tot verbindingsfouten! No comment provided by engineer. + + The future of messaging + De volgende generatie privéberichten + No comment provided by engineer. + The hash of the previous message is different. De hash van het vorige bericht is anders. @@ -6858,11 +7060,6 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast. De berichten worden voor alle leden als gemodereerd gemarkeerd. No comment provided by engineer. - - The next generation of private messaging - De volgende generatie privéberichten - No comment provided by engineer. - The old database was not removed during the migration, it can be deleted. De oude database is niet verwijderd tijdens de migratie, deze kan worden verwijderd. @@ -6873,6 +7070,10 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast. Het profiel wordt alleen gedeeld met uw contacten. No comment provided by engineer. + + The second preset operator in the app! + No comment provided by engineer. + The second tick we missed! ✅ De tweede vink die we gemist hebben! ✅ @@ -6888,6 +7089,10 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast. De servers voor nieuwe verbindingen van uw huidige chatprofiel **%@**. No comment provided by engineer. + + The servers for new files of your current chat profile **%@**. + No comment provided by engineer. + The text you pasted is not a SimpleX link. De tekst die u hebt geplakt is geen SimpleX link. @@ -6903,6 +7108,10 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast. Thema's No comment provided by engineer. + + These conditions will also apply for: **%@**. + No comment provided by engineer. + These settings are for your current profile **%@**. Deze instellingen zijn voor uw huidige profiel **%@**. @@ -7003,9 +7212,8 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast. Om een nieuwe verbinding te maken No comment provided by engineer. - - To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts. - Om de privacy te beschermen, heeft SimpleX in plaats van gebruikers-ID's die door alle andere platforms worden gebruikt, ID's voor berichten wachtrijen, afzonderlijk voor elk van uw contacten. + + To protect against your link being replaced, you can compare contact security codes. No comment provided by engineer. @@ -7025,6 +7233,15 @@ You will be prompted to complete authentication before this feature is enabled.< U wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingeschakeld. No comment provided by engineer. + + To protect your privacy, SimpleX uses separate IDs for each of your contacts. + Om de privacy te beschermen, heeft SimpleX in plaats van gebruikers-ID's die door alle andere platforms worden gebruikt, ID's voor berichten wachtrijen, afzonderlijk voor elk van uw contacten. + No comment provided by engineer. + + + To receive + No comment provided by engineer. + To record speech please grant permission to use Microphone. Geef toestemming om de microfoon te gebruiken om spraak op te nemen. @@ -7045,11 +7262,19 @@ U wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingesc Om uw verborgen profiel te onthullen, voert u een volledig wachtwoord in een zoek veld in op de pagina **Uw chatprofielen**. No comment provided by engineer. + + To send + No comment provided by engineer. + To support instant push notifications the chat database has to be migrated. Om directe push meldingen te ondersteunen, moet de chat database worden gemigreerd. No comment provided by engineer. + + To use the servers of **%@**, accept conditions of use. + No comment provided by engineer. + To verify end-to-end encryption with your contact compare (or scan) the code on your devices. Vergelijk (of scan) de code op uw apparaten om end-to-end-codering met uw contact te verifiëren. @@ -7140,6 +7365,10 @@ U wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingesc Lid deblokkeren? No comment provided by engineer. + + Undelivered messages + No comment provided by engineer. + Unexpected migration state Onverwachte migratiestatus @@ -7297,6 +7526,10 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Archief uploaden No comment provided by engineer. + + Use %@ + No comment provided by engineer. + Use .onion hosts Gebruik .onion-hosts @@ -7322,6 +7555,14 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Gebruik het huidige profiel No comment provided by engineer. + + Use for files + No comment provided by engineer. + + + Use for messages + No comment provided by engineer. + Use for new connections Gebruik voor nieuwe verbindingen @@ -7362,6 +7603,10 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Gebruik server No comment provided by engineer. + + Use servers + No comment provided by engineer. + Use the app while in the call. Gebruik de app tijdens het gesprek. @@ -7452,11 +7697,19 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Video's en bestanden tot 1 GB No comment provided by engineer. + + View conditions + No comment provided by engineer. + View security code Beveiligingscode bekijken No comment provided by engineer. + + View updated conditions + No comment provided by engineer. + Visible history Zichtbare geschiedenis @@ -7567,9 +7820,8 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Bij het verbinden van audio- en video-oproepen. No comment provided by engineer. - - When people request to connect, you can accept or reject it. - Wanneer mensen vragen om verbinding te maken, kunt u dit accepteren of weigeren. + + When more than one operator is enabled, none of them has metadata to learn who communicates with whom. No comment provided by engineer. @@ -7729,6 +7981,18 @@ Deelnameverzoek herhalen? U kunt dit wijzigen in de instellingen onder uiterlijk. No comment provided by engineer. + + You can configure operators in Network & servers settings. + No comment provided by engineer. + + + You can configure servers via settings. + No comment provided by engineer. + + + You can create it in user picker. + No comment provided by engineer. + You can create it later U kan het later maken @@ -7769,6 +8033,10 @@ Deelnameverzoek herhalen? U kunt berichten naar %@ sturen vanuit gearchiveerde contacten. No comment provided by engineer. + + You can set connection name, to remember who the link was shared with. + No comment provided by engineer. + You can set lock screen notification preview via settings. U kunt een voorbeeld van een melding op het vergrendeld scherm instellen via instellingen. @@ -7784,11 +8052,6 @@ Deelnameverzoek herhalen? U kunt dit adres delen met uw contacten om hen verbinding te laten maken met **%@**. No comment provided by engineer. - - You can share your address as a link or QR code - anybody can connect to you. - U kunt uw adres delen als een link of als een QR-code. Iedereen kan verbinding met u maken. - No comment provided by engineer. - You can start chat via app Settings / Database or by restarting the app U kunt de chat starten via app Instellingen / Database of door de app opnieuw op te starten @@ -7812,23 +8075,23 @@ Deelnameverzoek herhalen? You can view invitation link again in connection details. U kunt de uitnodigingslink opnieuw bekijken in de verbindingsdetails. - No comment provided by engineer. + alert message You can't send messages! Je kunt geen berichten versturen! No comment provided by engineer. - - You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them. - U bepaalt via welke server(s) de berichten **ontvangen**, uw contacten de servers die u gebruikt om ze berichten te sturen. - No comment provided by engineer. - You could not be verified; please try again. U kon niet worden geverifieerd; probeer het opnieuw. No comment provided by engineer. + + You decide who can connect. + Jij bepaalt wie er verbinding mag maken. + No comment provided by engineer. + You have already requested connection via this address! U heeft al een verbinding aangevraagd via dit adres! @@ -7951,11 +8214,6 @@ Verbindingsverzoek herhalen? Je gebruikt een incognito profiel voor deze groep. Om te voorkomen dat je je hoofdprofiel deelt, is het niet toegestaan om contacten uit te nodigen No comment provided by engineer. - - Your %@ servers - Uw %@ servers - No comment provided by engineer. - Your ICE servers Uw ICE servers @@ -7971,11 +8229,6 @@ Verbindingsverzoek herhalen? Uw SimpleX adres No comment provided by engineer. - - Your XFTP servers - Uw XFTP servers - No comment provided by engineer. - Your calls Uw oproepen @@ -8076,16 +8329,15 @@ Verbindingsverzoek herhalen? Je willekeurige profiel No comment provided by engineer. - - Your server - Uw server - No comment provided by engineer. - Your server address Uw server adres No comment provided by engineer. + + Your servers + No comment provided by engineer. + Your settings Uw instellingen @@ -8506,6 +8758,10 @@ Verbindingsverzoek herhalen? verlopen No comment provided by engineer. + + for better metadata privacy. + No comment provided by engineer. + forwarded doorgestuurd @@ -9128,6 +9384,33 @@ laatst ontvangen bericht: %2$@ + +
+ +
+ + + %d new events + notification body + + + From: %@ + notification body + + + New events + notification + + + New messages + notification + + + New messages in %d chats + notification body + + +
diff --git a/apps/ios/SimpleX Localizations/nl.xcloc/Source Contents/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/nl.xcloc/Source Contents/en.lproj/Localizable.strings index cf485752ea..cb83427195 100644 --- a/apps/ios/SimpleX Localizations/nl.xcloc/Source Contents/en.lproj/Localizable.strings +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Source Contents/en.lproj/Localizable.strings @@ -1,9 +1,6 @@ /* No comment provided by engineer. */ "_italic_" = "\\_italic_"; -/* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact."; - /* No comment provided by engineer. */ "*bold*" = "\\*bold*"; @@ -27,4 +24,3 @@ /* No comment provided by engineer. */ "No group!" = "Group not found!"; - diff --git a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff index ee3ef5b12e..531d50f522 100644 --- a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff @@ -12,21 +12,6 @@ No comment provided by engineer. - - - - No comment provided by engineer. - - - - - No comment provided by engineer. - - - - - No comment provided by engineer. - ( ( @@ -127,6 +112,14 @@ %@ jest zweryfikowany No comment provided by engineer. + + %@ server + No comment provided by engineer. + + + %@ servers + No comment provided by engineer. + %@ uploaded %@ wgrane @@ -352,28 +345,23 @@ ) No comment provided by engineer. - - **Add contact**: to create a new invitation link, or connect via a link you received. + + **Create 1-time link**: to create and share a new invitation link. **Dodaj kontakt**: aby utworzyć nowy link z zaproszeniem lub połączyć się za pomocą otrzymanego linku. No comment provided by engineer. - - **Add new contact**: to create your one-time QR Code or link for your contact. - **Dodaj nowy kontakt**: aby stworzyć swój jednorazowy kod QR lub link dla kontaktu. - No comment provided by engineer. - **Create group**: to create a new group. **Utwórz grupę**: aby utworzyć nową grupę. No comment provided by engineer. - - **More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have. + + **More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata. **Bardziej prywatny**: sprawdzanie nowych wiadomości odbywa się co 20 minut. Współdzielony z serwerem SimpleX Chat jest token urządzenia, lecz nie informacje o liczbie kontaktów lub wiadomości. No comment provided by engineer. - - **Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app). + + **Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app. **Najbardziej prywatny**: nie korzystaj z serwera powiadomień SimpleX Chat, wiadomości sprawdzane są co jakiś czas w tle (zależne od tego jak często korzystasz z aplikacji). No comment provided by engineer. @@ -387,11 +375,15 @@ **Uwaga**: NIE będziesz w stanie odzyskać lub zmienić kodu dostępu, jeśli go stracisz. No comment provided by engineer. - - **Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from. + + **Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from. **Zalecane**: do serwera powiadomień SimpleX Chat wysyłany jest token urządzenia i powiadomienia, lecz nie treść wiadomości, jej rozmiar lub od kogo ona jest. No comment provided by engineer. + + **Scan / Paste link**: to connect via a link you received. + No comment provided by engineer. + **Warning**: Instant push notifications require passphrase saved in Keychain. **Uwaga**: Natychmiastowe powiadomienia push wymagają zapisania kodu dostępu w Keychain. @@ -498,6 +490,14 @@ 1 tydzień time interval + + 1-time link + No comment provided by engineer. + + + 1-time link can be used *with one contact only* - share in person or via any messenger. + No comment provided by engineer. + 5 minutes 5 minut @@ -567,21 +567,11 @@ Przerwać zmianę adresu? No comment provided by engineer. - - About SimpleX - O SimpleX - No comment provided by engineer. - About SimpleX Chat O SimpleX Chat No comment provided by engineer. - - About SimpleX address - O adresie SimpleX - No comment provided by engineer. - Accent Akcent @@ -594,6 +584,10 @@ accept incoming call via notification swipe action + + Accept conditions + No comment provided by engineer. + Accept connection request? Zaakceptować prośbę o połączenie? @@ -610,6 +604,10 @@ accept contact request via notification swipe action + + Accepted conditions + No comment provided by engineer. + Acknowledged Potwierdzono @@ -630,16 +628,6 @@ Dodaj adres do swojego profilu, aby Twoje kontakty mogły go udostępnić innym osobom. Aktualizacja profilu zostanie wysłana do Twoich kontaktów. No comment provided by engineer. - - Add contact - Dodaj kontakt - No comment provided by engineer. - - - Add preset servers - Dodaj gotowe serwery - No comment provided by engineer. - Add profile Dodaj profil @@ -665,6 +653,14 @@ Dodaj wiadomość powitalną No comment provided by engineer. + + Added media & file servers + No comment provided by engineer. + + + Added message servers + No comment provided by engineer. + Additional accent Dodatkowy akcent @@ -690,6 +686,14 @@ Zmiana adresu zostanie przerwana. Użyty zostanie stary adres odbiorczy. No comment provided by engineer. + + Address or 1-time link? + No comment provided by engineer. + + + Address settings + No comment provided by engineer. + Admins can block a member for all. Administratorzy mogą blokować członka dla wszystkich. @@ -735,6 +739,10 @@ Wszyscy członkowie grupy pozostaną połączeni. No comment provided by engineer. + + All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages. + No comment provided by engineer. + All messages will be deleted - this cannot be undone! Wszystkie wiadomości zostaną usunięte – nie można tego cofnąć! @@ -915,6 +923,11 @@ Odbierz połączenie No comment provided by engineer. + + Anybody can host servers. + Każdy może hostować serwery. + No comment provided by engineer. + App build: %@ Kompilacja aplikacji: %@ @@ -1253,7 +1266,8 @@ Cancel Anuluj - alert button + alert action + alert button Cancel migration @@ -1336,6 +1350,10 @@ authentication reason set passcode view + + Change user profiles + authentication reason + Chat archive Archiwum czatu @@ -1421,10 +1439,18 @@ Czaty No comment provided by engineer. + + Check messages every 20 min. + No comment provided by engineer. + + + Check messages when allowed. + No comment provided by engineer. + Check server address and try again. Sprawdź adres serwera i spróbuj ponownie. - No comment provided by engineer. + alert title Chinese and Spanish interface @@ -1511,16 +1537,47 @@ Zakończono No comment provided by engineer. + + Conditions accepted on: %@. + No comment provided by engineer. + + + Conditions are accepted for the operator(s): **%@**. + No comment provided by engineer. + + + Conditions are already accepted for following operator(s): **%@**. + No comment provided by engineer. + + + Conditions of use + No comment provided by engineer. + + + Conditions will be accepted for enabled operators after 30 days. + No comment provided by engineer. + + + Conditions will be accepted for operator(s): **%@**. + No comment provided by engineer. + + + Conditions will be accepted for the operator(s): **%@**. + No comment provided by engineer. + + + Conditions will be accepted on: %@. + No comment provided by engineer. + + + Conditions will be automatically accepted for enabled operators on: %@. + No comment provided by engineer. + Configure ICE servers Skonfiguruj serwery ICE No comment provided by engineer. - - Configured %@ servers - Skonfigurowano %@ serwerów - No comment provided by engineer. - Confirm Potwierdź @@ -1710,6 +1767,10 @@ To jest twój jednorazowy link! Prośba o połączenie wysłana! No comment provided by engineer. + + Connection security + No comment provided by engineer. + Connection terminated Połączenie zakończone @@ -1825,6 +1886,10 @@ To jest twój jednorazowy link! Utwórz No comment provided by engineer. + + Create 1-time link + No comment provided by engineer. + Create SimpleX address Utwórz adres SimpleX @@ -1835,11 +1900,6 @@ To jest twój jednorazowy link! Utwórz grupę używając losowego profilu. No comment provided by engineer. - - Create an address to let people connect with you. - Utwórz adres, aby ludzie mogli się z Tobą połączyć. - No comment provided by engineer. - Create file Utwórz plik @@ -1920,6 +1980,10 @@ To jest twój jednorazowy link! Aktualny Pin No comment provided by engineer. + + Current conditions text couldn't be loaded, you can review conditions via this link: + No comment provided by engineer. + Current passphrase… Obecne hasło… @@ -2075,7 +2139,8 @@ To jest twój jednorazowy link! Delete Usuń - chat item action + alert action + chat item action swipe action @@ -2292,6 +2357,10 @@ To jest twój jednorazowy link! Błędy usuwania No comment provided by engineer. + + Delivered even when Apple drops them. + No comment provided by engineer. + Delivery Dostarczenie @@ -2573,6 +2642,10 @@ To jest twój jednorazowy link! Czas trwania No comment provided by engineer. + + E2E encrypted notifications. + No comment provided by engineer. + Edit Edytuj @@ -2593,6 +2666,10 @@ To jest twój jednorazowy link! Włącz (zachowaj nadpisania) No comment provided by engineer. + + Enable Flux + No comment provided by engineer. + Enable SimpleX Lock Włącz blokadę SimpleX @@ -2798,6 +2875,10 @@ To jest twój jednorazowy link! Błąd przerwania zmiany adresu No comment provided by engineer. + + Error accepting conditions + alert title + Error accepting contact request Błąd przyjmowania prośby o kontakt @@ -2813,6 +2894,10 @@ To jest twój jednorazowy link! Błąd dodawania członka(ów) No comment provided by engineer. + + Error adding server + alert title + Error changing address Błąd zmiany adresu @@ -2953,10 +3038,9 @@ To jest twój jednorazowy link! Błąd dołączenia do grupy No comment provided by engineer. - - Error loading %@ servers - Błąd ładowania %@ serwerów - No comment provided by engineer. + + Error loading servers + alert title Error migrating settings @@ -2993,11 +3077,6 @@ To jest twój jednorazowy link! Błąd resetowania statystyk No comment provided by engineer. - - Error saving %@ servers - Błąd zapisu %@ serwerów - No comment provided by engineer. - Error saving ICE servers Błąd zapisu serwerów ICE @@ -3018,6 +3097,10 @@ To jest twój jednorazowy link! Błąd zapisu hasła do pęku kluczy No comment provided by engineer. + + Error saving servers + alert title + Error saving settings Błąd zapisywania ustawień @@ -3088,6 +3171,10 @@ To jest twój jednorazowy link! Błąd aktualizacji wiadomości No comment provided by engineer. + + Error updating server + alert title + Error updating settings Błąd aktualizacji ustawień @@ -3133,6 +3220,10 @@ To jest twój jednorazowy link! Błędy No comment provided by engineer. + + Errors in servers configuration. + servers error + Even when disabled in the conversation. Nawet po wyłączeniu w rozmowie. @@ -3335,11 +3426,27 @@ To jest twój jednorazowy link! Naprawa nie jest obsługiwana przez członka grupy No comment provided by engineer. + + For chat profile %@: + servers error + For console Dla konsoli No comment provided by engineer. + + For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server. + No comment provided by engineer. + + + For private routing + No comment provided by engineer. + + + For social media + No comment provided by engineer. + Forward Przekaż dalej @@ -3648,9 +3755,12 @@ Błąd: %2$@ Jak działa SimpleX No comment provided by engineer. - - How it works - Jak to działa + + How it affects privacy + No comment provided by engineer. + + + How it helps privacy No comment provided by engineer. @@ -3723,8 +3833,8 @@ Błąd: %2$@ Natychmiast No comment provided by engineer. - - Immune to spam and abuse + + Immune to spam Odporność na spam i nadużycia No comment provided by engineer. @@ -3863,6 +3973,11 @@ More improvements are coming soon! Zainstaluj [SimpleX Chat na terminal](https://github.com/simplex-chat/simplex-chat) No comment provided by engineer. + + Instant + Natychmiastowo + No comment provided by engineer. + Instant push notifications will be hidden! @@ -3870,11 +3985,6 @@ More improvements are coming soon! No comment provided by engineer. - - Instantly - Natychmiastowo - No comment provided by engineer. - Interface Interfejs @@ -3923,7 +4033,7 @@ More improvements are coming soon! Invalid server address! Nieprawidłowy adres serwera! - No comment provided by engineer. + alert title Invalid status @@ -4051,7 +4161,7 @@ To jest twój link do grupy %@! Keep Zachowaj - No comment provided by engineer. + alert action Keep conversation @@ -4066,7 +4176,7 @@ To jest twój link do grupy %@! Keep unused invitation? Zachować nieużyte zaproszenie? - No comment provided by engineer. + alert title Keep your connections @@ -4153,11 +4263,6 @@ To jest twój link do grupy %@! Wiadomości na żywo No comment provided by engineer. - - Local - Lokalnie - No comment provided by engineer. - Local name Nazwa lokalna @@ -4178,11 +4283,6 @@ To jest twój link do grupy %@! Tryb blokady No comment provided by engineer. - - Make a private connection - Nawiąż prywatne połączenie - No comment provided by engineer. - Make one message disappear Spraw, aby jedna wiadomość zniknęła @@ -4193,21 +4293,11 @@ To jest twój link do grupy %@! Ustaw profil jako prywatny! No comment provided by engineer. - - Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@). - Upewnij się, że adresy serwerów %@ są w poprawnym formacie, rozdzielone liniami i nie są zduplikowane (%@). - No comment provided by engineer. - Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated. Upewnij się, że adresy serwerów WebRTC ICE są w poprawnym formacie, rozdzielone liniami i nie są zduplikowane. No comment provided by engineer. - - Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?* - Wiele osób pytało: *jeśli SimpleX nie ma identyfikatora użytkownika, jak może dostarczać wiadomości?* - No comment provided by engineer. - Mark deleted for everyone Oznacz jako usunięty dla wszystkich @@ -4488,6 +4578,10 @@ To jest twój link do grupy %@! Bardziej niezawodne połączenia sieciowe. No comment provided by engineer. + + More reliable notifications + No comment provided by engineer. + Most likely this connection is deleted. Najprawdopodobniej to połączenie jest usunięte. @@ -4523,6 +4617,10 @@ To jest twój link do grupy %@! Połączenie z siecią No comment provided by engineer. + + Network decentralization + No comment provided by engineer. + Network issues - message expired after many attempts to send it. Błąd sieciowy - wiadomość wygasła po wielu próbach wysłania jej. @@ -4533,6 +4631,10 @@ To jest twój link do grupy %@! Zarządzenie sieciowe No comment provided by engineer. + + Network operator + No comment provided by engineer. + Network settings Ustawienia sieci @@ -4593,6 +4695,10 @@ To jest twój link do grupy %@! Nowa wyświetlana nazwa No comment provided by engineer. + + New events + notification + New in %@ Nowość w %@ @@ -4618,6 +4724,10 @@ To jest twój link do grupy %@! Nowe hasło… No comment provided by engineer. + + New server + No comment provided by engineer. + No Nie @@ -4673,6 +4783,14 @@ To jest twój link do grupy %@! Brak informacji, spróbuj przeładować No comment provided by engineer. + + No media & file servers. + servers error + + + No message servers. + servers error + No network connection Brak połączenia z siecią @@ -4693,11 +4811,37 @@ To jest twój link do grupy %@! Brak uprawnień do nagrywania wiadomości głosowej No comment provided by engineer. + + No push server + Lokalnie + No comment provided by engineer. + No received or sent files Brak odebranych lub wysłanych plików No comment provided by engineer. + + No servers for private message routing. + servers error + + + No servers to receive files. + servers error + + + No servers to receive messages. + servers error + + + No servers to send files. + servers error + + + No user identifiers. + Brak identyfikatorów użytkownika. + No comment provided by engineer. + Not compatible! Nie kompatybilny! @@ -4723,6 +4867,10 @@ To jest twój link do grupy %@! Powiadomienia są wyłączone! No comment provided by engineer. + + Notifications privacy + No comment provided by engineer. + Now admins can: - delete members' messages. @@ -4781,8 +4929,8 @@ Wymaga włączenia VPN. Hosty onion nie będą używane. No comment provided by engineer. - - Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**. + + Only client devices store user profiles, contacts, groups, and messages. 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. @@ -4866,6 +5014,10 @@ Wymaga włączenia VPN. Otwórz Ustawienia No comment provided by engineer. + + Open changes + No comment provided by engineer. + Open chat Otwórz czat @@ -4876,6 +5028,10 @@ Wymaga włączenia VPN. Otwórz konsolę czatu authentication reason + + Open conditions + No comment provided by engineer. + Open group Grupa otwarta @@ -4886,26 +5042,19 @@ Wymaga włączenia VPN. Otwórz migrację na innym urządzeniu authentication reason - - Open server settings - Otwórz ustawienia serwera - No comment provided by engineer. - - - Open user profiles - Otwórz profile użytkownika - authentication reason - - - Open-source protocol and code – anybody can run the servers. - Otwarto źródłowy protokół i kod - każdy może uruchomić serwery. - No comment provided by engineer. - Opening app… Otwieranie aplikacji… No comment provided by engineer. + + Operator + No comment provided by engineer. + + + Operator server + alert title + Or paste archive link Lub wklej link archiwum @@ -4926,16 +5075,15 @@ Wymaga włączenia VPN. Lub pokaż ten kod No comment provided by engineer. + + Or to share privately + No comment provided by engineer. + Other Inne No comment provided by engineer. - - Other %@ servers - Inne %@ serwery - No comment provided by engineer. - Other file errors: %@ @@ -5018,13 +5166,8 @@ Wymaga włączenia VPN. Oczekujące No comment provided by engineer. - - People can connect to you only via the links you share. - Ludzie mogą się z Tobą połączyć tylko poprzez linki, które udostępniasz. - No comment provided by engineer. - - - Periodically + + Periodic Okresowo No comment provided by engineer. @@ -5147,16 +5290,15 @@ Błąd: %@ Zachowaj ostatnią wersję roboczą wiadomości wraz z załącznikami. No comment provided by engineer. - - Preset server - Wstępnie ustawiony serwer - No comment provided by engineer. - Preset server address Wstępnie ustawiony adres serwera No comment provided by engineer. + + Preset servers + No comment provided by engineer. + Preview Podgląd @@ -5235,7 +5377,7 @@ Błąd: %@ Profile update will be sent to your contacts. Aktualizacja profilu zostanie wysłana do Twoich kontaktów. - No comment provided by engineer. + alert message Prohibit audio/video calls. @@ -5329,6 +5471,10 @@ Włącz w ustawianiach *Sieć i serwery* . Proxy wymaga hasła No comment provided by engineer. + + Push Notifications + No comment provided by engineer. + Push notifications Powiadomienia push @@ -5369,26 +5515,21 @@ Włącz w ustawianiach *Sieć i serwery* . Przeczytaj więcej No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address). - Przeczytaj więcej w [Podręczniku Użytkownika](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address). - No comment provided by engineer. - Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). Przeczytaj więcej w [Poradniku Użytkownika](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). No comment provided by engineer. + + Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). + Przeczytaj więcej w [Podręczniku Użytkownika](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). + No comment provided by engineer. + Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends). Przeczytaj więcej w [Podręczniku Użytkownika](https://simplex.chat/docs/guide/readme.html#connect-to-friends). No comment provided by engineer. - - Read more in our GitHub repository. - Przeczytaj więcej na naszym repozytorium GitHub. - No comment provided by engineer. - Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme). Przeczytaj więcej na naszym [repozytorium GitHub](https://github.com/simplex-chat/simplex-chat#readme). @@ -5705,6 +5846,14 @@ Włącz w ustawianiach *Sieć i serwery* . Ujawnij chat item action + + Review conditions + No comment provided by engineer. + + + Review later + No comment provided by engineer. + Revoke Odwołaj @@ -5750,6 +5899,14 @@ Włącz w ustawianiach *Sieć i serwery* . Bezpieczniejsze grupy No comment provided by engineer. + + Same conditions will apply to operator **%@**. + No comment provided by engineer. + + + Same conditions will apply to operator(s): **%@**. + No comment provided by engineer. + Save Zapisz @@ -5819,7 +5976,7 @@ Włącz w ustawianiach *Sieć i serwery* . Save servers? Zapisać serwery? - No comment provided by engineer. + alert title Save welcome message? @@ -6031,11 +6188,6 @@ Włącz w ustawianiach *Sieć i serwery* . Wyślij powiadomienia No comment provided by engineer. - - Send notifications: - Wyślij powiadomienia: - No comment provided by engineer. - Send questions and ideas Wyślij pytania i pomysły @@ -6161,6 +6313,10 @@ Włącz w ustawianiach *Sieć i serwery* . Serwer No comment provided by engineer. + + Server added to operator %@. + alert message + Server address Adres serwera @@ -6176,6 +6332,18 @@ Włącz w ustawianiach *Sieć i serwery* . Adres serwera jest niekompatybilny z ustawieniami sieci: %@. No comment provided by engineer. + + Server operator changed. + alert title + + + Server operators + No comment provided by engineer. + + + Server protocol changed. + alert title + Server requires authorization to create queues, check password Serwer wymaga autoryzacji do tworzenia kolejek, sprawdź hasło @@ -6294,22 +6462,35 @@ Włącz w ustawianiach *Sieć i serwery* . Share Udostępnij - chat item action + alert action + chat item action Share 1-time link Udostępnij 1-razowy link No comment provided by engineer. + + Share 1-time link with a friend + No comment provided by engineer. + + + Share SimpleX address on social media. + No comment provided by engineer. + Share address Udostępnij adres No comment provided by engineer. + + Share address publicly + No comment provided by engineer. + Share address with contacts? Udostępnić adres kontaktom? - No comment provided by engineer. + alert title Share from other apps. @@ -6426,6 +6607,14 @@ Włącz w ustawianiach *Sieć i serwery* . Adres SimpleX No comment provided by engineer. + + SimpleX address and 1-time links are safe to share via any messenger. + No comment provided by engineer. + + + SimpleX address or 1-time link? + No comment provided by engineer. + SimpleX contact address Adres kontaktowy SimpleX @@ -6515,6 +6704,11 @@ Włącz w ustawianiach *Sieć i serwery* . Podczas importu wystąpiły niekrytyczne błędy: No comment provided by engineer. + + Some servers failed the test: +%@ + alert message + Somebody Ktoś @@ -6598,12 +6792,12 @@ Włącz w ustawianiach *Sieć i serwery* . Stop sharing Przestań udostępniać - No comment provided by engineer. + alert action Stop sharing address? Przestać udostępniać adres? - No comment provided by engineer. + alert title Stopping chat @@ -6751,7 +6945,7 @@ Włącz w ustawianiach *Sieć i serwery* . Tests failed! Testy nie powiodły się! - No comment provided by engineer. + alert title Thank you for installing SimpleX Chat! @@ -6768,11 +6962,6 @@ Włącz w ustawianiach *Sieć i serwery* . Podziękowania dla użytkowników - wkład za pośrednictwem Weblate! No comment provided by engineer. - - The 1st platform without any user identifiers – private by design. - Pierwsza platforma bez żadnych identyfikatorów użytkowników – z założenia prywatna. - No comment provided by engineer. - The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised. @@ -6785,6 +6974,10 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom Aplikacja może powiadamiać Cię, gdy otrzymujesz wiadomości lub prośby o kontakt — otwórz ustawienia, aby włączyć. No comment provided by engineer. + + The app protects your privacy by using different operators in each conversation. + No comment provided by engineer. + The app will ask to confirm downloads from unknown file servers (except .onion). Aplikacja zapyta o potwierdzenie pobierania od nieznanych serwerów plików (poza .onion). @@ -6800,6 +6993,10 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom Kod, który zeskanowałeś nie jest kodem QR linku SimpleX. No comment provided by engineer. + + The connection reached the limit of undelivered messages, your contact may be offline. + No comment provided by engineer. + The connection you accepted will be cancelled! Zaakceptowane przez Ciebie połączenie zostanie anulowane! @@ -6820,6 +7017,11 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom Szyfrowanie działa, a nowe uzgodnienie szyfrowania nie jest wymagane. Może to spowodować błędy w połączeniu! No comment provided by engineer. + + The future of messaging + Następna generacja prywatnych wiadomości + No comment provided by engineer. + The hash of the previous message is different. Hash poprzedniej wiadomości jest inny. @@ -6845,11 +7047,6 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom 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 - No comment provided by engineer. - The old database was not removed during the migration, it can be deleted. Stara baza danych nie została usunięta podczas migracji, można ją usunąć. @@ -6860,6 +7057,10 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom Profil jest udostępniany tylko Twoim kontaktom. No comment provided by engineer. + + The second preset operator in the app! + No comment provided by engineer. + The second tick we missed! ✅ Drugi tik, który przegapiliśmy! ✅ @@ -6875,6 +7076,10 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom Serwery dla nowych połączeń bieżącego profilu czatu **%@**. No comment provided by engineer. + + The servers for new files of your current chat profile **%@**. + No comment provided by engineer. + The text you pasted is not a SimpleX link. Tekst, który wkleiłeś nie jest linkiem SimpleX. @@ -6890,6 +7095,10 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom Motywy No comment provided by engineer. + + These conditions will also apply for: **%@**. + No comment provided by engineer. + These settings are for your current profile **%@**. Te ustawienia dotyczą Twojego bieżącego profilu **%@**. @@ -6990,9 +7199,8 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom Aby nawiązać nowe połączenie No comment provided by engineer. - - To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts. - Aby chronić prywatność, zamiast identyfikatorów użytkowników używanych przez wszystkie inne platformy, SimpleX ma identyfikatory dla kolejek wiadomości, oddzielne dla każdego z Twoich kontaktów. + + To protect against your link being replaced, you can compare contact security codes. No comment provided by engineer. @@ -7012,6 +7220,15 @@ You will be prompted to complete authentication before this feature is enabled.< Przed włączeniem tej funkcji zostanie wyświetlony monit uwierzytelniania. No comment provided by engineer. + + To protect your privacy, SimpleX uses separate IDs for each of your contacts. + Aby chronić prywatność, zamiast identyfikatorów użytkowników używanych przez wszystkie inne platformy, SimpleX ma identyfikatory dla kolejek wiadomości, oddzielne dla każdego z Twoich kontaktów. + No comment provided by engineer. + + + To receive + No comment provided by engineer. + To record speech please grant permission to use Microphone. Aby nagrać rozmowę, proszę zezwolić na użycie Mikrofonu. @@ -7032,11 +7249,19 @@ Przed włączeniem tej funkcji zostanie wyświetlony monit uwierzytelniania.Aby ujawnić Twój ukryty profil, wprowadź pełne hasło w pole wyszukiwania na stronie **Twoich profili czatu**. No comment provided by engineer. + + To send + No comment provided by engineer. + To support instant push notifications the chat database has to be migrated. Aby obsługiwać natychmiastowe powiadomienia push, należy zmigrować bazę danych czatu. No comment provided by engineer. + + To use the servers of **%@**, accept conditions of use. + 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. @@ -7127,6 +7352,10 @@ Przed włączeniem tej funkcji zostanie wyświetlony monit uwierzytelniania.Odblokować członka? No comment provided by engineer. + + Undelivered messages + No comment provided by engineer. + Unexpected migration state Nieoczekiwany stan migracji @@ -7284,6 +7513,10 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Wgrywanie archiwum No comment provided by engineer. + + Use %@ + No comment provided by engineer. + Use .onion hosts Użyj hostów .onion @@ -7309,6 +7542,14 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Użyj obecnego profilu No comment provided by engineer. + + Use for files + No comment provided by engineer. + + + Use for messages + No comment provided by engineer. + Use for new connections Użyj dla nowych połączeń @@ -7349,6 +7590,10 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Użyj serwera No comment provided by engineer. + + Use servers + No comment provided by engineer. + Use the app while in the call. Używaj aplikacji podczas połączenia. @@ -7439,11 +7684,19 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Filmy i pliki do 1gb No comment provided by engineer. + + View conditions + No comment provided by engineer. + View security code Pokaż kod bezpieczeństwa No comment provided by engineer. + + View updated conditions + No comment provided by engineer. + Visible history Widoczna historia @@ -7554,9 +7807,8 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Podczas łączenia połączeń audio i wideo. No comment provided by engineer. - - When people request to connect, you can accept or reject it. - Kiedy ludzie proszą o połączenie, możesz je zaakceptować lub odrzucić. + + When more than one operator is enabled, none of them has metadata to learn who communicates with whom. No comment provided by engineer. @@ -7716,6 +7968,18 @@ Powtórzyć prośbę dołączenia? Możesz to zmienić w ustawieniach wyglądu. No comment provided by engineer. + + You can configure operators in Network & servers settings. + No comment provided by engineer. + + + You can configure servers via settings. + No comment provided by engineer. + + + You can create it in user picker. + No comment provided by engineer. + You can create it later Możesz go utworzyć później @@ -7756,6 +8020,10 @@ Powtórzyć prośbę dołączenia? Możesz wysyłać wiadomości do %@ ze zarchiwizowanych kontaktów. No comment provided by engineer. + + You can set connection name, to remember who the link was shared with. + 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. @@ -7771,11 +8039,6 @@ Powtórzyć prośbę dołączenia? Możesz udostępnić ten adres Twoim kontaktom, aby umożliwić im połączenie z **%@**. No comment provided by engineer. - - You can share your address as a link or QR code - anybody can connect to you. - Możesz udostępnić swój adres jako link lub jako kod QR - każdy będzie mógł się z Tobą połączyć. - 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 @@ -7799,23 +8062,23 @@ Powtórzyć prośbę dołączenia? You can view invitation link again in connection details. Możesz zobaczyć link zaproszenia ponownie w szczegółach połączenia. - No comment provided by engineer. + alert message You can't send messages! Nie możesz wysyłać wiadomości! No comment provided by engineer. - - You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them. - Kontrolujesz przez który serwer(y) **odbierać** wiadomości, Twoje kontakty - serwery, których używasz do wysyłania im wiadomości. - No comment provided by engineer. - You could not be verified; please try again. Nie można zweryfikować użytkownika; proszę spróbować ponownie. No comment provided by engineer. + + You decide who can connect. + Ty decydujesz, kto może się połączyć. + No comment provided by engineer. + You have already requested connection via this address! Już prosiłeś o połączenie na ten adres! @@ -7938,11 +8201,6 @@ Powtórzyć prośbę połączenia? Używasz profilu incognito dla tej grupy - aby zapobiec udostępnianiu głównego profilu zapraszanie kontaktów jest zabronione No comment provided by engineer. - - Your %@ servers - Twoje serwery %@ - No comment provided by engineer. - Your ICE servers Twoje serwery ICE @@ -7958,11 +8216,6 @@ Powtórzyć prośbę połączenia? Twój adres SimpleX No comment provided by engineer. - - Your XFTP servers - Twoje serwery XFTP - No comment provided by engineer. - Your calls Twoje połączenia @@ -8063,16 +8316,15 @@ Powtórzyć prośbę połączenia? Twój losowy profil No comment provided by engineer. - - Your server - Twój serwer - No comment provided by engineer. - Your server address Twój adres serwera No comment provided by engineer. + + Your servers + No comment provided by engineer. + Your settings Twoje ustawienia @@ -8493,6 +8745,10 @@ Powtórzyć prośbę połączenia? wygasły No comment provided by engineer. + + for better metadata privacy. + No comment provided by engineer. + forwarded przekazane dalej @@ -9115,6 +9371,33 @@ ostatnia otrzymana wiadomość: %2$@ + +
+ +
+ + + %d new events + notification body + + + From: %@ + notification body + + + New events + notification + + + New messages + notification + + + New messages in %d chats + notification body + + +
diff --git a/apps/ios/SimpleX Localizations/pl.xcloc/Source Contents/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/pl.xcloc/Source Contents/en.lproj/Localizable.strings index cf485752ea..cb83427195 100644 --- a/apps/ios/SimpleX Localizations/pl.xcloc/Source Contents/en.lproj/Localizable.strings +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Source Contents/en.lproj/Localizable.strings @@ -1,9 +1,6 @@ /* No comment provided by engineer. */ "_italic_" = "\\_italic_"; -/* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact."; - /* No comment provided by engineer. */ "*bold*" = "\\*bold*"; @@ -27,4 +24,3 @@ /* No comment provided by engineer. */ "No group!" = "Group not found!"; - diff --git a/apps/ios/SimpleX Localizations/pt-BR.xcloc/Localized Contents/pt-BR.xliff b/apps/ios/SimpleX Localizations/pt-BR.xcloc/Localized Contents/pt-BR.xliff index c63fec4a08..ffbaec1d96 100644 --- a/apps/ios/SimpleX Localizations/pt-BR.xcloc/Localized Contents/pt-BR.xliff +++ b/apps/ios/SimpleX Localizations/pt-BR.xcloc/Localized Contents/pt-BR.xliff @@ -187,23 +187,18 @@ ) No comment provided by engineer. - - **Add new contact**: to create your one-time QR Code or link for your contact. - **Adicionar novo contato**: para criar seu QR Code ou link único para seu contato. - No comment provided by engineer. - **Create link / QR code** for your contact to use. **Crie um link / QR code** para seu contato usar. No comment provided by engineer. - - **More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have. + + **More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata. **Mais privado**: verifique as novas mensagens a cada 20 minutos. O token do dispositivo é compartilhado com o servidor do SimpleX Chat, mas não quantos contatos ou mensagens você tem. No comment provided by engineer. - - **Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app). + + **Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app. **Mais privado**: não use o servidor de notificações do SimpleX Chat, verifique as mensagens periodicamente em segundo plano (depende da frequência com que você usa o aplicativo). No comment provided by engineer. @@ -217,8 +212,8 @@ **Observação**: NÃO será possível recuperar ou alterar a frase secreta se você a perder. No comment provided by engineer. - - **Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from. + + **Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from. **Recomendado**: o token do dispositivo e as notificações são enviados para o servidor de notificações do SimpleX Chat, mas não o conteúdo, o tamanho ou o remetente da mensagem. No comment provided by engineer. @@ -1761,8 +1756,8 @@ A imagem será recebida quando seu contato estiver online, aguarde ou verifique mais tarde! No comment provided by engineer. - - Immune to spam and abuse + + Immune to spam Imune a spam e abuso No comment provided by engineer. @@ -2209,8 +2204,8 @@ We will be adding server redundancy to prevent lost messages. Hosts Onion não serão usados. No comment provided by engineer. - - Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**. + + Only client devices store user profiles, contacts, groups, and messages. No comment provided by engineer. @@ -2267,8 +2262,8 @@ We will be adding server redundancy to prevent lost messages. Abrir console de chat authentication reason - - Open-source protocol and code – anybody can run the servers. + + Anybody can host servers. Protocolo de código aberto – qualquer um pode executar os servidores. No comment provided by engineer. @@ -2306,8 +2301,8 @@ We will be adding server redundancy to prevent lost messages. Cole o link que você recebeu na caixa abaixo para conectar com o seu contato. No comment provided by engineer. - - People can connect to you only via the links you share. + + You decide who can connect. Pessoas podem se conectar com você somente via links compartilhados. No comment provided by engineer. @@ -2961,8 +2956,8 @@ We will be adding server redundancy to prevent lost messages. Thank you for installing SimpleX Chat! No comment provided by engineer. - - The 1st platform without any user identifiers – private by design. + + No user identifiers. A 1ª plataforma sem nenhum identificador de usuário – privada por design. No comment provided by engineer. @@ -2998,8 +2993,8 @@ We will be adding server redundancy to prevent lost messages. The microphone does not work when the app is in the background. No comment provided by engineer. - - The next generation of private messaging + + The future of messaging A próxima geração de mensageiros privados No comment provided by engineer. @@ -3071,8 +3066,8 @@ We will be adding server redundancy to prevent lost messages. To prevent the call interruption, enable Do Not Disturb mode. No comment provided by engineer. - - To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts. + + To protect your privacy, SimpleX uses separate IDs for each of your contacts. No comment provided by engineer. @@ -3402,10 +3397,6 @@ Para se conectar, peça ao seu contato para criar outro link de conexão e verif Você pode usar markdown para formatar mensagens: No comment provided by engineer. - - You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them. - No comment provided by engineer. - You could not be verified; please try again. Você não pôde ser verificado; por favor, tente novamente. @@ -5482,8 +5473,8 @@ Isso pode acontecer por causa de algum bug ou quando a conexão está comprometi (this device v%@) este dispositivo - - **Add contact**: to create a new invitation link, or connect via a link you received. + + **Create 1-time link**: to create and share a new invitation link. **Adicionar contato**: criar um novo link de convite ou conectar via um link que você recebeu. diff --git a/apps/ios/SimpleX Localizations/pt.xcloc/Localized Contents/pt.xliff b/apps/ios/SimpleX Localizations/pt.xcloc/Localized Contents/pt.xliff index a9bf86e778..cdadd677f9 100644 --- a/apps/ios/SimpleX Localizations/pt.xcloc/Localized Contents/pt.xliff +++ b/apps/ios/SimpleX Localizations/pt.xcloc/Localized Contents/pt.xliff @@ -214,22 +214,17 @@ Available in v5.1 ) No comment provided by engineer. - - **Add new contact**: to create your one-time QR Code or link for your contact. - **Adicionar novo contato**: para criar seu QR Code único ou link para seu contato. - No comment provided by engineer. - **Create link / QR code** for your contact to use. No comment provided by engineer. - - **More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have. + + **More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata. **Mais privado**: verifique novas mensagens a cada 20 minutos. O token do dispositivo é compartilhado com o servidor SimpleX Chat, mas não com quantos contatos ou mensagens você possui. No comment provided by engineer. - - **Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app). + + **Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app. **Totalmente privado**: não use o servidor de notificações do SimpleX Chat, verifique as mensagens periodicamente em segundo plano (depende da frequência com que você usa o aplicativo). No comment provided by engineer. @@ -242,8 +237,8 @@ Available in v5.1 **Atenção**: Você NÃO poderá recuperar ou alterar a senha caso a perca. No comment provided by engineer. - - **Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from. + + **Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from. **Recomendado**: O token do dispositivo e as notificações são enviados ao servidor de notificação do SimpleX Chat, mas não o conteúdo, o tamanho da mensagem ou de quem ela é. No comment provided by engineer. @@ -1812,8 +1807,8 @@ Available in v5.1 Immediately No comment provided by engineer. - - Immune to spam and abuse + + Immune to spam No comment provided by engineer. @@ -2278,8 +2273,8 @@ Available in v5.1 Onion hosts will not be used. No comment provided by engineer. - - Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**. + + Only client devices store user profiles, contacts, groups, and messages. No comment provided by engineer. @@ -2338,8 +2333,8 @@ Available in v5.1 Open user profiles authentication reason - - Open-source protocol and code – anybody can run the servers. + + Anybody can host servers. No comment provided by engineer. @@ -2394,8 +2389,8 @@ Available in v5.1 Paste the link you received into the box below to connect with your contact. No comment provided by engineer. - - People can connect to you only via the links you share. + + You decide who can connect. No comment provided by engineer. @@ -3098,8 +3093,8 @@ Available in v5.1 Thanks to the users – contribute via Weblate! No comment provided by engineer. - - The 1st platform without any user identifiers – private by design. + + No user identifiers. No comment provided by engineer. @@ -3143,8 +3138,8 @@ It can happen because of some bug or when the connection is compromised.The message will be marked as moderated for all members. No comment provided by engineer. - - The next generation of private messaging + + The future of messaging No comment provided by engineer. @@ -3215,8 +3210,8 @@ It can happen because of some bug or when the connection is compromised.To make a new connection No comment provided by engineer. - - To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts. + + To protect your privacy, SimpleX uses separate IDs for each of your contacts. No comment provided by engineer. @@ -3582,10 +3577,6 @@ SimpleX Lock must be enabled. You can't send messages! No comment provided by engineer. - - You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them. - No comment provided by engineer. - You could not be verified; please try again. No comment provided by engineer. @@ -4302,8 +4293,8 @@ SimpleX servers cannot see your profile. %lld novas interface de idiomas No comment provided by engineer. - - **Add contact**: to create a new invitation link, or connect via a link you received. + + **Create 1-time link**: to create and share a new invitation link. **Adicionar contato**: para criar um novo link de convite ou conectar-se por meio de um link que você recebeu. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff index ced93b4c12..119f1650a0 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff @@ -12,21 +12,6 @@ No comment provided by engineer. - - - - No comment provided by engineer. - - - - - No comment provided by engineer. - - - - - No comment provided by engineer. - ( ( @@ -127,6 +112,14 @@ %@ подтверждён No comment provided by engineer. + + %@ server + No comment provided by engineer. + + + %@ servers + No comment provided by engineer. + %@ uploaded %@ загружено @@ -352,14 +345,9 @@ ) No comment provided by engineer. - - **Add contact**: to create a new invitation link, or connect via a link you received. - **Добавить контакт**: создать новую ссылку-приглашение или подключиться через полученную ссылку. - No comment provided by engineer. - - - **Add new contact**: to create your one-time QR Code or link for your contact. - **Добавить новый контакт**: чтобы создать одноразовый QR код или ссылку для Вашего контакта. + + **Create 1-time link**: to create and share a new invitation link. + **Добавить контакт**: создать и поделиться новой ссылкой-приглашением. No comment provided by engineer. @@ -367,14 +355,14 @@ **Создать группу**: создать новую группу. No comment provided by engineer. - - **More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have. - **Более конфиденциально**: проверять новые сообщения каждые 20 минут. Токен устройства будет отправлен на сервер уведомлений SimpleX Chat, но у сервера не будет информации о количестве контактов и сообщений. + + **More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata. + **Более конфиденциально**: проверять новые сообщения каждые 20 минут. Только токен устройства будет отправлен на сервер уведомлений SimpleX Chat, но у сервера не будет информации о количестве контактов и какой либо информации о сообщениях. No comment provided by engineer. - - **Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app). - **Самый конфиденциальный**: не использовать сервер уведомлений SimpleX Chat, проверять сообщения периодически в фоновом режиме (зависит от того насколько часто Вы используете приложение). + + **Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app. + **Самый конфиденциальный**: не использовать сервер уведомлений SimpleX Chat. Сообщения проверяются в фоновом режиме, когда система позволяет, в зависимости от того, как часто Вы используете приложение. No comment provided by engineer. @@ -387,11 +375,15 @@ **Внимание**: Вы не сможете восстановить или поменять пароль, если Вы его потеряете. No comment provided by engineer. - - **Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from. + + **Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from. **Рекомендовано**: токен устройства и уведомления отправляются на сервер SimpleX Chat, но сервер не получает сами сообщения, их размер или от кого они. No comment provided by engineer. + + **Scan / Paste link**: to connect via a link you received. + No comment provided by engineer. + **Warning**: Instant push notifications require passphrase saved in Keychain. **Внимание**: для работы мгновенных уведомлений пароль должен быть сохранен в Keychain. @@ -498,6 +490,14 @@ 1 неделю time interval + + 1-time link + No comment provided by engineer. + + + 1-time link can be used *with one contact only* - share in person or via any messenger. + No comment provided by engineer. + 5 minutes 5 минут @@ -567,21 +567,11 @@ Прекратить изменение адреса? No comment provided by engineer. - - About SimpleX - О SimpleX - No comment provided by engineer. - About SimpleX Chat Информация о SimpleX Chat No comment provided by engineer. - - About SimpleX address - Об адресе SimpleX - No comment provided by engineer. - Accent Акцент @@ -594,6 +584,10 @@ accept incoming call via notification swipe action + + Accept conditions + No comment provided by engineer. + Accept connection request? Принять запрос? @@ -610,6 +604,10 @@ accept contact request via notification swipe action + + Accepted conditions + No comment provided by engineer. + Acknowledged Подтверждено @@ -630,16 +628,6 @@ Добавьте адрес в свой профиль, чтобы Ваши контакты могли поделиться им. Профиль будет отправлен Вашим контактам. No comment provided by engineer. - - Add contact - Добавить контакт - No comment provided by engineer. - - - Add preset servers - Добавить серверы по умолчанию - No comment provided by engineer. - Add profile Добавить профиль @@ -665,6 +653,14 @@ Добавить приветственное сообщение No comment provided by engineer. + + Added media & file servers + No comment provided by engineer. + + + Added message servers + No comment provided by engineer. + Additional accent Дополнительный акцент @@ -690,6 +686,14 @@ Изменение адреса будет прекращено. Будет использоваться старый адрес. No comment provided by engineer. + + Address or 1-time link? + No comment provided by engineer. + + + Address settings + No comment provided by engineer. + Admins can block a member for all. Админы могут заблокировать члена группы. @@ -735,6 +739,11 @@ Все члены группы, которые соединились через эту ссылку, останутся в группе. No comment provided by engineer. + + All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages. + Все сообщения и файлы отправляются с **end-to-end шифрованием**, с постквантовой безопасностью в прямых разговорах. + No comment provided by engineer. + All messages will be deleted - this cannot be undone! Все сообщения будут удалены - это нельзя отменить! @@ -915,6 +924,11 @@ Принять звонок No comment provided by engineer. + + Anybody can host servers. + Кто угодно может запустить сервер. + No comment provided by engineer. + App build: %@ Сборка приложения: %@ @@ -1258,7 +1272,8 @@ Cancel Отменить - alert button + alert action + alert button Cancel migration @@ -1341,6 +1356,10 @@ authentication reason set passcode view + + Change user profiles + authentication reason + Chat archive Архив чата @@ -1426,10 +1445,18 @@ Чаты No comment provided by engineer. + + Check messages every 20 min. + No comment provided by engineer. + + + Check messages when allowed. + No comment provided by engineer. + Check server address and try again. Проверьте адрес сервера и попробуйте снова. - No comment provided by engineer. + alert title Chinese and Spanish interface @@ -1516,16 +1543,47 @@ Готово No comment provided by engineer. + + Conditions accepted on: %@. + No comment provided by engineer. + + + Conditions are accepted for the operator(s): **%@**. + No comment provided by engineer. + + + Conditions are already accepted for following operator(s): **%@**. + No comment provided by engineer. + + + Conditions of use + No comment provided by engineer. + + + Conditions will be accepted for enabled operators after 30 days. + No comment provided by engineer. + + + Conditions will be accepted for operator(s): **%@**. + No comment provided by engineer. + + + Conditions will be accepted for the operator(s): **%@**. + No comment provided by engineer. + + + Conditions will be accepted on: %@. + No comment provided by engineer. + + + Conditions will be automatically accepted for enabled operators on: %@. + No comment provided by engineer. + Configure ICE servers Настройка ICE серверов No comment provided by engineer. - - Configured %@ servers - Настроенные %@ серверы - No comment provided by engineer. - Confirm Подтвердить @@ -1715,6 +1773,10 @@ This is your own one-time link! Запрос на соединение отправлен! No comment provided by engineer. + + Connection security + No comment provided by engineer. + Connection terminated Подключение прервано @@ -1830,6 +1892,10 @@ This is your own one-time link! Создать No comment provided by engineer. + + Create 1-time link + No comment provided by engineer. + Create SimpleX address Создать адрес SimpleX @@ -1840,11 +1906,6 @@ This is your own one-time link! Создайте группу, используя случайный профиль. No comment provided by engineer. - - Create an address to let people connect with you. - Создайте адрес, чтобы можно было соединиться с вами. - No comment provided by engineer. - Create file Создание файла @@ -1925,6 +1986,10 @@ This is your own one-time link! Текущий Код No comment provided by engineer. + + Current conditions text couldn't be loaded, you can review conditions via this link: + No comment provided by engineer. + Current passphrase… Текущий пароль… @@ -2081,7 +2146,8 @@ This is your own one-time link! Delete Удалить - chat item action + alert action + chat item action swipe action @@ -2299,6 +2365,10 @@ This is your own one-time link! Ошибки удаления No comment provided by engineer. + + Delivered even when Apple drops them. + No comment provided by engineer. + Delivery Доставка @@ -2580,6 +2650,10 @@ This is your own one-time link! Длительность No comment provided by engineer. + + E2E encrypted notifications. + No comment provided by engineer. + Edit Редактировать @@ -2600,6 +2674,10 @@ This is your own one-time link! Включить (кроме исключений) No comment provided by engineer. + + Enable Flux + No comment provided by engineer. + Enable SimpleX Lock Включить блокировку SimpleX @@ -2805,6 +2883,10 @@ This is your own one-time link! Ошибка при прекращении изменения адреса No comment provided by engineer. + + Error accepting conditions + alert title + Error accepting contact request Ошибка при принятии запроса на соединение @@ -2820,6 +2902,10 @@ This is your own one-time link! Ошибка при добавлении членов группы No comment provided by engineer. + + Error adding server + alert title + Error changing address Ошибка при изменении адреса @@ -2960,10 +3046,9 @@ This is your own one-time link! Ошибка при вступлении в группу No comment provided by engineer. - - Error loading %@ servers - Ошибка загрузки %@ серверов - No comment provided by engineer. + + Error loading servers + alert title Error migrating settings @@ -3000,11 +3085,6 @@ This is your own one-time link! Ошибка сброса статистики No comment provided by engineer. - - Error saving %@ servers - Ошибка при сохранении %@ серверов - No comment provided by engineer. - Error saving ICE servers Ошибка при сохранении ICE серверов @@ -3025,6 +3105,10 @@ This is your own one-time link! Ошибка сохранения пароля в Keychain No comment provided by engineer. + + Error saving servers + alert title + Error saving settings Ошибка сохранения настроек @@ -3095,6 +3179,10 @@ This is your own one-time link! Ошибка при обновлении сообщения No comment provided by engineer. + + Error updating server + alert title + Error updating settings Ошибка при сохранении настроек сети @@ -3140,6 +3228,10 @@ This is your own one-time link! Ошибки No comment provided by engineer. + + Errors in servers configuration. + servers error + Even when disabled in the conversation. Даже когда они выключены в разговоре. @@ -3342,11 +3434,27 @@ This is your own one-time link! Починка не поддерживается членом группы No comment provided by engineer. + + For chat profile %@: + servers error + For console Для консоли No comment provided by engineer. + + For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server. + No comment provided by engineer. + + + For private routing + No comment provided by engineer. + + + For social media + No comment provided by engineer. + Forward Переслать @@ -3656,9 +3764,12 @@ Error: %2$@ Как SimpleX работает No comment provided by engineer. - - How it works - Как это работает + + How it affects privacy + No comment provided by engineer. + + + How it helps privacy No comment provided by engineer. @@ -3668,7 +3779,7 @@ Error: %2$@ How to use it - Как использовать + Про адрес No comment provided by engineer. @@ -3731,8 +3842,8 @@ Error: %2$@ Сразу No comment provided by engineer. - - Immune to spam and abuse + + Immune to spam Защищен от спама No comment provided by engineer. @@ -3872,6 +3983,11 @@ More improvements are coming soon! [SimpleX Chat для терминала](https://github.com/simplex-chat/simplex-chat) No comment provided by engineer. + + Instant + Мгновенно + No comment provided by engineer. + Instant push notifications will be hidden! @@ -3879,11 +3995,6 @@ More improvements are coming soon! No comment provided by engineer. - - Instantly - Мгновенно - No comment provided by engineer. - Interface Интерфейс @@ -3932,7 +4043,7 @@ More improvements are coming soon! Invalid server address! Ошибка в адресе сервера! - No comment provided by engineer. + alert title Invalid status @@ -4060,7 +4171,7 @@ This is your link for group %@! Keep Оставить - No comment provided by engineer. + alert action Keep conversation @@ -4075,7 +4186,7 @@ This is your link for group %@! Keep unused invitation? Оставить неиспользованное приглашение? - No comment provided by engineer. + alert title Keep your connections @@ -4162,11 +4273,6 @@ This is your link for group %@! "Живые" сообщения No comment provided by engineer. - - Local - Локальные - No comment provided by engineer. - Local name Локальное имя @@ -4187,11 +4293,6 @@ This is your link for group %@! Режим блокировки No comment provided by engineer. - - Make a private connection - Добавьте контакт - No comment provided by engineer. - Make one message disappear Одно исчезающее сообщение @@ -4202,21 +4303,11 @@ This is your link for group %@! Сделайте профиль скрытым! No comment provided by engineer. - - Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@). - Пожалуйста, проверьте, что адреса %@ серверов имеют правильный формат, каждый адрес на отдельной строке и не повторяется (%@). - No comment provided by engineer. - Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated. Пожалуйста, проверьте, что адреса WebRTC ICE серверов имеют правильный формат, каждый адрес на отдельной строке и не повторяется. No comment provided by engineer. - - Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?* - Много пользователей спросили: *как SimpleX доставляет сообщения без идентификаторов пользователей?* - No comment provided by engineer. - Mark deleted for everyone Пометить как удаленное для всех @@ -4497,6 +4588,10 @@ This is your link for group %@! Более надежное соединение с сетью. No comment provided by engineer. + + More reliable notifications + No comment provided by engineer. + Most likely this connection is deleted. Скорее всего, соединение удалено. @@ -4532,6 +4627,10 @@ This is your link for group %@! Интернет-соединение No comment provided by engineer. + + Network decentralization + No comment provided by engineer. + Network issues - message expired after many attempts to send it. Ошибка сети - сообщение не было отправлено после многократных попыток. @@ -4542,6 +4641,10 @@ This is your link for group %@! Статус сети No comment provided by engineer. + + Network operator + No comment provided by engineer. + Network settings Настройки сети @@ -4602,6 +4705,10 @@ This is your link for group %@! Новое имя No comment provided by engineer. + + New events + notification + New in %@ Новое в %@ @@ -4627,6 +4734,10 @@ This is your link for group %@! Новый пароль… No comment provided by engineer. + + New server + No comment provided by engineer. + No Нет @@ -4682,6 +4793,14 @@ This is your link for group %@! Нет информации, попробуйте перезагрузить No comment provided by engineer. + + No media & file servers. + servers error + + + No message servers. + servers error + No network connection Нет интернет-соединения @@ -4702,11 +4821,37 @@ This is your link for group %@! Нет разрешения для записи голосового сообщения No comment provided by engineer. + + No push server + Локальные + No comment provided by engineer. + No received or sent files Нет полученных или отправленных файлов No comment provided by engineer. + + No servers for private message routing. + servers error + + + No servers to receive files. + servers error + + + No servers to receive messages. + servers error + + + No servers to send files. + servers error + + + No user identifiers. + Без идентификаторов пользователей. + No comment provided by engineer. + Not compatible! Несовместимая версия! @@ -4732,6 +4877,10 @@ This is your link for group %@! Уведомления выключены No comment provided by engineer. + + Notifications privacy + No comment provided by engineer. + Now admins can: - delete members' messages. @@ -4790,9 +4939,9 @@ Requires compatible VPN. Onion хосты не используются. No comment provided by engineer. - - Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**. - Только пользовательские устройства хранят контакты, группы и сообщения, которые отправляются **с двухуровневым end-to-end шифрованием**. + + Only client devices store user profiles, contacts, groups, and messages. + Только пользовательские устройства хранят контакты, группы и сообщения. No comment provided by engineer. @@ -4875,6 +5024,10 @@ Requires compatible VPN. Открыть Настройки No comment provided by engineer. + + Open changes + No comment provided by engineer. + Open chat Открыть чат @@ -4885,6 +5038,10 @@ Requires compatible VPN. Открыть консоль authentication reason + + Open conditions + No comment provided by engineer. + Open group Открыть группу @@ -4895,26 +5052,19 @@ Requires compatible VPN. Открытие миграции на другое устройство authentication reason - - Open server settings - Открыть настройки серверов - No comment provided by engineer. - - - Open user profiles - Открыть профили пользователя - authentication reason - - - Open-source protocol and code – anybody can run the servers. - Открытый протокол и код - кто угодно может запустить сервер. - No comment provided by engineer. - Opening app… Приложение отрывается… No comment provided by engineer. + + Operator + No comment provided by engineer. + + + Operator server + alert title + Or paste archive link Или вставьте ссылку архива @@ -4935,16 +5085,15 @@ Requires compatible VPN. Или покажите этот код No comment provided by engineer. + + Or to share privately + No comment provided by engineer. + Other Другaя сеть No comment provided by engineer. - - Other %@ servers - Другие %@ серверы - No comment provided by engineer. - Other file errors: %@ @@ -5027,13 +5176,8 @@ Requires compatible VPN. В ожидании No comment provided by engineer. - - People can connect to you only via the links you share. - С Вами можно соединиться только через созданные Вами ссылки. - No comment provided by engineer. - - - Periodically + + Periodic Периодически No comment provided by engineer. @@ -5156,16 +5300,15 @@ Error: %@ Сохранить последний черновик, вместе с вложениями. No comment provided by engineer. - - Preset server - Сервер по умолчанию - No comment provided by engineer. - Preset server address Адрес сервера по умолчанию No comment provided by engineer. + + Preset servers + No comment provided by engineer. + Preview Просмотр @@ -5244,7 +5387,7 @@ Error: %@ Profile update will be sent to your contacts. Обновлённый профиль будет отправлен Вашим контактам. - No comment provided by engineer. + alert message Prohibit audio/video calls. @@ -5338,6 +5481,10 @@ Enable in *Network & servers* settings. Прокси требует пароль No comment provided by engineer. + + Push Notifications + No comment provided by engineer. + Push notifications Доставка уведомлений @@ -5378,26 +5525,21 @@ Enable in *Network & servers* settings. Узнать больше No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address). - Узнать больше в [Руководстве пользователя](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address). - No comment provided by engineer. - Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). Дополнительная информация в [Руководстве пользователя](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). No comment provided by engineer. + + Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). + Узнать больше в [Руководстве пользователя](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). + No comment provided by engineer. + Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends). Узнать больше в [Руководстве пользователя](https://simplex.chat/docs/guide/readme.html#connect-to-friends). No comment provided by engineer. - - Read more in our GitHub repository. - Узнайте больше из нашего GitHub репозитория. - No comment provided by engineer. - Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme). Узнайте больше из нашего [GitHub репозитория](https://github.com/simplex-chat/simplex-chat#readme). @@ -5714,6 +5856,14 @@ Enable in *Network & servers* settings. Показать chat item action + + Review conditions + No comment provided by engineer. + + + Review later + No comment provided by engineer. + Revoke Отозвать @@ -5759,6 +5909,14 @@ Enable in *Network & servers* settings. Более безопасные группы No comment provided by engineer. + + Same conditions will apply to operator **%@**. + No comment provided by engineer. + + + Same conditions will apply to operator(s): **%@**. + No comment provided by engineer. + Save Сохранить @@ -5828,7 +5986,7 @@ Enable in *Network & servers* settings. Save servers? Сохранить серверы? - No comment provided by engineer. + alert title Save welcome message? @@ -6040,11 +6198,6 @@ Enable in *Network & servers* settings. Отправлять уведомления No comment provided by engineer. - - Send notifications: - Отправлять уведомления: - No comment provided by engineer. - Send questions and ideas Отправьте вопросы и идеи @@ -6170,6 +6323,10 @@ Enable in *Network & servers* settings. Сервер No comment provided by engineer. + + Server added to operator %@. + alert message + Server address Адрес сервера @@ -6185,6 +6342,18 @@ Enable in *Network & servers* settings. Адрес сервера несовместим с сетевыми настройками: %@. No comment provided by engineer. + + Server operator changed. + alert title + + + Server operators + No comment provided by engineer. + + + Server protocol changed. + alert title + Server requires authorization to create queues, check password Сервер требует авторизации для создания очередей, проверьте пароль @@ -6303,22 +6472,35 @@ Enable in *Network & servers* settings. Share Поделиться - chat item action + alert action + chat item action Share 1-time link Поделиться одноразовой ссылкой No comment provided by engineer. + + Share 1-time link with a friend + No comment provided by engineer. + + + Share SimpleX address on social media. + No comment provided by engineer. + Share address Поделиться адресом No comment provided by engineer. + + Share address publicly + No comment provided by engineer. + Share address with contacts? Поделиться адресом с контактами? - No comment provided by engineer. + alert title Share from other apps. @@ -6435,6 +6617,14 @@ Enable in *Network & servers* settings. Адрес SimpleX No comment provided by engineer. + + SimpleX address and 1-time links are safe to share via any messenger. + No comment provided by engineer. + + + SimpleX address or 1-time link? + No comment provided by engineer. + SimpleX contact address SimpleX ссылка-контакт @@ -6525,6 +6715,11 @@ Enable in *Network & servers* settings. Во время импорта произошли некоторые ошибки: No comment provided by engineer. + + Some servers failed the test: +%@ + alert message + Somebody Контакт @@ -6608,12 +6803,12 @@ Enable in *Network & servers* settings. Stop sharing Прекратить делиться - No comment provided by engineer. + alert action Stop sharing address? Прекратить делиться адресом? - No comment provided by engineer. + alert title Stopping chat @@ -6763,7 +6958,7 @@ Enable in *Network & servers* settings. Tests failed! Ошибка тестов! - No comment provided by engineer. + alert title Thank you for installing SimpleX Chat! @@ -6780,11 +6975,6 @@ Enable in *Network & servers* settings. Благодаря пользователям – добавьте переводы через Weblate! No comment provided by engineer. - - The 1st platform without any user identifiers – private by design. - Первая в мире платформа без идентификаторов пользователей. - No comment provided by engineer. - The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised. @@ -6797,6 +6987,10 @@ It can happen because of some bug or when the connection is compromised.Приложение может посылать Вам уведомления о сообщениях и запросах на соединение - уведомления можно включить в Настройках. No comment provided by engineer. + + The app protects your privacy by using different operators in each conversation. + No comment provided by engineer. + The app will ask to confirm downloads from unknown file servers (except .onion). Приложение будет запрашивать подтверждение загрузки с неизвестных серверов (за исключением .onion адресов). @@ -6812,6 +7006,10 @@ It can happen because of some bug or when the connection is compromised.Этот QR код не является SimpleX-ccылкой. No comment provided by engineer. + + The connection reached the limit of undelivered messages, your contact may be offline. + No comment provided by engineer. + The connection you accepted will be cancelled! Подтвержденное соединение будет отменено! @@ -6832,6 +7030,11 @@ It can happen because of some bug or when the connection is compromised.Шифрование работает, и новое соглашение не требуется. Это может привести к ошибкам соединения! No comment provided by engineer. + + The future of messaging + Будущее коммуникаций + No comment provided by engineer. + The hash of the previous message is different. Хэш предыдущего сообщения отличается. @@ -6857,11 +7060,6 @@ It can happen because of some bug or when the connection is compromised.Сообщения будут помечены как удаленные для всех членов группы. No comment provided by engineer. - - The next generation of private messaging - Новое поколение приватных сообщений - No comment provided by engineer. - The old database was not removed during the migration, it can be deleted. Предыдущая версия данных чата не удалена при перемещении, её можно удалить. @@ -6872,6 +7070,10 @@ It can happen because of some bug or when the connection is compromised.Профиль отправляется только Вашим контактам. No comment provided by engineer. + + The second preset operator in the app! + No comment provided by engineer. + The second tick we missed! ✅ Вторая галочка - знать, что доставлено! ✅ @@ -6887,6 +7089,10 @@ It can happen because of some bug or when the connection is compromised.Серверы для новых соединений Вашего текущего профиля чата **%@**. No comment provided by engineer. + + The servers for new files of your current chat profile **%@**. + No comment provided by engineer. + The text you pasted is not a SimpleX link. Вставленный текст не является SimpleX-ссылкой. @@ -6902,6 +7108,10 @@ It can happen because of some bug or when the connection is compromised.Темы No comment provided by engineer. + + These conditions will also apply for: **%@**. + No comment provided by engineer. + These settings are for your current profile **%@**. Установки для Вашего активного профиля **%@**. @@ -7002,9 +7212,8 @@ It can happen because of some bug or when the connection is compromised.Чтобы соединиться No comment provided by engineer. - - To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts. - Чтобы защитить Вашу конфиденциальность, вместо ID пользователей, которые есть в других платформах, SimpleX использует ID для очередей сообщений, разные для каждого контакта. + + To protect against your link being replaced, you can compare contact security codes. No comment provided by engineer. @@ -7024,6 +7233,15 @@ You will be prompted to complete authentication before this feature is enabled.< Вам будет нужно пройти аутентификацию для включения блокировки. No comment provided by engineer. + + To protect your privacy, SimpleX uses separate IDs for each of your contacts. + Чтобы защитить Вашу конфиденциальность, SimpleX использует разные идентификаторы для каждого Вашeго контакта. + No comment provided by engineer. + + + To receive + No comment provided by engineer. + To record speech please grant permission to use Microphone. Для записи речи, пожалуйста, дайте разрешение на использование микрофона. @@ -7044,11 +7262,19 @@ You will be prompted to complete authentication before this feature is enabled.< Чтобы показать Ваш скрытый профиль, введите его пароль в поле поиска на странице **Ваши профили чата**. No comment provided by engineer. + + To send + No comment provided by engineer. + To support instant push notifications the chat database has to be migrated. Для поддержки мгновенный доставки уведомлений данные чата должны быть перемещены. No comment provided by engineer. + + To use the servers of **%@**, accept conditions of use. + No comment provided by engineer. + To verify end-to-end encryption with your contact compare (or scan) the code on your devices. Чтобы подтвердить end-to-end шифрование с Вашим контактом сравните (или сканируйте) код безопасности на Ваших устройствах. @@ -7139,6 +7365,10 @@ You will be prompted to complete authentication before this feature is enabled.< Разблокировать члена группы? No comment provided by engineer. + + Undelivered messages + No comment provided by engineer. + Unexpected migration state Неожиданная ошибка при перемещении данных чата @@ -7296,6 +7526,10 @@ To connect, please ask your contact to create another connection link and check Загрузка архива No comment provided by engineer. + + Use %@ + No comment provided by engineer. + Use .onion hosts Использовать .onion хосты @@ -7321,6 +7555,14 @@ To connect, please ask your contact to create another connection link and check Использовать активный профиль No comment provided by engineer. + + Use for files + No comment provided by engineer. + + + Use for messages + No comment provided by engineer. + Use for new connections Использовать для новых соединений @@ -7361,6 +7603,10 @@ To connect, please ask your contact to create another connection link and check Использовать сервер No comment provided by engineer. + + Use servers + No comment provided by engineer. + Use the app while in the call. Используйте приложение во время звонка. @@ -7451,11 +7697,19 @@ To connect, please ask your contact to create another connection link and check Видео и файлы до 1гб No comment provided by engineer. + + View conditions + No comment provided by engineer. + View security code Показать код безопасности No comment provided by engineer. + + View updated conditions + No comment provided by engineer. + Visible history Доступ к истории @@ -7566,9 +7820,8 @@ To connect, please ask your contact to create another connection link and check Во время соединения аудио и видео звонков. No comment provided by engineer. - - When people request to connect, you can accept or reject it. - Когда Вы получите запрос на соединение, Вы можете принять или отклонить его. + + When more than one operator is enabled, none of them has metadata to learn who communicates with whom. No comment provided by engineer. @@ -7728,6 +7981,18 @@ Repeat join request? Вы можете изменить это в настройках Интерфейса. No comment provided by engineer. + + You can configure operators in Network & servers settings. + No comment provided by engineer. + + + You can configure servers via settings. + No comment provided by engineer. + + + You can create it in user picker. + No comment provided by engineer. + You can create it later Вы можете создать его позже @@ -7768,6 +8033,10 @@ Repeat join request? Вы можете отправлять сообщения %@ из Архивированных контактов. No comment provided by engineer. + + You can set connection name, to remember who the link was shared with. + No comment provided by engineer. + You can set lock screen notification preview via settings. Вы можете установить просмотр уведомлений на экране блокировки в настройках. @@ -7783,11 +8052,6 @@ Repeat join request? Вы можете поделиться этим адресом с Вашими контактами, чтобы они могли соединиться с **%@**. No comment provided by engineer. - - You can share your address as a link or QR code - anybody can connect to you. - Вы можете использовать Ваш адрес как ссылку или как QR код - кто угодно сможет соединиться с Вами. - No comment provided by engineer. - You can start chat via app Settings / Database or by restarting the app Вы можете запустить чат через Настройки приложения или перезапустив приложение. @@ -7811,23 +8075,23 @@ Repeat join request? You can view invitation link again in connection details. Вы можете увидеть ссылку-приглашение снова открыв соединение. - No comment provided by engineer. + alert message You can't send messages! Вы не можете отправлять сообщения! No comment provided by engineer. - - You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them. - Вы определяете через какие серверы Вы **получаете сообщения**, Ваши контакты - серверы, которые Вы используете для отправки. - No comment provided by engineer. - You could not be verified; please try again. Верификация не удалась; пожалуйста, попробуйте ещё раз. No comment provided by engineer. + + You decide who can connect. + Вы определяете, кто может соединиться. + No comment provided by engineer. + You have already requested connection via this address! Вы уже запросили соединение через этот адрес! @@ -7950,11 +8214,6 @@ Repeat connection request? Вы используете инкогнито профиль для этой группы - чтобы предотвратить раскрытие Вашего основного профиля, приглашать контакты не разрешено No comment provided by engineer. - - Your %@ servers - Ваши %@ серверы - No comment provided by engineer. - Your ICE servers Ваши ICE серверы @@ -7970,11 +8229,6 @@ Repeat connection request? Ваш адрес SimpleX No comment provided by engineer. - - Your XFTP servers - Ваши XFTP серверы - No comment provided by engineer. - Your calls Ваши звонки @@ -8075,16 +8329,15 @@ Repeat connection request? Случайный профиль No comment provided by engineer. - - Your server - Ваш сервер - No comment provided by engineer. - Your server address Адрес Вашего сервера No comment provided by engineer. + + Your servers + No comment provided by engineer. + Your settings Настройки @@ -8505,6 +8758,10 @@ Repeat connection request? истекло No comment provided by engineer. + + for better metadata privacy. + No comment provided by engineer. + forwarded переслано @@ -9127,6 +9384,33 @@ last received msg: %2$@ + +
+ +
+ + + %d new events + notification body + + + From: %@ + notification body + + + New events + notification + + + New messages + notification + + + New messages in %d chats + notification body + + +
diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/en.lproj/Localizable.strings index cf485752ea..cb83427195 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/en.lproj/Localizable.strings +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/en.lproj/Localizable.strings @@ -1,9 +1,6 @@ /* No comment provided by engineer. */ "_italic_" = "\\_italic_"; -/* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact."; - /* No comment provided by engineer. */ "*bold*" = "\\*bold*"; @@ -27,4 +24,3 @@ /* No comment provided by engineer. */ "No group!" = "Group not found!"; - diff --git a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff index 37ade821f0..e16565b6fa 100644 --- a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff +++ b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff @@ -12,21 +12,6 @@ No comment provided by engineer. - - - - No comment provided by engineer. - - - - - No comment provided by engineer. - - - - - No comment provided by engineer. - ( ( @@ -120,6 +105,14 @@ %@ ได้รับการตรวจสอบแล้ว No comment provided by engineer. + + %@ server + No comment provided by engineer. + + + %@ servers + No comment provided by engineer. + %@ uploaded No comment provided by engineer. @@ -328,26 +321,21 @@ ) No comment provided by engineer. - - **Add contact**: to create a new invitation link, or connect via a link you received. - No comment provided by engineer. - - - **Add new contact**: to create your one-time QR Code or link for your contact. - **เพิ่มผู้ติดต่อใหม่**: เพื่อสร้างคิวอาร์โค้ดแบบใช้ครั้งเดียวหรือลิงก์สำหรับผู้ติดต่อของคุณ + + **Create 1-time link**: to create and share a new invitation link. No comment provided by engineer. **Create group**: to create a new group. No comment provided by engineer. - - **More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have. + + **More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata. **เป็นส่วนตัวมากขึ้น**: ตรวจสอบข้อความใหม่ทุกๆ 20 นาที โทเค็นอุปกรณ์แชร์กับเซิร์ฟเวอร์ SimpleX Chat แต่ไม่ระบุจำนวนผู้ติดต่อหรือข้อความที่คุณมี No comment provided by engineer. - - **Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app). + + **Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app. **ส่วนตัวที่สุด**: ไม่ใช้เซิร์ฟเวอร์การแจ้งเตือนของ SimpleX Chat ตรวจสอบข้อความเป็นระยะในพื้นหลัง (ขึ้นอยู่กับความถี่ที่คุณใช้แอป) No comment provided by engineer. @@ -360,11 +348,15 @@ **โปรดทราบ**: คุณจะไม่สามารถกู้คืนหรือเปลี่ยนรหัสผ่านได้หากคุณทำรหัสผ่านหาย No comment provided by engineer. - - **Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from. + + **Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from. **แนะนำ**: โทเค็นอุปกรณ์และการแจ้งเตือนจะถูกส่งไปยังเซิร์ฟเวอร์การแจ้งเตือนของ SimpleX Chat แต่ไม่ใช่เนื้อหาข้อความ ขนาด หรือผู้ที่ส่ง No comment provided by engineer. + + **Scan / Paste link**: to connect via a link you received. + No comment provided by engineer. + **Warning**: Instant push notifications require passphrase saved in Keychain. **คำเตือน**: การแจ้งเตือนแบบพุชทันทีจำเป็นต้องบันทึกรหัสผ่านไว้ใน Keychain @@ -463,6 +455,14 @@ 1 สัปดาห์ time interval + + 1-time link + No comment provided by engineer. + + + 1-time link can be used *with one contact only* - share in person or via any messenger. + No comment provided by engineer. + 5 minutes 5 นาที @@ -531,21 +531,11 @@ ยกเลิกการเปลี่ยนที่อยู่? No comment provided by engineer. - - About SimpleX - เกี่ยวกับ SimpleX - No comment provided by engineer. - About SimpleX Chat เกี่ยวกับ SimpleX Chat No comment provided by engineer. - - About SimpleX address - เกี่ยวกับที่อยู่ SimpleX - No comment provided by engineer. - Accent No comment provided by engineer. @@ -557,6 +547,10 @@ accept incoming call via notification swipe action + + Accept conditions + No comment provided by engineer. + Accept connection request? No comment provided by engineer. @@ -572,6 +566,10 @@ accept contact request via notification swipe action + + Accepted conditions + No comment provided by engineer. + Acknowledged No comment provided by engineer. @@ -589,15 +587,6 @@ เพิ่มที่อยู่ลงในโปรไฟล์ของคุณ เพื่อให้ผู้ติดต่อของคุณสามารถแชร์กับผู้อื่นได้ การอัปเดตโปรไฟล์จะถูกส่งไปยังผู้ติดต่อของคุณ No comment provided by engineer. - - Add contact - No comment provided by engineer. - - - Add preset servers - เพิ่มเซิร์ฟเวอร์ที่ตั้งไว้ล่วงหน้า - No comment provided by engineer. - Add profile เพิ่มโปรไฟล์ @@ -623,6 +612,14 @@ เพิ่มข้อความต้อนรับ No comment provided by engineer. + + Added media & file servers + No comment provided by engineer. + + + Added message servers + No comment provided by engineer. + Additional accent No comment provided by engineer. @@ -645,6 +642,14 @@ การเปลี่ยนแปลงที่อยู่จะถูกยกเลิก จะใช้ที่อยู่เก่าของผู้รับ No comment provided by engineer. + + Address or 1-time link? + No comment provided by engineer. + + + Address settings + No comment provided by engineer. + Admins can block a member for all. No comment provided by engineer. @@ -687,6 +692,10 @@ สมาชิกในกลุ่มทุกคนจะยังคงเชื่อมต่ออยู่. No comment provided by engineer. + + All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages. + No comment provided by engineer. + All messages will be deleted - this cannot be undone! No comment provided by engineer. @@ -856,6 +865,11 @@ รับสาย No comment provided by engineer. + + Anybody can host servers. + โปรโตคอลและโค้ดโอเพ่นซอร์ส – ใคร ๆ ก็สามารถเปิดใช้เซิร์ฟเวอร์ได้ + No comment provided by engineer. + App build: %@ รุ่นแอป: %@ @@ -1164,7 +1178,8 @@ Cancel ยกเลิก - alert button + alert action + alert button Cancel migration @@ -1243,6 +1258,10 @@ authentication reason set passcode view + + Change user profiles + authentication reason + Chat archive ที่เก็บแชทถาวร @@ -1321,10 +1340,18 @@ แชท No comment provided by engineer. + + Check messages every 20 min. + No comment provided by engineer. + + + Check messages when allowed. + No comment provided by engineer. + Check server address and try again. ตรวจสอบที่อยู่เซิร์ฟเวอร์แล้วลองอีกครั้ง - No comment provided by engineer. + alert title Chinese and Spanish interface @@ -1403,15 +1430,47 @@ Completed No comment provided by engineer. + + Conditions accepted on: %@. + No comment provided by engineer. + + + Conditions are accepted for the operator(s): **%@**. + No comment provided by engineer. + + + Conditions are already accepted for following operator(s): **%@**. + No comment provided by engineer. + + + Conditions of use + No comment provided by engineer. + + + Conditions will be accepted for enabled operators after 30 days. + No comment provided by engineer. + + + Conditions will be accepted for operator(s): **%@**. + No comment provided by engineer. + + + Conditions will be accepted for the operator(s): **%@**. + No comment provided by engineer. + + + Conditions will be accepted on: %@. + No comment provided by engineer. + + + Conditions will be automatically accepted for enabled operators on: %@. + No comment provided by engineer. + Configure ICE servers กำหนดค่าเซิร์ฟเวอร์ ICE No comment provided by engineer. - - Configured %@ servers - No comment provided by engineer. - Confirm ยืนยัน @@ -1575,6 +1634,10 @@ This is your own one-time link! ส่งคําขอเชื่อมต่อแล้ว! No comment provided by engineer. + + Connection security + No comment provided by engineer. + Connection terminated No comment provided by engineer. @@ -1680,6 +1743,10 @@ This is your own one-time link! สร้าง No comment provided by engineer. + + Create 1-time link + No comment provided by engineer. + Create SimpleX address สร้างที่อยู่ SimpleX @@ -1689,11 +1756,6 @@ This is your own one-time link! Create a group using a random profile. No comment provided by engineer. - - Create an address to let people connect with you. - สร้างที่อยู่เพื่อให้ผู้อื่นเชื่อมต่อกับคุณ - No comment provided by engineer. - Create file สร้างไฟล์ @@ -1766,6 +1828,10 @@ This is your own one-time link! รหัสผ่านปัจจุบัน No comment provided by engineer. + + Current conditions text couldn't be loaded, you can review conditions via this link: + No comment provided by engineer. + Current passphrase… รหัสผ่านปัจจุบัน… @@ -1917,7 +1983,8 @@ This is your own one-time link! Delete ลบ - chat item action + alert action + chat item action swipe action @@ -2125,6 +2192,10 @@ This is your own one-time link! Deletion errors No comment provided by engineer. + + Delivered even when Apple drops them. + No comment provided by engineer. + Delivery No comment provided by engineer. @@ -2380,6 +2451,10 @@ This is your own one-time link! ระยะเวลา No comment provided by engineer. + + E2E encrypted notifications. + No comment provided by engineer. + Edit แก้ไข @@ -2400,6 +2475,10 @@ This is your own one-time link! เปิดใช้งาน (เก็บการแทนที่) No comment provided by engineer. + + Enable Flux + No comment provided by engineer. + Enable SimpleX Lock เปิดใช้งาน SimpleX Lock @@ -2592,6 +2671,10 @@ This is your own one-time link! เกิดข้อผิดพลาดในการยกเลิกการเปลี่ยนที่อยู่ No comment provided by engineer. + + Error accepting conditions + alert title + Error accepting contact request เกิดข้อผิดพลาดในการรับคำขอติดต่อ @@ -2607,6 +2690,10 @@ This is your own one-time link! เกิดข้อผิดพลาดในการเพิ่มสมาชิก No comment provided by engineer. + + Error adding server + alert title + Error changing address เกิดข้อผิดพลาดในการเปลี่ยนที่อยู่ @@ -2739,10 +2826,9 @@ This is your own one-time link! เกิดข้อผิดพลาดในการเข้าร่วมกลุ่ม No comment provided by engineer. - - Error loading %@ servers - โหลดเซิร์ฟเวอร์ %@ ผิดพลาด - No comment provided by engineer. + + Error loading servers + alert title Error migrating settings @@ -2774,11 +2860,6 @@ This is your own one-time link! Error resetting statistics No comment provided by engineer. - - Error saving %@ servers - เกิดข้อผิดพลาดในการบันทึกเซิร์ฟเวอร์ %@ - No comment provided by engineer. - Error saving ICE servers เกิดข้อผิดพลาดในการบันทึกเซิร์ฟเวอร์ ICE @@ -2799,6 +2880,10 @@ This is your own one-time link! เกิดข้อผิดพลาดในการบันทึกรหัสผ่านไปยัง keychain No comment provided by engineer. + + Error saving servers + alert title + Error saving settings when migrating @@ -2865,6 +2950,10 @@ This is your own one-time link! เกิดข้อผิดพลาดในการอัปเดตข้อความ No comment provided by engineer. + + Error updating server + alert title + Error updating settings เกิดข้อผิดพลาดในการอัปเดตการตั้งค่า @@ -2907,6 +2996,10 @@ This is your own one-time link! Errors No comment provided by engineer. + + Errors in servers configuration. + servers error + Even when disabled in the conversation. แม้ในขณะที่ปิดใช้งานในการสนทนา @@ -3094,11 +3187,27 @@ This is your own one-time link! การแก้ไขไม่สนับสนุนโดยสมาชิกกลุ่ม No comment provided by engineer. + + For chat profile %@: + servers error + For console สำหรับคอนโซล No comment provided by engineer. + + For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server. + No comment provided by engineer. + + + For private routing + No comment provided by engineer. + + + For social media + No comment provided by engineer. + Forward chat item action @@ -3384,9 +3493,12 @@ Error: %2$@ วิธีการ SimpleX ทํางานอย่างไร No comment provided by engineer. - - How it works - มันทำงานอย่างไร + + How it affects privacy + No comment provided by engineer. + + + How it helps privacy No comment provided by engineer. @@ -3457,8 +3569,8 @@ Error: %2$@ โดยทันที No comment provided by engineer. - - Immune to spam and abuse + + Immune to spam มีภูมิคุ้มกันต่อสแปมและการละเมิด No comment provided by engineer. @@ -3588,6 +3700,11 @@ More improvements are coming soon! ติดตั้ง [SimpleX Chat สำหรับเทอร์มินัล](https://github.com/simplex-chat/simplex-chat) No comment provided by engineer. + + Instant + ทันที + No comment provided by engineer. + Instant push notifications will be hidden! @@ -3595,11 +3712,6 @@ More improvements are coming soon! No comment provided by engineer. - - Instantly - ทันที - No comment provided by engineer. - Interface อินเตอร์เฟซ @@ -3641,7 +3753,7 @@ More improvements are coming soon! Invalid server address! ที่อยู่เซิร์ฟเวอร์ไม่ถูกต้อง! - No comment provided by engineer. + alert title Invalid status @@ -3761,7 +3873,7 @@ This is your link for group %@! Keep - No comment provided by engineer. + alert action Keep conversation @@ -3773,7 +3885,7 @@ This is your link for group %@! Keep unused invitation? - No comment provided by engineer. + alert title Keep your connections @@ -3857,11 +3969,6 @@ This is your link for group %@! ข้อความสด No comment provided by engineer. - - Local - ในเครื่อง - No comment provided by engineer. - Local name ชื่อภายในเครื่องเท่านั้น @@ -3882,11 +3989,6 @@ This is your link for group %@! โหมดล็อค No comment provided by engineer. - - Make a private connection - สร้างการเชื่อมต่อแบบส่วนตัว - No comment provided by engineer. - Make one message disappear ทำให้ข้อความหายไปหนึ่งข้อความ @@ -3897,21 +3999,11 @@ This is your link for group %@! ทำให้โปรไฟล์เป็นส่วนตัว! No comment provided by engineer. - - Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@). - ตรวจสอบให้แน่ใจว่าที่อยู่เซิร์ฟเวอร์ %@ อยู่ในรูปแบบที่ถูกต้อง แยกบรรทัดและไม่ซ้ำกัน (%@) - No comment provided by engineer. - Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated. ตรวจสอบให้แน่ใจว่าที่อยู่เซิร์ฟเวอร์ WebRTC ICE อยู่ในรูปแบบที่ถูกต้อง แยกบรรทัดและไม่ซ้ำกัน No comment provided by engineer. - - Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?* - หลายคนถามว่า: *หาก SimpleX ไม่มีตัวระบุผู้ใช้ จะส่งข้อความได้อย่างไร?* - No comment provided by engineer. - Mark deleted for everyone ทำเครื่องหมายว่าลบแล้วสำหรับทุกคน @@ -4163,6 +4255,10 @@ This is your link for group %@! More reliable network connection. No comment provided by engineer. + + More reliable notifications + No comment provided by engineer. + Most likely this connection is deleted. item status description @@ -4196,6 +4292,10 @@ This is your link for group %@! Network connection No comment provided by engineer. + + Network decentralization + No comment provided by engineer. + Network issues - message expired after many attempts to send it. snd error text @@ -4204,6 +4304,10 @@ This is your link for group %@! Network management No comment provided by engineer. + + Network operator + No comment provided by engineer. + Network settings การตั้งค่าเครือข่าย @@ -4259,6 +4363,10 @@ This is your link for group %@! ชื่อที่แสดงใหม่ No comment provided by engineer. + + New events + notification + New in %@ ใหม่ใน %@ @@ -4283,6 +4391,10 @@ This is your link for group %@! รหัสผ่านใหม่… No comment provided by engineer. + + New server + No comment provided by engineer. + No เลขที่ @@ -4335,6 +4447,14 @@ This is your link for group %@! No info, try to reload No comment provided by engineer. + + No media & file servers. + servers error + + + No message servers. + servers error + No network connection No comment provided by engineer. @@ -4352,11 +4472,37 @@ This is your link for group %@! ไม่อนุญาตให้บันทึกข้อความเสียง No comment provided by engineer. + + No push server + ในเครื่อง + No comment provided by engineer. + No received or sent files ไม่มีไฟล์ที่ได้รับหรือส่ง No comment provided by engineer. + + No servers for private message routing. + servers error + + + No servers to receive files. + servers error + + + No servers to receive messages. + servers error + + + No servers to send files. + servers error + + + No user identifiers. + แพลตฟอร์มแรกที่ไม่มีตัวระบุผู้ใช้ - ถูกออกแบบให้เป็นส่วนตัว + No comment provided by engineer. + Not compatible! No comment provided by engineer. @@ -4379,6 +4525,10 @@ This is your link for group %@! ปิดการแจ้งเตือน! No comment provided by engineer. + + Notifications privacy + No comment provided by engineer. + Now admins can: - delete members' messages. @@ -4434,8 +4584,8 @@ Requires compatible VPN. โฮสต์หัวหอมจะไม่ถูกใช้ No comment provided by engineer. - - Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**. + + Only client devices store user profiles, contacts, groups, and messages. เฉพาะอุปกรณ์ไคลเอนต์เท่านั้นที่จัดเก็บโปรไฟล์ผู้ใช้ ผู้ติดต่อ กลุ่ม และข้อความที่ส่งด้วย **การเข้ารหัส encrypt แบบ 2 ชั้น** No comment provided by engineer. @@ -4517,6 +4667,10 @@ Requires compatible VPN. เปิดการตั้งค่า No comment provided by engineer. + + Open changes + No comment provided by engineer. + Open chat เปิดแชท @@ -4527,6 +4681,10 @@ Requires compatible VPN. เปิดคอนโซลการแชท authentication reason + + Open conditions + No comment provided by engineer. + Open group No comment provided by engineer. @@ -4535,24 +4693,18 @@ Requires compatible VPN. Open migration to another device authentication reason - - Open server settings - No comment provided by engineer. - - - Open user profiles - เปิดโปรไฟล์ผู้ใช้ - authentication reason - - - Open-source protocol and code – anybody can run the servers. - โปรโตคอลและโค้ดโอเพ่นซอร์ส – ใคร ๆ ก็สามารถเปิดใช้เซิร์ฟเวอร์ได้ - No comment provided by engineer. - Opening app… No comment provided by engineer. + + Operator + No comment provided by engineer. + + + Operator server + alert title + Or paste archive link No comment provided by engineer. @@ -4569,12 +4721,12 @@ Requires compatible VPN. Or show this code No comment provided by engineer. - - Other + + Or to share privately No comment provided by engineer. - - Other %@ servers + + Other No comment provided by engineer. @@ -4651,13 +4803,8 @@ Requires compatible VPN. Pending No comment provided by engineer. - - People can connect to you only via the links you share. - ผู้คนสามารถเชื่อมต่อกับคุณผ่านลิงก์ที่คุณแบ่งปันเท่านั้น - No comment provided by engineer. - - - Periodically + + Periodic เป็นระยะๆ No comment provided by engineer. @@ -4771,16 +4918,15 @@ Error: %@ เก็บข้อความที่ร่างไว้ล่าสุดพร้อมไฟล์แนบ No comment provided by engineer. - - Preset server - เซิร์ฟเวอร์ที่ตั้งไว้ล่วงหน้า - No comment provided by engineer. - Preset server address ที่อยู่เซิร์ฟเวอร์ที่ตั้งไว้ล่วงหน้า No comment provided by engineer. + + Preset servers + No comment provided by engineer. + Preview ดูตัวอย่าง @@ -4851,7 +4997,7 @@ Error: %@ Profile update will be sent to your contacts. การอัปเดตโปรไฟล์จะถูกส่งไปยังผู้ติดต่อของคุณ - No comment provided by engineer. + alert message Prohibit audio/video calls. @@ -4938,6 +5084,10 @@ Enable in *Network & servers* settings. Proxy requires password No comment provided by engineer. + + Push Notifications + No comment provided by engineer. + Push notifications การแจ้งเตือนแบบทันที @@ -4975,25 +5125,20 @@ Enable in *Network & servers* settings. อ่านเพิ่มเติม No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address). - อ่านเพิ่มเติมใน[คู่มือผู้ใช้](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address) - No comment provided by engineer. - Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). No comment provided by engineer. + + Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). + อ่านเพิ่มเติมใน[คู่มือผู้ใช้](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses) + No comment provided by engineer. + Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends). อ่านเพิ่มเติมใน[คู่มือผู้ใช้](https://simplex.chat/docs/guide/readme.html#connect-to-friends) No comment provided by engineer. - - Read more in our GitHub repository. - อ่านเพิ่มเติมในที่เก็บ GitHub ของเรา - No comment provided by engineer. - Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme). อ่านเพิ่มเติมใน[พื้นที่เก็บข้อมูล GitHub](https://github.com/simplex-chat/simplex-chat#readme) @@ -5284,6 +5429,14 @@ Enable in *Network & servers* settings. เปิดเผย chat item action + + Review conditions + No comment provided by engineer. + + + Review later + No comment provided by engineer. + Revoke ถอน @@ -5325,6 +5478,14 @@ Enable in *Network & servers* settings. Safer groups No comment provided by engineer. + + Same conditions will apply to operator **%@**. + No comment provided by engineer. + + + Same conditions will apply to operator(s): **%@**. + No comment provided by engineer. + Save บันทึก @@ -5393,7 +5554,7 @@ Enable in *Network & servers* settings. Save servers? บันทึกเซิร์ฟเวอร์? - No comment provided by engineer. + alert title Save welcome message? @@ -5585,11 +5746,6 @@ Enable in *Network & servers* settings. ส่งการแจ้งเตือน No comment provided by engineer. - - Send notifications: - ส่งการแจ้งเตือน: - No comment provided by engineer. - Send questions and ideas ส่งคําถามและความคิด @@ -5706,6 +5862,10 @@ Enable in *Network & servers* settings. Server No comment provided by engineer. + + Server added to operator %@. + alert message + Server address No comment provided by engineer. @@ -5718,6 +5878,18 @@ Enable in *Network & servers* settings. Server address is incompatible with network settings: %@. No comment provided by engineer. + + Server operator changed. + alert title + + + Server operators + No comment provided by engineer. + + + Server protocol changed. + alert title + Server requires authorization to create queues, check password เซิร์ฟเวอร์ต้องการการอนุญาตในการสร้างคิว โปรดตรวจสอบรหัสผ่าน @@ -5826,22 +5998,35 @@ Enable in *Network & servers* settings. Share แชร์ - chat item action + alert action + chat item action Share 1-time link แชร์ลิงก์แบบใช้ครั้งเดียว No comment provided by engineer. + + Share 1-time link with a friend + No comment provided by engineer. + + + Share SimpleX address on social media. + No comment provided by engineer. + Share address แชร์ที่อยู่ No comment provided by engineer. + + Share address publicly + No comment provided by engineer. + Share address with contacts? แชร์ที่อยู่กับผู้ติดต่อ? - No comment provided by engineer. + alert title Share from other apps. @@ -5948,6 +6133,14 @@ Enable in *Network & servers* settings. ที่อยู่ SimpleX No comment provided by engineer. + + SimpleX address and 1-time links are safe to share via any messenger. + No comment provided by engineer. + + + SimpleX address or 1-time link? + No comment provided by engineer. + SimpleX contact address ที่อยู่ติดต่อ SimpleX @@ -6028,6 +6221,11 @@ Enable in *Network & servers* settings. Some non-fatal errors occurred during import: No comment provided by engineer. + + Some servers failed the test: +%@ + alert message + Somebody ใครบางคน @@ -6106,12 +6304,12 @@ Enable in *Network & servers* settings. Stop sharing หยุดแชร์ - No comment provided by engineer. + alert action Stop sharing address? หยุดแชร์ที่อยู่ไหม? - No comment provided by engineer. + alert title Stopping chat @@ -6248,7 +6446,7 @@ Enable in *Network & servers* settings. Tests failed! การทดสอบล้มเหลว! - No comment provided by engineer. + alert title Thank you for installing SimpleX Chat! @@ -6265,11 +6463,6 @@ Enable in *Network & servers* settings. ขอบคุณผู้ใช้ – มีส่วนร่วมผ่าน Weblate! No comment provided by engineer. - - The 1st platform without any user identifiers – private by design. - แพลตฟอร์มแรกที่ไม่มีตัวระบุผู้ใช้ - ถูกออกแบบให้เป็นส่วนตัว - No comment provided by engineer. - The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised. @@ -6283,6 +6476,10 @@ It can happen because of some bug or when the connection is compromised.แอปสามารถแจ้งให้คุณทราบเมื่อคุณได้รับข้อความหรือคำขอติดต่อ - โปรดเปิดการตั้งค่าเพื่อเปิดใช้งาน No comment provided by engineer. + + The app protects your privacy by using different operators in each conversation. + No comment provided by engineer. + The app will ask to confirm downloads from unknown file servers (except .onion). No comment provided by engineer. @@ -6296,6 +6493,10 @@ It can happen because of some bug or when the connection is compromised.The code you scanned is not a SimpleX link QR code. No comment provided by engineer. + + The connection reached the limit of undelivered messages, your contact may be offline. + No comment provided by engineer. + The connection you accepted will be cancelled! การเชื่อมต่อที่คุณยอมรับจะถูกยกเลิก! @@ -6316,6 +6517,11 @@ It can happen because of some bug or when the connection is compromised.encryption กำลังทำงานและไม่จำเป็นต้องใช้ข้อตกลง encryption ใหม่ อาจทำให้การเชื่อมต่อผิดพลาดได้! No comment provided by engineer. + + The future of messaging + การส่งข้อความส่วนตัวรุ่นต่อไป + No comment provided by engineer. + The hash of the previous message is different. แฮชของข้อความก่อนหน้านี้แตกต่างกัน @@ -6339,11 +6545,6 @@ It can happen because of some bug or when the connection is compromised.The messages will be marked as moderated for all members. No comment provided by engineer. - - The next generation of private messaging - การส่งข้อความส่วนตัวรุ่นต่อไป - No comment provided by engineer. - The old database was not removed during the migration, it can be deleted. ฐานข้อมูลเก่าไม่ได้ถูกลบในระหว่างการย้ายข้อมูล แต่สามารถลบได้ @@ -6354,6 +6555,10 @@ It can happen because of some bug or when the connection is compromised.โปรไฟล์นี้แชร์กับผู้ติดต่อของคุณเท่านั้น No comment provided by engineer. + + The second preset operator in the app! + No comment provided by engineer. + The second tick we missed! ✅ ขีดที่สองที่เราพลาด! ✅ @@ -6369,6 +6574,10 @@ It can happen because of some bug or when the connection is compromised.เซิร์ฟเวอร์สำหรับการเชื่อมต่อใหม่ของโปรไฟล์การแชทปัจจุบันของคุณ **%@** No comment provided by engineer. + + The servers for new files of your current chat profile **%@**. + No comment provided by engineer. + The text you pasted is not a SimpleX link. No comment provided by engineer. @@ -6381,6 +6590,10 @@ It can happen because of some bug or when the connection is compromised.Themes No comment provided by engineer. + + These conditions will also apply for: **%@**. + No comment provided by engineer. + These settings are for your current profile **%@**. การตั้งค่าเหล่านี้ใช้สำหรับโปรไฟล์ปัจจุบันของคุณ **%@** @@ -6470,9 +6683,8 @@ It can happen because of some bug or when the connection is compromised.เพื่อสร้างการเชื่อมต่อใหม่ No comment provided by engineer. - - To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts. - เพื่อปกป้องความเป็นส่วนตัว แทนที่จะใช้ ID ผู้ใช้เหมือนที่แพลตฟอร์มอื่นๆใช้ SimpleX มีตัวระบุสำหรับคิวข้อความ โดยแยกจากกันสำหรับผู้ติดต่อแต่ละราย + + To protect against your link being replaced, you can compare contact security codes. No comment provided by engineer. @@ -6491,6 +6703,15 @@ You will be prompted to complete authentication before this feature is enabled.< คุณจะได้รับแจ้งให้ยืนยันตัวตนให้เสร็จสมบูรณ์ก่อนที่จะเปิดใช้งานคุณลักษณะนี้ No comment provided by engineer. + + To protect your privacy, SimpleX uses separate IDs for each of your contacts. + เพื่อปกป้องความเป็นส่วนตัว แทนที่จะใช้ ID ผู้ใช้เหมือนที่แพลตฟอร์มอื่นๆใช้ SimpleX มีตัวระบุสำหรับคิวข้อความ โดยแยกจากกันสำหรับผู้ติดต่อแต่ละราย + No comment provided by engineer. + + + To receive + No comment provided by engineer. + To record speech please grant permission to use Microphone. No comment provided by engineer. @@ -6509,11 +6730,19 @@ You will be prompted to complete authentication before this feature is enabled.< หากต้องการเปิดเผยโปรไฟล์ที่ซ่อนอยู่ของคุณ ให้ป้อนรหัสผ่านแบบเต็มในช่องค้นหาในหน้า **โปรไฟล์แชทของคุณ** No comment provided by engineer. + + To send + No comment provided by engineer. + To support instant push notifications the chat database has to be migrated. เพื่อรองรับการแจ้งเตือนแบบทันที ฐานข้อมูลการแชทจะต้องได้รับการโยกย้าย No comment provided by engineer. + + To use the servers of **%@**, accept conditions of use. + No comment provided by engineer. + To verify end-to-end encryption with your contact compare (or scan) the code on your devices. ในการตรวจสอบการเข้ารหัสแบบ encrypt จากต้นจนจบ กับผู้ติดต่อของคุณ ให้เปรียบเทียบ (หรือสแกน) รหัสบนอุปกรณ์ของคุณ @@ -6593,6 +6822,10 @@ You will be prompted to complete authentication before this feature is enabled.< Unblock member? No comment provided by engineer. + + Undelivered messages + No comment provided by engineer. + Unexpected migration state สถานะการย้ายข้อมูลที่ไม่คาดคิด @@ -6740,6 +6973,10 @@ To connect, please ask your contact to create another connection link and check Uploading archive No comment provided by engineer. + + Use %@ + No comment provided by engineer. + Use .onion hosts ใช้โฮสต์ .onion @@ -6763,6 +7000,14 @@ To connect, please ask your contact to create another connection link and check Use current profile No comment provided by engineer. + + Use for files + No comment provided by engineer. + + + Use for messages + No comment provided by engineer. + Use for new connections ใช้สำหรับการเชื่อมต่อใหม่ @@ -6798,6 +7043,10 @@ To connect, please ask your contact to create another connection link and check ใช้เซิร์ฟเวอร์ No comment provided by engineer. + + Use servers + No comment provided by engineer. + Use the app while in the call. No comment provided by engineer. @@ -6878,11 +7127,19 @@ To connect, please ask your contact to create another connection link and check วิดีโอและไฟล์สูงสุด 1gb No comment provided by engineer. + + View conditions + No comment provided by engineer. + View security code ดูรหัสความปลอดภัย No comment provided by engineer. + + View updated conditions + No comment provided by engineer. + Visible history chat feature @@ -6985,9 +7242,8 @@ To connect, please ask your contact to create another connection link and check When connecting audio and video calls. No comment provided by engineer. - - When people request to connect, you can accept or reject it. - เมื่อมีคนขอเชื่อมต่อ คุณสามารถยอมรับหรือปฏิเสธได้ + + When more than one operator is enabled, none of them has metadata to learn who communicates with whom. No comment provided by engineer. @@ -7126,6 +7382,18 @@ Repeat join request? You can change it in Appearance settings. No comment provided by engineer. + + You can configure operators in Network & servers settings. + No comment provided by engineer. + + + You can configure servers via settings. + No comment provided by engineer. + + + You can create it in user picker. + No comment provided by engineer. + You can create it later คุณสามารถสร้างได้ในภายหลัง @@ -7163,6 +7431,10 @@ Repeat join request? You can send messages to %@ from Archived contacts. No comment provided by engineer. + + You can set connection name, to remember who the link was shared with. + No comment provided by engineer. + You can set lock screen notification preview via settings. คุณสามารถตั้งค่าแสดงตัวอย่างการแจ้งเตือนบนหน้าจอล็อคผ่านการตั้งค่า @@ -7178,11 +7450,6 @@ Repeat join request? คุณสามารถแบ่งปันที่อยู่นี้กับผู้ติดต่อของคุณเพื่อให้พวกเขาเชื่อมต่อกับ **%@** No comment provided by engineer. - - You can share your address as a link or QR code - anybody can connect to you. - คุณสามารถแชร์ที่อยู่ของคุณเป็นลิงก์หรือรหัสคิวอาร์ - ใคร ๆ ก็สามารถเชื่อมต่อกับคุณได้ - No comment provided by engineer. - You can start chat via app Settings / Database or by restarting the app คุณสามารถเริ่มแชทผ่านการตั้งค่าแอป / ฐานข้อมูล หรือโดยการรีสตาร์ทแอป @@ -7204,23 +7471,23 @@ Repeat join request? You can view invitation link again in connection details. - No comment provided by engineer. + alert message You can't send messages! คุณไม่สามารถส่งข้อความได้! No comment provided by engineer. - - You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them. - คุณควบคุมผ่านเซิร์ฟเวอร์ **เพื่อรับ** ข้อความผู้ติดต่อของคุณ - เซิร์ฟเวอร์ที่คุณใช้เพื่อส่งข้อความถึงพวกเขา - No comment provided by engineer. - You could not be verified; please try again. เราไม่สามารถตรวจสอบคุณได้ กรุณาลองอีกครั้ง. No comment provided by engineer. + + You decide who can connect. + ผู้คนสามารถเชื่อมต่อกับคุณผ่านลิงก์ที่คุณแบ่งปันเท่านั้น + No comment provided by engineer. + You have already requested connection via this address! No comment provided by engineer. @@ -7334,11 +7601,6 @@ Repeat connection request? คุณกำลังใช้โปรไฟล์ที่ไม่ระบุตัวตนสำหรับกลุ่มนี้ - ไม่อนุญาตให้เชิญผู้ติดต่อเพื่อป้องกันการแชร์โปรไฟล์หลักของคุณ No comment provided by engineer. - - Your %@ servers - เซิร์ฟเวอร์ %@ ของคุณ - No comment provided by engineer. - Your ICE servers เซิร์ฟเวอร์ ICE ของคุณ @@ -7354,11 +7616,6 @@ Repeat connection request? ที่อยู่ SimpleX ของคุณ No comment provided by engineer. - - Your XFTP servers - เซิร์ฟเวอร์ XFTP ของคุณ - No comment provided by engineer. - Your calls การโทรของคุณ @@ -7453,16 +7710,15 @@ Repeat connection request? โปรไฟล์แบบสุ่มของคุณ No comment provided by engineer. - - Your server - เซิร์ฟเวอร์ของคุณ - No comment provided by engineer. - Your server address ที่อยู่เซิร์ฟเวอร์ของคุณ No comment provided by engineer. + + Your servers + No comment provided by engineer. + Your settings การตั้งค่าของคุณ @@ -7866,6 +8122,10 @@ Repeat connection request? expired No comment provided by engineer. + + for better metadata privacy. + No comment provided by engineer. + forwarded No comment provided by engineer. @@ -8453,6 +8713,33 @@ last received msg: %2$@ + +
+ +
+ + + %d new events + notification body + + + From: %@ + notification body + + + New events + notification + + + New messages + notification + + + New messages in %d chats + notification body + + +
diff --git a/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/en.lproj/Localizable.strings index cf485752ea..cb83427195 100644 --- a/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/en.lproj/Localizable.strings +++ b/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/en.lproj/Localizable.strings @@ -1,9 +1,6 @@ /* No comment provided by engineer. */ "_italic_" = "\\_italic_"; -/* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact."; - /* No comment provided by engineer. */ "*bold*" = "\\*bold*"; @@ -27,4 +24,3 @@ /* No comment provided by engineer. */ "No group!" = "Group not found!"; - diff --git a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff index b911eb1220..0b2149e9ce 100644 --- a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff @@ -12,21 +12,6 @@ No comment provided by engineer. - - - - No comment provided by engineer. - - - - - No comment provided by engineer. - - - - - No comment provided by engineer. - ( ( @@ -127,6 +112,14 @@ %@ onaylandı No comment provided by engineer. + + %@ server + No comment provided by engineer. + + + %@ servers + No comment provided by engineer. + %@ uploaded %@ yüklendi @@ -352,28 +345,23 @@ ) No comment provided by engineer. - - **Add contact**: to create a new invitation link, or connect via a link you received. + + **Create 1-time link**: to create and share a new invitation link. **Kişi ekle**: yeni bir davet bağlantısı oluşturmak için, ya da aldığın bağlantıyla bağlan. No comment provided by engineer. - - **Add new contact**: to create your one-time QR Code or link for your contact. - **Yeni kişi ekleyin**: tek seferlik QR Kodunuzu oluşturmak veya kişisel ulaşım bilgileri bağlantısı için. - No comment provided by engineer. - **Create group**: to create a new group. **Grup oluştur**: yeni bir grup oluşturmak için. No comment provided by engineer. - - **More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have. + + **More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata. **Daha gizli**: her 20 dakikada yeni mesajlar için kontrol et. Cihaz jetonu SimpleX Chat sunucusuyla paylaşılacak, ama ne kadar kişi veya mesaja sahip olduğun paylaşılmayacak. No comment provided by engineer. - - **Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app). + + **Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app. **En gizli**: SimpleX Chat bildirim sunucusunu kullanma, arkaplanda mesajları periyodik olarak kontrol edin (uygulamayı ne sıklıkta kullandığınıza bağlıdır). No comment provided by engineer. @@ -387,11 +375,15 @@ **Lütfen aklınızda bulunsun**: eğer parolanızı kaybederseniz parolanızı değiştirme veya geri kurtarma ihtimaliniz YOKTUR. No comment provided by engineer. - - **Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from. + + **Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from. **Önerilen**: cihaz tokeni ve bildirimler SimpleX Chat bildirim sunucularına gönderilir, ama mesajın içeriği, boyutu veya kimden geldiği gönderilmez. No comment provided by engineer. + + **Scan / Paste link**: to connect via a link you received. + No comment provided by engineer. + **Warning**: Instant push notifications require passphrase saved in Keychain. **Dikkat**: Anında iletilen bildirimlere Anahtar Zinciri'nde kaydedilmiş parola gereklidir. @@ -498,6 +490,14 @@ 1 hafta time interval + + 1-time link + No comment provided by engineer. + + + 1-time link can be used *with one contact only* - share in person or via any messenger. + No comment provided by engineer. + 5 minutes 5 dakika @@ -567,21 +567,11 @@ Adres değişimi iptal edilsin mi? No comment provided by engineer. - - About SimpleX - SimpleX Hakkında - No comment provided by engineer. - About SimpleX Chat SimpleX Chat hakkında No comment provided by engineer. - - About SimpleX address - SimpleX Chat adresi hakkında - No comment provided by engineer. - Accent Ana renk @@ -594,6 +584,10 @@ accept incoming call via notification swipe action + + Accept conditions + No comment provided by engineer. + Accept connection request? Bağlantı isteği kabul edilsin mi? @@ -610,6 +604,10 @@ accept contact request via notification swipe action + + Accepted conditions + No comment provided by engineer. + Acknowledged Onaylandı @@ -630,16 +628,6 @@ Kişilerinizin başkalarıyla paylaşabilmesi için profilinize adres ekleyin. Profil güncellemesi kişilerinize gönderilecek. No comment provided by engineer. - - Add contact - Kişi ekle - No comment provided by engineer. - - - Add preset servers - Önceden ayarlanmış sunucu ekle - No comment provided by engineer. - Add profile Profil ekle @@ -665,6 +653,14 @@ Karşılama mesajı ekleyin No comment provided by engineer. + + Added media & file servers + No comment provided by engineer. + + + Added message servers + No comment provided by engineer. + Additional accent Ek ana renk @@ -690,6 +686,14 @@ Adres değişikliği iptal edilecek. Eski alıcı adresi kullanılacaktır. No comment provided by engineer. + + Address or 1-time link? + No comment provided by engineer. + + + Address settings + No comment provided by engineer. + Admins can block a member for all. Yöneticiler bir üyeyi tamamen engelleyebilirler. @@ -735,6 +739,10 @@ Tüm grup üyeleri bağlı kalacaktır. No comment provided by engineer. + + All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages. + No comment provided by engineer. + All messages will be deleted - this cannot be undone! Tüm mesajlar silinecektir - bu geri alınamaz! @@ -915,6 +923,11 @@ Aramayı cevapla No comment provided by engineer. + + Anybody can host servers. + Açık kaynak protokolü ve kodu - herhangi biri sunucuları çalıştırabilir. + No comment provided by engineer. + App build: %@ Uygulama sürümü: %@ @@ -1258,7 +1271,8 @@ Cancel İptal et - alert button + alert action + alert button Cancel migration @@ -1341,6 +1355,10 @@ authentication reason set passcode view + + Change user profiles + authentication reason + Chat archive Sohbet arşivi @@ -1426,10 +1444,18 @@ Sohbetler No comment provided by engineer. + + Check messages every 20 min. + No comment provided by engineer. + + + Check messages when allowed. + No comment provided by engineer. + Check server address and try again. Sunucu adresini kontrol edip tekrar deneyin. - No comment provided by engineer. + alert title Chinese and Spanish interface @@ -1516,16 +1542,47 @@ Tamamlandı No comment provided by engineer. + + Conditions accepted on: %@. + No comment provided by engineer. + + + Conditions are accepted for the operator(s): **%@**. + No comment provided by engineer. + + + Conditions are already accepted for following operator(s): **%@**. + No comment provided by engineer. + + + Conditions of use + No comment provided by engineer. + + + Conditions will be accepted for enabled operators after 30 days. + No comment provided by engineer. + + + Conditions will be accepted for operator(s): **%@**. + No comment provided by engineer. + + + Conditions will be accepted for the operator(s): **%@**. + No comment provided by engineer. + + + Conditions will be accepted on: %@. + No comment provided by engineer. + + + Conditions will be automatically accepted for enabled operators on: %@. + No comment provided by engineer. + Configure ICE servers ICE sunucularını ayarla No comment provided by engineer. - - Configured %@ servers - Yapılandırılmış %@ sunucuları - No comment provided by engineer. - Confirm Onayla @@ -1715,6 +1772,10 @@ Bu senin kendi tek kullanımlık bağlantın! Bağlantı daveti gönderildi! No comment provided by engineer. + + Connection security + No comment provided by engineer. + Connection terminated Bağlantı sonlandırılmış @@ -1830,6 +1891,10 @@ Bu senin kendi tek kullanımlık bağlantın! Oluştur No comment provided by engineer. + + Create 1-time link + No comment provided by engineer. + Create SimpleX address SimpleX adresi oluştur @@ -1840,11 +1905,6 @@ Bu senin kendi tek kullanımlık bağlantın! Rasgele profil kullanarak grup oluştur. No comment provided by engineer. - - Create an address to let people connect with you. - İnsanların seninle bağlanması için bir adres oluştur. - No comment provided by engineer. - Create file Dosya oluştur @@ -1925,6 +1985,10 @@ Bu senin kendi tek kullanımlık bağlantın! Şu anki şifre No comment provided by engineer. + + Current conditions text couldn't be loaded, you can review conditions via this link: + No comment provided by engineer. + Current passphrase… Şu anki parola… @@ -2081,7 +2145,8 @@ Bu senin kendi tek kullanımlık bağlantın! Delete Sil - chat item action + alert action + chat item action swipe action @@ -2299,6 +2364,10 @@ Bu senin kendi tek kullanımlık bağlantın! Silme hatası No comment provided by engineer. + + Delivered even when Apple drops them. + No comment provided by engineer. + Delivery Teslimat @@ -2580,6 +2649,10 @@ Bu senin kendi tek kullanımlık bağlantın! Süre No comment provided by engineer. + + E2E encrypted notifications. + No comment provided by engineer. + Edit Düzenle @@ -2600,6 +2673,10 @@ Bu senin kendi tek kullanımlık bağlantın! Etkinleştir (geçersiz kılmaları koru) No comment provided by engineer. + + Enable Flux + No comment provided by engineer. + Enable SimpleX Lock SimpleX Kilidini etkinleştir @@ -2805,6 +2882,10 @@ Bu senin kendi tek kullanımlık bağlantın! Adres değişikliği iptal edilirken hata oluştu No comment provided by engineer. + + Error accepting conditions + alert title + Error accepting contact request Bağlantı isteği kabul edilirken hata oluştu @@ -2820,6 +2901,10 @@ Bu senin kendi tek kullanımlık bağlantın! Üye(ler) eklenirken hata oluştu No comment provided by engineer. + + Error adding server + alert title + Error changing address Adres değiştirilirken hata oluştu @@ -2960,10 +3045,9 @@ Bu senin kendi tek kullanımlık bağlantın! Gruba katılırken hata oluştu No comment provided by engineer. - - Error loading %@ servers - %@ sunucuları yüklenirken hata oluştu - No comment provided by engineer. + + Error loading servers + alert title Error migrating settings @@ -3000,11 +3084,6 @@ Bu senin kendi tek kullanımlık bağlantın! Hata istatistikler sıfırlanıyor No comment provided by engineer. - - Error saving %@ servers - %@ sunucuları kaydedilirken sorun oluştu - No comment provided by engineer. - Error saving ICE servers ICE sunucularını kaydedirken sorun oluştu @@ -3025,6 +3104,10 @@ Bu senin kendi tek kullanımlık bağlantın! Parolayı Anahtar Zincirine kaydederken hata oluştu No comment provided by engineer. + + Error saving servers + alert title + Error saving settings Ayarlar kaydedilirken hata oluştu @@ -3095,6 +3178,10 @@ Bu senin kendi tek kullanımlık bağlantın! Mesaj güncellenirken hata oluştu No comment provided by engineer. + + Error updating server + alert title + Error updating settings Ayarları güncellerken hata oluştu @@ -3140,6 +3227,10 @@ Bu senin kendi tek kullanımlık bağlantın! Hatalar No comment provided by engineer. + + Errors in servers configuration. + servers error + Even when disabled in the conversation. Konuşma sırasında devre dışı bırakılsa bile. @@ -3342,11 +3433,27 @@ Bu senin kendi tek kullanımlık bağlantın! Düzeltme grup üyesi tarafından desteklenmiyor No comment provided by engineer. + + For chat profile %@: + servers error + For console Konsol için No comment provided by engineer. + + For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server. + No comment provided by engineer. + + + For private routing + No comment provided by engineer. + + + For social media + No comment provided by engineer. + Forward İlet @@ -3656,9 +3763,12 @@ Hata: %2$@ SimpleX nasıl çalışır No comment provided by engineer. - - How it works - Nasıl çalışıyor + + How it affects privacy + No comment provided by engineer. + + + How it helps privacy No comment provided by engineer. @@ -3731,8 +3841,8 @@ Hata: %2$@ Hemen No comment provided by engineer. - - Immune to spam and abuse + + Immune to spam Spam ve kötüye kullanıma karşı bağışıklı No comment provided by engineer. @@ -3873,6 +3983,11 @@ Daha fazla iyileştirme yakında geliyor! [Terminal için SimpleX Chat]i indir(https://github.com/simplex-chat/simplex-chat) No comment provided by engineer. + + Instant + Anında + No comment provided by engineer. + Instant push notifications will be hidden! @@ -3880,11 +3995,6 @@ Daha fazla iyileştirme yakında geliyor! No comment provided by engineer. - - Instantly - Anında - No comment provided by engineer. - Interface Arayüz @@ -3933,7 +4043,7 @@ Daha fazla iyileştirme yakında geliyor! Invalid server address! Geçersiz sunucu adresi! - No comment provided by engineer. + alert title Invalid status @@ -4061,7 +4171,7 @@ Bu senin grup için bağlantın %@! Keep Tut - No comment provided by engineer. + alert action Keep conversation @@ -4076,7 +4186,7 @@ Bu senin grup için bağlantın %@! Keep unused invitation? Kullanılmamış davet tutulsun mu? - No comment provided by engineer. + alert title Keep your connections @@ -4163,11 +4273,6 @@ Bu senin grup için bağlantın %@! Canlı mesajlar No comment provided by engineer. - - Local - Yerel - No comment provided by engineer. - Local name Yerel isim @@ -4188,11 +4293,6 @@ Bu senin grup için bağlantın %@! Kilit modu No comment provided by engineer. - - Make a private connection - Gizli bir bağlantı oluştur - No comment provided by engineer. - Make one message disappear Bir mesajın kaybolmasını sağlayın @@ -4203,21 +4303,11 @@ Bu senin grup için bağlantın %@! Profili gizli yap! No comment provided by engineer. - - Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@). - %@ sunucu adreslerinin doğru formatta olduğundan, satır ayrımı yapıldığından ve yinelenmediğinden (%@) emin olun. - No comment provided by engineer. - Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated. WebRTC ICE sunucu adreslerinin doğru formatta olduğundan, satırlara ayrıldığından ve yinelenmediğinden emin olun. No comment provided by engineer. - - Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?* - Çoğu kişi sordu: *eğer SimpleX'in hiç kullanıcı tanımlayıcıları yok, o zaman mesajları nasıl gönderebiliyor?* - No comment provided by engineer. - Mark deleted for everyone Herkes için silinmiş olarak işaretle @@ -4498,6 +4588,10 @@ Bu senin grup için bağlantın %@! Daha güvenilir ağ bağlantısı. No comment provided by engineer. + + More reliable notifications + No comment provided by engineer. + Most likely this connection is deleted. Büyük ihtimalle bu bağlantı silinmiş. @@ -4533,6 +4627,10 @@ Bu senin grup için bağlantın %@! Ağ bağlantısı No comment provided by engineer. + + Network decentralization + No comment provided by engineer. + Network issues - message expired after many attempts to send it. Ağ sorunları - birçok gönderme denemesinden sonra mesajın süresi doldu. @@ -4543,6 +4641,10 @@ Bu senin grup için bağlantın %@! Ağ yönetimi No comment provided by engineer. + + Network operator + No comment provided by engineer. + Network settings Ağ ayarları @@ -4603,6 +4705,10 @@ Bu senin grup için bağlantın %@! Yeni görünen ad No comment provided by engineer. + + New events + notification + New in %@ %@ da yeni @@ -4628,6 +4734,10 @@ Bu senin grup için bağlantın %@! Yeni parola… No comment provided by engineer. + + New server + No comment provided by engineer. + No Hayır @@ -4683,6 +4793,14 @@ Bu senin grup için bağlantın %@! Bilgi yok, yenilemeyi deneyin No comment provided by engineer. + + No media & file servers. + servers error + + + No message servers. + servers error + No network connection Ağ bağlantısı yok @@ -4703,11 +4821,37 @@ Bu senin grup için bağlantın %@! Sesli mesaj kaydetmek için izin yok No comment provided by engineer. + + No push server + Yerel + No comment provided by engineer. + No received or sent files Hiç alınmış veya gönderilmiş dosya yok No comment provided by engineer. + + No servers for private message routing. + servers error + + + No servers to receive files. + servers error + + + No servers to receive messages. + servers error + + + No servers to send files. + servers error + + + No user identifiers. + Herhangi bir kullanıcı tanımlayıcısı yok. + No comment provided by engineer. + Not compatible! Uyumlu değil! @@ -4733,6 +4877,10 @@ Bu senin grup için bağlantın %@! Bildirimler devre dışı! No comment provided by engineer. + + Notifications privacy + No comment provided by engineer. + Now admins can: - delete members' messages. @@ -4791,8 +4939,8 @@ VPN'nin etkinleştirilmesi gerekir. Onion ana bilgisayarları kullanılmayacaktır. No comment provided by engineer. - - Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**. + + Only client devices store user profiles, contacts, groups, and messages. Yalnızca istemci cihazlar kullanıcı profillerini, kişileri, grupları ve **2 katmanlı uçtan uca şifreleme** ile gönderilen mesajları depolar. No comment provided by engineer. @@ -4876,6 +5024,10 @@ VPN'nin etkinleştirilmesi gerekir. Ayarları aç No comment provided by engineer. + + Open changes + No comment provided by engineer. + Open chat Sohbeti aç @@ -4886,6 +5038,10 @@ VPN'nin etkinleştirilmesi gerekir. Sohbet konsolunu aç authentication reason + + Open conditions + No comment provided by engineer. + Open group Grubu aç @@ -4896,26 +5052,19 @@ VPN'nin etkinleştirilmesi gerekir. Başka bir cihaza açık geçiş authentication reason - - Open server settings - Sunucu ayarlarını aç - No comment provided by engineer. - - - Open user profiles - Kullanıcı profillerini aç - authentication reason - - - Open-source protocol and code – anybody can run the servers. - Açık kaynak protokolü ve kodu - herhangi biri sunucuları çalıştırabilir. - No comment provided by engineer. - Opening app… Uygulama açılıyor… No comment provided by engineer. + + Operator + No comment provided by engineer. + + + Operator server + alert title + Or paste archive link Veya arşiv bağlantısını yapıştırın @@ -4936,16 +5085,15 @@ VPN'nin etkinleştirilmesi gerekir. Veya bu kodu göster No comment provided by engineer. + + Or to share privately + No comment provided by engineer. + Other Diğer No comment provided by engineer. - - Other %@ servers - Diğer %@ sunucuları - No comment provided by engineer. - Other file errors: %@ @@ -5028,13 +5176,8 @@ VPN'nin etkinleştirilmesi gerekir. Bekleniyor No comment provided by engineer. - - People can connect to you only via the links you share. - İnsanlar size yalnızca paylaştığınız bağlantılar üzerinden ulaşabilir. - No comment provided by engineer. - - - Periodically + + Periodic Periyodik olarak No comment provided by engineer. @@ -5157,16 +5300,15 @@ Hata: %@ Son mesaj taslağını ekleriyle birlikte koru. No comment provided by engineer. - - Preset server - Ön ayarlı sunucu - No comment provided by engineer. - Preset server address Ön ayarlı sunucu adresi No comment provided by engineer. + + Preset servers + No comment provided by engineer. + Preview Ön izleme @@ -5245,7 +5387,7 @@ Hata: %@ Profile update will be sent to your contacts. Profil güncellemesi kişilerinize gönderilecektir. - No comment provided by engineer. + alert message Prohibit audio/video calls. @@ -5339,6 +5481,10 @@ Enable in *Network & servers* settings. Proxy şifre gerektirir No comment provided by engineer. + + Push Notifications + No comment provided by engineer. + Push notifications Anında bildirimler @@ -5379,26 +5525,21 @@ Enable in *Network & servers* settings. Dahasını oku No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address). - [Kullanıcı Rehberi]nde daha fazlasını okuyun(https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address). - No comment provided by engineer. - Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). [Kullanıcı Rehberi]nde daha fazlasını okuyun(https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). No comment provided by engineer. + + Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). + [Kullanıcı Rehberi]nde daha fazlasını okuyun(https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). + No comment provided by engineer. + Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends). [Kullanıcı Rehberi]nde daha fazlasını okuyun(https://simplex.chat/docs/guide/readme.html#connect-to-friends). No comment provided by engineer. - - Read more in our GitHub repository. - Daha fazlasını GitHub depomuzdan oku. - No comment provided by engineer. - Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme). [GitHub deposu]nda daha fazlasını okuyun(https://github.com/simplex-chat/simplex-chat#readme). @@ -5715,6 +5856,14 @@ Enable in *Network & servers* settings. Göster chat item action + + Review conditions + No comment provided by engineer. + + + Review later + No comment provided by engineer. + Revoke İptal et @@ -5760,6 +5909,14 @@ Enable in *Network & servers* settings. Daha güvenli gruplar No comment provided by engineer. + + Same conditions will apply to operator **%@**. + No comment provided by engineer. + + + Same conditions will apply to operator(s): **%@**. + No comment provided by engineer. + Save Kaydet @@ -5829,7 +5986,7 @@ Enable in *Network & servers* settings. Save servers? Sunucular kaydedilsin mi? - No comment provided by engineer. + alert title Save welcome message? @@ -6041,11 +6198,6 @@ Enable in *Network & servers* settings. Bildirimler gönder No comment provided by engineer. - - Send notifications: - Bildirimler gönder: - No comment provided by engineer. - Send questions and ideas Fikirler ve sorular gönderin @@ -6171,6 +6323,10 @@ Enable in *Network & servers* settings. Sunucu No comment provided by engineer. + + Server added to operator %@. + alert message + Server address Sunucu adresi @@ -6186,6 +6342,18 @@ Enable in *Network & servers* settings. Sunucu adresi ağ ayarlarıyla uyumsuz: %@. No comment provided by engineer. + + Server operator changed. + alert title + + + Server operators + No comment provided by engineer. + + + Server protocol changed. + alert title + Server requires authorization to create queues, check password Sunucunun sıra oluşturması için yetki gereklidir, şifreyi kontrol edin @@ -6304,22 +6472,35 @@ Enable in *Network & servers* settings. Share Paylaş - chat item action + alert action + chat item action Share 1-time link Tek kullanımlık bağlantıyı paylaş No comment provided by engineer. + + Share 1-time link with a friend + No comment provided by engineer. + + + Share SimpleX address on social media. + No comment provided by engineer. + Share address Adresi paylaş No comment provided by engineer. + + Share address publicly + No comment provided by engineer. + Share address with contacts? Kişilerle adres paylaşılsın mı? - No comment provided by engineer. + alert title Share from other apps. @@ -6436,6 +6617,14 @@ Enable in *Network & servers* settings. SimpleX adresi No comment provided by engineer. + + SimpleX address and 1-time links are safe to share via any messenger. + No comment provided by engineer. + + + SimpleX address or 1-time link? + No comment provided by engineer. + SimpleX contact address SimpleX kişi adresi @@ -6526,6 +6715,11 @@ Enable in *Network & servers* settings. İçe aktarma sırasında bazı önemli olmayan hatalar oluştu: No comment provided by engineer. + + Some servers failed the test: +%@ + alert message + Somebody Biri @@ -6609,12 +6803,12 @@ Enable in *Network & servers* settings. Stop sharing Paylaşmayı durdur - No comment provided by engineer. + alert action Stop sharing address? Adresi paylaşmak durdurulsun mu? - No comment provided by engineer. + alert title Stopping chat @@ -6764,7 +6958,7 @@ Enable in *Network & servers* settings. Tests failed! Testler başarısız oldu! - No comment provided by engineer. + alert title Thank you for installing SimpleX Chat! @@ -6781,11 +6975,6 @@ Enable in *Network & servers* settings. Kullanıcılar için teşekkürler - Weblate aracılığıyla katkıda bulun! No comment provided by engineer. - - The 1st platform without any user identifiers – private by design. - Herhangi bir kullanıcı tanımlayıcısı olmayan ilk platform - tasarım gereği gizli. - No comment provided by engineer. - The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised. @@ -6798,6 +6987,10 @@ Bazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir. Uygulama, mesaj veya iletişim isteği aldığınızda sizi bilgilendirebilir - etkinleştirmek için lütfen ayarları açın. No comment provided by engineer. + + The app protects your privacy by using different operators in each conversation. + No comment provided by engineer. + The app will ask to confirm downloads from unknown file servers (except .onion). Uygulama bilinmeyen dosya sunucularından indirmeleri onaylamanızı isteyecektir (.onion hariç). @@ -6813,6 +7006,10 @@ Bazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir. Taradığınız kod bir SimpleX bağlantı QR kodu değildir. No comment provided by engineer. + + The connection reached the limit of undelivered messages, your contact may be offline. + No comment provided by engineer. + The connection you accepted will be cancelled! Bağlantı kabulünüz iptal edilecektir! @@ -6833,6 +7030,11 @@ Bazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir. Şifreleme çalışıyor ve yeni şifreleme anlaşması gerekli değil. Bağlantı hatalarına neden olabilir! No comment provided by engineer. + + The future of messaging + Gizli mesajlaşmanın yeni nesli + No comment provided by engineer. + The hash of the previous message is different. Önceki mesajın hash'i farklı. @@ -6858,11 +7060,6 @@ Bazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir. Mesajlar tüm üyeler için moderasyonlu olarak işaretlenecektir. No comment provided by engineer. - - The next generation of private messaging - Gizli mesajlaşmanın yeni nesli - No comment provided by engineer. - The old database was not removed during the migration, it can be deleted. Eski veritabanı geçiş sırasında kaldırılmadı, silinebilir. @@ -6873,6 +7070,10 @@ Bazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir. Profil sadece kişilerinle paylaşılacak. No comment provided by engineer. + + The second preset operator in the app! + No comment provided by engineer. + The second tick we missed! ✅ Özlediğimiz ikinci tik! ✅ @@ -6888,6 +7089,10 @@ Bazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir. Mevcut sohbet profilinizin yeni bağlantıları için sunucular **%@**. No comment provided by engineer. + + The servers for new files of your current chat profile **%@**. + No comment provided by engineer. + The text you pasted is not a SimpleX link. Yapıştırdığın metin bir SimpleX bağlantısı değildir. @@ -6903,6 +7108,10 @@ Bazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir. Temalar No comment provided by engineer. + + These conditions will also apply for: **%@**. + No comment provided by engineer. + These settings are for your current profile **%@**. Bu ayarlar mevcut profiliniz **%@** içindir. @@ -7003,9 +7212,8 @@ Bazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir. Yeni bir bağlantı oluşturmak için No comment provided by engineer. - - To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts. - Gizliliği korumak için, diğer tüm platformlar gibi kullanıcı kimliği kullanmak yerine, SimpleX mesaj kuyrukları için kişilerinizin her biri için ayrı tanımlayıcılara sahiptir. + + To protect against your link being replaced, you can compare contact security codes. No comment provided by engineer. @@ -7025,6 +7233,15 @@ You will be prompted to complete authentication before this feature is enabled.< Bu özellik etkinleştirilmeden önce kimlik doğrulamayı tamamlamanız istenecektir. No comment provided by engineer. + + To protect your privacy, SimpleX uses separate IDs for each of your contacts. + Gizliliği korumak için, diğer tüm platformlar gibi kullanıcı kimliği kullanmak yerine, SimpleX mesaj kuyrukları için kişilerinizin her biri için ayrı tanımlayıcılara sahiptir. + No comment provided by engineer. + + + To receive + No comment provided by engineer. + To record speech please grant permission to use Microphone. Konuşmayı kaydetmek için lütfen Mikrofon kullanma izni verin. @@ -7045,11 +7262,19 @@ Bu özellik etkinleştirilmeden önce kimlik doğrulamayı tamamlamanız istenec Gizli profilinizi ortaya çıkarmak için **Sohbet profilleriniz** sayfasındaki arama alanına tam bir şifre girin. No comment provided by engineer. + + To send + No comment provided by engineer. + To support instant push notifications the chat database has to be migrated. Anlık anlık bildirimleri desteklemek için sohbet veritabanının taşınması gerekir. No comment provided by engineer. + + To use the servers of **%@**, accept conditions of use. + No comment provided by engineer. + To verify end-to-end encryption with your contact compare (or scan) the code on your devices. Kişinizle uçtan uca şifrelemeyi doğrulamak için cihazlarınızdaki kodu karşılaştırın (veya tarayın). @@ -7140,6 +7365,10 @@ Bu özellik etkinleştirilmeden önce kimlik doğrulamayı tamamlamanız istenec Üyenin engeli kaldırılsın mı? No comment provided by engineer. + + Undelivered messages + No comment provided by engineer. + Unexpected migration state Beklenmeyen geçiş durumu @@ -7297,6 +7526,10 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Arşiv yükleme No comment provided by engineer. + + Use %@ + No comment provided by engineer. + Use .onion hosts .onion ana bilgisayarlarını kullan @@ -7322,6 +7555,14 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Şu anki profili kullan No comment provided by engineer. + + Use for files + No comment provided by engineer. + + + Use for messages + No comment provided by engineer. + Use for new connections Yeni bağlantılar için kullan @@ -7362,6 +7603,10 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Sunucu kullan No comment provided by engineer. + + Use servers + No comment provided by engineer. + Use the app while in the call. Görüşme sırasında uygulamayı kullanın. @@ -7452,11 +7697,19 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste 1gb'a kadar videolar ve dosyalar No comment provided by engineer. + + View conditions + No comment provided by engineer. + View security code Güvenlik kodunu görüntüle No comment provided by engineer. + + View updated conditions + No comment provided by engineer. + Visible history Görünür geçmiş @@ -7567,9 +7820,8 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Sesli ve görüntülü aramalara bağlanırken. No comment provided by engineer. - - When people request to connect, you can accept or reject it. - İnsanlar bağlantı talebinde bulunduğunda, kabul edebilir veya reddedebilirsiniz. + + When more than one operator is enabled, none of them has metadata to learn who communicates with whom. No comment provided by engineer. @@ -7729,6 +7981,18 @@ Katılma isteği tekrarlansın mı? Görünüm ayarlarından değiştirebilirsiniz. No comment provided by engineer. + + You can configure operators in Network & servers settings. + No comment provided by engineer. + + + You can configure servers via settings. + No comment provided by engineer. + + + You can create it in user picker. + No comment provided by engineer. + You can create it later Daha sonra oluşturabilirsiniz @@ -7769,6 +8033,10 @@ Katılma isteği tekrarlansın mı? Arşivlenen kişilerden %@'ya mesaj gönderebilirsiniz. No comment provided by engineer. + + You can set connection name, to remember who the link was shared with. + No comment provided by engineer. + You can set lock screen notification preview via settings. Kilit ekranı bildirim önizlemesini ayarlar üzerinden ayarlayabilirsiniz. @@ -7784,11 +8052,6 @@ Katılma isteği tekrarlansın mı? Bu adresi kişilerinizle paylaşarak onların **%@** ile bağlantı kurmasını sağlayabilirsiniz. No comment provided by engineer. - - You can share your address as a link or QR code - anybody can connect to you. - Adresinizi bir bağlantı veya QR kodu olarak paylaşabilirsiniz - herkes size bağlanabilir. - No comment provided by engineer. - You can start chat via app Settings / Database or by restarting the app Sohbeti uygulamada Ayarlar / Veritabanı üzerinden veya uygulamayı yeniden başlatarak başlatabilirsiniz @@ -7812,23 +8075,23 @@ Katılma isteği tekrarlansın mı? You can view invitation link again in connection details. Bağlantı detaylarından davet bağlantısını yeniden görüntüleyebilirsin. - No comment provided by engineer. + alert message You can't send messages! Mesajlar gönderemezsiniz! No comment provided by engineer. - - You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them. - Mesajların hangi sunucu(lar)dan **alınacağını**, kişilerinizi - onlara mesaj göndermek için kullandığınız sunucuları - siz kontrol edersiniz. - No comment provided by engineer. - You could not be verified; please try again. Doğrulanamadınız; lütfen tekrar deneyin. No comment provided by engineer. + + You decide who can connect. + Kimin bağlanabileceğine siz karar verirsiniz. + No comment provided by engineer. + You have already requested connection via this address! Bu adres üzerinden zaten bağlantı talebinde bulundunuz! @@ -7951,11 +8214,6 @@ Bağlantı isteği tekrarlansın mı? Bu grup için gizli bir profil kullanıyorsunuz - ana profilinizi paylaşmayı önlemek için kişileri davet etmeye izin verilmiyor No comment provided by engineer. - - Your %@ servers - %@ sunucularınız - No comment provided by engineer. - Your ICE servers ICE sunucularınız @@ -7971,11 +8229,6 @@ Bağlantı isteği tekrarlansın mı? SimpleX adresin No comment provided by engineer. - - Your XFTP servers - XFTP sunucularınız - No comment provided by engineer. - Your calls Aramaların @@ -8076,16 +8329,15 @@ Bağlantı isteği tekrarlansın mı? Rasgele profiliniz No comment provided by engineer. - - Your server - Sunucunuz - No comment provided by engineer. - Your server address Sunucu adresiniz No comment provided by engineer. + + Your servers + No comment provided by engineer. + Your settings Ayarlarınız @@ -8506,6 +8758,10 @@ Bağlantı isteği tekrarlansın mı? Süresi dolmuş No comment provided by engineer. + + for better metadata privacy. + No comment provided by engineer. + forwarded iletildi @@ -9128,6 +9384,33 @@ son alınan msj: %2$@ + +
+ +
+ + + %d new events + notification body + + + From: %@ + notification body + + + New events + notification + + + New messages + notification + + + New messages in %d chats + notification body + + +
diff --git a/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/en.lproj/Localizable.strings index cf485752ea..cb83427195 100644 --- a/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/en.lproj/Localizable.strings +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/en.lproj/Localizable.strings @@ -1,9 +1,6 @@ /* No comment provided by engineer. */ "_italic_" = "\\_italic_"; -/* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact."; - /* No comment provided by engineer. */ "*bold*" = "\\*bold*"; @@ -27,4 +24,3 @@ /* No comment provided by engineer. */ "No group!" = "Group not found!"; - diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff index ce37b43c23..339f06687d 100644 --- a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff @@ -12,21 +12,6 @@ No comment provided by engineer. - - - - No comment provided by engineer. - - - - - No comment provided by engineer. - - - - - No comment provided by engineer. - ( ( @@ -127,6 +112,14 @@ %@ перевірено No comment provided by engineer. + + %@ server + No comment provided by engineer. + + + %@ servers + No comment provided by engineer. + %@ uploaded %@ завантажено @@ -346,14 +339,9 @@ ) No comment provided by engineer. - - **Add contact**: to create a new invitation link, or connect via a link you received. - **Додати контакт**: створити нове посилання-запрошення або підключитися за отриманим посиланням. - No comment provided by engineer. - - - **Add new contact**: to create your one-time QR Code or link for your contact. - **Додати новий контакт**: щоб створити одноразовий QR-код або посилання для свого контакту. + + **Create 1-time link**: to create and share a new invitation link. + **Додати контакт**: створити нове посилання-запрошення. No comment provided by engineer. @@ -361,13 +349,13 @@ **Створити групу**: створити нову групу. No comment provided by engineer. - - **More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have. + + **More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata. **Більш приватний**: перевіряти нові повідомлення кожні 20 хвилин. Серверу SimpleX Chat передається токен пристрою, але не кількість контактів або повідомлень, які ви маєте. No comment provided by engineer. - - **Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app). + + **Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app. **Найбільш приватний**: не використовуйте сервер сповіщень SimpleX Chat, періодично перевіряйте повідомлення у фоновому режимі (залежить від того, як часто ви користуєтесь додатком). No comment provided by engineer. @@ -381,11 +369,15 @@ **Зверніть увагу: ви НЕ зможете відновити або змінити пароль, якщо втратите його. No comment provided by engineer. - - **Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from. + + **Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from. **Рекомендується**: токен пристрою та сповіщення надсилаються на сервер сповіщень SimpleX Chat, але не вміст повідомлення, його розмір або від кого воно надійшло. No comment provided by engineer. + + **Scan / Paste link**: to connect via a link you received. + No comment provided by engineer. + **Warning**: Instant push notifications require passphrase saved in Keychain. **Попередження**: Для отримання миттєвих пуш-сповіщень потрібна парольна фраза, збережена у брелоку. @@ -492,6 +484,14 @@ 1 тиждень time interval + + 1-time link + No comment provided by engineer. + + + 1-time link can be used *with one contact only* - share in person or via any messenger. + No comment provided by engineer. + 5 minutes 5 хвилин @@ -561,21 +561,11 @@ Скасувати зміну адреси? No comment provided by engineer. - - About SimpleX - Про SimpleX - No comment provided by engineer. - About SimpleX Chat Про чат SimpleX No comment provided by engineer. - - About SimpleX address - Про адресу SimpleX - No comment provided by engineer. - Accent Акцент @@ -588,6 +578,10 @@ accept incoming call via notification swipe action + + Accept conditions + No comment provided by engineer. + Accept connection request? Прийняти запит на підключення? @@ -604,6 +598,10 @@ accept contact request via notification swipe action + + Accepted conditions + No comment provided by engineer. + Acknowledged Визнано @@ -624,16 +622,6 @@ Додайте адресу до свого профілю, щоб ваші контакти могли поділитися нею з іншими людьми. Повідомлення про оновлення профілю буде надіслано вашим контактам. No comment provided by engineer. - - Add contact - Додати контакт - No comment provided by engineer. - - - Add preset servers - Додавання попередньо встановлених серверів - No comment provided by engineer. - Add profile Додати профіль @@ -659,6 +647,14 @@ Додати вітальне повідомлення No comment provided by engineer. + + Added media & file servers + No comment provided by engineer. + + + Added message servers + No comment provided by engineer. + Additional accent Додатковий акцент @@ -684,6 +680,14 @@ Зміна адреси буде скасована. Буде використано стару адресу отримання. No comment provided by engineer. + + Address or 1-time link? + No comment provided by engineer. + + + Address settings + No comment provided by engineer. + Admins can block a member for all. Адміністратори можуть заблокувати користувача для всіх. @@ -729,6 +733,10 @@ Всі учасники групи залишаться на зв'язку. No comment provided by engineer. + + All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages. + No comment provided by engineer. + All messages will be deleted - this cannot be undone! Усі повідомлення будуть видалені - цю дію не можна скасувати! @@ -909,6 +917,11 @@ Відповісти на дзвінок No comment provided by engineer. + + Anybody can host servers. + Кожен може хостити сервери. + No comment provided by engineer. + App build: %@ Збірка програми: %@ @@ -1245,7 +1258,8 @@ Cancel Скасувати - alert button + alert action + alert button Cancel migration @@ -1328,6 +1342,10 @@ authentication reason set passcode view + + Change user profiles + authentication reason + Chat archive Архів чату @@ -1412,10 +1430,18 @@ Чати No comment provided by engineer. + + Check messages every 20 min. + No comment provided by engineer. + + + Check messages when allowed. + No comment provided by engineer. + Check server address and try again. Перевірте адресу сервера та спробуйте ще раз. - No comment provided by engineer. + alert title Chinese and Spanish interface @@ -1502,16 +1528,47 @@ Завершено No comment provided by engineer. + + Conditions accepted on: %@. + No comment provided by engineer. + + + Conditions are accepted for the operator(s): **%@**. + No comment provided by engineer. + + + Conditions are already accepted for following operator(s): **%@**. + No comment provided by engineer. + + + Conditions of use + No comment provided by engineer. + + + Conditions will be accepted for enabled operators after 30 days. + No comment provided by engineer. + + + Conditions will be accepted for operator(s): **%@**. + No comment provided by engineer. + + + Conditions will be accepted for the operator(s): **%@**. + No comment provided by engineer. + + + Conditions will be accepted on: %@. + No comment provided by engineer. + + + Conditions will be automatically accepted for enabled operators on: %@. + No comment provided by engineer. + Configure ICE servers Налаштування серверів ICE No comment provided by engineer. - - Configured %@ servers - Налаштовані сервери %@ - No comment provided by engineer. - Confirm Підтвердити @@ -1701,6 +1758,10 @@ This is your own one-time link! Запит на підключення відправлено! No comment provided by engineer. + + Connection security + No comment provided by engineer. + Connection terminated З'єднання розірвано @@ -1815,6 +1876,10 @@ This is your own one-time link! Створити No comment provided by engineer. + + Create 1-time link + No comment provided by engineer. + Create SimpleX address Створіть адресу SimpleX @@ -1825,11 +1890,6 @@ This is your own one-time link! Створіть групу, використовуючи випадковий профіль. No comment provided by engineer. - - Create an address to let people connect with you. - Створіть адресу, щоб люди могли з вами зв'язатися. - No comment provided by engineer. - Create file Створити файл @@ -1910,6 +1970,10 @@ This is your own one-time link! Поточний пароль No comment provided by engineer. + + Current conditions text couldn't be loaded, you can review conditions via this link: + No comment provided by engineer. + Current passphrase… Поточна парольна фраза… @@ -2065,7 +2129,8 @@ This is your own one-time link! Delete Видалити - chat item action + alert action + chat item action swipe action @@ -2282,6 +2347,10 @@ This is your own one-time link! Помилки видалення No comment provided by engineer. + + Delivered even when Apple drops them. + No comment provided by engineer. + Delivery Доставка @@ -2561,6 +2630,10 @@ This is your own one-time link! Тривалість No comment provided by engineer. + + E2E encrypted notifications. + No comment provided by engineer. + Edit Редагувати @@ -2581,6 +2654,10 @@ This is your own one-time link! Увімкнути (зберегти перевизначення) No comment provided by engineer. + + Enable Flux + No comment provided by engineer. + Enable SimpleX Lock Увімкнути SimpleX Lock @@ -2786,6 +2863,10 @@ This is your own one-time link! Помилка скасування зміни адреси No comment provided by engineer. + + Error accepting conditions + alert title + Error accepting contact request Помилка при прийнятті запиту на контакт @@ -2801,6 +2882,10 @@ This is your own one-time link! Помилка додавання користувача(ів) No comment provided by engineer. + + Error adding server + alert title + Error changing address Помилка зміни адреси @@ -2939,10 +3024,9 @@ This is your own one-time link! Помилка приєднання до групи No comment provided by engineer. - - Error loading %@ servers - Помилка завантаження %@ серверів - No comment provided by engineer. + + Error loading servers + alert title Error migrating settings @@ -2978,11 +3062,6 @@ This is your own one-time link! Статистика скидання помилок No comment provided by engineer. - - Error saving %@ servers - Помилка збереження %@ серверів - No comment provided by engineer. - Error saving ICE servers Помилка збереження серверів ICE @@ -3003,6 +3082,10 @@ This is your own one-time link! Помилка збереження пароля на keychain No comment provided by engineer. + + Error saving servers + alert title + Error saving settings Налаштування збереження помилок @@ -3072,6 +3155,10 @@ This is your own one-time link! Повідомлення про помилку оновлення No comment provided by engineer. + + Error updating server + alert title + Error updating settings Помилка оновлення налаштувань @@ -3117,6 +3204,10 @@ This is your own one-time link! Помилки No comment provided by engineer. + + Errors in servers configuration. + servers error + Even when disabled in the conversation. Навіть коли вимкнений у розмові. @@ -3317,11 +3408,27 @@ This is your own one-time link! Виправлення не підтримується учасником групи No comment provided by engineer. + + For chat profile %@: + servers error + For console Для консолі No comment provided by engineer. + + For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server. + No comment provided by engineer. + + + For private routing + No comment provided by engineer. + + + For social media + No comment provided by engineer. + Forward Пересилання @@ -3626,9 +3733,12 @@ Error: %2$@ Як працює SimpleX No comment provided by engineer. - - How it works - Як це працює + + How it affects privacy + No comment provided by engineer. + + + How it helps privacy No comment provided by engineer. @@ -3700,8 +3810,8 @@ Error: %2$@ Негайно No comment provided by engineer. - - Immune to spam and abuse + + Immune to spam Імунітет до спаму та зловживань No comment provided by engineer. @@ -3840,6 +3950,11 @@ More improvements are coming soon! Встановіть [SimpleX Chat для терміналу](https://github.com/simplex-chat/simplex-chat) No comment provided by engineer. + + Instant + Миттєво + No comment provided by engineer. + Instant push notifications will be hidden! @@ -3847,11 +3962,6 @@ More improvements are coming soon! No comment provided by engineer. - - Instantly - Миттєво - No comment provided by engineer. - Interface Інтерфейс @@ -3900,7 +4010,7 @@ More improvements are coming soon! Invalid server address! Неправильна адреса сервера! - No comment provided by engineer. + alert title Invalid status @@ -4028,7 +4138,7 @@ This is your link for group %@! Keep Тримай - No comment provided by engineer. + alert action Keep conversation @@ -4043,7 +4153,7 @@ This is your link for group %@! Keep unused invitation? Зберігати невикористані запрошення? - No comment provided by engineer. + alert title Keep your connections @@ -4130,11 +4240,6 @@ This is your link for group %@! Живі повідомлення No comment provided by engineer. - - Local - Локально - No comment provided by engineer. - Local name Місцева назва @@ -4155,11 +4260,6 @@ This is your link for group %@! Режим блокування No comment provided by engineer. - - Make a private connection - Створіть приватне з'єднання - No comment provided by engineer. - Make one message disappear Зробити так, щоб одне повідомлення зникло @@ -4170,21 +4270,11 @@ This is your link for group %@! Зробіть профіль приватним! No comment provided by engineer. - - Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@). - Переконайтеся, що адреси серверів %@ мають правильний формат, розділені рядками і не дублюються (%@). - No comment provided by engineer. - Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated. Переконайтеся, що адреси серверів WebRTC ICE мають правильний формат, розділені рядками і не дублюються. No comment provided by engineer. - - Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?* - Багато людей запитували: *якщо SimpleX не має ідентифікаторів користувачів, як він може доставляти повідомлення?* - No comment provided by engineer. - Mark deleted for everyone Позначити видалено для всіх @@ -4463,6 +4553,10 @@ This is your link for group %@! Більш надійне з'єднання з мережею. No comment provided by engineer. + + More reliable notifications + No comment provided by engineer. + Most likely this connection is deleted. Швидше за все, це з'єднання видалено. @@ -4498,6 +4592,10 @@ This is your link for group %@! Підключення до мережі No comment provided by engineer. + + Network decentralization + No comment provided by engineer. + Network issues - message expired after many attempts to send it. Проблеми з мережею - термін дії повідомлення закінчився після багатьох спроб надіслати його. @@ -4508,6 +4606,10 @@ This is your link for group %@! Керування мережею No comment provided by engineer. + + Network operator + No comment provided by engineer. + Network settings Налаштування мережі @@ -4566,6 +4668,10 @@ This is your link for group %@! Нове ім'я відображення No comment provided by engineer. + + New events + notification + New in %@ Нове в %@ @@ -4591,6 +4697,10 @@ This is your link for group %@! Новий пароль… No comment provided by engineer. + + New server + No comment provided by engineer. + No Ні @@ -4646,6 +4756,14 @@ This is your link for group %@! Немає інформації, спробуйте перезавантажити No comment provided by engineer. + + No media & file servers. + servers error + + + No message servers. + servers error + No network connection Немає підключення до мережі @@ -4664,11 +4782,37 @@ This is your link for group %@! Немає дозволу на запис голосового повідомлення No comment provided by engineer. + + No push server + Локально + No comment provided by engineer. + No received or sent files Немає отриманих або відправлених файлів No comment provided by engineer. + + No servers for private message routing. + servers error + + + No servers to receive files. + servers error + + + No servers to receive messages. + servers error + + + No servers to send files. + servers error + + + No user identifiers. + Ніяких ідентифікаторів користувачів. + No comment provided by engineer. + Not compatible! Не сумісні! @@ -4693,6 +4837,10 @@ This is your link for group %@! Сповіщення вимкнено! No comment provided by engineer. + + Notifications privacy + No comment provided by engineer. + Now admins can: - delete members' messages. @@ -4751,8 +4899,8 @@ Requires compatible VPN. Onion хости не будуть використовуватися. No comment provided by engineer. - - Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**. + + Only client devices store user profiles, contacts, groups, and messages. Тільки клієнтські пристрої зберігають профілі користувачів, контакти, групи та повідомлення, надіслані за допомогою **2-шарового наскрізного шифрування**. No comment provided by engineer. @@ -4836,6 +4984,10 @@ Requires compatible VPN. Відкрийте Налаштування No comment provided by engineer. + + Open changes + No comment provided by engineer. + Open chat Відкритий чат @@ -4846,6 +4998,10 @@ Requires compatible VPN. Відкрийте консоль чату authentication reason + + Open conditions + No comment provided by engineer. + Open group Відкрита група @@ -4856,26 +5012,19 @@ Requires compatible VPN. Відкрита міграція на інший пристрій authentication reason - - Open server settings - Відкрити налаштування сервера - No comment provided by engineer. - - - Open user profiles - Відкрити профілі користувачів - authentication reason - - - Open-source protocol and code – anybody can run the servers. - Протокол і код з відкритим вихідним кодом - будь-хто може запускати сервери. - No comment provided by engineer. - Opening app… Відкриваємо програму… No comment provided by engineer. + + Operator + No comment provided by engineer. + + + Operator server + alert title + Or paste archive link Або вставте посилання на архів @@ -4896,16 +5045,15 @@ Requires compatible VPN. Або покажіть цей код No comment provided by engineer. + + Or to share privately + No comment provided by engineer. + Other Інше No comment provided by engineer. - - Other %@ servers - Інші сервери %@ - No comment provided by engineer. - Other file errors: %@ @@ -4985,13 +5133,8 @@ Requires compatible VPN. В очікуванні No comment provided by engineer. - - People can connect to you only via the links you share. - Люди можуть зв'язатися з вами лише за посиланнями, якими ви ділитеся. - No comment provided by engineer. - - - Periodically + + Periodic Періодично No comment provided by engineer. @@ -5113,16 +5256,15 @@ Error: %@ Зберегти чернетку останнього повідомлення з вкладеннями. No comment provided by engineer. - - Preset server - Попередньо встановлений сервер - No comment provided by engineer. - Preset server address Попередньо встановлена адреса сервера No comment provided by engineer. + + Preset servers + No comment provided by engineer. + Preview Попередній перегляд @@ -5201,7 +5343,7 @@ Error: %@ Profile update will be sent to your contacts. Оновлення профілю буде надіслано вашим контактам. - No comment provided by engineer. + alert message Prohibit audio/video calls. @@ -5294,6 +5436,10 @@ Enable in *Network & servers* settings. Proxy requires password No comment provided by engineer. + + Push Notifications + No comment provided by engineer. + Push notifications Push-повідомлення @@ -5334,26 +5480,21 @@ Enable in *Network & servers* settings. Читати далі No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address). - Читайте більше в [Посібнику користувача](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address). - No comment provided by engineer. - Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). Читайте більше в [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). No comment provided by engineer. + + Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). + Читайте більше в [Посібнику користувача](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). + No comment provided by engineer. + Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends). Читайте більше в [Посібнику користувача](https://simplex.chat/docs/guide/readme.html#connect-to-friends). No comment provided by engineer. - - Read more in our GitHub repository. - Читайте більше в нашому репозиторії на GitHub. - No comment provided by engineer. - Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme). Читайте більше в нашому [GitHub репозиторії](https://github.com/simplex-chat/simplex-chat#readme). @@ -5669,6 +5810,14 @@ Enable in *Network & servers* settings. Показувати chat item action + + Review conditions + No comment provided by engineer. + + + Review later + No comment provided by engineer. + Revoke Відкликати @@ -5713,6 +5862,14 @@ Enable in *Network & servers* settings. Безпечніші групи No comment provided by engineer. + + Same conditions will apply to operator **%@**. + No comment provided by engineer. + + + Same conditions will apply to operator(s): **%@**. + No comment provided by engineer. + Save Зберегти @@ -5782,7 +5939,7 @@ Enable in *Network & servers* settings. Save servers? Зберегти сервери? - No comment provided by engineer. + alert title Save welcome message? @@ -5991,11 +6148,6 @@ Enable in *Network & servers* settings. Надсилати сповіщення No comment provided by engineer. - - Send notifications: - Надсилати сповіщення: - No comment provided by engineer. - Send questions and ideas Надсилайте запитання та ідеї @@ -6120,6 +6272,10 @@ Enable in *Network & servers* settings. Server No comment provided by engineer. + + Server added to operator %@. + alert message + Server address Адреса сервера @@ -6135,6 +6291,18 @@ Enable in *Network & servers* settings. Адреса сервера несумісна з налаштуваннями мережі: %@. No comment provided by engineer. + + Server operator changed. + alert title + + + Server operators + No comment provided by engineer. + + + Server protocol changed. + alert title + Server requires authorization to create queues, check password Сервер вимагає авторизації для створення черг, перевірте пароль @@ -6252,22 +6420,35 @@ Enable in *Network & servers* settings. Share Поділіться - chat item action + alert action + chat item action Share 1-time link Поділитися 1-разовим посиланням No comment provided by engineer. + + Share 1-time link with a friend + No comment provided by engineer. + + + Share SimpleX address on social media. + No comment provided by engineer. + Share address Поділитися адресою No comment provided by engineer. + + Share address publicly + No comment provided by engineer. + Share address with contacts? Поділіться адресою з контактами? - No comment provided by engineer. + alert title Share from other apps. @@ -6383,6 +6564,14 @@ Enable in *Network & servers* settings. Адреса SimpleX No comment provided by engineer. + + SimpleX address and 1-time links are safe to share via any messenger. + No comment provided by engineer. + + + SimpleX address or 1-time link? + No comment provided by engineer. + SimpleX contact address Контактна адреса SimpleX @@ -6471,6 +6660,11 @@ Enable in *Network & servers* settings. Під час імпорту виникли деякі несмертельні помилки: No comment provided by engineer. + + Some servers failed the test: +%@ + alert message + Somebody Хтось @@ -6554,12 +6748,12 @@ Enable in *Network & servers* settings. Stop sharing Припиніть ділитися - No comment provided by engineer. + alert action Stop sharing address? Припинити ділитися адресою? - No comment provided by engineer. + alert title Stopping chat @@ -6706,7 +6900,7 @@ Enable in *Network & servers* settings. Tests failed! Тести не пройшли! - No comment provided by engineer. + alert title Thank you for installing SimpleX Chat! @@ -6723,11 +6917,6 @@ Enable in *Network & servers* settings. Дякуємо користувачам - зробіть свій внесок через Weblate! No comment provided by engineer. - - The 1st platform without any user identifiers – private by design. - Перша платформа без жодних ідентифікаторів користувачів – приватна за дизайном. - No comment provided by engineer. - The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised. @@ -6740,6 +6929,10 @@ It can happen because of some bug or when the connection is compromised.Додаток може сповіщати вас, коли ви отримуєте повідомлення або запити на контакт - будь ласка, відкрийте налаштування, щоб увімкнути цю функцію. No comment provided by engineer. + + The app protects your privacy by using different operators in each conversation. + No comment provided by engineer. + The app will ask to confirm downloads from unknown file servers (except .onion). Програма попросить підтвердити завантаження з невідомих файлових серверів (крім .onion). @@ -6755,6 +6948,10 @@ It can happen because of some bug or when the connection is compromised.Відсканований вами код не є QR-кодом посилання SimpleX. No comment provided by engineer. + + The connection reached the limit of undelivered messages, your contact may be offline. + No comment provided by engineer. + The connection you accepted will be cancelled! Прийняте вами з'єднання буде скасовано! @@ -6775,6 +6972,11 @@ It can happen because of some bug or when the connection is compromised.Шифрування працює і нова угода про шифрування не потрібна. Це може призвести до помилок з'єднання! No comment provided by engineer. + + The future of messaging + Наступне покоління приватних повідомлень + No comment provided by engineer. + The hash of the previous message is different. Хеш попереднього повідомлення відрізняється. @@ -6800,11 +7002,6 @@ It can happen because of some bug or when the connection is compromised.Повідомлення будуть позначені як модеровані для всіх учасників. No comment provided by engineer. - - The next generation of private messaging - Наступне покоління приватних повідомлень - No comment provided by engineer. - The old database was not removed during the migration, it can be deleted. Стара база даних не була видалена під час міграції, її можна видалити. @@ -6815,6 +7012,10 @@ It can happen because of some bug or when the connection is compromised.Профіль доступний лише вашим контактам. No comment provided by engineer. + + The second preset operator in the app! + No comment provided by engineer. + The second tick we missed! ✅ Другу галочку ми пропустили! ✅ @@ -6830,6 +7031,10 @@ It can happen because of some bug or when the connection is compromised.Сервери для нових підключень вашого поточного профілю чату **%@**. No comment provided by engineer. + + The servers for new files of your current chat profile **%@**. + No comment provided by engineer. + The text you pasted is not a SimpleX link. Текст, який ви вставили, не є посиланням SimpleX. @@ -6844,6 +7049,10 @@ It can happen because of some bug or when the connection is compromised.Теми No comment provided by engineer. + + These conditions will also apply for: **%@**. + No comment provided by engineer. + These settings are for your current profile **%@**. Ці налаштування стосуються вашого поточного профілю **%@**. @@ -6944,9 +7153,8 @@ It can happen because of some bug or when the connection is compromised.Щоб створити нове з'єднання No comment provided by engineer. - - To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts. - Щоб захистити конфіденційність, замість ідентифікаторів користувачів, які використовуються на всіх інших платформах, SimpleX має ідентифікатори для черг повідомлень, окремі для кожного з ваших контактів. + + To protect against your link being replaced, you can compare contact security codes. No comment provided by engineer. @@ -6966,6 +7174,15 @@ You will be prompted to complete authentication before this feature is enabled.< Перед увімкненням цієї функції вам буде запропоновано пройти автентифікацію. No comment provided by engineer. + + To protect your privacy, SimpleX uses separate IDs for each of your contacts. + Щоб захистити конфіденційність, замість ідентифікаторів користувачів, які використовуються на всіх інших платформах, SimpleX має ідентифікатори для черг повідомлень, окремі для кожного з ваших контактів. + No comment provided by engineer. + + + To receive + No comment provided by engineer. + To record speech please grant permission to use Microphone. No comment provided by engineer. @@ -6984,11 +7201,19 @@ You will be prompted to complete authentication before this feature is enabled.< Щоб відкрити свій прихований профіль, введіть повний пароль у поле пошуку на сторінці **Ваші профілі чату**. No comment provided by engineer. + + To send + No comment provided by engineer. + To support instant push notifications the chat database has to be migrated. Для підтримки миттєвих push-повідомлень необхідно перенести базу даних чату. No comment provided by engineer. + + To use the servers of **%@**, accept conditions of use. + No comment provided by engineer. + To verify end-to-end encryption with your contact compare (or scan) the code on your devices. Щоб перевірити наскрізне шифрування з вашим контактом, порівняйте (або відскануйте) код на ваших пристроях. @@ -7079,6 +7304,10 @@ You will be prompted to complete authentication before this feature is enabled.< Розблокувати учасника? No comment provided by engineer. + + Undelivered messages + No comment provided by engineer. + Unexpected migration state Неочікуваний стан міграції @@ -7236,6 +7465,10 @@ To connect, please ask your contact to create another connection link and check Завантаження архіву No comment provided by engineer. + + Use %@ + No comment provided by engineer. + Use .onion hosts Використовуйте хости .onion @@ -7260,6 +7493,14 @@ To connect, please ask your contact to create another connection link and check Використовувати поточний профіль No comment provided by engineer. + + Use for files + No comment provided by engineer. + + + Use for messages + No comment provided by engineer. + Use for new connections Використовуйте для нових з'єднань @@ -7300,6 +7541,10 @@ To connect, please ask your contact to create another connection link and check Використовувати сервер No comment provided by engineer. + + Use servers + No comment provided by engineer. + Use the app while in the call. Використовуйте додаток під час розмови. @@ -7389,11 +7634,19 @@ To connect, please ask your contact to create another connection link and check Відео та файли до 1 Гб No comment provided by engineer. + + View conditions + No comment provided by engineer. + View security code Переглянути код безпеки No comment provided by engineer. + + View updated conditions + No comment provided by engineer. + Visible history Видима історія @@ -7504,9 +7757,8 @@ To connect, please ask your contact to create another connection link and check При підключенні аудіо та відеодзвінків. No comment provided by engineer. - - When people request to connect, you can accept or reject it. - Коли люди звертаються із запитом на підключення, ви можете прийняти або відхилити його. + + When more than one operator is enabled, none of them has metadata to learn who communicates with whom. No comment provided by engineer. @@ -7666,6 +7918,18 @@ Repeat join request? Ви можете змінити його в налаштуваннях зовнішнього вигляду. No comment provided by engineer. + + You can configure operators in Network & servers settings. + No comment provided by engineer. + + + You can configure servers via settings. + No comment provided by engineer. + + + You can create it in user picker. + No comment provided by engineer. + You can create it later Ви можете створити його пізніше @@ -7706,6 +7970,10 @@ Repeat join request? Ви можете надсилати повідомлення на %@ з архівних контактів. No comment provided by engineer. + + You can set connection name, to remember who the link was shared with. + No comment provided by engineer. + You can set lock screen notification preview via settings. Ви можете налаштувати попередній перегляд сповіщень на екрані блокування за допомогою налаштувань. @@ -7721,11 +7989,6 @@ Repeat join request? Ви можете поділитися цією адресою зі своїми контактами, щоб вони могли зв'язатися з **%@**. No comment provided by engineer. - - You can share your address as a link or QR code - anybody can connect to you. - Ви можете поділитися своєю адресою у вигляді посилання або QR-коду - будь-хто зможе зв'язатися з вами. - No comment provided by engineer. - You can start chat via app Settings / Database or by restarting the app Запустити чат можна через Налаштування програми / База даних або перезапустивши програму @@ -7749,23 +8012,23 @@ Repeat join request? You can view invitation link again in connection details. Ви можете переглянути посилання на запрошення ще раз у деталях підключення. - No comment provided by engineer. + alert message You can't send messages! Ви не можете надсилати повідомлення! No comment provided by engineer. - - You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them. - Ви контролюєте, через який(і) сервер(и) **отримувати** повідомлення, ваші контакти - сервери, які ви використовуєте для надсилання їм повідомлень. - No comment provided by engineer. - You could not be verified; please try again. Вас не вдалося верифікувати, спробуйте ще раз. No comment provided by engineer. + + You decide who can connect. + Ви вирішуєте, хто може під'єднатися. + No comment provided by engineer. + You have already requested connection via this address! Ви вже надсилали запит на підключення за цією адресою! @@ -7888,11 +8151,6 @@ Repeat connection request? Ви використовуєте профіль інкогніто для цієї групи - щоб запобігти поширенню вашого основного профілю, запрошення контактів заборонено No comment provided by engineer. - - Your %@ servers - Ваші сервери %@ - No comment provided by engineer. - Your ICE servers Ваші сервери ICE @@ -7908,11 +8166,6 @@ Repeat connection request? Ваша адреса SimpleX No comment provided by engineer. - - Your XFTP servers - Ваші XFTP-сервери - No comment provided by engineer. - Your calls Твої дзвінки @@ -8009,16 +8262,15 @@ Repeat connection request? Ваш випадковий профіль No comment provided by engineer. - - Your server - Ваш сервер - No comment provided by engineer. - Your server address Адреса вашого сервера No comment provided by engineer. + + Your servers + No comment provided by engineer. + Your settings Ваші налаштування @@ -8439,6 +8691,10 @@ Repeat connection request? закінчився No comment provided by engineer. + + for better metadata privacy. + No comment provided by engineer. + forwarded переслано @@ -9061,6 +9317,33 @@ last received msg: %2$@ + +
+ +
+ + + %d new events + notification body + + + From: %@ + notification body + + + New events + notification + + + New messages + notification + + + New messages in %d chats + notification body + + +
diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/en.lproj/Localizable.strings index cf485752ea..cb83427195 100644 --- a/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/en.lproj/Localizable.strings +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/en.lproj/Localizable.strings @@ -1,9 +1,6 @@ /* No comment provided by engineer. */ "_italic_" = "\\_italic_"; -/* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact."; - /* No comment provided by engineer. */ "*bold*" = "\\*bold*"; @@ -27,4 +24,3 @@ /* No comment provided by engineer. */ "No group!" = "Group not found!"; - diff --git a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff index 91893dd939..3f48211025 100644 --- a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff +++ b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff @@ -12,21 +12,6 @@ No comment provided by engineer. - - - - No comment provided by engineer. - - - - - No comment provided by engineer. - - - - - No comment provided by engineer. - ( ( @@ -127,6 +112,14 @@ %@ 已认证 No comment provided by engineer. + + %@ server + No comment provided by engineer. + + + %@ servers + No comment provided by engineer. + %@ uploaded %@ 已上传 @@ -346,28 +339,23 @@ ) No comment provided by engineer. - - **Add contact**: to create a new invitation link, or connect via a link you received. + + **Create 1-time link**: to create and share a new invitation link. **添加联系人**: 创建新的邀请链接,或通过您收到的链接进行连接. No comment provided by engineer. - - **Add new contact**: to create your one-time QR Code or link for your contact. - **添加新联系人**:为您的联系人创建一次性二维码或者链接。 - No comment provided by engineer. - **Create group**: to create a new group. **创建群组**: 创建一个新群组. No comment provided by engineer. - - **More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have. + + **More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata. **更私密**:每20分钟检查新消息。设备令牌和 SimpleX Chat 服务器共享,但是不会共享有您有多少联系人或者消息。 No comment provided by engineer. - - **Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app). + + **Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app. **最私密**:不使用 SimpleX Chat 通知服务器,在后台定期检查消息(取决于您多经常使用应用程序)。 No comment provided by engineer. @@ -381,11 +369,15 @@ **请注意**:如果您丢失密码,您将无法恢复或者更改密码。 No comment provided by engineer. - - **Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from. + + **Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from. **推荐**:设备令牌和通知会发送至 SimpleX Chat 通知服务器,但是消息内容、大小或者发送人不会。 No comment provided by engineer. + + **Scan / Paste link**: to connect via a link you received. + No comment provided by engineer. + **Warning**: Instant push notifications require passphrase saved in Keychain. **警告**:及时推送通知需要保存在钥匙串的密码。 @@ -492,6 +484,14 @@ 1周 time interval + + 1-time link + No comment provided by engineer. + + + 1-time link can be used *with one contact only* - share in person or via any messenger. + No comment provided by engineer. + 5 minutes 5分钟 @@ -561,21 +561,11 @@ 中止地址更改? No comment provided by engineer. - - About SimpleX - 关于SimpleX - No comment provided by engineer. - About SimpleX Chat 关于SimpleX Chat No comment provided by engineer. - - About SimpleX address - 关于 SimpleX 地址 - No comment provided by engineer. - Accent 强调 @@ -588,6 +578,10 @@ accept incoming call via notification swipe action + + Accept conditions + No comment provided by engineer. + Accept connection request? 接受联系人? @@ -604,6 +598,10 @@ accept contact request via notification swipe action + + Accepted conditions + No comment provided by engineer. + Acknowledged 确认 @@ -624,16 +622,6 @@ 将地址添加到您的个人资料,以便您的联系人可以与其他人共享。个人资料更新将发送给您的联系人。 No comment provided by engineer. - - Add contact - 添加联系人 - No comment provided by engineer. - - - Add preset servers - 添加预设服务器 - No comment provided by engineer. - Add profile 添加个人资料 @@ -659,6 +647,14 @@ 添加欢迎信息 No comment provided by engineer. + + Added media & file servers + No comment provided by engineer. + + + Added message servers + No comment provided by engineer. + Additional accent 附加重音 @@ -684,6 +680,14 @@ 将中止地址更改。将使用旧接收地址。 No comment provided by engineer. + + Address or 1-time link? + No comment provided by engineer. + + + Address settings + No comment provided by engineer. + Admins can block a member for all. 管理员可以为所有人封禁一名成员。 @@ -729,6 +733,10 @@ 所有群组成员将保持连接。 No comment provided by engineer. + + All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages. + No comment provided by engineer. + All messages will be deleted - this cannot be undone! 所有消息都将被删除 - 这无法被撤销! @@ -909,6 +917,11 @@ 接听来电 No comment provided by engineer. + + Anybody can host servers. + 任何人都可以托管服务器。 + No comment provided by engineer. + App build: %@ 应用程序构建:%@ @@ -1245,7 +1258,8 @@ Cancel 取消 - alert button + alert action + alert button Cancel migration @@ -1328,6 +1342,10 @@ authentication reason set passcode view + + Change user profiles + authentication reason + Chat archive 聊天档案 @@ -1412,10 +1430,18 @@ 聊天 No comment provided by engineer. + + Check messages every 20 min. + No comment provided by engineer. + + + Check messages when allowed. + No comment provided by engineer. + Check server address and try again. 检查服务器地址并再试一次。 - No comment provided by engineer. + alert title Chinese and Spanish interface @@ -1502,16 +1528,47 @@ 已完成 No comment provided by engineer. + + Conditions accepted on: %@. + No comment provided by engineer. + + + Conditions are accepted for the operator(s): **%@**. + No comment provided by engineer. + + + Conditions are already accepted for following operator(s): **%@**. + No comment provided by engineer. + + + Conditions of use + No comment provided by engineer. + + + Conditions will be accepted for enabled operators after 30 days. + No comment provided by engineer. + + + Conditions will be accepted for operator(s): **%@**. + No comment provided by engineer. + + + Conditions will be accepted for the operator(s): **%@**. + No comment provided by engineer. + + + Conditions will be accepted on: %@. + No comment provided by engineer. + + + Conditions will be automatically accepted for enabled operators on: %@. + No comment provided by engineer. + Configure ICE servers 配置 ICE 服务器 No comment provided by engineer. - - Configured %@ servers - 已配置 %@ 服务器 - No comment provided by engineer. - Confirm 确认 @@ -1701,6 +1758,10 @@ This is your own one-time link! 已发送连接请求! No comment provided by engineer. + + Connection security + No comment provided by engineer. + Connection terminated 连接被终止 @@ -1815,6 +1876,10 @@ This is your own one-time link! 创建 No comment provided by engineer. + + Create 1-time link + No comment provided by engineer. + Create SimpleX address 创建 SimpleX 地址 @@ -1825,11 +1890,6 @@ This is your own one-time link! 使用随机身份创建群组. No comment provided by engineer. - - Create an address to let people connect with you. - 创建一个地址,让人们与您联系。 - No comment provided by engineer. - Create file 创建文件 @@ -1910,6 +1970,10 @@ This is your own one-time link! 当前密码 No comment provided by engineer. + + Current conditions text couldn't be loaded, you can review conditions via this link: + No comment provided by engineer. + Current passphrase… 现有密码…… @@ -2065,7 +2129,8 @@ This is your own one-time link! Delete 删除 - chat item action + alert action + chat item action swipe action @@ -2282,6 +2347,10 @@ This is your own one-time link! 删除错误 No comment provided by engineer. + + Delivered even when Apple drops them. + No comment provided by engineer. + Delivery 传送 @@ -2561,6 +2630,10 @@ This is your own one-time link! 时长 No comment provided by engineer. + + E2E encrypted notifications. + No comment provided by engineer. + Edit 编辑 @@ -2581,6 +2654,10 @@ This is your own one-time link! 启用(保持覆盖) No comment provided by engineer. + + Enable Flux + No comment provided by engineer. + Enable SimpleX Lock 启用 SimpleX 锁定 @@ -2786,6 +2863,10 @@ This is your own one-time link! 中止地址更改错误 No comment provided by engineer. + + Error accepting conditions + alert title + Error accepting contact request 接受联系人请求错误 @@ -2801,6 +2882,10 @@ This is your own one-time link! 添加成员错误 No comment provided by engineer. + + Error adding server + alert title + Error changing address 更改地址错误 @@ -2939,10 +3024,9 @@ This is your own one-time link! 加入群组错误 No comment provided by engineer. - - Error loading %@ servers - 加载 %@ 服务器错误 - No comment provided by engineer. + + Error loading servers + alert title Error migrating settings @@ -2978,11 +3062,6 @@ This is your own one-time link! 重置统计信息时出错 No comment provided by engineer. - - Error saving %@ servers - 保存 %@ 服务器错误 - No comment provided by engineer. - Error saving ICE servers 保存 ICE 服务器错误 @@ -3003,6 +3082,10 @@ This is your own one-time link! 保存密码到钥匙串错误 No comment provided by engineer. + + Error saving servers + alert title + Error saving settings 保存设置出错 @@ -3072,6 +3155,10 @@ This is your own one-time link! 更新消息错误 No comment provided by engineer. + + Error updating server + alert title + Error updating settings 更新设置错误 @@ -3117,6 +3204,10 @@ This is your own one-time link! 错误 No comment provided by engineer. + + Errors in servers configuration. + servers error + Even when disabled in the conversation. 即使在对话中被禁用。 @@ -3317,11 +3408,27 @@ This is your own one-time link! 修复群组成员不支持的问题 No comment provided by engineer. + + For chat profile %@: + servers error + For console 用于控制台 No comment provided by engineer. + + For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server. + No comment provided by engineer. + + + For private routing + No comment provided by engineer. + + + For social media + No comment provided by engineer. + Forward 转发 @@ -3626,9 +3733,12 @@ Error: %2$@ SimpleX的工作原理 No comment provided by engineer. - - How it works - 工作原理 + + How it affects privacy + No comment provided by engineer. + + + How it helps privacy No comment provided by engineer. @@ -3700,8 +3810,8 @@ Error: %2$@ 立即 No comment provided by engineer. - - Immune to spam and abuse + + Immune to spam 不受垃圾和骚扰消息影响 No comment provided by engineer. @@ -3840,6 +3950,11 @@ More improvements are coming soon! 安装[用于终端的 SimpleX Chat](https://github.com/simplex-chat/simplex-chat) No comment provided by engineer. + + Instant + 即时 + No comment provided by engineer. + Instant push notifications will be hidden! @@ -3847,11 +3962,6 @@ More improvements are coming soon! No comment provided by engineer. - - Instantly - 即时 - No comment provided by engineer. - Interface 界面 @@ -3900,7 +4010,7 @@ More improvements are coming soon! Invalid server address! 无效的服务器地址! - No comment provided by engineer. + alert title Invalid status @@ -4028,7 +4138,7 @@ This is your link for group %@! Keep 保留 - No comment provided by engineer. + alert action Keep conversation @@ -4043,7 +4153,7 @@ This is your link for group %@! Keep unused invitation? 保留未使用的邀请吗? - No comment provided by engineer. + alert title Keep your connections @@ -4130,11 +4240,6 @@ This is your link for group %@! 实时消息 No comment provided by engineer. - - Local - 本地 - No comment provided by engineer. - Local name 本地名称 @@ -4155,11 +4260,6 @@ This is your link for group %@! 锁定模式 No comment provided by engineer. - - Make a private connection - 建立私密连接 - No comment provided by engineer. - Make one message disappear 使一条消息消失 @@ -4170,21 +4270,11 @@ This is your link for group %@! 将个人资料设为私密! No comment provided by engineer. - - Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@). - 请确保 %@服 务器地址格式正确,每行一个地址并且不重复 (%@)。 - No comment provided by engineer. - Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated. 确保 WebRTC ICE 服务器地址格式正确、每行分开且不重复。 No comment provided by engineer. - - Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?* - 许多人问: *如果SimpleX没有用户标识符,它怎么传递信息?* - No comment provided by engineer. - Mark deleted for everyone 标记为所有人已删除 @@ -4463,6 +4553,10 @@ This is your link for group %@! 更可靠的网络连接。 No comment provided by engineer. + + More reliable notifications + No comment provided by engineer. + Most likely this connection is deleted. 此连接很可能已被删除。 @@ -4498,6 +4592,10 @@ This is your link for group %@! 网络连接 No comment provided by engineer. + + Network decentralization + No comment provided by engineer. + Network issues - message expired after many attempts to send it. 网络问题 - 消息在多次尝试发送后过期。 @@ -4508,6 +4606,10 @@ This is your link for group %@! 网络管理 No comment provided by engineer. + + Network operator + No comment provided by engineer. + Network settings 网络设置 @@ -4566,6 +4668,10 @@ This is your link for group %@! 新显示名 No comment provided by engineer. + + New events + notification + New in %@ %@ 的新内容 @@ -4591,6 +4697,10 @@ This is your link for group %@! 新密码…… No comment provided by engineer. + + New server + No comment provided by engineer. + No @@ -4646,6 +4756,14 @@ This is your link for group %@! 无信息,尝试重新加载 No comment provided by engineer. + + No media & file servers. + servers error + + + No message servers. + servers error + No network connection 无网络连接 @@ -4664,11 +4782,37 @@ This is your link for group %@! 没有录制语音消息的权限 No comment provided by engineer. + + No push server + 本地 + No comment provided by engineer. + No received or sent files 未收到或发送文件 No comment provided by engineer. + + No servers for private message routing. + servers error + + + No servers to receive files. + servers error + + + No servers to receive messages. + servers error + + + No servers to send files. + servers error + + + No user identifiers. + 没有用户标识符。 + No comment provided by engineer. + Not compatible! 不兼容! @@ -4693,6 +4837,10 @@ This is your link for group %@! 通知被禁用! No comment provided by engineer. + + Notifications privacy + No comment provided by engineer. + Now admins can: - delete members' messages. @@ -4751,8 +4899,8 @@ Requires compatible VPN. 将不会使用 Onion 主机。 No comment provided by engineer. - - Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**. + + Only client devices store user profiles, contacts, groups, and messages. 只有客户端设备存储用户资料、联系人、群组和**双层端到端加密**发送的消息。 No comment provided by engineer. @@ -4836,6 +4984,10 @@ Requires compatible VPN. 打开设置 No comment provided by engineer. + + Open changes + No comment provided by engineer. + Open chat 打开聊天 @@ -4846,6 +4998,10 @@ Requires compatible VPN. 打开聊天控制台 authentication reason + + Open conditions + No comment provided by engineer. + Open group 打开群 @@ -4856,26 +5012,19 @@ Requires compatible VPN. 打开迁移到另一台设备 authentication reason - - Open server settings - 打开服务器设置 - No comment provided by engineer. - - - Open user profiles - 打开用户个人资料 - authentication reason - - - Open-source protocol and code – anybody can run the servers. - 开源协议和代码——任何人都可以运行服务器。 - No comment provided by engineer. - Opening app… 正在打开应用程序… No comment provided by engineer. + + Operator + No comment provided by engineer. + + + Operator server + alert title + Or paste archive link 或粘贴存档链接 @@ -4896,16 +5045,15 @@ Requires compatible VPN. 或者显示此码 No comment provided by engineer. + + Or to share privately + No comment provided by engineer. + Other 其他 No comment provided by engineer. - - Other %@ servers - 其他 %@ 服务器 - No comment provided by engineer. - Other file errors: %@ @@ -4985,13 +5133,8 @@ Requires compatible VPN. 待定 No comment provided by engineer. - - People can connect to you only via the links you share. - 人们只能通过您共享的链接与您建立联系。 - No comment provided by engineer. - - - Periodically + + Periodic 定期 No comment provided by engineer. @@ -5113,16 +5256,15 @@ Error: %@ 保留最后的消息草稿及其附件。 No comment provided by engineer. - - Preset server - 预设服务器 - No comment provided by engineer. - Preset server address 预设服务器地址 No comment provided by engineer. + + Preset servers + No comment provided by engineer. + Preview 预览 @@ -5201,7 +5343,7 @@ Error: %@ Profile update will be sent to your contacts. 个人资料更新将被发送给您的联系人。 - No comment provided by engineer. + alert message Prohibit audio/video calls. @@ -5294,6 +5436,10 @@ Enable in *Network & servers* settings. Proxy requires password No comment provided by engineer. + + Push Notifications + No comment provided by engineer. + Push notifications 推送通知 @@ -5334,26 +5480,21 @@ Enable in *Network & servers* settings. 阅读更多 No comment provided by engineer. - - Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address). - 在 [用户指南](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address) 中阅读更多内容。 - No comment provided by engineer. - Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). 阅读更多[User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)。 No comment provided by engineer. + + Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). + 在 [用户指南](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses) 中阅读更多内容。 + No comment provided by engineer. + Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends). 在 [用户指南](https://simplex.chat/docs/guide/readme.html#connect-to-friends) 中阅读更多内容。 No comment provided by engineer. - - Read more in our GitHub repository. - 在我们的 GitHub 仓库中阅读更多内容。 - No comment provided by engineer. - Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme). 在我们的 [GitHub 仓库](https://github.com/simplex-chat/simplex-chat#readme) 中阅读更多信息。 @@ -5669,6 +5810,14 @@ Enable in *Network & servers* settings. 揭示 chat item action + + Review conditions + No comment provided by engineer. + + + Review later + No comment provided by engineer. + Revoke 撤销 @@ -5713,6 +5862,14 @@ Enable in *Network & servers* settings. 更安全的群组 No comment provided by engineer. + + Same conditions will apply to operator **%@**. + No comment provided by engineer. + + + Same conditions will apply to operator(s): **%@**. + No comment provided by engineer. + Save 保存 @@ -5782,7 +5939,7 @@ Enable in *Network & servers* settings. Save servers? 保存服务器? - No comment provided by engineer. + alert title Save welcome message? @@ -5991,11 +6148,6 @@ Enable in *Network & servers* settings. 发送通知 No comment provided by engineer. - - Send notifications: - 发送通知: - No comment provided by engineer. - Send questions and ideas 发送问题和想法 @@ -6120,6 +6272,10 @@ Enable in *Network & servers* settings. Server No comment provided by engineer. + + Server added to operator %@. + alert message + Server address 服务器地址 @@ -6135,6 +6291,18 @@ Enable in *Network & servers* settings. 服务器地址与网络设置不兼容:%@。 No comment provided by engineer. + + Server operator changed. + alert title + + + Server operators + No comment provided by engineer. + + + Server protocol changed. + alert title + Server requires authorization to create queues, check password 服务器需要授权才能创建队列,检查密码 @@ -6252,22 +6420,35 @@ Enable in *Network & servers* settings. Share 分享 - chat item action + alert action + chat item action Share 1-time link 分享一次性链接 No comment provided by engineer. + + Share 1-time link with a friend + No comment provided by engineer. + + + Share SimpleX address on social media. + No comment provided by engineer. + Share address 分享地址 No comment provided by engineer. + + Share address publicly + No comment provided by engineer. + Share address with contacts? 与联系人分享地址? - No comment provided by engineer. + alert title Share from other apps. @@ -6383,6 +6564,14 @@ Enable in *Network & servers* settings. SimpleX 地址 No comment provided by engineer. + + SimpleX address and 1-time links are safe to share via any messenger. + No comment provided by engineer. + + + SimpleX address or 1-time link? + No comment provided by engineer. + SimpleX contact address SimpleX 联系地址 @@ -6471,6 +6660,11 @@ Enable in *Network & servers* settings. 导入过程中出现一些非致命错误: No comment provided by engineer. + + Some servers failed the test: +%@ + alert message + Somebody 某人 @@ -6554,12 +6748,12 @@ Enable in *Network & servers* settings. Stop sharing 停止分享 - No comment provided by engineer. + alert action Stop sharing address? 停止分享地址? - No comment provided by engineer. + alert title Stopping chat @@ -6706,7 +6900,7 @@ Enable in *Network & servers* settings. Tests failed! 测试失败! - No comment provided by engineer. + alert title Thank you for installing SimpleX Chat! @@ -6723,11 +6917,6 @@ Enable in *Network & servers* settings. 感谢用户——通过 Weblate 做出贡献! No comment provided by engineer. - - The 1st platform without any user identifiers – private by design. - 第一个没有任何用户标识符的平台 - 隐私设计. - No comment provided by engineer. - The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised. @@ -6740,6 +6929,10 @@ It can happen because of some bug or when the connection is compromised.该应用可以在您收到消息或联系人请求时通知您——请打开设置以启用通知。 No comment provided by engineer. + + The app protects your privacy by using different operators in each conversation. + No comment provided by engineer. + The app will ask to confirm downloads from unknown file servers (except .onion). 该应用程序将要求确认从未知文件服务器(.onion 除外)下载。 @@ -6755,6 +6948,10 @@ It can happen because of some bug or when the connection is compromised.您扫描的码不是 SimpleX 链接的二维码。 No comment provided by engineer. + + The connection reached the limit of undelivered messages, your contact may be offline. + No comment provided by engineer. + The connection you accepted will be cancelled! 您接受的连接将被取消! @@ -6775,6 +6972,11 @@ It can happen because of some bug or when the connection is compromised.加密正在运行,不需要新的加密协议。这可能会导致连接错误! No comment provided by engineer. + + The future of messaging + 下一代私密通讯软件 + No comment provided by engineer. + The hash of the previous message is different. 上一条消息的散列不同。 @@ -6800,11 +7002,6 @@ It can happen because of some bug or when the connection is compromised.对于所有成员,这些消息将被标记为已审核。 No comment provided by engineer. - - The next generation of private messaging - 下一代私密通讯软件 - No comment provided by engineer. - The old database was not removed during the migration, it can be deleted. 旧数据库在迁移过程中没有被移除,可以删除。 @@ -6815,6 +7012,10 @@ It can happen because of some bug or when the connection is compromised.该资料仅与您的联系人共享。 No comment provided by engineer. + + The second preset operator in the app! + No comment provided by engineer. + The second tick we missed! ✅ 我们错过的第二个"√"!✅ @@ -6830,6 +7031,10 @@ It can happen because of some bug or when the connection is compromised.您当前聊天资料 **%@** 的新连接服务器。 No comment provided by engineer. + + The servers for new files of your current chat profile **%@**. + No comment provided by engineer. + The text you pasted is not a SimpleX link. 您粘贴的文本不是 SimpleX 链接。 @@ -6844,6 +7049,10 @@ It can happen because of some bug or when the connection is compromised.主题 No comment provided by engineer. + + These conditions will also apply for: **%@**. + No comment provided by engineer. + These settings are for your current profile **%@**. 这些设置适用于您当前的配置文件 **%@**。 @@ -6944,9 +7153,8 @@ It can happen because of some bug or when the connection is compromised.建立新连接 No comment provided by engineer. - - To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts. - 为了保护隐私,SimpleX使用针对消息队列的标识符,而不是所有其他平台使用的用户ID,每个联系人都有独立的标识符。 + + To protect against your link being replaced, you can compare contact security codes. No comment provided by engineer. @@ -6966,6 +7174,15 @@ You will be prompted to complete authentication before this feature is enabled.< 在启用此功能之前,系统将提示您完成身份验证。 No comment provided by engineer. + + To protect your privacy, SimpleX uses separate IDs for each of your contacts. + 为了保护隐私,SimpleX使用针对消息队列的标识符,而不是所有其他平台使用的用户ID,每个联系人都有独立的标识符。 + No comment provided by engineer. + + + To receive + No comment provided by engineer. + To record speech please grant permission to use Microphone. No comment provided by engineer. @@ -6984,11 +7201,19 @@ You will be prompted to complete authentication before this feature is enabled.< 要显示您的隐藏的个人资料,请在**您的聊天个人资料**页面的搜索字段中输入完整密码。 No comment provided by engineer. + + To send + No comment provided by engineer. + To support instant push notifications the chat database has to be migrated. 为了支持即时推送通知,聊天数据库必须被迁移。 No comment provided by engineer. + + To use the servers of **%@**, accept conditions of use. + No comment provided by engineer. + To verify end-to-end encryption with your contact compare (or scan) the code on your devices. 要与您的联系人验证端到端加密,请比较(或扫描)您设备上的代码。 @@ -7079,6 +7304,10 @@ You will be prompted to complete authentication before this feature is enabled.< 解封成员吗? No comment provided by engineer. + + Undelivered messages + No comment provided by engineer. + Unexpected migration state 未预料的迁移状态 @@ -7236,6 +7465,10 @@ To connect, please ask your contact to create another connection link and check 正在上传存档 No comment provided by engineer. + + Use %@ + No comment provided by engineer. + Use .onion hosts 使用 .onion 主机 @@ -7260,6 +7493,14 @@ To connect, please ask your contact to create another connection link and check 使用当前配置文件 No comment provided by engineer. + + Use for files + No comment provided by engineer. + + + Use for messages + No comment provided by engineer. + Use for new connections 用于新连接 @@ -7300,6 +7541,10 @@ To connect, please ask your contact to create another connection link and check 使用服务器 No comment provided by engineer. + + Use servers + No comment provided by engineer. + Use the app while in the call. 通话时使用本应用. @@ -7389,11 +7634,19 @@ To connect, please ask your contact to create another connection link and check 最大 1gb 的视频和文件 No comment provided by engineer. + + View conditions + No comment provided by engineer. + View security code 查看安全码 No comment provided by engineer. + + View updated conditions + No comment provided by engineer. + Visible history 可见的历史 @@ -7504,9 +7757,8 @@ To connect, please ask your contact to create another connection link and check 连接音频和视频通话时。 No comment provided by engineer. - - When people request to connect, you can accept or reject it. - 当人们请求连接时,您可以接受或拒绝它。 + + When more than one operator is enabled, none of them has metadata to learn who communicates with whom. No comment provided by engineer. @@ -7666,6 +7918,18 @@ Repeat join request? 您可以在外观设置中更改它。 No comment provided by engineer. + + You can configure operators in Network & servers settings. + No comment provided by engineer. + + + You can configure servers via settings. + No comment provided by engineer. + + + You can create it in user picker. + No comment provided by engineer. + You can create it later 您可以以后创建它 @@ -7706,6 +7970,10 @@ Repeat join request? 您可以从存档的联系人向%@发送消息。 No comment provided by engineer. + + You can set connection name, to remember who the link was shared with. + No comment provided by engineer. + You can set lock screen notification preview via settings. 您可以通过设置来设置锁屏通知预览。 @@ -7721,11 +7989,6 @@ Repeat join request? 您可以与您的联系人分享该地址,让他们与 **%@** 联系。 No comment provided by engineer. - - You can share your address as a link or QR code - anybody can connect to you. - 您可以将您的地址作为链接或二维码共享——任何人都可以连接到您。 - No comment provided by engineer. - You can start chat via app Settings / Database or by restarting the app 您可以通过应用程序设置/数据库或重新启动应用程序开始聊天 @@ -7749,23 +8012,23 @@ Repeat join request? You can view invitation link again in connection details. 您可以在连接详情中再次查看邀请链接。 - No comment provided by engineer. + alert message You can't send messages! 您无法发送消息! No comment provided by engineer. - - You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them. - 您可以控制接收信息使用的服务器,您的联系人则使用您发送信息时所使用的服务器。 - No comment provided by engineer. - You could not be verified; please try again. 您的身份无法验证,请再试一次。 No comment provided by engineer. + + You decide who can connect. + 你决定谁可以连接。 + No comment provided by engineer. + You have already requested connection via this address! 你已经请求通过此地址进行连接! @@ -7888,11 +8151,6 @@ Repeat connection request? 您正在为该群组使用隐身个人资料——为防止共享您的主要个人资料,不允许邀请联系人 No comment provided by engineer. - - Your %@ servers - 您的 %@ 服务器 - No comment provided by engineer. - Your ICE servers 您的 ICE 服务器 @@ -7908,11 +8166,6 @@ Repeat connection request? 您的 SimpleX 地址 No comment provided by engineer. - - Your XFTP servers - 您的 XFTP 服务器 - No comment provided by engineer. - Your calls 您的通话 @@ -8009,16 +8262,15 @@ Repeat connection request? 您的随机资料 No comment provided by engineer. - - Your server - 您的服务器 - No comment provided by engineer. - Your server address 您的服务器地址 No comment provided by engineer. + + Your servers + No comment provided by engineer. + Your settings 您的设置 @@ -8439,6 +8691,10 @@ Repeat connection request? 过期 No comment provided by engineer. + + for better metadata privacy. + No comment provided by engineer. + forwarded 已转发 @@ -9061,6 +9317,33 @@ last received msg: %2$@ + +
+ +
+ + + %d new events + notification body + + + From: %@ + notification body + + + New events + notification + + + New messages + notification + + + New messages in %d chats + notification body + + +
diff --git a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Source Contents/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Source Contents/en.lproj/Localizable.strings index cf485752ea..cb83427195 100644 --- a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Source Contents/en.lproj/Localizable.strings +++ b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Source Contents/en.lproj/Localizable.strings @@ -1,9 +1,6 @@ /* No comment provided by engineer. */ "_italic_" = "\\_italic_"; -/* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact."; - /* No comment provided by engineer. */ "*bold*" = "\\*bold*"; @@ -27,4 +24,3 @@ /* No comment provided by engineer. */ "No group!" = "Group not found!"; - diff --git a/apps/ios/SimpleX Localizations/zh-Hant.xcloc/Localized Contents/zh-Hant.xliff b/apps/ios/SimpleX Localizations/zh-Hant.xcloc/Localized Contents/zh-Hant.xliff index 2b8649935c..1c1ae53673 100644 --- a/apps/ios/SimpleX Localizations/zh-Hant.xcloc/Localized Contents/zh-Hant.xliff +++ b/apps/ios/SimpleX Localizations/zh-Hant.xcloc/Localized Contents/zh-Hant.xliff @@ -187,23 +187,18 @@ ) No comment provided by engineer. - - **Add new contact**: to create your one-time QR Code or link for your contact. - **新增新的聯絡人**:建立一次性二維碼或連結連接聯絡人。 - No comment provided by engineer. - **Create link / QR code** for your contact to use. **建立連結 / 二維碼** 讓你的聯絡人使用。 No comment provided by engineer. - - **More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have. + + **More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata. **更有私隱**:每20分鐘會檢查一次訊息。裝置權杖與 SimpleX Chat 伺服器分享中,但是不包括你的聯絡人和訊息資料。 No comment provided by engineer. - - **Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app). + + **Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app. **最有私隱**:不使用 SimpleX Chat 通知服務器,在後台定期檢查訊息(取決於你使用應用程序的頻率)。 No comment provided by engineer. @@ -217,8 +212,8 @@ **請注意**:如果你忘記了密碼你將不能再次復原或更改密碼。 No comment provided by engineer. - - **Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from. + + **Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from. **建議**:裝置權杖和通知都會傳送去 SimpeleX Chat 的通知伺服器,但是不包括訊息內容、大小或傳送者資料。 No comment provided by engineer. @@ -1747,8 +1742,8 @@ 下載圖片需要傳送者上線的時候才能下載圖片,請等待對方上線! No comment provided by engineer. - - Immune to spam and abuse + + Immune to spam 不受垃圾郵件和濫用行為影響 No comment provided by engineer. @@ -2217,8 +2212,8 @@ We will be adding server redundancy to prevent lost messages. Onion 主機不會啟用。 No comment provided by engineer. - - Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**. + + Only client devices store user profiles, contacts, groups, and messages. 只有客戶端裝置才會儲存你的個人檔案、聯絡人,群組,所有訊息都會經過**兩層的端對端加密**。 No comment provided by engineer. @@ -2277,8 +2272,8 @@ We will be adding server redundancy to prevent lost messages. 使用終端機開啟對話 authentication reason - - Open-source protocol and code – anybody can run the servers. + + Anybody can host servers. 開放源碼協議和程式碼 – 任何人也可以運行伺服器。 No comment provided by engineer. @@ -2317,8 +2312,8 @@ We will be adding server redundancy to prevent lost messages. 將你接收到的連結貼上至下面的框內,以開始你與你的聯絡人對話。 No comment provided by engineer. - - People can connect to you only via the links you share. + + You decide who can connect. 人們只能在你分享了連結後,才能和你連接。 No comment provided by engineer. @@ -3010,8 +3005,8 @@ We will be adding server redundancy to prevent lost messages. 感謝你安裝SimpleX Chat! No comment provided by engineer. - - The 1st platform without any user identifiers – private by design. + + No user identifiers. 第一個沒有任何用戶識別符的通訊平台 – 以私隱為設計。 No comment provided by engineer. @@ -3049,8 +3044,8 @@ We will be adding server redundancy to prevent lost messages. The microphone does not work when the app is in the background. No comment provided by engineer. - - The next generation of private messaging + + The future of messaging 新一代的私密訊息平台 No comment provided by engineer. @@ -3118,8 +3113,8 @@ We will be adding server redundancy to prevent lost messages. To prevent the call interruption, enable Do Not Disturb mode. No comment provided by engineer. - - To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts. + + To protect your privacy, SimpleX uses separate IDs for each of your contacts. 為了保護隱私,而不像是其他平台般需要提取和存儲用戶的 IDs 資料,SimpleX 平台有自家佇列的標識符,這對於你的每個聯絡人也是獨一無二的。 No comment provided by engineer. @@ -3455,11 +3450,6 @@ To connect, please ask your contact to create another connection link and check 你可以使用 Markdown 語法以更清楚標明訊息: No comment provided by engineer. - - You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them. - 你可以控制通過哪一個伺服器 **來接收** 你的聯絡人訊息 – 這些伺服器用來接收他們傳送給你的訊息。 - No comment provided by engineer. - You could not be verified; please try again. 你未能通過認證;請再試一次。 diff --git a/apps/ios/bg.lproj/Localizable.strings b/apps/ios/bg.lproj/Localizable.strings index ff8a76828c..ccfd9a7b98 100644 --- a/apps/ios/bg.lproj/Localizable.strings +++ b/apps/ios/bg.lproj/Localizable.strings @@ -65,10 +65,7 @@ "[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[Звезда в GitHub](https://github.com/simplex-chat/simplex-chat)"; /* No comment provided by engineer. */ -"**Add contact**: to create a new invitation link, or connect via a link you received." = "**Добави контакт**: за създаване на нов линк или свързване чрез получен линк за връзка."; - -/* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Добави нов контакт**: за да създадете своя еднократен QR код или линк за вашия контакт."; +"**Create 1-time link**: to create and share a new invitation link." = "**Добави контакт**: за създаване на нов линк."; /* No comment provided by engineer. */ "**Create group**: to create a new group." = "**Създай група**: за създаване на нова група."; @@ -80,10 +77,10 @@ "**e2e encrypted** video call" = "**e2e криптирано** видео разговор"; /* No comment provided by engineer. */ -"**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." = "**По поверително**: проверявайте новите съобщения на всеки 20 минути. Токенът на устройството се споделя със сървъра за чат SimpleX, но не и колко контакти или съобщения имате."; +"**More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata." = "**По поверително**: проверявайте новите съобщения на всеки 20 минути. Токенът на устройството се споделя със сървъра за чат SimpleX, но не и колко контакти или съобщения имате."; /* No comment provided by engineer. */ -"**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." = "**Най-поверително**: не използвайте сървъра за известия SimpleX Chat, периодично проверявайте съобщенията във фонов режим (зависи от това колко често използвате приложението)."; +"**Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app." = "**Най-поверително**: не използвайте сървъра за известия SimpleX Chat, периодично проверявайте съобщенията във фонов режим (зависи от това колко често използвате приложението)."; /* No comment provided by engineer. */ "**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." = "**Моля, обърнете внимание**: използването на една и съща база данни на две устройства ще наруши декриптирането на съобщенията от вашите връзки като защита на сигурността."; @@ -92,7 +89,7 @@ "**Please note**: you will NOT be able to recover or change passphrase if you lose it." = "**Моля, обърнете внимание**: НЯМА да можете да възстановите или промените паролата, ако я загубите."; /* No comment provided by engineer. */ -"**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." = "**Препоръчително**: токенът на устройството и известията се изпращат до сървъра за уведомяване на SimpleX Chat, но не и съдържанието, размерът на съобщението или от кого е."; +"**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from." = "**Препоръчително**: токенът на устройството и известията се изпращат до сървъра за уведомяване на SimpleX Chat, но не и съдържанието, размерът на съобщението или от кого е."; /* No comment provided by engineer. */ "**Warning**: Instant push notifications require passphrase saved in Keychain." = "**Внимание**: Незабавните push известия изискват парола, запазена в Keychain."; @@ -2075,7 +2072,7 @@ "Immediately" = "Веднага"; /* No comment provided by engineer. */ -"Immune to spam and abuse" = "Защитен от спам и злоупотреби"; +"Immune to spam" = "Защитен от спам и злоупотреби"; /* No comment provided by engineer. */ "Import" = "Импортиране"; @@ -2168,7 +2165,7 @@ "Instant push notifications will be hidden!\n" = "Незабавните push известия ще бъдат скрити!\n"; /* No comment provided by engineer. */ -"Instantly" = "Мигновено"; +"Instant" = "Мигновено"; /* No comment provided by engineer. */ "Interface" = "Интерфейс"; @@ -2363,7 +2360,7 @@ "Live messages" = "Съобщения на живо"; /* No comment provided by engineer. */ -"Local" = "Локално"; +"No push server" = "Локално"; /* No comment provided by engineer. */ "Local name" = "Локално име"; @@ -2716,7 +2713,7 @@ "Onion hosts will not be used." = "Няма се използват Onion хостове."; /* No comment provided by engineer. */ -"Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." = "Само потребителските устройства съхраняват потребителски профили, контакти, групи и съобщения, изпратени с **двуслойно криптиране от край до край**."; +"Only client devices store user profiles, contacts, groups, and messages." = "Само потребителските устройства съхраняват потребителски профили, контакти, групи и съобщения, изпратени с **двуслойно криптиране от край до край**."; /* No comment provided by engineer. */ "Only group owners can change group preferences." = "Само собствениците на групата могат да променят груповите настройки."; @@ -2779,7 +2776,7 @@ "Open user profiles" = "Отвори потребителските профили"; /* No comment provided by engineer. */ -"Open-source protocol and code – anybody can run the servers." = "Протокол и код с отворен код – всеки може да оперира собствени сървъри."; +"Anybody can host servers." = "Протокол и код с отворен код – всеки може да оперира собствени сървъри."; /* No comment provided by engineer. */ "Opening app…" = "Приложението се отваря…"; @@ -2842,10 +2839,10 @@ "peer-to-peer" = "peer-to-peer"; /* No comment provided by engineer. */ -"People can connect to you only via the links you share." = "Хората могат да се свържат с вас само чрез ликовете, които споделяте."; +"You decide who can connect." = "Хората могат да се свържат с вас само чрез ликовете, които споделяте."; /* No comment provided by engineer. */ -"Periodically" = "Периодично"; +"Periodic" = "Периодично"; /* message decrypt error item */ "Permanent decryption error" = "Постоянна грешка при декриптиране"; @@ -3010,7 +3007,7 @@ "Read more" = "Прочетете още"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." = "Прочетете повече в [Ръководство за потребителя](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)."; +"Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." = "Прочетете повече в [Ръководство за потребителя](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)."; /* No comment provided by engineer. */ "Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." = "Прочетете повече в [Ръководство за потребителя](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)."; @@ -3693,7 +3690,7 @@ "Thanks to the users – contribute via Weblate!" = "Благодарение на потребителите – допринесете през Weblate!"; /* No comment provided by engineer. */ -"The 1st platform without any user identifiers – private by design." = "Първата платформа без никакви потребителски идентификатори – поверителна по дизайн."; +"No user identifiers." = "Първата платформа без никакви потребителски идентификатори – поверителна по дизайн."; /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "Приложението може да ви уведоми, когато получите съобщения или заявки за контакт - моля, отворете настройките, за да активирате."; @@ -3729,7 +3726,7 @@ "The message will be marked as moderated for all members." = "Съобщението ще бъде маркирано като модерирано за всички членове."; /* No comment provided by engineer. */ -"The next generation of private messaging" = "Ново поколение поверителни съобщения"; +"The future of messaging" = "Ново поколение поверителни съобщения"; /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "Старата база данни не бе премахната по време на миграцията, тя може да бъде изтрита."; @@ -3807,7 +3804,7 @@ "To make a new connection" = "За да направите нова връзка"; /* No comment provided by engineer. */ -"To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." = "За да се защити поверителността, вместо потребителски идентификатори, използвани от всички други платформи, SimpleX има идентификатори за опашки от съобщения, отделни за всеки от вашите контакти."; +"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "За да се защити поверителността, вместо потребителски идентификатори, използвани от всички други платформи, SimpleX има идентификатори за опашки от съобщения, отделни за всеки от вашите контакти."; /* No comment provided by engineer. */ "To protect timezone, image/voice files use UTC." = "За да не се разкрива часовата зона, файловете с изображения/глас използват UTC."; @@ -4280,9 +4277,6 @@ /* snd group event chat item */ "you changed role of %@ to %@" = "променихте ролята на %1$@ на %2$@"; -/* No comment provided by engineer. */ -"You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." = "Вие контролирате през кой сървър(и) **да получавате** съобщенията, вашите контакти – сървърите, които използвате, за да им изпращате съобщения."; - /* No comment provided by engineer. */ "You could not be verified; please try again." = "Не можахте да бъдете потвърдени; Моля, опитайте отново."; diff --git a/apps/ios/cs.lproj/Localizable.strings b/apps/ios/cs.lproj/Localizable.strings index 618cd90aba..a00adef700 100644 --- a/apps/ios/cs.lproj/Localizable.strings +++ b/apps/ios/cs.lproj/Localizable.strings @@ -55,9 +55,6 @@ /* No comment provided by engineer. */ "[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[Hvězda na GitHubu](https://github.com/simplex-chat/simplex-chat)"; -/* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Přidat nový kontakt**: pro vytvoření jednorázového QR kódu nebo odkazu pro váš kontakt."; - /* No comment provided by engineer. */ "**e2e encrypted** audio call" = "**e2e šifrovaný** audio hovor"; @@ -65,16 +62,16 @@ "**e2e encrypted** video call" = "**e2e šifrovaný** videohovor"; /* No comment provided by engineer. */ -"**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." = "**Soukromější**: kontrolovat nové zprávy každých 20 minut. Token zařízení je sdílen se serverem SimpleX Chat, ale ne kolik máte kontaktů nebo zpráv."; +"**More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata." = "**Soukromější**: kontrolovat nové zprávy každých 20 minut. Token zařízení je sdílen se serverem SimpleX Chat, ale ne kolik máte kontaktů nebo zpráv."; /* No comment provided by engineer. */ -"**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." = "**Nejsoukromější**: nepoužívejte server oznámení SimpleX Chat, pravidelně kontrolujte zprávy na pozadí (závisí na tom, jak často aplikaci používáte)."; +"**Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app." = "**Nejsoukromější**: nepoužívejte server oznámení SimpleX Chat, pravidelně kontrolujte zprávy na pozadí (závisí na tom, jak často aplikaci používáte)."; /* No comment provided by engineer. */ "**Please note**: you will NOT be able to recover or change passphrase if you lose it." = "**Upozornění**: Pokud heslo ztratíte, NEBUDETE jej moci obnovit ani změnit."; /* No comment provided by engineer. */ -"**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." = "**Doporučeno**: Token zařízení a oznámení se odesílají na oznamovací server SimpleX Chat, ale nikoli obsah, velikost nebo od koho jsou zprávy."; +"**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from." = "**Doporučeno**: Token zařízení a oznámení se odesílají na oznamovací server SimpleX Chat, ale nikoli obsah, velikost nebo od koho jsou zprávy."; /* No comment provided by engineer. */ "**Warning**: Instant push notifications require passphrase saved in Keychain." = "**Upozornění**: Okamžitě doručovaná oznámení vyžadují přístupové heslo uložené v Klíčence."; @@ -1702,7 +1699,7 @@ "Immediately" = "Ihned"; /* No comment provided by engineer. */ -"Immune to spam and abuse" = "Odolná vůči spamu a zneužití"; +"Immune to spam" = "Odolná vůči spamu a zneužití"; /* No comment provided by engineer. */ "Import" = "Import"; @@ -1774,7 +1771,7 @@ "Instant push notifications will be hidden!\n" = "Okamžitá oznámení budou skryta!\n"; /* No comment provided by engineer. */ -"Instantly" = "Okamžitě"; +"Instant" = "Okamžitě"; /* No comment provided by engineer. */ "Interface" = "Rozhranní"; @@ -1921,7 +1918,7 @@ "Live messages" = "Živé zprávy"; /* No comment provided by engineer. */ -"Local" = "Místní"; +"No push server" = "Místní"; /* No comment provided by engineer. */ "Local name" = "Místní název"; @@ -2214,7 +2211,7 @@ "Onion hosts will not be used." = "Onion hostitelé nebudou použiti."; /* No comment provided by engineer. */ -"Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." = "Pouze klientská zařízení ukládají uživatelské profily, kontakty, skupiny a zprávy odeslané s **2vrstvým šifrováním typu end-to-end**."; +"Only client devices store user profiles, contacts, groups, and messages." = "Pouze klientská zařízení ukládají uživatelské profily, kontakty, skupiny a zprávy odeslané s **2vrstvým šifrováním typu end-to-end**."; /* No comment provided by engineer. */ "Only group owners can change group preferences." = "Předvolby skupiny mohou měnit pouze vlastníci skupiny."; @@ -2271,7 +2268,7 @@ "Open user profiles" = "Otevřít uživatelské profily"; /* No comment provided by engineer. */ -"Open-source protocol and code – anybody can run the servers." = "Protokol a kód s otevřeným zdrojovým kódem - servery může provozovat kdokoli."; +"Anybody can host servers." = "Servery může provozovat kdokoli."; /* member role */ "owner" = "vlastník"; @@ -2301,10 +2298,10 @@ "peer-to-peer" = "peer-to-peer"; /* No comment provided by engineer. */ -"People can connect to you only via the links you share." = "Lidé se s vámi mohou spojit pouze prostřednictvím odkazů, které sdílíte."; +"You decide who can connect." = "Lidé se s vámi mohou spojit pouze prostřednictvím odkazu, který sdílíte."; /* No comment provided by engineer. */ -"Periodically" = "Pravidelně"; +"Periodic" = "Pravidelně"; /* message decrypt error item */ "Permanent decryption error" = "Chyba dešifrování"; @@ -2442,7 +2439,7 @@ "Read more" = "Přečíst více"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." = "Další informace naleznete v [Uživatelské příručce](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)."; +"Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." = "Další informace naleznete v [Uživatelské příručce](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)."; /* No comment provided by engineer. */ "Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." = "Přečtěte si více v [Uživatelské příručce](https://simplex.chat/docs/guide/readme.html#connect-to-friends)."; @@ -3011,7 +3008,7 @@ "Thanks to the users – contribute via Weblate!" = "Díky uživatelům - přispívejte prostřednictvím Weblate!"; /* No comment provided by engineer. */ -"The 1st platform without any user identifiers – private by design." = "1. Platforma bez identifikátorů uživatelů - soukromá už od záměru."; +"No user identifiers." = "Bez uživatelských identifikátorů"; /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "Aplikace vás může upozornit na přijaté zprávy nebo žádosti o kontakt - povolte to v nastavení."; @@ -3044,7 +3041,7 @@ "The message will be marked as moderated for all members." = "Zpráva bude pro všechny členy označena jako moderovaná."; /* No comment provided by engineer. */ -"The next generation of private messaging" = "Nová generace soukromých zpráv"; +"The future of messaging" = "Nová generace soukromých zpráv"; /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "Stará databáze nebyla během přenášení odstraněna, lze ji smazat."; @@ -3098,7 +3095,7 @@ "To make a new connection" = "Vytvoření nového připojení"; /* No comment provided by engineer. */ -"To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." = "Pro ochranu soukromí namísto ID uživatelů používaných všemi ostatními platformami má SimpleX identifikátory pro fronty zpráv, oddělené pro každý z vašich kontaktů."; +"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Pro ochranu soukromí namísto ID uživatelů používaných všemi ostatními platformami má SimpleX identifikátory pro fronty zpráv, oddělené pro každý z vašich kontaktů."; /* No comment provided by engineer. */ "To protect timezone, image/voice files use UTC." = "K ochraně časového pásma používají obrazové/hlasové soubory UTC."; @@ -3427,9 +3424,6 @@ /* snd group event chat item */ "you changed role of %@ to %@" = "změnili jste roli z %1$@ na %2$@"; -/* No comment provided by engineer. */ -"You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." = "Sami řídíte, přes který server(y) **přijímat** zprávy, své kontakty – servery, které používáte k odesílání zpráv."; - /* No comment provided by engineer. */ "You could not be verified; please try again." = "Nemohli jste být ověřeni; Zkuste to prosím znovu."; diff --git a/apps/ios/de.lproj/Localizable.strings b/apps/ios/de.lproj/Localizable.strings index 7334314c3e..f526eaf7e1 100644 --- a/apps/ios/de.lproj/Localizable.strings +++ b/apps/ios/de.lproj/Localizable.strings @@ -65,10 +65,7 @@ "[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[Stern auf GitHub vergeben](https://github.com/simplex-chat/simplex-chat)"; /* No comment provided by engineer. */ -"**Add contact**: to create a new invitation link, or connect via a link you received." = "**Kontakt hinzufügen**: Um einen neuen Einladungslink zu erstellen oder eine Verbindung über einen Link herzustellen, den Sie erhalten haben."; - -/* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Neuen Kontakt hinzufügen**: Um einen Einmal-QR-Code oder -Link für Ihren Kontakt zu erzeugen."; +"**Create 1-time link**: to create and share a new invitation link." = "**Kontakt hinzufügen**: Um einen neuen Einladungslink zu erstellen."; /* No comment provided by engineer. */ "**Create group**: to create a new group." = "**Gruppe erstellen**: Um eine neue Gruppe zu erstellen."; @@ -80,10 +77,10 @@ "**e2e encrypted** video call" = "**E2E-verschlüsselter** Videoanruf"; /* No comment provided by engineer. */ -"**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." = "**Mehr Privatsphäre**: Es wird alle 20 Minuten auf neue Nachrichten geprüft. Nur Ihr Geräte-Token wird dem SimpleX-Chat-Server mitgeteilt, aber nicht wie viele Kontakte Sie haben oder welche Nachrichten Sie empfangen."; +"**More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata." = "**Mehr Privatsphäre**: Es wird alle 20 Minuten auf neue Nachrichten geprüft. Nur Ihr Geräte-Token wird dem SimpleX-Chat-Server mitgeteilt, aber nicht wie viele Kontakte Sie haben oder welche Nachrichten Sie empfangen."; /* No comment provided by engineer. */ -"**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." = "**Beste Privatsphäre**: Es wird kein SimpleX-Chat-Benachrichtigungs-Server genutzt, Nachrichten werden in periodischen Abständen im Hintergrund geprüft (dies hängt davon ab, wie häufig Sie die App nutzen)."; +"**Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app." = "**Beste Privatsphäre**: Es wird kein SimpleX-Chat-Benachrichtigungs-Server genutzt, Nachrichten werden in periodischen Abständen im Hintergrund geprüft (dies hängt davon ab, wie häufig Sie die App nutzen)."; /* No comment provided by engineer. */ "**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." = "**Bitte beachten Sie**: Aus Sicherheitsgründen wird die Nachrichtenentschlüsselung Ihrer Verbindungen abgebrochen, wenn Sie die gleiche Datenbank auf zwei Geräten nutzen."; @@ -92,7 +89,7 @@ "**Please note**: you will NOT be able to recover or change passphrase if you lose it." = "**Bitte beachten Sie**: Das Passwort kann NICHT wiederhergestellt oder geändert werden, wenn Sie es vergessen haben oder verlieren."; /* No comment provided by engineer. */ -"**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." = "**Empfohlen**: Nur Ihr Geräte-Token und ihre Benachrichtigungen werden an den SimpleX-Chat-Benachrichtigungs-Server gesendet, aber weder der Nachrichteninhalt noch deren Größe oder von wem sie gesendet wurde."; +"**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from." = "**Empfohlen**: Nur Ihr Geräte-Token und ihre Benachrichtigungen werden an den SimpleX-Chat-Benachrichtigungs-Server gesendet, aber weder der Nachrichteninhalt noch deren Größe oder von wem sie gesendet wurde."; /* No comment provided by engineer. */ "**Warning**: Instant push notifications require passphrase saved in Keychain." = "**Warnung**: Sofortige Push-Benachrichtigungen erfordern die Eingabe eines Passworts, welches in Ihrem Schlüsselbund gespeichert ist."; @@ -2474,7 +2471,7 @@ "Immediately" = "Sofort"; /* No comment provided by engineer. */ -"Immune to spam and abuse" = "Immun gegen Spam und Missbrauch"; +"Immune to spam" = "Immun gegen Spam und Missbrauch"; /* No comment provided by engineer. */ "Import" = "Importieren"; @@ -2576,7 +2573,7 @@ "Instant push notifications will be hidden!\n" = "Sofortige Push-Benachrichtigungen werden verborgen!\n"; /* No comment provided by engineer. */ -"Instantly" = "Sofort"; +"Instant" = "Sofort"; /* No comment provided by engineer. */ "Interface" = "Schnittstelle"; @@ -2786,7 +2783,7 @@ "Live messages" = "Live Nachrichten"; /* No comment provided by engineer. */ -"Local" = "Lokal"; +"No push server" = "Lokal"; /* No comment provided by engineer. */ "Local name" = "Lokaler Name"; @@ -3226,7 +3223,7 @@ "Onion hosts will not be used." = "Onion-Hosts werden nicht verwendet."; /* No comment provided by engineer. */ -"Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." = "Nur die Endgeräte speichern die Benutzerprofile, Kontakte, Gruppen und Nachrichten, welche über eine **2-Schichten Ende-zu-Ende-Verschlüsselung** gesendet werden."; +"Only client devices store user profiles, contacts, groups, and messages." = "Nur die Endgeräte speichern die Benutzerprofile, Kontakte, Gruppen und Nachrichten, welche über eine **2-Schichten Ende-zu-Ende-Verschlüsselung** gesendet werden."; /* No comment provided by engineer. */ "Only delete conversation" = "Nur die Chat-Inhalte löschen"; @@ -3295,7 +3292,7 @@ "Open user profiles" = "Benutzerprofile öffnen"; /* No comment provided by engineer. */ -"Open-source protocol and code – anybody can run the servers." = "Open-Source-Protokoll und -Code – Jede Person kann ihre eigenen Server aufsetzen und nutzen."; +"Anybody can host servers." = "Jeder kann seine eigenen Server aufsetzen."; /* No comment provided by engineer. */ "Opening app…" = "App wird geöffnet…"; @@ -3376,10 +3373,10 @@ "Pending" = "Ausstehend"; /* No comment provided by engineer. */ -"People can connect to you only via the links you share." = "Verbindungen mit Kontakten sind nur über Links möglich, die Sie oder Ihre Kontakte untereinander teilen."; +"You decide who can connect." = "Sie entscheiden, wer sich mit Ihnen verbinden kann."; /* No comment provided by engineer. */ -"Periodically" = "Periodisch"; +"Periodic" = "Periodisch"; /* message decrypt error item */ "Permanent decryption error" = "Entschlüsselungsfehler"; @@ -3592,7 +3589,7 @@ "Read more" = "Mehr erfahren"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." = "Mehr dazu in der [Benutzeranleitung](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address) lesen."; +"Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." = "Mehr dazu in der [Benutzeranleitung](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses) lesen."; /* No comment provided by engineer. */ "Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." = "Lesen Sie mehr dazu im [Benutzerhandbuch](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)."; @@ -4500,7 +4497,7 @@ "Thanks to the users – contribute via Weblate!" = "Dank der Nutzer - Tragen Sie per Weblate bei!"; /* No comment provided by engineer. */ -"The 1st platform without any user identifiers – private by design." = "Die erste Plattform ohne Benutzerkennungen – Privat per Design."; +"No user identifiers." = "Keine Benutzerkennungen."; /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "Wenn sie Nachrichten oder Kontaktanfragen empfangen, kann Sie die App benachrichtigen - Um dies zu aktivieren, öffnen Sie bitte die Einstellungen."; @@ -4545,7 +4542,7 @@ "The messages will be marked as moderated for all members." = "Die Nachrichten werden für alle Mitglieder als moderiert gekennzeichnet werden."; /* No comment provided by engineer. */ -"The next generation of private messaging" = "Die nächste Generation von privatem Messaging"; +"The future of messaging" = "Die nächste Generation von privatem Messaging"; /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "Die alte Datenbank wurde während der Migration nicht entfernt. Sie kann gelöscht werden."; @@ -4635,7 +4632,7 @@ "To make a new connection" = "Um eine Verbindung mit einem neuen Kontakt zu erstellen"; /* No comment provided by engineer. */ -"To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." = "Zum Schutz Ihrer Privatsphäre verwendet SimpleX an Stelle von Benutzerkennungen, die von allen anderen Plattformen verwendet werden, Kennungen für Nachrichtenwarteschlangen, die für jeden Ihrer Kontakte individuell sind."; +"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Zum Schutz Ihrer Privatsphäre verwendet SimpleX an Stelle von Benutzerkennungen, die von allen anderen Plattformen verwendet werden, Kennungen für Nachrichtenwarteschlangen, die für jeden Ihrer Kontakte individuell sind."; /* No comment provided by engineer. */ "To protect timezone, image/voice files use UTC." = "Bild- und Sprachdateinamen enthalten UTC, um Informationen zur Zeitzone zu schützen."; @@ -5210,9 +5207,6 @@ /* snd group event chat item */ "you changed role of %@ to %@" = "Sie haben die Rolle von %1$@ auf %2$@ geändert"; -/* No comment provided by engineer. */ -"You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." = "Sie können selbst festlegen, über welche Server Sie Ihre Nachrichten **empfangen** und an Ihre Kontakte **senden** wollen."; - /* No comment provided by engineer. */ "You could not be verified; please try again." = "Sie konnten nicht überprüft werden; bitte versuchen Sie es erneut."; diff --git a/apps/ios/en.lproj/Localizable.strings b/apps/ios/en.lproj/Localizable.strings index cf485752ea..cb83427195 100644 --- a/apps/ios/en.lproj/Localizable.strings +++ b/apps/ios/en.lproj/Localizable.strings @@ -1,9 +1,6 @@ /* No comment provided by engineer. */ "_italic_" = "\\_italic_"; -/* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact."; - /* No comment provided by engineer. */ "*bold*" = "\\*bold*"; @@ -27,4 +24,3 @@ /* No comment provided by engineer. */ "No group!" = "Group not found!"; - diff --git a/apps/ios/es.lproj/Localizable.strings b/apps/ios/es.lproj/Localizable.strings index 70c29f49e0..c2f982d0d4 100644 --- a/apps/ios/es.lproj/Localizable.strings +++ b/apps/ios/es.lproj/Localizable.strings @@ -65,10 +65,7 @@ "[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[Estrella en GitHub](https://github.com/simplex-chat/simplex-chat)"; /* No comment provided by engineer. */ -"**Add contact**: to create a new invitation link, or connect via a link you received." = "**Añadir contacto**: crea un enlace de invitación nuevo o usa un enlace recibido."; - -/* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Añadir nuevo contacto**: para crear tu código QR o enlace de un uso para tu contacto."; +"**Create 1-time link**: to create and share a new invitation link." = "**Añadir contacto**: crea un enlace de invitación nuevo."; /* No comment provided by engineer. */ "**Create group**: to create a new group." = "**Crear grupo**: crea un grupo nuevo."; @@ -80,10 +77,10 @@ "**e2e encrypted** video call" = "Videollamada con **cifrado de extremo a extremo**"; /* No comment provided by engineer. */ -"**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." = "**Más privado**: comprueba los mensajes nuevos cada 20 minutos. El token del dispositivo se comparte con el servidor de SimpleX Chat, pero no cuántos contactos o mensajes tienes."; +"**More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata." = "**Más privado**: comprueba los mensajes nuevos cada 20 minutos. El token del dispositivo se comparte con el servidor de SimpleX Chat, pero no cuántos contactos o mensajes tienes."; /* No comment provided by engineer. */ -"**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." = "**Más privado**: no se usa el servidor de notificaciones de SimpleX Chat, los mensajes se comprueban periódicamente en segundo plano (dependiendo de la frecuencia con la que utilices la aplicación)."; +"**Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app." = "**Más privado**: no se usa el servidor de notificaciones de SimpleX Chat, los mensajes se comprueban periódicamente en segundo plano (dependiendo de la frecuencia con la que utilices la aplicación)."; /* No comment provided by engineer. */ "**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." = "**Recuarda**: usar la misma base de datos en dos dispositivos hará que falle el descifrado de mensajes como protección de seguridad."; @@ -92,7 +89,7 @@ "**Please note**: you will NOT be able to recover or change passphrase if you lose it." = "**Atención**: NO podrás recuperar o cambiar la contraseña si la pierdes."; /* No comment provided by engineer. */ -"**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." = "**Recomendado**: el token del dispositivo y las notificaciones se envían al servidor de notificaciones de SimpleX Chat, pero no el contenido del mensaje, su tamaño o su procedencia."; +"**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from." = "**Recomendado**: el token del dispositivo y las notificaciones se envían al servidor de notificaciones de SimpleX Chat, pero no el contenido del mensaje, su tamaño o su procedencia."; /* No comment provided by engineer. */ "**Warning**: Instant push notifications require passphrase saved in Keychain." = "**Advertencia**: Las notificaciones automáticas instantáneas requieren una contraseña guardada en Keychain."; @@ -2474,7 +2471,7 @@ "Immediately" = "Inmediatamente"; /* No comment provided by engineer. */ -"Immune to spam and abuse" = "Inmune a spam y abuso"; +"Immune to spam" = "Inmune a spam y abuso"; /* No comment provided by engineer. */ "Import" = "Importar"; @@ -2576,7 +2573,7 @@ "Instant push notifications will be hidden!\n" = "¡Las notificaciones automáticas estarán ocultas!\n"; /* No comment provided by engineer. */ -"Instantly" = "Al instante"; +"Instant" = "Al instante"; /* No comment provided by engineer. */ "Interface" = "Interfaz"; @@ -2786,7 +2783,7 @@ "Live messages" = "Mensajes en vivo"; /* No comment provided by engineer. */ -"Local" = "Local"; +"No push server" = "No push server"; /* No comment provided by engineer. */ "Local name" = "Nombre local"; @@ -3226,7 +3223,7 @@ "Onion hosts will not be used." = "No se usarán hosts .onion."; /* No comment provided by engineer. */ -"Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." = "Sólo los dispositivos cliente almacenan perfiles de usuario, contactos, grupos y mensajes enviados con **cifrado de extremo a extremo de 2 capas**."; +"Only client devices store user profiles, contacts, groups, and messages." = "Sólo los dispositivos cliente almacenan perfiles de usuario, contactos, grupos y mensajes enviados con **cifrado de extremo a extremo de 2 capas**."; /* No comment provided by engineer. */ "Only delete conversation" = "Sólo borrar la conversación"; @@ -3295,7 +3292,7 @@ "Open user profiles" = "Abrir perfil de usuario"; /* No comment provided by engineer. */ -"Open-source protocol and code – anybody can run the servers." = "Protocolo y código abiertos: cualquiera puede usar los servidores."; +"Anybody can host servers." = "Cualquiera puede alojar servidores."; /* No comment provided by engineer. */ "Opening app…" = "Iniciando aplicación…"; @@ -3376,10 +3373,10 @@ "Pending" = "Pendientes"; /* No comment provided by engineer. */ -"People can connect to you only via the links you share." = "Las personas pueden conectarse contigo solo mediante los enlaces que compartes."; +"You decide who can connect." = "Tu decides quién se conecta."; /* No comment provided by engineer. */ -"Periodically" = "Periódicamente"; +"Periodic" = "Periódicamente"; /* message decrypt error item */ "Permanent decryption error" = "Error permanente descifrado"; @@ -3592,7 +3589,7 @@ "Read more" = "Conoce más"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." = "Conoce más en el [Manual del Usuario](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)."; +"Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." = "Conoce más en el [Manual del Usuario](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)."; /* No comment provided by engineer. */ "Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." = "Conoce más en la [Guía del Usuario](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)."; @@ -4500,7 +4497,7 @@ "Thanks to the users – contribute via Weblate!" = "¡Nuestro agradecimiento a todos los colaboradores! Puedes contribuir a través de Weblate"; /* No comment provided by engineer. */ -"The 1st platform without any user identifiers – private by design." = "La primera plataforma sin identificadores de usuario: diseñada para la privacidad."; +"No user identifiers." = "Sin identificadores de usuario."; /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "La aplicación puede notificarte cuando recibas mensajes o solicitudes de contacto: por favor, abre la configuración para activarlo."; @@ -4545,7 +4542,7 @@ "The messages will be marked as moderated for all members." = "Los mensajes serán marcados como moderados para todos los miembros."; /* No comment provided by engineer. */ -"The next generation of private messaging" = "La nueva generación de mensajería privada"; +"The future of messaging" = "La nueva generación de mensajería privada"; /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "La base de datos antigua no se eliminó durante la migración, puede eliminarse."; @@ -4635,7 +4632,7 @@ "To make a new connection" = "Para hacer una conexión nueva"; /* No comment provided by engineer. */ -"To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." = "Para proteger tu privacidad, en lugar de los identificadores de usuario que usan el resto de plataformas, SimpleX dispone de identificadores para las colas de mensajes, independientes para cada uno de tus contactos."; +"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Para proteger tu privacidad, en lugar de los identificadores de usuario que usan el resto de plataformas, SimpleX dispone de identificadores para las colas de mensajes, independientes para cada uno de tus contactos."; /* No comment provided by engineer. */ "To protect timezone, image/voice files use UTC." = "Para proteger la zona horaria, los archivos de imagen/voz usan la hora UTC."; @@ -5210,9 +5207,6 @@ /* snd group event chat item */ "you changed role of %@ to %@" = "has cambiado el rol de %1$@ a %2$@"; -/* No comment provided by engineer. */ -"You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." = "Tú controlas a través de qué servidor(es) **recibes** los mensajes. Tus contactos controlan a través de qué servidor(es) **envías** tus mensajes."; - /* No comment provided by engineer. */ "You could not be verified; please try again." = "No has podido ser autenticado. Inténtalo de nuevo."; diff --git a/apps/ios/fi.lproj/Localizable.strings b/apps/ios/fi.lproj/Localizable.strings index d1605152c0..2faab1dbd9 100644 --- a/apps/ios/fi.lproj/Localizable.strings +++ b/apps/ios/fi.lproj/Localizable.strings @@ -52,9 +52,6 @@ /* No comment provided by engineer. */ "[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[Tähti GitHubissa](https://github.com/simplex-chat/simplex-chat)"; -/* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Lisää uusi kontakti**: luo kertakäyttöinen QR-koodi tai linkki kontaktille."; - /* No comment provided by engineer. */ "**e2e encrypted** audio call" = "**e2e-salattu** äänipuhelu"; @@ -62,16 +59,16 @@ "**e2e encrypted** video call" = "**e2e-salattu** videopuhelu"; /* No comment provided by engineer. */ -"**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." = "**Yksityisempi**: tarkista uudet viestit 20 minuutin välein. Laitetunnus jaetaan SimpleX Chat -palvelimen kanssa, mutta ei sitä, kuinka monta yhteystietoa tai viestiä sinulla on."; +"**More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata." = "**Yksityisempi**: tarkista uudet viestit 20 minuutin välein. Laitetunnus jaetaan SimpleX Chat -palvelimen kanssa, mutta ei sitä, kuinka monta yhteystietoa tai viestiä sinulla on."; /* No comment provided by engineer. */ -"**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." = "**Yksityisin**: älä käytä SimpleX Chat -ilmoituspalvelinta, tarkista viestit ajoittain taustalla (riippuu siitä, kuinka usein käytät sovellusta)."; +"**Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app." = "**Yksityisin**: älä käytä SimpleX Chat -ilmoituspalvelinta, tarkista viestit ajoittain taustalla (riippuu siitä, kuinka usein käytät sovellusta)."; /* No comment provided by engineer. */ "**Please note**: you will NOT be able to recover or change passphrase if you lose it." = "**Huomaa**: et voi palauttaa tai muuttaa tunnuslausetta, jos kadotat sen."; /* No comment provided by engineer. */ -"**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." = "**Suositus**: laitetunnus ja ilmoitukset lähetetään SimpleX Chat -ilmoituspalvelimelle, mutta ei viestin sisältöä, kokoa tai sitä, keneltä se on peräisin."; +"**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from." = "**Suositus**: laitetunnus ja ilmoitukset lähetetään SimpleX Chat -ilmoituspalvelimelle, mutta ei viestin sisältöä, kokoa tai sitä, keneltä se on peräisin."; /* No comment provided by engineer. */ "**Warning**: Instant push notifications require passphrase saved in Keychain." = "**Varoitus**: Välittömät push-ilmoitukset vaativat tunnuslauseen, joka on tallennettu Keychainiin."; @@ -1678,7 +1675,7 @@ "Immediately" = "Heti"; /* No comment provided by engineer. */ -"Immune to spam and abuse" = "Immuuni roskapostille ja väärinkäytöksille"; +"Immune to spam" = "Immuuni roskapostille ja väärinkäytöksille"; /* No comment provided by engineer. */ "Import" = "Tuo"; @@ -1750,7 +1747,7 @@ "Instant push notifications will be hidden!\n" = "Välittömät push-ilmoitukset ovat piilossa!\n"; /* No comment provided by engineer. */ -"Instantly" = "Heti"; +"Instant" = "Heti"; /* No comment provided by engineer. */ "Interface" = "Käyttöliittymä"; @@ -1897,7 +1894,7 @@ "Live messages" = "Live-viestit"; /* No comment provided by engineer. */ -"Local" = "Paikallinen"; +"No push server" = "Paikallinen"; /* No comment provided by engineer. */ "Local name" = "Paikallinen nimi"; @@ -2187,7 +2184,7 @@ "Onion hosts will not be used." = "Onion-isäntiä ei käytetä."; /* No comment provided by engineer. */ -"Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." = "Vain asiakaslaitteet tallentavat käyttäjäprofiileja, yhteystietoja, ryhmiä ja viestejä, jotka on lähetetty **kaksinkertaisella päästä päähän -salauksella**."; +"Only client devices store user profiles, contacts, groups, and messages." = "Vain asiakaslaitteet tallentavat käyttäjäprofiileja, yhteystietoja, ryhmiä ja viestejä, jotka on lähetetty **kaksinkertaisella päästä päähän -salauksella**."; /* No comment provided by engineer. */ "Only group owners can change group preferences." = "Vain ryhmän omistajat voivat muuttaa ryhmän asetuksia."; @@ -2241,7 +2238,7 @@ "Open user profiles" = "Avaa käyttäjäprofiilit"; /* No comment provided by engineer. */ -"Open-source protocol and code – anybody can run the servers." = "Avoimen lähdekoodin protokolla ja koodi - kuka tahansa voi käyttää palvelimia."; +"Anybody can host servers." = "Avoimen lähdekoodin protokolla ja koodi - kuka tahansa voi käyttää palvelimia."; /* member role */ "owner" = "omistaja"; @@ -2271,10 +2268,10 @@ "peer-to-peer" = "vertais"; /* No comment provided by engineer. */ -"People can connect to you only via the links you share." = "Ihmiset voivat ottaa sinuun yhteyttä vain jakamiesi linkkien kautta."; +"You decide who can connect." = "Kimin bağlanabileceğine siz karar verirsiniz."; /* No comment provided by engineer. */ -"Periodically" = "Ajoittain"; +"Periodic" = "Ajoittain"; /* message decrypt error item */ "Permanent decryption error" = "Pysyvä salauksen purkuvirhe"; @@ -2412,7 +2409,7 @@ "Read more" = "Lue lisää"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." = "Lue lisää [Käyttöoppaasta](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)."; +"Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." = "Lue lisää [Käyttöoppaasta](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)."; /* No comment provided by engineer. */ "Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." = "Lue lisää [Käyttöoppaasta](https://simplex.chat/docs/guide/readme.html#connect-to-friends)."; @@ -2972,7 +2969,7 @@ "Thanks to the users – contribute via Weblate!" = "Kiitokset käyttäjille – osallistu Weblaten kautta!"; /* No comment provided by engineer. */ -"The 1st platform without any user identifiers – private by design." = "Ensimmäinen alusta ilman käyttäjätunnisteita – suunniteltu yksityiseksi."; +"No user identifiers." = "Ensimmäinen alusta ilman käyttäjätunnisteita – suunniteltu yksityiseksi."; /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "Sovellus voi ilmoittaa sinulle, kun saat viestejä tai yhteydenottopyyntöjä - avaa asetukset ottaaksesi ne käyttöön."; @@ -3005,7 +3002,7 @@ "The message will be marked as moderated for all members." = "Viesti merkitään moderoiduksi kaikille jäsenille."; /* No comment provided by engineer. */ -"The next generation of private messaging" = "Seuraavan sukupolven yksityisviestit"; +"The future of messaging" = "Seuraavan sukupolven yksityisviestit"; /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "Vanhaa tietokantaa ei poistettu siirron aikana, se voidaan kuitenkin poistaa."; @@ -3059,7 +3056,7 @@ "To make a new connection" = "Uuden yhteyden luominen"; /* No comment provided by engineer. */ -"To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." = "Yksityisyyden suojaamiseksi kaikkien muiden alustojen käyttämien käyttäjätunnusten sijaan SimpleX käyttää viestijonojen tunnisteita, jotka ovat kaikille kontakteille erillisiä."; +"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Yksityisyyden suojaamiseksi kaikkien muiden alustojen käyttämien käyttäjätunnusten sijaan SimpleX käyttää viestijonojen tunnisteita, jotka ovat kaikille kontakteille erillisiä."; /* No comment provided by engineer. */ "To protect timezone, image/voice files use UTC." = "Aikavyöhykkeen suojaamiseksi kuva-/äänitiedostot käyttävät UTC:tä."; @@ -3385,9 +3382,6 @@ /* snd group event chat item */ "you changed role of %@ to %@" = "olet vaihtanut %1$@:n roolin %2$@:ksi"; -/* No comment provided by engineer. */ -"You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." = "Sinä hallitset, minkä palvelim(i)en kautta **viestit vastaanotetaan**, kontaktisi - palvelimet, joita käytät viestien lähettämiseen niille."; - /* No comment provided by engineer. */ "You could not be verified; please try again." = "Sinua ei voitu todentaa; yritä uudelleen."; diff --git a/apps/ios/fr.lproj/Localizable.strings b/apps/ios/fr.lproj/Localizable.strings index 5d08240a52..92b66efb72 100644 --- a/apps/ios/fr.lproj/Localizable.strings +++ b/apps/ios/fr.lproj/Localizable.strings @@ -65,10 +65,7 @@ "[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[Star sur GitHub](https://github.com/simplex-chat/simplex-chat)"; /* No comment provided by engineer. */ -"**Add contact**: to create a new invitation link, or connect via a link you received." = "**Ajouter un contact** : pour créer un nouveau lien d'invitation ou vous connecter via un lien que vous avez reçu."; - -/* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Ajouter un nouveau contact** : pour créer un lien ou code QR unique pour votre contact."; +"**Create 1-time link**: to create and share a new invitation link." = "**Ajouter un contact** : pour créer un nouveau lien d'invitation."; /* No comment provided by engineer. */ "**Create group**: to create a new group." = "**Créer un groupe** : pour créer un nouveau groupe."; @@ -80,10 +77,10 @@ "**e2e encrypted** video call" = "appel vidéo **chiffré de bout en bout**"; /* No comment provided by engineer. */ -"**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." = "**Vie privée** : vérification de nouveaux messages toute les 20 minutes. Le token de l'appareil est partagé avec le serveur SimpleX, mais pas le nombre de messages ou de contacts."; +"**More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata." = "**Vie privée** : vérification de nouveaux messages toute les 20 minutes. Le token de l'appareil est partagé avec le serveur SimpleX, mais pas le nombre de messages ou de contacts."; /* No comment provided by engineer. */ -"**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." = "**Confidentiel** : ne pas utiliser le serveur de notifications SimpleX, vérification de nouveaux messages periodiquement en arrière plan (dépend de l'utilisation de l'app)."; +"**Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app." = "**Confidentiel** : ne pas utiliser le serveur de notifications SimpleX, vérification de nouveaux messages periodiquement en arrière plan (dépend de l'utilisation de l'app)."; /* No comment provided by engineer. */ "**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." = "**Remarque** : l'utilisation de la même base de données sur deux appareils interrompt le déchiffrement des messages provenant de vos connexions, par mesure de sécurité."; @@ -92,7 +89,7 @@ "**Please note**: you will NOT be able to recover or change passphrase if you lose it." = "**Veuillez noter** : vous NE pourrez PAS récupérer ou modifier votre phrase secrète si vous la perdez."; /* No comment provided by engineer. */ -"**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." = "**Recommandé** : le token de l'appareil et les notifications sont envoyés au serveur de notifications SimpleX, mais pas le contenu du message, sa taille ou son auteur."; +"**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from." = "**Recommandé** : le token de l'appareil et les notifications sont envoyés au serveur de notifications SimpleX, mais pas le contenu du message, sa taille ou son auteur."; /* No comment provided by engineer. */ "**Warning**: Instant push notifications require passphrase saved in Keychain." = "**Avertissement** : les notifications push instantanées nécessitent une phrase secrète enregistrée dans la keychain."; @@ -2387,7 +2384,7 @@ "Immediately" = "Immédiatement"; /* No comment provided by engineer. */ -"Immune to spam and abuse" = "Protégé du spam et des abus"; +"Immune to spam" = "Protégé du spam et des abus"; /* No comment provided by engineer. */ "Import" = "Importer"; @@ -2486,7 +2483,7 @@ "Instant push notifications will be hidden!\n" = "Les notifications push instantanées vont être cachées !\n"; /* No comment provided by engineer. */ -"Instantly" = "Instantané"; +"Instant" = "Instantané"; /* No comment provided by engineer. */ "Interface" = "Interface"; @@ -2693,7 +2690,7 @@ "Live messages" = "Messages dynamiques"; /* No comment provided by engineer. */ -"Local" = "Local"; +"No push server" = "No push server"; /* No comment provided by engineer. */ "Local name" = "Nom local"; @@ -3112,7 +3109,7 @@ "Onion hosts will not be used." = "Les hôtes .onion ne seront pas utilisés."; /* No comment provided by engineer. */ -"Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." = "Seuls les appareils clients stockent les profils des utilisateurs, les contacts, les groupes et les messages envoyés avec un **chiffrement de bout en bout à deux couches**."; +"Only client devices store user profiles, contacts, groups, and messages." = "Seuls les appareils clients stockent les profils des utilisateurs, les contacts, les groupes et les messages envoyés avec un **chiffrement de bout en bout à deux couches**."; /* No comment provided by engineer. */ "Only delete conversation" = "Ne supprimer que la conversation"; @@ -3181,7 +3178,7 @@ "Open user profiles" = "Ouvrir les profils d'utilisateurs"; /* No comment provided by engineer. */ -"Open-source protocol and code – anybody can run the servers." = "Protocole et code open-source – n'importe qui peut heberger un serveur."; +"Anybody can host servers." = "N\'importe qui peut heberger un serveur."; /* No comment provided by engineer. */ "Opening app…" = "Ouverture de l'app…"; @@ -3256,10 +3253,10 @@ "Pending" = "En attente"; /* No comment provided by engineer. */ -"People can connect to you only via the links you share." = "On ne peut se connecter à vous qu’avec les liens que vous partagez."; +"You decide who can connect." = "Vous choisissez qui peut se connecter."; /* No comment provided by engineer. */ -"Periodically" = "Périodique"; +"Periodic" = "Périodique"; /* message decrypt error item */ "Permanent decryption error" = "Erreur de déchiffrement"; @@ -3466,7 +3463,7 @@ "Read more" = "En savoir plus"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." = "Pour en savoir plus, consultez le [Guide de l'utilisateur](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)."; +"Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." = "Pour en savoir plus, consultez le [Guide de l'utilisateur](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)."; /* No comment provided by engineer. */ "Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." = "Pour en savoir plus, consultez le [Guide de l'utilisateur](https ://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)."; @@ -4335,7 +4332,7 @@ "Thanks to the users – contribute via Weblate!" = "Merci aux utilisateurs - contribuez via Weblate !"; /* No comment provided by engineer. */ -"The 1st platform without any user identifiers – private by design." = "La 1ère plateforme sans aucun identifiant d'utilisateur – privée par design."; +"No user identifiers." = "Aucun identifiant d\'utilisateur."; /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "L'application peut vous avertir lorsque vous recevez des messages ou des demandes de contact - veuillez ouvrir les paramètres pour les activer."; @@ -4380,7 +4377,7 @@ "The messages will be marked as moderated for all members." = "Les messages seront marqués comme modérés pour tous les membres."; /* No comment provided by engineer. */ -"The next generation of private messaging" = "La nouvelle génération de messagerie privée"; +"The future of messaging" = "La nouvelle génération de messagerie privée"; /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "L'ancienne base de données n'a pas été supprimée lors de la migration, elle peut être supprimée."; @@ -4467,7 +4464,7 @@ "To make a new connection" = "Pour établir une nouvelle connexion"; /* No comment provided by engineer. */ -"To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." = "Pour protéger votre vie privée, au lieu d’IDs utilisés par toutes les autres plateformes, SimpleX a des IDs pour les queues de messages, distinctes pour chacun de vos contacts."; +"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Pour protéger votre vie privée, au lieu d’IDs utilisés par toutes les autres plateformes, SimpleX a des IDs pour les queues de messages, distinctes pour chacun de vos contacts."; /* No comment provided by engineer. */ "To protect timezone, image/voice files use UTC." = "Pour préserver le fuseau horaire, les fichiers image/voix utilisent le système UTC."; @@ -5030,9 +5027,6 @@ /* snd group event chat item */ "you changed role of %@ to %@" = "vous avez modifié le rôle de %1$@ pour %2$@"; -/* No comment provided by engineer. */ -"You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." = "Vous contrôlez par quel·s serveur·s vous pouvez **transmettre** ainsi que par quel·s serveur·s vous pouvez **recevoir** les messages de vos contacts."; - /* No comment provided by engineer. */ "You could not be verified; please try again." = "Vous n'avez pas pu être vérifié·e ; veuillez réessayer."; diff --git a/apps/ios/hu.lproj/Localizable.strings b/apps/ios/hu.lproj/Localizable.strings index c707f72bf6..5668b5367f 100644 --- a/apps/ios/hu.lproj/Localizable.strings +++ b/apps/ios/hu.lproj/Localizable.strings @@ -65,10 +65,7 @@ "[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[Csillagozás a GitHubon](https://github.com/simplex-chat/simplex-chat)"; /* No comment provided by engineer. */ -"**Add contact**: to create a new invitation link, or connect via a link you received." = "**Ismerős hozzáadása:** új meghívó-hivatkozás létrehozásához, vagy egy kapott hivatkozáson keresztül történő kapcsolódáshoz."; - -/* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Új ismerős hozzáadása:** egyszer használható QR-kód vagy hivatkozás létrehozása az ismerőse számára."; +"**Create 1-time link**: to create and share a new invitation link." = "**Ismerős hozzáadása:** új meghívó-hivatkozás létrehozásához, vagy egy kapott hivatkozáson keresztül történő kapcsolódáshoz."; /* No comment provided by engineer. */ "**Create group**: to create a new group." = "**Csoport létrehozása:** új csoport létrehozásához."; @@ -80,10 +77,10 @@ "**e2e encrypted** video call" = "**e2e titkosított** videóhívás"; /* No comment provided by engineer. */ -"**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." = "**Privátabb:** 20 percenként ellenőrzi az új üzeneteket. Az eszköztoken megosztásra kerül a SimpleX Chat-kiszolgálóval, de az nem, hogy hány ismerőse vagy üzenete van."; +"**More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata." = "**Privátabb:** 20 percenként ellenőrzi az új üzeneteket. Az eszköztoken megosztásra kerül a SimpleX Chat-kiszolgálóval, de az nem, hogy hány ismerőse vagy üzenete van."; /* No comment provided by engineer. */ -"**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." = "**Legprivátabb:** ne használja a SimpleX Chat értesítési kiszolgálót, rendszeresen ellenőrizze az üzeneteket a háttérben (attól függően, hogy milyen gyakran használja az alkalmazást)."; +"**Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app." = "**Legprivátabb:** ne használja a SimpleX Chat értesítési kiszolgálót, rendszeresen ellenőrizze az üzeneteket a háttérben (attól függően, hogy milyen gyakran használja az alkalmazást)."; /* No comment provided by engineer. */ "**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." = "**Megjegyzés:** ha két eszközön is ugyanazt az adatbázist használja, akkor biztonsági védelemként megszakítja az ismerőseitől érkező üzenetek visszafejtését."; @@ -92,7 +89,7 @@ "**Please note**: you will NOT be able to recover or change passphrase if you lose it." = "**Megjegyzés:** NEM tudja visszaállítani vagy megváltoztatni jelmondatát, ha elveszíti azt."; /* No comment provided by engineer. */ -"**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." = "**Megjegyzés:** az eszköztoken és az értesítések elküldésre kerülnek a SimpleX Chat értesítési kiszolgálóra, kivéve az üzenet tartalma, mérete vagy az, hogy kitől származik."; +"**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from." = "**Megjegyzés:** az eszköztoken és az értesítések elküldésre kerülnek a SimpleX Chat értesítési kiszolgálóra, kivéve az üzenet tartalma, mérete vagy az, hogy kitől származik."; /* No comment provided by engineer. */ "**Warning**: Instant push notifications require passphrase saved in Keychain." = "**Figyelmeztetés:** Az azonnali push-értesítésekhez a kulcstartóban tárolt jelmondat megadása szükséges."; @@ -2474,7 +2471,7 @@ "Immediately" = "Azonnal"; /* No comment provided by engineer. */ -"Immune to spam and abuse" = "Spam és visszaélések elleni védelem"; +"Immune to spam" = "Spam és visszaélések elleni védelem"; /* No comment provided by engineer. */ "Import" = "Importálás"; @@ -2576,7 +2573,7 @@ "Instant push notifications will be hidden!\n" = "Az azonnali push-értesítések elrejtésre kerülnek!\n"; /* No comment provided by engineer. */ -"Instantly" = "Azonnal"; +"Instant" = "Azonnal"; /* No comment provided by engineer. */ "Interface" = "Felület"; @@ -2786,7 +2783,7 @@ "Live messages" = "Élő üzenetek"; /* No comment provided by engineer. */ -"Local" = "Helyi"; +"No push server" = "Helyi"; /* No comment provided by engineer. */ "Local name" = "Helyi név"; @@ -3226,7 +3223,7 @@ "Onion hosts will not be used." = "Onion-kiszolgálók nem lesznek használva."; /* No comment provided by engineer. */ -"Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." = "Csak az eszközök alkalmazásai tárolják a felhasználó-profilokat, névjegyeket, csoportokat és a **2 rétegű végpontok közötti titkosítással** küldött üzeneteket."; +"Only client devices store user profiles, contacts, groups, and messages." = "Csak az eszközök alkalmazásai tárolják a felhasználó-profilokat, névjegyeket, csoportokat és a **2 rétegű végpontok közötti titkosítással** küldött üzeneteket."; /* No comment provided by engineer. */ "Only delete conversation" = "Csak a beszélgetés törlése"; @@ -3295,7 +3292,7 @@ "Open user profiles" = "Felhasználó-profilok megnyitása"; /* No comment provided by engineer. */ -"Open-source protocol and code – anybody can run the servers." = "Nyílt forráskódú protokoll és forráskód – bárki üzemeltethet kiszolgálókat."; +"Anybody can host servers." = "Bárki üzemeltethet kiszolgálókat."; /* No comment provided by engineer. */ "Opening app…" = "Az alkalmazás megnyitása…"; @@ -3376,10 +3373,10 @@ "Pending" = "Függőben"; /* No comment provided by engineer. */ -"People can connect to you only via the links you share." = "Az emberek csak az Ön által megosztott hivatkozáson keresztül kapcsolódhatnak."; +"You decide who can connect." = "Ön dönti el, hogy kivel beszélget."; /* No comment provided by engineer. */ -"Periodically" = "Rendszeresen"; +"Periodic" = "Rendszeresen"; /* message decrypt error item */ "Permanent decryption error" = "Végleges visszafejtési hiba"; @@ -3592,7 +3589,7 @@ "Read more" = "Tudjon meg többet"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." = "További információ a [Használati útmutatóban](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)."; +"Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." = "További információ a [Használati útmutatóban](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)."; /* No comment provided by engineer. */ "Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." = "További információ a [Használati útmutatóban](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)."; @@ -4500,7 +4497,7 @@ "Thanks to the users – contribute via Weblate!" = "Köszönet a felhasználóknak - hozzájárulás a Weblate-en!"; /* No comment provided by engineer. */ -"The 1st platform without any user identifiers – private by design." = "Az első csevegési rendszer bármiféle felhasználó-azonosító nélkül - privátra lett tervezre."; +"No user identifiers." = "Nincsenek felhasználó-azonosítók."; /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "Az alkalmazás értesíteni fogja, amikor üzeneteket vagy kapcsolatkéréseket kap – beállítások megnyitása az engedélyezéshez."; @@ -4545,7 +4542,7 @@ "The messages will be marked as moderated for all members." = "Az üzenetek moderáltként lesznek megjelölve minden tag számára."; /* No comment provided by engineer. */ -"The next generation of private messaging" = "A privát üzenetküldés következő generációja"; +"The future of messaging" = "A privát üzenetküldés következő generációja"; /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "A régi adatbázis nem került eltávolításra az átköltöztetéskor, így törölhető."; @@ -4635,7 +4632,7 @@ "To make a new connection" = "Új kapcsolat létrehozásához"; /* No comment provided by engineer. */ -"To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." = "Az adatvédelem érdekében, a más csevegési platformokon megszokott felhasználó-azonosítók helyett, a SimpleX csak az üzenetek sorbaállításához használ azonosítókat, minden egyes ismerőshöz egy-egy különbözőt."; +"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Az adatvédelem érdekében, a más csevegési platformokon megszokott felhasználó-azonosítók helyett, a SimpleX csak az üzenetek sorbaállításához használ azonosítókat, minden egyes ismerőshöz egy-egy különbözőt."; /* No comment provided by engineer. */ "To protect timezone, image/voice files use UTC." = "Az időzóna védelme érdekében a kép-/hangfájlok UTC-t használnak."; @@ -5210,9 +5207,6 @@ /* snd group event chat item */ "you changed role of %@ to %@" = "Ön megváltoztatta %1$@ szerepkörét erre: %@"; -/* No comment provided by engineer. */ -"You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." = "Ön szabályozhatja, hogy mely kiszogál(ók)ón keresztül **kapja** az üzeneteket, az ismerősöket - az üzenetküldéshez használt kiszolgálókon."; - /* No comment provided by engineer. */ "You could not be verified; please try again." = "Nem sikerült hitelesíteni; próbálja meg újra."; diff --git a/apps/ios/it.lproj/Localizable.strings b/apps/ios/it.lproj/Localizable.strings index 308ff5d18e..2e2aea3f3c 100644 --- a/apps/ios/it.lproj/Localizable.strings +++ b/apps/ios/it.lproj/Localizable.strings @@ -65,10 +65,7 @@ "[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[Dai una stella su GitHub](https://github.com/simplex-chat/simplex-chat)"; /* No comment provided by engineer. */ -"**Add contact**: to create a new invitation link, or connect via a link you received." = "**Aggiungi contatto**: per creare un nuovo link di invito o connetterti tramite un link che hai ricevuto."; - -/* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Aggiungi un contatto**: per creare il tuo codice QR o link una tantum per il tuo contatto."; +"**Create 1-time link**: to create and share a new invitation link." = "**Aggiungi contatto**: per creare un nuovo link di invito."; /* No comment provided by engineer. */ "**Create group**: to create a new group." = "**Crea gruppo**: per creare un nuovo gruppo."; @@ -80,10 +77,10 @@ "**e2e encrypted** video call" = "Videochiamata **crittografata e2e**"; /* No comment provided by engineer. */ -"**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." = "**Più privato**: controlla messaggi nuovi ogni 20 minuti. Viene condiviso il token del dispositivo con il server di SimpleX Chat, ma non quanti contatti o messaggi hai."; +"**More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata." = "**Più privato**: controlla messaggi nuovi ogni 20 minuti. Viene condiviso il token del dispositivo con il server di SimpleX Chat, ma non quanti contatti o messaggi hai."; /* No comment provided by engineer. */ -"**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." = "**Il più privato**: non usare il server di notifica di SimpleX Chat, controlla i messaggi periodicamente in secondo piano (dipende da quanto spesso usi l'app)."; +"**Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app." = "**Il più privato**: non usare il server di notifica di SimpleX Chat, controlla i messaggi periodicamente in secondo piano (dipende da quanto spesso usi l'app)."; /* No comment provided by engineer. */ "**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." = "**Nota bene**: usare lo stesso database su due dispositivi bloccherà la decifrazione dei messaggi dalle tue connessioni, come misura di sicurezza."; @@ -92,7 +89,7 @@ "**Please note**: you will NOT be able to recover or change passphrase if you lose it." = "**Nota bene**: NON potrai recuperare o cambiare la password se la perdi."; /* No comment provided by engineer. */ -"**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." = "**Consigliato**: vengono inviati il token del dispositivo e le notifiche al server di notifica di SimpleX Chat, ma non il contenuto del messaggio,la sua dimensione o il suo mittente."; +"**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from." = "**Consigliato**: vengono inviati il token del dispositivo e le notifiche al server di notifica di SimpleX Chat, ma non il contenuto del messaggio,la sua dimensione o il suo mittente."; /* No comment provided by engineer. */ "**Warning**: Instant push notifications require passphrase saved in Keychain." = "**Attenzione**: le notifiche push istantanee richiedono una password salvata nel portachiavi."; @@ -2474,7 +2471,7 @@ "Immediately" = "Immediatamente"; /* No comment provided by engineer. */ -"Immune to spam and abuse" = "Immune a spam e abusi"; +"Immune to spam" = "Immune a spam e abusi"; /* No comment provided by engineer. */ "Import" = "Importa"; @@ -2576,7 +2573,7 @@ "Instant push notifications will be hidden!\n" = "Le notifiche push istantanee saranno nascoste!\n"; /* No comment provided by engineer. */ -"Instantly" = "Istantaneamente"; +"Instant" = "Istantaneamente"; /* No comment provided by engineer. */ "Interface" = "Interfaccia"; @@ -2786,7 +2783,7 @@ "Live messages" = "Messaggi in diretta"; /* No comment provided by engineer. */ -"Local" = "Locale"; +"No push server" = "Locale"; /* No comment provided by engineer. */ "Local name" = "Nome locale"; @@ -3226,7 +3223,7 @@ "Onion hosts will not be used." = "Gli host Onion non verranno usati."; /* No comment provided by engineer. */ -"Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." = "Solo i dispositivi client archiviano profili utente, i contatti, i gruppi e i messaggi inviati con la **crittografia end-to-end a 2 livelli**."; +"Only client devices store user profiles, contacts, groups, and messages." = "Solo i dispositivi client archiviano profili utente, i contatti, i gruppi e i messaggi inviati con la **crittografia end-to-end a 2 livelli**."; /* No comment provided by engineer. */ "Only delete conversation" = "Elimina solo la conversazione"; @@ -3295,7 +3292,7 @@ "Open user profiles" = "Apri i profili utente"; /* No comment provided by engineer. */ -"Open-source protocol and code – anybody can run the servers." = "Protocollo e codice open source: chiunque può gestire i server."; +"Anybody can host servers." = "Chiunque può installare i server."; /* No comment provided by engineer. */ "Opening app…" = "Apertura dell'app…"; @@ -3376,10 +3373,10 @@ "Pending" = "In attesa"; /* No comment provided by engineer. */ -"People can connect to you only via the links you share." = "Le persone possono connettersi a te solo tramite i link che condividi."; +"You decide who can connect." = "Sei tu a decidere chi può connettersi."; /* No comment provided by engineer. */ -"Periodically" = "Periodicamente"; +"Periodic" = "Periodicamente"; /* message decrypt error item */ "Permanent decryption error" = "Errore di decifrazione"; @@ -3592,7 +3589,7 @@ "Read more" = "Leggi tutto"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." = "Maggiori informazioni nella [Guida per l'utente](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)."; +"Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." = "Maggiori informazioni nella [Guida per l'utente](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)."; /* No comment provided by engineer. */ "Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." = "Leggi di più nella [Guida utente](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)."; @@ -4500,7 +4497,7 @@ "Thanks to the users – contribute via Weblate!" = "Grazie agli utenti – contribuite via Weblate!"; /* No comment provided by engineer. */ -"The 1st platform without any user identifiers – private by design." = "La prima piattaforma senza alcun identificatore utente – privata by design."; +"No user identifiers." = "Nessun identificatore utente."; /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "L'app può avvisarti quando ricevi messaggi o richieste di contatto: apri le impostazioni per attivare."; @@ -4545,7 +4542,7 @@ "The messages will be marked as moderated for all members." = "I messaggi verranno contrassegnati come moderati per tutti i membri."; /* No comment provided by engineer. */ -"The next generation of private messaging" = "La nuova generazione di messaggistica privata"; +"The future of messaging" = "La nuova generazione di messaggistica privata"; /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "Il database vecchio non è stato rimosso durante la migrazione, può essere eliminato."; @@ -4635,7 +4632,7 @@ "To make a new connection" = "Per creare una nuova connessione"; /* No comment provided by engineer. */ -"To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." = "Per proteggere la privacy, invece degli ID utente utilizzati da tutte le altre piattaforme, SimpleX ha identificatori per le code di messaggi, separati per ciascuno dei tuoi contatti."; +"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Per proteggere la privacy, invece degli ID utente utilizzati da tutte le altre piattaforme, SimpleX ha identificatori per le code di messaggi, separati per ciascuno dei tuoi contatti."; /* No comment provided by engineer. */ "To protect timezone, image/voice files use UTC." = "Per proteggere il fuso orario, i file immagine/vocali usano UTC."; @@ -5210,9 +5207,6 @@ /* snd group event chat item */ "you changed role of %@ to %@" = "hai cambiato il ruolo di %1$@ in %2$@"; -/* No comment provided by engineer. */ -"You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." = "Tu decidi attraverso quale/i server **ricevere** i messaggi, i tuoi contatti quali server usi per inviare loro i messaggi."; - /* No comment provided by engineer. */ "You could not be verified; please try again." = "Non è stato possibile verificarti, riprova."; diff --git a/apps/ios/ja.lproj/Localizable.strings b/apps/ios/ja.lproj/Localizable.strings index 20c4819d87..c2f2717b1b 100644 --- a/apps/ios/ja.lproj/Localizable.strings +++ b/apps/ios/ja.lproj/Localizable.strings @@ -59,10 +59,7 @@ "[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[GitHub でスターを付ける](https://github.com/simplex-chat/simplex-chat)"; /* No comment provided by engineer. */ -"**Add contact**: to create a new invitation link, or connect via a link you received." = "**コンタクトの追加**: 新しい招待リンクを作成するか、受け取ったリンクから接続します。"; - -/* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**新しい連絡先を追加**: 連絡先のワンタイム QR コードまたはリンクを作成します。"; +"**Create 1-time link**: to create and share a new invitation link." = "**コンタクトの追加**: 新しい招待リンクを作成するか、受け取ったリンクから接続します。"; /* No comment provided by engineer. */ "**Create group**: to create a new group." = "**グループ作成**: 新しいグループを作成する。"; @@ -74,10 +71,10 @@ "**e2e encrypted** video call" = "**エンドツーエンド暗号化済み**の テレビ電話 通話"; /* No comment provided by engineer. */ -"**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." = "**よりプライベート**: 20 分ごとに新しいメッセージを確認します。 デバイス トークンは SimpleX Chat サーバーと共有されますが、連絡先やメッセージの数は共有されません。"; +"**More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata." = "**よりプライベート**: 20 分ごとに新しいメッセージを確認します。 デバイス トークンは SimpleX Chat サーバーと共有されますが、連絡先やメッセージの数は共有されません。"; /* No comment provided by engineer. */ -"**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." = "**最もプライベート**: SimpleX Chat 通知サーバーを使用せず、バックグラウンドで定期的にメッセージをチェックします (アプリの使用頻度によって異なります)。"; +"**Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app." = "**最もプライベート**: SimpleX Chat 通知サーバーを使用せず、バックグラウンドで定期的にメッセージをチェックします (アプリの使用頻度によって異なります)。"; /* No comment provided by engineer. */ "**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." = "**注意**: 2つの端末で同じデータベースを使用すると、セキュリティ保護として、あなたが接続しているメッセージの復号化が解除されます。"; @@ -86,7 +83,7 @@ "**Please note**: you will NOT be able to recover or change passphrase if you lose it." = "**注意**: パスフレーズを紛失すると、パスフレーズを復元または変更できなくなります。"; /* No comment provided by engineer. */ -"**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." = "**推奨**: デバイス トークンと通知は SimpleX Chat 通知サーバーに送信されますが、メッセージの内容、サイズ、送信者は送信されません。"; +"**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from." = "**推奨**: デバイス トークンと通知は SimpleX Chat 通知サーバーに送信されますが、メッセージの内容、サイズ、送信者は送信されません。"; /* No comment provided by engineer. */ "**Warning**: Instant push notifications require passphrase saved in Keychain." = "**警告**: 即時の プッシュ通知には、キーチェーンに保存されたパスフレーズが必要です。"; @@ -1753,7 +1750,7 @@ "Immediately" = "即座に"; /* No comment provided by engineer. */ -"Immune to spam and abuse" = "スパムや悪質送信を防止"; +"Immune to spam" = "スパムや悪質送信を防止"; /* No comment provided by engineer. */ "Import" = "読み込む"; @@ -1825,7 +1822,7 @@ "Instant push notifications will be hidden!\n" = "インスタントプッシュ通知は非表示になります!\n"; /* No comment provided by engineer. */ -"Instantly" = "すぐに"; +"Instant" = "すぐに"; /* No comment provided by engineer. */ "Interface" = "インターフェース"; @@ -1972,7 +1969,7 @@ "Live messages" = "ライブメッセージ"; /* No comment provided by engineer. */ -"Local" = "自分のみ"; +"No push server" = "自分のみ"; /* No comment provided by engineer. */ "Local name" = "ローカルネーム"; @@ -2268,7 +2265,7 @@ "Onion hosts will not be used." = "オニオンのホストが使われません。"; /* No comment provided by engineer. */ -"Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." = "**2 レイヤーのエンドツーエンド暗号化**を使用して送信されたユーザー プロファイル、連絡先、グループ、メッセージを保存できるのはクライアント デバイスのみです。"; +"Only client devices store user profiles, contacts, groups, and messages." = "**2 レイヤーのエンドツーエンド暗号化**を使用して送信されたユーザー プロファイル、連絡先、グループ、メッセージを保存できるのはクライアント デバイスのみです。"; /* No comment provided by engineer. */ "Only group owners can change group preferences." = "グループ設定を変えられるのはグループのオーナーだけです。"; @@ -2325,7 +2322,7 @@ "Open user profiles" = "ユーザープロフィールを開く"; /* No comment provided by engineer. */ -"Open-source protocol and code – anybody can run the servers." = "プロトコル技術とコードはオープンソースで、どなたでもご自分のサーバを運用できます。"; +"Anybody can host servers." = "プロトコル技術とコードはオープンソースで、どなたでもご自分のサーバを運用できます。"; /* member role */ "owner" = "オーナー"; @@ -2355,10 +2352,10 @@ "peer-to-peer" = "P2P"; /* No comment provided by engineer. */ -"People can connect to you only via the links you share." = "あなたと繋がることができるのは、あなたからリンクを頂いた方のみです。"; +"You decide who can connect." = "あなたと繋がることができるのは、あなたからリンクを頂いた方のみです。"; /* No comment provided by engineer. */ -"Periodically" = "定期的に"; +"Periodic" = "定期的に"; /* message decrypt error item */ "Permanent decryption error" = "永続的な復号化エラー"; @@ -2496,13 +2493,13 @@ "Read more" = "続きを読む"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." = "詳しくは[ユーザーガイド](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)をご覧ください。"; +"Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." = "詳しくは[ユーザーガイド](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)をご覧ください。"; /* No comment provided by engineer. */ "Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." = "詳しくは[ユーザーガイド](https://simplex.chat/docs/guide/readme.html#connect-to-friends)をご覧ください。"; /* No comment provided by engineer. */ -"Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." = "詳しくは[GitHubリポジトリ](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)をご覧ください。"; +"Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." = "詳しくは[GitHubリポジトリ](https://github.com/simplex-chat/simplex-chat#readme)をご覧ください。"; /* No comment provided by engineer. */ "Read more in our GitHub repository." = "GitHubリポジトリで詳細をご確認ください。"; @@ -3035,7 +3032,7 @@ "Thanks to the users – contribute via Weblate!" = "ユーザーに感謝します – Weblate 経由で貢献してください!"; /* No comment provided by engineer. */ -"The 1st platform without any user identifiers – private by design." = "世界初のユーザーIDのないプラットフォーム|設計も元からプライベート。"; +"No user identifiers." = "世界初のユーザーIDのないプラットフォーム|設計も元からプライベート。"; /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "アプリは、メッセージや連絡先のリクエストを受信したときに通知することができます - 設定を開いて有効にしてください。"; @@ -3068,7 +3065,7 @@ "The message will be marked as moderated for all members." = "メッセージは、すべてのメンバーに対してモデレートされたものとして表示されます。"; /* No comment provided by engineer. */ -"The next generation of private messaging" = "次世代のプライバシー・メッセンジャー"; +"The future of messaging" = "次世代のプライバシー・メッセンジャー"; /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "古いデータベースは移行時に削除されなかったので、削除することができます。"; @@ -3119,7 +3116,7 @@ "To make a new connection" = "新規に接続する場合"; /* No comment provided by engineer. */ -"To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." = "プライバシーを保護するために、SimpleX には、他のすべてのプラットフォームで使用されるユーザー ID の代わりに、連絡先ごとに個別のメッセージ キューの識別子があります。"; +"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "プライバシーを保護するために、SimpleX には、他のすべてのプラットフォームで使用されるユーザー ID の代わりに、連絡先ごとに個別のメッセージ キューの識別子があります。"; /* No comment provided by engineer. */ "To protect timezone, image/voice files use UTC." = "時間帯を漏らさないために、画像と音声ファイルはUTCを使います。"; @@ -3445,9 +3442,6 @@ /* snd group event chat item */ "you changed role of %@ to %@" = "%1$@ の役割を %2$@ に変更しました"; -/* No comment provided by engineer. */ -"You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." = "あなたはメッセージの受信に使用するサーバーを制御し、連絡先はあなたがメッセージの送信に使用するサーバーを使用することができます。"; - /* No comment provided by engineer. */ "You could not be verified; please try again." = "確認できませんでした。 もう一度お試しください。"; diff --git a/apps/ios/nl.lproj/Localizable.strings b/apps/ios/nl.lproj/Localizable.strings index b9caba8463..e6320e6208 100644 --- a/apps/ios/nl.lproj/Localizable.strings +++ b/apps/ios/nl.lproj/Localizable.strings @@ -65,10 +65,7 @@ "[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[Star on GitHub](https://github.com/simplex-chat/simplex-chat)"; /* No comment provided by engineer. */ -"**Add contact**: to create a new invitation link, or connect via a link you received." = "**Contact toevoegen**: om een nieuwe uitnodigingslink aan te maken, of verbinding te maken via een link die u heeft ontvangen."; - -/* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Nieuw contact toevoegen**: om uw eenmalige QR-code of link voor uw contact te maken."; +"**Create 1-time link**: to create and share a new invitation link." = "**Contact toevoegen**: om een nieuwe uitnodigingslink aan te maken, of verbinding te maken via een link die u heeft ontvangen."; /* No comment provided by engineer. */ "**Create group**: to create a new group." = "**Groep aanmaken**: om een nieuwe groep aan te maken."; @@ -80,10 +77,10 @@ "**e2e encrypted** video call" = "**e2e versleuteld** video gesprek"; /* No comment provided by engineer. */ -"**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." = "**Meer privé**: bekijk elke 20 minuten nieuwe berichten. Apparaattoken wordt gedeeld met de SimpleX Chat-server, maar niet hoeveel contacten of berichten u heeft."; +"**More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata." = "**Meer privé**: bekijk elke 20 minuten nieuwe berichten. Apparaattoken wordt gedeeld met de SimpleX Chat-server, maar niet hoeveel contacten of berichten u heeft."; /* No comment provided by engineer. */ -"**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." = "**Meest privé**: gebruik geen SimpleX Chat-notificatie server, controleer berichten regelmatig op de achtergrond (afhankelijk van hoe vaak u de app gebruikt)."; +"**Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app." = "**Meest privé**: gebruik geen SimpleX Chat-notificatie server, controleer berichten regelmatig op de achtergrond (afhankelijk van hoe vaak u de app gebruikt)."; /* No comment provided by engineer. */ "**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." = "**Let op**: als u dezelfde database op twee apparaten gebruikt, wordt de decodering van berichten van uw verbindingen verbroken, als veiligheidsmaatregel."; @@ -92,7 +89,7 @@ "**Please note**: you will NOT be able to recover or change passphrase if you lose it." = "**Let op**: u kunt het wachtwoord NIET herstellen of wijzigen als u het kwijtraakt."; /* No comment provided by engineer. */ -"**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." = "**Aanbevolen**: apparaattoken en meldingen worden naar de SimpleX Chat-meldingsserver gestuurd, maar niet de berichtinhoud, -grootte of van wie het afkomstig is."; +"**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from." = "**Aanbevolen**: apparaattoken en meldingen worden naar de SimpleX Chat-meldingsserver gestuurd, maar niet de berichtinhoud, -grootte of van wie het afkomstig is."; /* No comment provided by engineer. */ "**Warning**: Instant push notifications require passphrase saved in Keychain." = "**Waarschuwing**: voor directe push meldingen is een wachtwoord vereist dat is opgeslagen in de Keychain."; @@ -2474,7 +2471,7 @@ "Immediately" = "Onmiddellijk"; /* No comment provided by engineer. */ -"Immune to spam and abuse" = "Immuun voor spam en misbruik"; +"Immune to spam" = "Immuun voor spam en misbruik"; /* No comment provided by engineer. */ "Import" = "Importeren"; @@ -2576,7 +2573,7 @@ "Instant push notifications will be hidden!\n" = "Directe push meldingen worden verborgen!\n"; /* No comment provided by engineer. */ -"Instantly" = "Direct"; +"Instant" = "Direct"; /* No comment provided by engineer. */ "Interface" = "Interface"; @@ -2786,7 +2783,7 @@ "Live messages" = "Live berichten"; /* No comment provided by engineer. */ -"Local" = "Lokaal"; +"No push server" = "Lokaal"; /* No comment provided by engineer. */ "Local name" = "Lokale naam"; @@ -3226,7 +3223,7 @@ "Onion hosts will not be used." = "Onion hosts worden niet gebruikt."; /* No comment provided by engineer. */ -"Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." = "Alleen client apparaten slaan gebruikers profielen, contacten, groepen en berichten op die zijn verzonden met **2-laags end-to-end-codering**."; +"Only client devices store user profiles, contacts, groups, and messages." = "Alleen client apparaten slaan gebruikers profielen, contacten, groepen en berichten op die zijn verzonden met **2-laags end-to-end-codering**."; /* No comment provided by engineer. */ "Only delete conversation" = "Alleen conversatie verwijderen"; @@ -3295,7 +3292,7 @@ "Open user profiles" = "Gebruikers profielen openen"; /* No comment provided by engineer. */ -"Open-source protocol and code – anybody can run the servers." = "Open-source protocol en code. Iedereen kan de servers draaien."; +"Anybody can host servers." = "Iedereen kan servers hosten."; /* No comment provided by engineer. */ "Opening app…" = "App openen…"; @@ -3376,10 +3373,10 @@ "Pending" = "in behandeling"; /* No comment provided by engineer. */ -"People can connect to you only via the links you share." = "Mensen kunnen alleen verbinding met u maken via de links die u deelt."; +"You decide who can connect." = "Jij bepaalt wie er verbinding mag maken."; /* No comment provided by engineer. */ -"Periodically" = "Periodiek"; +"Periodic" = "Periodiek"; /* message decrypt error item */ "Permanent decryption error" = "Decodering fout"; @@ -3592,7 +3589,7 @@ "Read more" = "Lees meer"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." = "Lees meer in de [Gebruikershandleiding](https://simplex.chat/docs/guide/app-settings.html#uw-simplex-contactadres)."; +"Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." = "Lees meer in de [Gebruikershandleiding](https://simplex.chat/docs/guide/app-settings.html#uw-simplex-contactadres)."; /* No comment provided by engineer. */ "Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." = "Lees meer in de [Gebruikershandleiding](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)."; @@ -4500,7 +4497,7 @@ "Thanks to the users – contribute via Weblate!" = "Dank aan de gebruikers – draag bij via Weblate!"; /* No comment provided by engineer. */ -"The 1st platform without any user identifiers – private by design." = "Het eerste platform zonder gebruikers-ID's, privé door ontwerp."; +"No user identifiers." = "Geen gebruikers-ID\'s."; /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "De app kan u op de hoogte stellen wanneer u berichten of contact verzoeken ontvangt - open de instellingen om dit in te schakelen."; @@ -4545,7 +4542,7 @@ "The messages will be marked as moderated for all members." = "De berichten worden voor alle leden als gemodereerd gemarkeerd."; /* No comment provided by engineer. */ -"The next generation of private messaging" = "De volgende generatie privéberichten"; +"The future of messaging" = "De volgende generatie privéberichten"; /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "De oude database is niet verwijderd tijdens de migratie, deze kan worden verwijderd."; @@ -4635,7 +4632,7 @@ "To make a new connection" = "Om een nieuwe verbinding te maken"; /* No comment provided by engineer. */ -"To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." = "Om de privacy te beschermen, heeft SimpleX in plaats van gebruikers-ID's die door alle andere platforms worden gebruikt, ID's voor berichten wachtrijen, afzonderlijk voor elk van uw contacten."; +"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Om de privacy te beschermen, heeft SimpleX in plaats van gebruikers-ID's die door alle andere platforms worden gebruikt, ID's voor berichten wachtrijen, afzonderlijk voor elk van uw contacten."; /* No comment provided by engineer. */ "To protect timezone, image/voice files use UTC." = "Om de tijdzone te beschermen, gebruiken afbeeldings-/spraakbestanden UTC."; @@ -5210,9 +5207,6 @@ /* snd group event chat item */ "you changed role of %@ to %@" = "je veranderde de rol van %1$@ in %2$@"; -/* No comment provided by engineer. */ -"You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." = "U bepaalt via welke server(s) de berichten **ontvangen**, uw contacten de servers die u gebruikt om ze berichten te sturen."; - /* No comment provided by engineer. */ "You could not be verified; please try again." = "U kon niet worden geverifieerd; probeer het opnieuw."; diff --git a/apps/ios/pl.lproj/Localizable.strings b/apps/ios/pl.lproj/Localizable.strings index b8883ac092..2dde086020 100644 --- a/apps/ios/pl.lproj/Localizable.strings +++ b/apps/ios/pl.lproj/Localizable.strings @@ -65,10 +65,7 @@ "[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[Daj gwiazdkę na GitHub](https://github.com/simplex-chat/simplex-chat)"; /* No comment provided by engineer. */ -"**Add contact**: to create a new invitation link, or connect via a link you received." = "**Dodaj kontakt**: aby utworzyć nowy link z zaproszeniem lub połączyć się za pomocą otrzymanego linku."; - -/* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Dodaj nowy kontakt**: aby stworzyć swój jednorazowy kod QR lub link dla kontaktu."; +"**Create 1-time link**: to create and share a new invitation link." = "**Dodaj kontakt**: aby utworzyć nowy link z zaproszeniem lub połączyć się za pomocą otrzymanego linku."; /* No comment provided by engineer. */ "**Create group**: to create a new group." = "**Utwórz grupę**: aby utworzyć nową grupę."; @@ -80,10 +77,10 @@ "**e2e encrypted** video call" = "**szyfrowane e2e** połączenie wideo"; /* No comment provided by engineer. */ -"**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." = "**Bardziej prywatny**: sprawdzanie nowych wiadomości odbywa się co 20 minut. Współdzielony z serwerem SimpleX Chat jest token urządzenia, lecz nie informacje o liczbie kontaktów lub wiadomości."; +"**More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata." = "**Bardziej prywatny**: sprawdzanie nowych wiadomości odbywa się co 20 minut. Współdzielony z serwerem SimpleX Chat jest token urządzenia, lecz nie informacje o liczbie kontaktów lub wiadomości."; /* No comment provided by engineer. */ -"**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." = "**Najbardziej prywatny**: nie korzystaj z serwera powiadomień SimpleX Chat, wiadomości sprawdzane są co jakiś czas w tle (zależne od tego jak często korzystasz z aplikacji)."; +"**Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app." = "**Najbardziej prywatny**: nie korzystaj z serwera powiadomień SimpleX Chat, wiadomości sprawdzane są co jakiś czas w tle (zależne od tego jak często korzystasz z aplikacji)."; /* No comment provided by engineer. */ "**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." = "*Uwaga*: w celach bezpieczeństwa użycie tej samej bazy danych na dwóch różnych urządzeniach spowoduje brak możliwości odszyfrowywania wiadomości z Twoich połączeń."; @@ -92,7 +89,7 @@ "**Please note**: you will NOT be able to recover or change passphrase if you lose it." = "**Uwaga**: NIE będziesz w stanie odzyskać lub zmienić kodu dostępu, jeśli go stracisz."; /* No comment provided by engineer. */ -"**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." = "**Zalecane**: do serwera powiadomień SimpleX Chat wysyłany jest token urządzenia i powiadomienia, lecz nie treść wiadomości, jej rozmiar lub od kogo ona jest."; +"**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from." = "**Zalecane**: do serwera powiadomień SimpleX Chat wysyłany jest token urządzenia i powiadomienia, lecz nie treść wiadomości, jej rozmiar lub od kogo ona jest."; /* No comment provided by engineer. */ "**Warning**: Instant push notifications require passphrase saved in Keychain." = "**Uwaga**: Natychmiastowe powiadomienia push wymagają zapisania kodu dostępu w Keychain."; @@ -2450,7 +2447,7 @@ "Immediately" = "Natychmiast"; /* No comment provided by engineer. */ -"Immune to spam and abuse" = "Odporność na spam i nadużycia"; +"Immune to spam" = "Odporność na spam i nadużycia"; /* No comment provided by engineer. */ "Import" = "Importuj"; @@ -2549,7 +2546,7 @@ "Instant push notifications will be hidden!\n" = "Natychmiastowe powiadomienia push będą ukryte!\n"; /* No comment provided by engineer. */ -"Instantly" = "Natychmiastowo"; +"Instant" = "Natychmiastowo"; /* No comment provided by engineer. */ "Interface" = "Interfejs"; @@ -2759,7 +2756,7 @@ "Live messages" = "Wiadomości na żywo"; /* No comment provided by engineer. */ -"Local" = "Lokalnie"; +"No push server" = "Lokalnie"; /* No comment provided by engineer. */ "Local name" = "Nazwa lokalna"; @@ -3199,7 +3196,7 @@ "Onion hosts will not be used." = "Hosty onion nie będą używane."; /* 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**."; +"Only client devices store user profiles, contacts, groups, and messages." = "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ę"; @@ -3268,7 +3265,7 @@ "Open user profiles" = "Otwórz profile użytkownika"; /* No comment provided by engineer. */ -"Open-source protocol and code – anybody can run the servers." = "Otwarto źródłowy protokół i kod - każdy może uruchomić serwery."; +"Anybody can host servers." = "Każdy może hostować serwery."; /* No comment provided by engineer. */ "Opening app…" = "Otwieranie aplikacji…"; @@ -3349,10 +3346,10 @@ "Pending" = "Oczekujące"; /* No comment provided by engineer. */ -"People can connect to you only via the links you share." = "Ludzie mogą się z Tobą połączyć tylko poprzez linki, które udostępniasz."; +"You decide who can connect." = "Ty decydujesz, kto może się połączyć."; /* No comment provided by engineer. */ -"Periodically" = "Okresowo"; +"Periodic" = "Okresowo"; /* message decrypt error item */ "Permanent decryption error" = "Stały błąd odszyfrowania"; @@ -3565,7 +3562,7 @@ "Read more" = "Przeczytaj więcej"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." = "Przeczytaj więcej w [Podręczniku Użytkownika](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)."; +"Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." = "Przeczytaj więcej w [Podręczniku Użytkownika](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)."; /* No comment provided by engineer. */ "Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." = "Przeczytaj więcej w [Poradniku Użytkownika](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)."; @@ -4464,7 +4461,7 @@ "Thanks to the users – contribute via Weblate!" = "Podziękowania dla użytkowników - wkład za pośrednictwem Weblate!"; /* No comment provided by engineer. */ -"The 1st platform without any user identifiers – private by design." = "Pierwsza platforma bez żadnych identyfikatorów użytkowników – z założenia prywatna."; +"No user identifiers." = "Brak identyfikatorów użytkownika."; /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "Aplikacja może powiadamiać Cię, gdy otrzymujesz wiadomości lub prośby o kontakt — otwórz ustawienia, aby włączyć."; @@ -4509,7 +4506,7 @@ "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"; +"The future of messaging" = "Następna generacja prywatnych wiadomości"; /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "Stara baza danych nie została usunięta podczas migracji, można ją usunąć."; @@ -4599,7 +4596,7 @@ "To make a new connection" = "Aby nawiązać nowe połączenie"; /* No comment provided by engineer. */ -"To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." = "Aby chronić prywatność, zamiast identyfikatorów użytkowników używanych przez wszystkie inne platformy, SimpleX ma identyfikatory dla kolejek wiadomości, oddzielne dla każdego z Twoich kontaktów."; +"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Aby chronić prywatność, zamiast identyfikatorów użytkowników używanych przez wszystkie inne platformy, SimpleX ma identyfikatory dla kolejek wiadomości, oddzielne dla każdego z Twoich kontaktów."; /* No comment provided by engineer. */ "To protect timezone, image/voice files use UTC." = "Aby chronić strefę czasową, pliki obrazów/głosów używają UTC."; @@ -5174,9 +5171,6 @@ /* snd group event chat item */ "you changed role of %@ to %@" = "zmieniłeś rolę %1$@ na %2$@"; -/* No comment provided by engineer. */ -"You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." = "Kontrolujesz przez który serwer(y) **odbierać** wiadomości, Twoje kontakty - serwery, których używasz do wysyłania im wiadomości."; - /* No comment provided by engineer. */ "You could not be verified; please try again." = "Nie można zweryfikować użytkownika; proszę spróbować ponownie."; diff --git a/apps/ios/ru.lproj/Localizable.strings b/apps/ios/ru.lproj/Localizable.strings index 63b7285a45..631280ae84 100644 --- a/apps/ios/ru.lproj/Localizable.strings +++ b/apps/ios/ru.lproj/Localizable.strings @@ -65,10 +65,7 @@ "[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[Поставить звездочку в GitHub](https://github.com/simplex-chat/simplex-chat)"; /* No comment provided by engineer. */ -"**Add contact**: to create a new invitation link, or connect via a link you received." = "**Добавить контакт**: создать новую ссылку-приглашение или подключиться через полученную ссылку."; - -/* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Добавить новый контакт**: чтобы создать одноразовый QR код или ссылку для Вашего контакта."; +"**Create 1-time link**: to create and share a new invitation link." = "**Добавить контакт**: создать и поделиться новой ссылкой-приглашением."; /* No comment provided by engineer. */ "**Create group**: to create a new group." = "**Создать группу**: создать новую группу."; @@ -80,10 +77,10 @@ "**e2e encrypted** video call" = "**e2e зашифрованный** видеозвонок"; /* No comment provided by engineer. */ -"**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." = "**Более конфиденциально**: проверять новые сообщения каждые 20 минут. Токен устройства будет отправлен на сервер уведомлений SimpleX Chat, но у сервера не будет информации о количестве контактов и сообщений."; +"**More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata." = "**Более конфиденциально**: проверять новые сообщения каждые 20 минут. Только токен устройства будет отправлен на сервер уведомлений SimpleX Chat, но у сервера не будет информации о количестве контактов и какой либо информации о сообщениях."; /* No comment provided by engineer. */ -"**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." = "**Самый конфиденциальный**: не использовать сервер уведомлений SimpleX Chat, проверять сообщения периодически в фоновом режиме (зависит от того насколько часто Вы используете приложение)."; +"**Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app." = "**Самый конфиденциальный**: не использовать сервер уведомлений SimpleX Chat. Сообщения проверяются в фоновом режиме, когда система позволяет, в зависимости от того, как часто Вы используете приложение."; /* No comment provided by engineer. */ "**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." = "**Обратите внимание**: использование одной и той же базы данных на двух устройствах нарушит расшифровку сообщений от ваших контактов, как свойство защиты соединений."; @@ -92,7 +89,7 @@ "**Please note**: you will NOT be able to recover or change passphrase if you lose it." = "**Внимание**: Вы не сможете восстановить или поменять пароль, если Вы его потеряете."; /* No comment provided by engineer. */ -"**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." = "**Рекомендовано**: токен устройства и уведомления отправляются на сервер SimpleX Chat, но сервер не получает сами сообщения, их размер или от кого они."; +"**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from." = "**Рекомендовано**: токен устройства и уведомления отправляются на сервер SimpleX Chat, но сервер не получает сами сообщения, их размер или от кого они."; /* No comment provided by engineer. */ "**Warning**: Instant push notifications require passphrase saved in Keychain." = "**Внимание**: для работы мгновенных уведомлений пароль должен быть сохранен в Keychain."; @@ -2438,7 +2435,7 @@ "How to" = "Инфо"; /* No comment provided by engineer. */ -"How to use it" = "Как использовать"; +"How to use it" = "Про адрес"; /* No comment provided by engineer. */ "How to use your servers" = "Как использовать серверы"; @@ -2474,7 +2471,7 @@ "Immediately" = "Сразу"; /* No comment provided by engineer. */ -"Immune to spam and abuse" = "Защищен от спама"; +"Immune to spam" = "Защищен от спама"; /* No comment provided by engineer. */ "Import" = "Импортировать"; @@ -2576,7 +2573,7 @@ "Instant push notifications will be hidden!\n" = "Мгновенные уведомления будут скрыты!\n"; /* No comment provided by engineer. */ -"Instantly" = "Мгновенно"; +"Instant" = "Мгновенно"; /* No comment provided by engineer. */ "Interface" = "Интерфейс"; @@ -2786,7 +2783,7 @@ "Live messages" = "\"Живые\" сообщения"; /* No comment provided by engineer. */ -"Local" = "Локальные"; +"No push server" = "Локальные"; /* No comment provided by engineer. */ "Local name" = "Локальное имя"; @@ -3226,7 +3223,10 @@ "Onion hosts will not be used." = "Onion хосты не используются."; /* No comment provided by engineer. */ -"Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." = "Только пользовательские устройства хранят контакты, группы и сообщения, которые отправляются **с двухуровневым end-to-end шифрованием**."; +"Only client devices store user profiles, contacts, groups, and messages." = "Только пользовательские устройства хранят контакты, группы и сообщения."; + +/* No comment provided by engineer. */ +"All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages." = "Все сообщения и файлы отправляются с **end-to-end шифрованием**, с постквантовой безопасностью в прямых разговорах."; /* No comment provided by engineer. */ "Only delete conversation" = "Удалить только разговор"; @@ -3295,7 +3295,7 @@ "Open user profiles" = "Открыть профили пользователя"; /* No comment provided by engineer. */ -"Open-source protocol and code – anybody can run the servers." = "Открытый протокол и код - кто угодно может запустить сервер."; +"Anybody can host servers." = "Кто угодно может запустить сервер."; /* No comment provided by engineer. */ "Opening app…" = "Приложение отрывается…"; @@ -3376,10 +3376,10 @@ "Pending" = "В ожидании"; /* No comment provided by engineer. */ -"People can connect to you only via the links you share." = "С Вами можно соединиться только через созданные Вами ссылки."; +"You decide who can connect." = "Вы определяете, кто может соединиться."; /* No comment provided by engineer. */ -"Periodically" = "Периодически"; +"Periodic" = "Периодически"; /* message decrypt error item */ "Permanent decryption error" = "Ошибка расшифровки"; @@ -3592,7 +3592,7 @@ "Read more" = "Узнать больше"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." = "Узнать больше в [Руководстве пользователя](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)."; +"Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." = "Узнать больше в [Руководстве пользователя](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)."; /* No comment provided by engineer. */ "Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." = "Дополнительная информация в [Руководстве пользователя](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)."; @@ -4500,7 +4500,7 @@ "Thanks to the users – contribute via Weblate!" = "Благодаря пользователям – добавьте переводы через Weblate!"; /* No comment provided by engineer. */ -"The 1st platform without any user identifiers – private by design." = "Первая в мире платформа без идентификаторов пользователей."; +"No user identifiers." = "Без идентификаторов пользователей."; /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "Приложение может посылать Вам уведомления о сообщениях и запросах на соединение - уведомления можно включить в Настройках."; @@ -4545,7 +4545,7 @@ "The messages will be marked as moderated for all members." = "Сообщения будут помечены как удаленные для всех членов группы."; /* No comment provided by engineer. */ -"The next generation of private messaging" = "Новое поколение приватных сообщений"; +"The future of messaging" = "Будущее коммуникаций"; /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "Предыдущая версия данных чата не удалена при перемещении, её можно удалить."; @@ -4635,7 +4635,7 @@ "To make a new connection" = "Чтобы соединиться"; /* No comment provided by engineer. */ -"To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." = "Чтобы защитить Вашу конфиденциальность, вместо ID пользователей, которые есть в других платформах, SimpleX использует ID для очередей сообщений, разные для каждого контакта."; +"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Чтобы защитить Вашу конфиденциальность, SimpleX использует разные идентификаторы для каждого Вашeго контакта."; /* No comment provided by engineer. */ "To protect timezone, image/voice files use UTC." = "Чтобы защитить Ваш часовой пояс, файлы картинок и голосовых сообщений используют UTC."; @@ -5210,9 +5210,6 @@ /* snd group event chat item */ "you changed role of %@ to %@" = "Вы поменяли роль члена %1$@ на: %2$@"; -/* No comment provided by engineer. */ -"You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." = "Вы определяете через какие серверы Вы **получаете сообщения**, Ваши контакты - серверы, которые Вы используете для отправки."; - /* No comment provided by engineer. */ "You could not be verified; please try again." = "Верификация не удалась; пожалуйста, попробуйте ещё раз."; diff --git a/apps/ios/th.lproj/Localizable.strings b/apps/ios/th.lproj/Localizable.strings index 6125694835..d37dd725df 100644 --- a/apps/ios/th.lproj/Localizable.strings +++ b/apps/ios/th.lproj/Localizable.strings @@ -52,9 +52,6 @@ /* No comment provided by engineer. */ "[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[ติดดาวบน GitHub](https://github.com/simplex-chat/simplex-chat)"; -/* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**เพิ่มผู้ติดต่อใหม่**: เพื่อสร้างคิวอาร์โค้ดแบบใช้ครั้งเดียวหรือลิงก์สำหรับผู้ติดต่อของคุณ"; - /* No comment provided by engineer. */ "**e2e encrypted** audio call" = "การโทรเสียงแบบ **encrypted จากต้นจนจบ**"; @@ -62,16 +59,16 @@ "**e2e encrypted** video call" = "**encrypted จากต้นจนจบ** การสนทนาทางวิดีโอ"; /* No comment provided by engineer. */ -"**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." = "**เป็นส่วนตัวมากขึ้น**: ตรวจสอบข้อความใหม่ทุกๆ 20 นาที โทเค็นอุปกรณ์แชร์กับเซิร์ฟเวอร์ SimpleX Chat แต่ไม่ระบุจำนวนผู้ติดต่อหรือข้อความที่คุณมี"; +"**More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata." = "**เป็นส่วนตัวมากขึ้น**: ตรวจสอบข้อความใหม่ทุกๆ 20 นาที โทเค็นอุปกรณ์แชร์กับเซิร์ฟเวอร์ SimpleX Chat แต่ไม่ระบุจำนวนผู้ติดต่อหรือข้อความที่คุณมี"; /* No comment provided by engineer. */ -"**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." = "**ส่วนตัวที่สุด**: ไม่ใช้เซิร์ฟเวอร์การแจ้งเตือนของ SimpleX Chat ตรวจสอบข้อความเป็นระยะในพื้นหลัง (ขึ้นอยู่กับความถี่ที่คุณใช้แอป)"; +"**Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app." = "**ส่วนตัวที่สุด**: ไม่ใช้เซิร์ฟเวอร์การแจ้งเตือนของ SimpleX Chat ตรวจสอบข้อความเป็นระยะในพื้นหลัง (ขึ้นอยู่กับความถี่ที่คุณใช้แอป)"; /* No comment provided by engineer. */ "**Please note**: you will NOT be able to recover or change passphrase if you lose it." = "**โปรดทราบ**: คุณจะไม่สามารถกู้คืนหรือเปลี่ยนรหัสผ่านได้หากคุณทำรหัสผ่านหาย"; /* No comment provided by engineer. */ -"**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." = "**แนะนำ**: โทเค็นอุปกรณ์และการแจ้งเตือนจะถูกส่งไปยังเซิร์ฟเวอร์การแจ้งเตือนของ SimpleX Chat แต่ไม่ใช่เนื้อหาข้อความ ขนาด หรือผู้ที่ส่ง"; +"**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from." = "**แนะนำ**: โทเค็นอุปกรณ์และการแจ้งเตือนจะถูกส่งไปยังเซิร์ฟเวอร์การแจ้งเตือนของ SimpleX Chat แต่ไม่ใช่เนื้อหาข้อความ ขนาด หรือผู้ที่ส่ง"; /* No comment provided by engineer. */ "**Warning**: Instant push notifications require passphrase saved in Keychain." = "**คำเตือน**: การแจ้งเตือนแบบพุชทันทีจำเป็นต้องบันทึกรหัสผ่านไว้ใน Keychain"; @@ -1627,7 +1624,7 @@ "Immediately" = "โดยทันที"; /* No comment provided by engineer. */ -"Immune to spam and abuse" = "มีภูมิคุ้มกันต่อสแปมและการละเมิด"; +"Immune to spam" = "มีภูมิคุ้มกันต่อสแปมและการละเมิด"; /* No comment provided by engineer. */ "Import" = "นำเข้า"; @@ -1696,7 +1693,7 @@ "Instant push notifications will be hidden!\n" = "การแจ้งเตือนโดยทันทีจะถูกซ่อน!\n"; /* No comment provided by engineer. */ -"Instantly" = "ทันที"; +"Instant" = "ทันที"; /* No comment provided by engineer. */ "Interface" = "อินเตอร์เฟซ"; @@ -1840,7 +1837,7 @@ "Live messages" = "ข้อความสด"; /* No comment provided by engineer. */ -"Local" = "ในเครื่อง"; +"No push server" = "ในเครื่อง"; /* No comment provided by engineer. */ "Local name" = "ชื่อภายในเครื่องเท่านั้น"; @@ -2124,7 +2121,7 @@ "Onion hosts will not be used." = "โฮสต์หัวหอมจะไม่ถูกใช้"; /* No comment provided by engineer. */ -"Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." = "เฉพาะอุปกรณ์ไคลเอนต์เท่านั้นที่จัดเก็บโปรไฟล์ผู้ใช้ ผู้ติดต่อ กลุ่ม และข้อความที่ส่งด้วย **การเข้ารหัส encrypt แบบ 2 ชั้น**"; +"Only client devices store user profiles, contacts, groups, and messages." = "เฉพาะอุปกรณ์ไคลเอนต์เท่านั้นที่จัดเก็บโปรไฟล์ผู้ใช้ ผู้ติดต่อ กลุ่ม และข้อความที่ส่งด้วย **การเข้ารหัส encrypt แบบ 2 ชั้น**"; /* No comment provided by engineer. */ "Only group owners can change group preferences." = "เฉพาะเจ้าของกลุ่มเท่านั้นที่สามารถเปลี่ยนค่ากําหนดลักษณะกลุ่มได้"; @@ -2178,7 +2175,7 @@ "Open user profiles" = "เปิดโปรไฟล์ผู้ใช้"; /* No comment provided by engineer. */ -"Open-source protocol and code – anybody can run the servers." = "โปรโตคอลและโค้ดโอเพ่นซอร์ส – ใคร ๆ ก็สามารถเปิดใช้เซิร์ฟเวอร์ได้"; +"Anybody can host servers." = "โปรโตคอลและโค้ดโอเพ่นซอร์ส – ใคร ๆ ก็สามารถเปิดใช้เซิร์ฟเวอร์ได้"; /* member role */ "owner" = "เจ้าของ"; @@ -2208,10 +2205,10 @@ "peer-to-peer" = "เพื่อนต่อเพื่อน"; /* No comment provided by engineer. */ -"People can connect to you only via the links you share." = "ผู้คนสามารถเชื่อมต่อกับคุณผ่านลิงก์ที่คุณแบ่งปันเท่านั้น"; +"You decide who can connect." = "ผู้คนสามารถเชื่อมต่อกับคุณผ่านลิงก์ที่คุณแบ่งปันเท่านั้น"; /* No comment provided by engineer. */ -"Periodically" = "เป็นระยะๆ"; +"Periodic" = "เป็นระยะๆ"; /* message decrypt error item */ "Permanent decryption error" = "ข้อผิดพลาดในการถอดรหัสอย่างถาวร"; @@ -2349,7 +2346,7 @@ "Read more" = "อ่านเพิ่มเติม"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." = "อ่านเพิ่มเติมใน[คู่มือผู้ใช้](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)"; +"Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." = "อ่านเพิ่มเติมใน[คู่มือผู้ใช้](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)"; /* No comment provided by engineer. */ "Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." = "อ่านเพิ่มเติมใน[คู่มือผู้ใช้](https://simplex.chat/docs/guide/readme.html#connect-to-friends)"; @@ -2891,7 +2888,7 @@ "Thanks to the users – contribute via Weblate!" = "ขอบคุณผู้ใช้ – มีส่วนร่วมผ่าน Weblate!"; /* No comment provided by engineer. */ -"The 1st platform without any user identifiers – private by design." = "แพลตฟอร์มแรกที่ไม่มีตัวระบุผู้ใช้ - ถูกออกแบบให้เป็นส่วนตัว"; +"No user identifiers." = "แพลตฟอร์มแรกที่ไม่มีตัวระบุผู้ใช้ - ถูกออกแบบให้เป็นส่วนตัว"; /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "แอปสามารถแจ้งให้คุณทราบเมื่อคุณได้รับข้อความหรือคำขอติดต่อ - โปรดเปิดการตั้งค่าเพื่อเปิดใช้งาน"; @@ -2924,7 +2921,7 @@ "The message will be marked as moderated for all members." = "ข้อความจะถูกทำเครื่องหมายว่ากลั่นกรองสำหรับสมาชิกทุกคน"; /* No comment provided by engineer. */ -"The next generation of private messaging" = "การส่งข้อความส่วนตัวรุ่นต่อไป"; +"The future of messaging" = "การส่งข้อความส่วนตัวรุ่นต่อไป"; /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "ฐานข้อมูลเก่าไม่ได้ถูกลบในระหว่างการย้ายข้อมูล แต่สามารถลบได้"; @@ -2972,7 +2969,7 @@ "To make a new connection" = "เพื่อสร้างการเชื่อมต่อใหม่"; /* No comment provided by engineer. */ -"To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." = "เพื่อปกป้องความเป็นส่วนตัว แทนที่จะใช้ ID ผู้ใช้เหมือนที่แพลตฟอร์มอื่นๆใช้ SimpleX มีตัวระบุสำหรับคิวข้อความ โดยแยกจากกันสำหรับผู้ติดต่อแต่ละราย"; +"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "เพื่อปกป้องความเป็นส่วนตัว แทนที่จะใช้ ID ผู้ใช้เหมือนที่แพลตฟอร์มอื่นๆใช้ SimpleX มีตัวระบุสำหรับคิวข้อความ โดยแยกจากกันสำหรับผู้ติดต่อแต่ละราย"; /* No comment provided by engineer. */ "To protect timezone, image/voice files use UTC." = "ไฟล์ภาพ/เสียงใช้ UTC เพื่อป้องกันเขตเวลา"; @@ -3292,9 +3289,6 @@ /* snd group event chat item */ "you changed role of %@ to %@" = "คุณเปลี่ยนบทบาทของ %1$@ เป็น %2$@"; -/* No comment provided by engineer. */ -"You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." = "คุณควบคุมผ่านเซิร์ฟเวอร์ **เพื่อรับ** ข้อความผู้ติดต่อของคุณ - เซิร์ฟเวอร์ที่คุณใช้เพื่อส่งข้อความถึงพวกเขา"; - /* No comment provided by engineer. */ "You could not be verified; please try again." = "เราไม่สามารถตรวจสอบคุณได้ กรุณาลองอีกครั้ง."; diff --git a/apps/ios/tr.lproj/Localizable.strings b/apps/ios/tr.lproj/Localizable.strings index 63ca78bccf..cc250808be 100644 --- a/apps/ios/tr.lproj/Localizable.strings +++ b/apps/ios/tr.lproj/Localizable.strings @@ -65,10 +65,7 @@ "[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[Bize GitHub'da yıldız verin](https://github.com/simplex-chat/simplex-chat)"; /* No comment provided by engineer. */ -"**Add contact**: to create a new invitation link, or connect via a link you received." = "**Kişi ekle**: yeni bir davet bağlantısı oluşturmak için, ya da aldığın bağlantıyla bağlan."; - -/* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Yeni kişi ekleyin**: tek seferlik QR Kodunuzu oluşturmak veya kişisel ulaşım bilgileri bağlantısı için."; +"**Create 1-time link**: to create and share a new invitation link." = "**Kişi ekle**: yeni bir davet bağlantısı oluşturmak için, ya da aldığın bağlantıyla bağlan."; /* No comment provided by engineer. */ "**Create group**: to create a new group." = "**Grup oluştur**: yeni bir grup oluşturmak için."; @@ -80,10 +77,10 @@ "**e2e encrypted** video call" = "**uçtan uca şifrelenmiş** görüntülü arama"; /* No comment provided by engineer. */ -"**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." = "**Daha gizli**: her 20 dakikada yeni mesajlar için kontrol et. Cihaz jetonu SimpleX Chat sunucusuyla paylaşılacak, ama ne kadar kişi veya mesaja sahip olduğun paylaşılmayacak."; +"**More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata." = "**Daha gizli**: her 20 dakikada yeni mesajlar için kontrol et. Cihaz jetonu SimpleX Chat sunucusuyla paylaşılacak, ama ne kadar kişi veya mesaja sahip olduğun paylaşılmayacak."; /* No comment provided by engineer. */ -"**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." = "**En gizli**: SimpleX Chat bildirim sunucusunu kullanma, arkaplanda mesajları periyodik olarak kontrol edin (uygulamayı ne sıklıkta kullandığınıza bağlıdır)."; +"**Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app." = "**En gizli**: SimpleX Chat bildirim sunucusunu kullanma, arkaplanda mesajları periyodik olarak kontrol edin (uygulamayı ne sıklıkta kullandığınıza bağlıdır)."; /* No comment provided by engineer. */ "**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." = "**Lütfen dikkat**: Aynı veritabanını iki cihazda kullanmak, güvenlik koruması olarak bağlantılarınızdaki mesajların şifresinin çözülmesini engelleyecektir."; @@ -92,7 +89,7 @@ "**Please note**: you will NOT be able to recover or change passphrase if you lose it." = "**Lütfen aklınızda bulunsun**: eğer parolanızı kaybederseniz parolanızı değiştirme veya geri kurtarma ihtimaliniz YOKTUR."; /* No comment provided by engineer. */ -"**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." = "**Önerilen**: cihaz tokeni ve bildirimler SimpleX Chat bildirim sunucularına gönderilir, ama mesajın içeriği, boyutu veya kimden geldiği gönderilmez."; +"**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from." = "**Önerilen**: cihaz tokeni ve bildirimler SimpleX Chat bildirim sunucularına gönderilir, ama mesajın içeriği, boyutu veya kimden geldiği gönderilmez."; /* No comment provided by engineer. */ "**Warning**: Instant push notifications require passphrase saved in Keychain." = "**Dikkat**: Anında iletilen bildirimlere Anahtar Zinciri'nde kaydedilmiş parola gereklidir."; @@ -2474,7 +2471,7 @@ "Immediately" = "Hemen"; /* No comment provided by engineer. */ -"Immune to spam and abuse" = "Spam ve kötüye kullanıma karşı bağışıklı"; +"Immune to spam" = "Spam ve kötüye kullanıma karşı bağışıklı"; /* No comment provided by engineer. */ "Import" = "İçe aktar"; @@ -2576,7 +2573,7 @@ "Instant push notifications will be hidden!\n" = "Anlık bildirimler gizlenecek!\n"; /* No comment provided by engineer. */ -"Instantly" = "Anında"; +"Instant" = "Anında"; /* No comment provided by engineer. */ "Interface" = "Arayüz"; @@ -2786,7 +2783,7 @@ "Live messages" = "Canlı mesajlar"; /* No comment provided by engineer. */ -"Local" = "Yerel"; +"No push server" = "Yerel"; /* No comment provided by engineer. */ "Local name" = "Yerel isim"; @@ -3226,7 +3223,7 @@ "Onion hosts will not be used." = "Onion ana bilgisayarları kullanılmayacaktır."; /* No comment provided by engineer. */ -"Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." = "Yalnızca istemci cihazlar kullanıcı profillerini, kişileri, grupları ve **2 katmanlı uçtan uca şifreleme** ile gönderilen mesajları depolar."; +"Only client devices store user profiles, contacts, groups, and messages." = "Yalnızca istemci cihazlar kullanıcı profillerini, kişileri, grupları ve **2 katmanlı uçtan uca şifreleme** ile gönderilen mesajları depolar."; /* No comment provided by engineer. */ "Only delete conversation" = "Sadece sohbeti sil"; @@ -3295,7 +3292,7 @@ "Open user profiles" = "Kullanıcı profillerini aç"; /* No comment provided by engineer. */ -"Open-source protocol and code – anybody can run the servers." = "Açık kaynak protokolü ve kodu - herhangi biri sunucuları çalıştırabilir."; +"Anybody can host servers." = "Açık kaynak protokolü ve kodu - herhangi biri sunucuları çalıştırabilir."; /* No comment provided by engineer. */ "Opening app…" = "Uygulama açılıyor…"; @@ -3376,10 +3373,10 @@ "Pending" = "Bekleniyor"; /* No comment provided by engineer. */ -"People can connect to you only via the links you share." = "İnsanlar size yalnızca paylaştığınız bağlantılar üzerinden ulaşabilir."; +"You decide who can connect." = "Kimin bağlanabileceğine siz karar verirsiniz."; /* No comment provided by engineer. */ -"Periodically" = "Periyodik olarak"; +"Periodic" = "Periyodik olarak"; /* message decrypt error item */ "Permanent decryption error" = "Kalıcı şifre çözümü hatası"; @@ -3592,7 +3589,7 @@ "Read more" = "Dahasını oku"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." = "[Kullanıcı Rehberi]nde daha fazlasını okuyun(https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)."; +"Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." = "[Kullanıcı Rehberi]nde daha fazlasını okuyun(https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)."; /* No comment provided by engineer. */ "Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." = "[Kullanıcı Rehberi]nde daha fazlasını okuyun(https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)."; @@ -4500,7 +4497,7 @@ "Thanks to the users – contribute via Weblate!" = "Kullanıcılar için teşekkürler - Weblate aracılığıyla katkıda bulun!"; /* No comment provided by engineer. */ -"The 1st platform without any user identifiers – private by design." = "Herhangi bir kullanıcı tanımlayıcısı olmayan ilk platform - tasarım gereği gizli."; +"No user identifiers." = "Herhangi bir kullanıcı tanımlayıcısı yok."; /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "Uygulama, mesaj veya iletişim isteği aldığınızda sizi bilgilendirebilir - etkinleştirmek için lütfen ayarları açın."; @@ -4545,7 +4542,7 @@ "The messages will be marked as moderated for all members." = "Mesajlar tüm üyeler için moderasyonlu olarak işaretlenecektir."; /* No comment provided by engineer. */ -"The next generation of private messaging" = "Gizli mesajlaşmanın yeni nesli"; +"The future of messaging" = "Gizli mesajlaşmanın yeni nesli"; /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "Eski veritabanı geçiş sırasında kaldırılmadı, silinebilir."; @@ -4635,7 +4632,7 @@ "To make a new connection" = "Yeni bir bağlantı oluşturmak için"; /* No comment provided by engineer. */ -"To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." = "Gizliliği korumak için, diğer tüm platformlar gibi kullanıcı kimliği kullanmak yerine, SimpleX mesaj kuyrukları için kişilerinizin her biri için ayrı tanımlayıcılara sahiptir."; +"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Gizliliği korumak için, diğer tüm platformlar gibi kullanıcı kimliği kullanmak yerine, SimpleX mesaj kuyrukları için kişilerinizin her biri için ayrı tanımlayıcılara sahiptir."; /* No comment provided by engineer. */ "To protect timezone, image/voice files use UTC." = "Zaman bölgesini korumak için,fotoğraf/ses dosyaları UTC kullanır."; @@ -5210,9 +5207,6 @@ /* snd group event chat item */ "you changed role of %@ to %@" = "%1$@'in yetkisini %2$@ olarak değiştirdiniz"; -/* No comment provided by engineer. */ -"You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." = "Mesajların hangi sunucu(lar)dan **alınacağını**, kişilerinizi - onlara mesaj göndermek için kullandığınız sunucuları - siz kontrol edersiniz."; - /* No comment provided by engineer. */ "You could not be verified; please try again." = "Doğrulanamadınız; lütfen tekrar deneyin."; diff --git a/apps/ios/uk.lproj/Localizable.strings b/apps/ios/uk.lproj/Localizable.strings index 2647fe49d0..eb92d191c6 100644 --- a/apps/ios/uk.lproj/Localizable.strings +++ b/apps/ios/uk.lproj/Localizable.strings @@ -65,10 +65,7 @@ "[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[Зірка на GitHub](https://github.com/simplex-chat/simplex-chat)"; /* No comment provided by engineer. */ -"**Add contact**: to create a new invitation link, or connect via a link you received." = "**Додати контакт**: створити нове посилання-запрошення або підключитися за отриманим посиланням."; - -/* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Додати новий контакт**: щоб створити одноразовий QR-код або посилання для свого контакту."; +"**Create 1-time link**: to create and share a new invitation link." = "**Додати контакт**: створити нове посилання-запрошення."; /* No comment provided by engineer. */ "**Create group**: to create a new group." = "**Створити групу**: створити нову групу."; @@ -80,10 +77,10 @@ "**e2e encrypted** video call" = "**e2e encrypted** відеодзвінок"; /* No comment provided by engineer. */ -"**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." = "**Більш приватний**: перевіряти нові повідомлення кожні 20 хвилин. Серверу SimpleX Chat передається токен пристрою, але не кількість контактів або повідомлень, які ви маєте."; +"**More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata." = "**Більш приватний**: перевіряти нові повідомлення кожні 20 хвилин. Серверу SimpleX Chat передається токен пристрою, але не кількість контактів або повідомлень, які ви маєте."; /* No comment provided by engineer. */ -"**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." = "**Найбільш приватний**: не використовуйте сервер сповіщень SimpleX Chat, періодично перевіряйте повідомлення у фоновому режимі (залежить від того, як часто ви користуєтесь додатком)."; +"**Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app." = "**Найбільш приватний**: не використовуйте сервер сповіщень SimpleX Chat, періодично перевіряйте повідомлення у фоновому режимі (залежить від того, як часто ви користуєтесь додатком)."; /* No comment provided by engineer. */ "**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." = "**Зверніть увагу**: використання однієї і тієї ж бази даних на двох пристроях порушить розшифровку повідомлень з ваших з'єднань, як захист безпеки."; @@ -92,7 +89,7 @@ "**Please note**: you will NOT be able to recover or change passphrase if you lose it." = "**Зверніть увагу: ви НЕ зможете відновити або змінити пароль, якщо втратите його."; /* No comment provided by engineer. */ -"**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." = "**Рекомендується**: токен пристрою та сповіщення надсилаються на сервер сповіщень SimpleX Chat, але не вміст повідомлення, його розмір або від кого воно надійшло."; +"**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from." = "**Рекомендується**: токен пристрою та сповіщення надсилаються на сервер сповіщень SimpleX Chat, але не вміст повідомлення, його розмір або від кого воно надійшло."; /* No comment provided by engineer. */ "**Warning**: Instant push notifications require passphrase saved in Keychain." = "**Попередження**: Для отримання миттєвих пуш-сповіщень потрібна парольна фраза, збережена у брелоку."; @@ -2387,7 +2384,7 @@ "Immediately" = "Негайно"; /* No comment provided by engineer. */ -"Immune to spam and abuse" = "Імунітет до спаму та зловживань"; +"Immune to spam" = "Імунітет до спаму та зловживань"; /* No comment provided by engineer. */ "Import" = "Імпорт"; @@ -2486,7 +2483,7 @@ "Instant push notifications will be hidden!\n" = "Миттєві пуш-сповіщення будуть приховані!\n"; /* No comment provided by engineer. */ -"Instantly" = "Миттєво"; +"Instant" = "Миттєво"; /* No comment provided by engineer. */ "Interface" = "Інтерфейс"; @@ -2693,7 +2690,7 @@ "Live messages" = "Живі повідомлення"; /* No comment provided by engineer. */ -"Local" = "Локально"; +"No push server" = "Локально"; /* No comment provided by engineer. */ "Local name" = "Місцева назва"; @@ -3112,7 +3109,7 @@ "Onion hosts will not be used." = "Onion хости не будуть використовуватися."; /* No comment provided by engineer. */ -"Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." = "Тільки клієнтські пристрої зберігають профілі користувачів, контакти, групи та повідомлення, надіслані за допомогою **2-шарового наскрізного шифрування**."; +"Only client devices store user profiles, contacts, groups, and messages." = "Тільки клієнтські пристрої зберігають профілі користувачів, контакти, групи та повідомлення, надіслані за допомогою **2-шарового наскрізного шифрування**."; /* No comment provided by engineer. */ "Only delete conversation" = "Видаляйте тільки розмови"; @@ -3181,7 +3178,7 @@ "Open user profiles" = "Відкрити профілі користувачів"; /* No comment provided by engineer. */ -"Open-source protocol and code – anybody can run the servers." = "Протокол і код з відкритим вихідним кодом - будь-хто може запускати сервери."; +"Anybody can host servers." = "Кожен може хостити сервери."; /* No comment provided by engineer. */ "Opening app…" = "Відкриваємо програму…"; @@ -3256,10 +3253,10 @@ "Pending" = "В очікуванні"; /* No comment provided by engineer. */ -"People can connect to you only via the links you share." = "Люди можуть зв'язатися з вами лише за посиланнями, якими ви ділитеся."; +"You decide who can connect." = "Ви вирішуєте, хто може під\'єднатися."; /* No comment provided by engineer. */ -"Periodically" = "Періодично"; +"Periodic" = "Періодично"; /* message decrypt error item */ "Permanent decryption error" = "Постійна помилка розшифрування"; @@ -3466,7 +3463,7 @@ "Read more" = "Читати далі"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." = "Читайте більше в [Посібнику користувача](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)."; +"Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." = "Читайте більше в [Посібнику користувача](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)."; /* No comment provided by engineer. */ "Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." = "Читайте більше в [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)."; @@ -4335,7 +4332,7 @@ "Thanks to the users – contribute via Weblate!" = "Дякуємо користувачам - зробіть свій внесок через Weblate!"; /* No comment provided by engineer. */ -"The 1st platform without any user identifiers – private by design." = "Перша платформа без жодних ідентифікаторів користувачів – приватна за дизайном."; +"No user identifiers." = "Ніяких ідентифікаторів користувачів."; /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "Додаток може сповіщати вас, коли ви отримуєте повідомлення або запити на контакт - будь ласка, відкрийте налаштування, щоб увімкнути цю функцію."; @@ -4380,7 +4377,7 @@ "The messages will be marked as moderated for all members." = "Повідомлення будуть позначені як модеровані для всіх учасників."; /* No comment provided by engineer. */ -"The next generation of private messaging" = "Наступне покоління приватних повідомлень"; +"The future of messaging" = "Наступне покоління приватних повідомлень"; /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "Стара база даних не була видалена під час міграції, її можна видалити."; @@ -4467,7 +4464,7 @@ "To make a new connection" = "Щоб створити нове з'єднання"; /* No comment provided by engineer. */ -"To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." = "Щоб захистити конфіденційність, замість ідентифікаторів користувачів, які використовуються на всіх інших платформах, SimpleX має ідентифікатори для черг повідомлень, окремі для кожного з ваших контактів."; +"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Щоб захистити конфіденційність, замість ідентифікаторів користувачів, які використовуються на всіх інших платформах, SimpleX має ідентифікатори для черг повідомлень, окремі для кожного з ваших контактів."; /* No comment provided by engineer. */ "To protect timezone, image/voice files use UTC." = "Для захисту часового поясу у файлах зображень/голосу використовується UTC."; @@ -5030,9 +5027,6 @@ /* snd group event chat item */ "you changed role of %@ to %@" = "ви змінили роль %1$@ на %2$@"; -/* No comment provided by engineer. */ -"You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." = "Ви контролюєте, через який(і) сервер(и) **отримувати** повідомлення, ваші контакти - сервери, які ви використовуєте для надсилання їм повідомлень."; - /* No comment provided by engineer. */ "You could not be verified; please try again." = "Вас не вдалося верифікувати, спробуйте ще раз."; diff --git a/apps/ios/zh-Hans.lproj/Localizable.strings b/apps/ios/zh-Hans.lproj/Localizable.strings index 2c3a5e588d..89773a7481 100644 --- a/apps/ios/zh-Hans.lproj/Localizable.strings +++ b/apps/ios/zh-Hans.lproj/Localizable.strings @@ -65,10 +65,7 @@ "[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[在 GitHub 上加星](https://github.com/simplex-chat/simplex-chat)"; /* No comment provided by engineer. */ -"**Add contact**: to create a new invitation link, or connect via a link you received." = "**添加联系人**: 创建新的邀请链接,或通过您收到的链接进行连接."; - -/* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**添加新联系人**:为您的联系人创建一次性二维码或者链接。"; +"**Create 1-time link**: to create and share a new invitation link." = "**添加联系人**: 创建新的邀请链接,或通过您收到的链接进行连接."; /* No comment provided by engineer. */ "**Create group**: to create a new group." = "**创建群组**: 创建一个新群组."; @@ -80,10 +77,10 @@ "**e2e encrypted** video call" = "**端到端加密** 视频通话"; /* No comment provided by engineer. */ -"**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." = "**更私密**:每20分钟检查新消息。设备令牌和 SimpleX Chat 服务器共享,但是不会共享有您有多少联系人或者消息。"; +"**More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata." = "**更私密**:每20分钟检查新消息。设备令牌和 SimpleX Chat 服务器共享,但是不会共享有您有多少联系人或者消息。"; /* No comment provided by engineer. */ -"**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." = "**最私密**:不使用 SimpleX Chat 通知服务器,在后台定期检查消息(取决于您多经常使用应用程序)。"; +"**Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app." = "**最私密**:不使用 SimpleX Chat 通知服务器,在后台定期检查消息(取决于您多经常使用应用程序)。"; /* No comment provided by engineer. */ "**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." = "**请注意**: 在两台设备上使用相同的数据库将破坏来自您的连接的消息解密,作为一种安全保护."; @@ -92,7 +89,7 @@ "**Please note**: you will NOT be able to recover or change passphrase if you lose it." = "**请注意**:如果您丢失密码,您将无法恢复或者更改密码。"; /* No comment provided by engineer. */ -"**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." = "**推荐**:设备令牌和通知会发送至 SimpleX Chat 通知服务器,但是消息内容、大小或者发送人不会。"; +"**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from." = "**推荐**:设备令牌和通知会发送至 SimpleX Chat 通知服务器,但是消息内容、大小或者发送人不会。"; /* No comment provided by engineer. */ "**Warning**: Instant push notifications require passphrase saved in Keychain." = "**警告**:及时推送通知需要保存在钥匙串的密码。"; @@ -2387,7 +2384,7 @@ "Immediately" = "立即"; /* No comment provided by engineer. */ -"Immune to spam and abuse" = "不受垃圾和骚扰消息影响"; +"Immune to spam" = "不受垃圾和骚扰消息影响"; /* No comment provided by engineer. */ "Import" = "导入"; @@ -2486,7 +2483,7 @@ "Instant push notifications will be hidden!\n" = "即时推送通知将被隐藏!\n"; /* No comment provided by engineer. */ -"Instantly" = "即时"; +"Instant" = "即时"; /* No comment provided by engineer. */ "Interface" = "界面"; @@ -2693,7 +2690,7 @@ "Live messages" = "实时消息"; /* No comment provided by engineer. */ -"Local" = "本地"; +"No push server" = "本地"; /* No comment provided by engineer. */ "Local name" = "本地名称"; @@ -3112,7 +3109,7 @@ "Onion hosts will not be used." = "将不会使用 Onion 主机。"; /* No comment provided by engineer. */ -"Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." = "只有客户端设备存储用户资料、联系人、群组和**双层端到端加密**发送的消息。"; +"Only client devices store user profiles, contacts, groups, and messages." = "只有客户端设备存储用户资料、联系人、群组和**双层端到端加密**发送的消息。"; /* No comment provided by engineer. */ "Only delete conversation" = "仅删除对话"; @@ -3181,7 +3178,7 @@ "Open user profiles" = "打开用户个人资料"; /* No comment provided by engineer. */ -"Open-source protocol and code – anybody can run the servers." = "开源协议和代码——任何人都可以运行服务器。"; +"Anybody can host servers." = "任何人都可以托管服务器。"; /* No comment provided by engineer. */ "Opening app…" = "正在打开应用程序…"; @@ -3256,10 +3253,10 @@ "Pending" = "待定"; /* No comment provided by engineer. */ -"People can connect to you only via the links you share." = "人们只能通过您共享的链接与您建立联系。"; +"You decide who can connect." = "你决定谁可以连接。"; /* No comment provided by engineer. */ -"Periodically" = "定期"; +"Periodic" = "定期"; /* message decrypt error item */ "Permanent decryption error" = "解密错误"; @@ -3466,7 +3463,7 @@ "Read more" = "阅读更多"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." = "在 [用户指南](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address) 中阅读更多内容。"; +"Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." = "在 [用户指南](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses) 中阅读更多内容。"; /* No comment provided by engineer. */ "Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." = "阅读更多[User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)。"; @@ -4335,7 +4332,7 @@ "Thanks to the users – contribute via Weblate!" = "感谢用户——通过 Weblate 做出贡献!"; /* No comment provided by engineer. */ -"The 1st platform without any user identifiers – private by design." = "第一个没有任何用户标识符的平台 - 隐私设计."; +"No user identifiers." = "没有用户标识符。"; /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "该应用可以在您收到消息或联系人请求时通知您——请打开设置以启用通知。"; @@ -4380,7 +4377,7 @@ "The messages will be marked as moderated for all members." = "对于所有成员,这些消息将被标记为已审核。"; /* No comment provided by engineer. */ -"The next generation of private messaging" = "下一代私密通讯软件"; +"The future of messaging" = "下一代私密通讯软件"; /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "旧数据库在迁移过程中没有被移除,可以删除。"; @@ -4467,7 +4464,7 @@ "To make a new connection" = "建立新连接"; /* No comment provided by engineer. */ -"To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." = "为了保护隐私,SimpleX使用针对消息队列的标识符,而不是所有其他平台使用的用户ID,每个联系人都有独立的标识符。"; +"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "为了保护隐私,SimpleX使用针对消息队列的标识符,而不是所有其他平台使用的用户ID,每个联系人都有独立的标识符。"; /* No comment provided by engineer. */ "To protect timezone, image/voice files use UTC." = "为了保护时区,图像/语音文件使用 UTC。"; @@ -5030,9 +5027,6 @@ /* snd group event chat item */ "you changed role of %@ to %@" = "您已将 %1$@ 的角色更改为 %2$@"; -/* No comment provided by engineer. */ -"You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." = "您可以控制接收信息使用的服务器,您的联系人则使用您发送信息时所使用的服务器。"; - /* No comment provided by engineer. */ "You could not be verified; please try again." = "您的身份无法验证,请再试一次。"; From 22d7db89d8457b72be22bbbe4e87254038c26f94 Mon Sep 17 00:00:00 2001 From: Diogo Date: Wed, 27 Nov 2024 20:32:18 +0000 Subject: [PATCH 077/167] ios: database error screens redesign (#5256) * ios: database error screens redesign (wip) * refactor * remove code to simulate errors * fix * fix texts --------- Co-authored-by: Evgeny Poberezkin --- .../Views/Database/DatabaseErrorView.swift | 115 ++++++++++++++---- .../bg.xcloc/Localized Contents/bg.xliff | 6 +- .../bn.xcloc/Localized Contents/bn.xliff | 4 +- .../cs.xcloc/Localized Contents/cs.xliff | 6 +- .../de.xcloc/Localized Contents/de.xliff | 6 +- .../el.xcloc/Localized Contents/el.xliff | 4 +- .../en.xcloc/Localized Contents/en.xliff | 6 +- .../es.xcloc/Localized Contents/es.xliff | 6 +- .../fi.xcloc/Localized Contents/fi.xliff | 6 +- .../fr.xcloc/Localized Contents/fr.xliff | 6 +- .../he.xcloc/Localized Contents/he.xliff | 6 +- .../hu.xcloc/Localized Contents/hu.xliff | 6 +- .../it.xcloc/Localized Contents/it.xliff | 6 +- .../ja.xcloc/Localized Contents/ja.xliff | 6 +- .../nl.xcloc/Localized Contents/nl.xliff | 6 +- .../pl.xcloc/Localized Contents/pl.xliff | 6 +- .../pt.xcloc/Localized Contents/pt.xliff | 4 +- .../ru.xcloc/Localized Contents/ru.xliff | 6 +- .../th.xcloc/Localized Contents/th.xliff | 6 +- .../tr.xcloc/Localized Contents/tr.xliff | 6 +- .../uk.xcloc/Localized Contents/uk.xliff | 6 +- .../Localized Contents/zh-Hans.xliff | 6 +- .../Localized Contents/zh-Hant.xliff | 4 +- apps/ios/bg.lproj/Localizable.strings | 2 +- apps/ios/cs.lproj/Localizable.strings | 2 +- apps/ios/de.lproj/Localizable.strings | 2 +- apps/ios/es.lproj/Localizable.strings | 2 +- apps/ios/fi.lproj/Localizable.strings | 2 +- apps/ios/fr.lproj/Localizable.strings | 2 +- apps/ios/hu.lproj/Localizable.strings | 2 +- apps/ios/it.lproj/Localizable.strings | 2 +- apps/ios/ja.lproj/Localizable.strings | 2 +- apps/ios/nl.lproj/Localizable.strings | 2 +- apps/ios/pl.lproj/Localizable.strings | 2 +- apps/ios/ru.lproj/Localizable.strings | 2 +- apps/ios/th.lproj/Localizable.strings | 2 +- apps/ios/tr.lproj/Localizable.strings | 2 +- apps/ios/uk.lproj/Localizable.strings | 2 +- apps/ios/zh-Hans.lproj/Localizable.strings | 2 +- 39 files changed, 169 insertions(+), 102 deletions(-) diff --git a/apps/ios/Shared/Views/Database/DatabaseErrorView.swift b/apps/ios/Shared/Views/Database/DatabaseErrorView.swift index 9d71e2a788..6222a28fb4 100644 --- a/apps/ios/Shared/Views/Database/DatabaseErrorView.swift +++ b/apps/ios/Shared/Views/Database/DatabaseErrorView.swift @@ -11,6 +11,7 @@ import SimpleXChat struct DatabaseErrorView: View { @EnvironmentObject var m: ChatModel + @EnvironmentObject var theme: AppTheme @State var status: DBMigrationResult @State private var dbKey = "" @State private var storedDBKey = kcDatabasePassword.get() @@ -28,23 +29,39 @@ struct DatabaseErrorView: View { } @ViewBuilder private func databaseErrorView() -> some View { - VStack(alignment: .leading, spacing: 16) { + VStack(alignment: .center, spacing: 20) { switch status { case let .errorNotADatabase(dbFile): if useKeychain && storedDBKey != nil && storedDBKey != "" { titleText("Wrong database passphrase") Text("Database passphrase is different from saved in the keychain.") + .font(.callout) + .foregroundColor(theme.colors.secondary) + .multilineTextAlignment(.center) + .padding(.horizontal, 25) + databaseKeyField(onSubmit: saveAndRunChat) - saveAndOpenButton() - fileNameText(dbFile) + Spacer() + VStack(spacing: 10) { + saveAndOpenButton() + fileNameText(dbFile) + } } else { titleText("Encrypted database") Text("Database passphrase is required to open chat.") + .font(.callout) + .foregroundColor(theme.colors.secondary) + .multilineTextAlignment(.center) + .padding(.horizontal, 25) + .padding(.bottom, 5) + if useKeychain { databaseKeyField(onSubmit: saveAndRunChat) + Spacer() saveAndOpenButton() } else { databaseKeyField(onSubmit: { runChat() }) + Spacer() openChatButton() } } @@ -52,73 +69,105 @@ struct DatabaseErrorView: View { switch migrationError { case let .upgrade(upMigrations): titleText("Database upgrade") - Button("Upgrade and open chat") { runChat(confirmMigrations: .yesUp) } - fileNameText(dbFile) migrationsText(upMigrations.map(\.upName)) + Spacer() + VStack(spacing: 10) { + Button("Upgrade and open chat") { + runChat(confirmMigrations: .yesUp) + }.buttonStyle(OnboardingButtonStyle(isDisabled: false)) + fileNameText(dbFile) + } case let .downgrade(downMigrations): titleText("Database downgrade") - Text("Warning: you may lose some data!").bold() - Button("Downgrade and open chat") { runChat(confirmMigrations: .yesUpDown) } - fileNameText(dbFile) + Text("Warning: you may lose some data!") + .bold() + .padding(.horizontal, 25) + .multilineTextAlignment(.center) + migrationsText(downMigrations) + Spacer() + VStack(spacing: 10) { + Button("Downgrade and open chat") { + runChat(confirmMigrations: .yesUpDown) + }.buttonStyle(OnboardingButtonStyle(isDisabled: false)) + fileNameText(dbFile) + } case let .migrationError(mtrError): titleText("Incompatible database version") - fileNameText(dbFile) - Text("Error: ") + Text(mtrErrorDescription(mtrError)) + fileNameText(dbFile, font: .callout) + errorView(Text(mtrErrorDescription(mtrError))) } case let .errorSQL(dbFile, migrationSQLError): titleText("Database error") - fileNameText(dbFile) - Text("Error: \(migrationSQLError)") + fileNameText(dbFile, font: .callout) + errorView(Text("Error: \(migrationSQLError)")) case .errorKeychain: titleText("Keychain error") - Text("Cannot access keychain to save database password") + errorView(Text("Cannot access keychain to save database password")) case .invalidConfirmation: // this can only happen if incorrect parameter is passed - Text(String("Invalid migration confirmation")).font(.title) + titleText("Invalid migration confirmation") + errorView() + case let .unknown(json): titleText("Database error") - Text("Unknown database error: \(json)") + errorView(Text("Unknown database error: \(json)")) case .ok: EmptyView() } if showRestoreDbButton { - Spacer().frame(height: 10) + Spacer() Text("The attempt to change database passphrase was not completed.") + .multilineTextAlignment(.center) + .padding(.horizontal, 25) + .font(.footnote) + restoreDbButton() } } - .padding() + .padding(.horizontal, 25) + .padding(.top, 75) + .padding(.bottom, 25) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) .onAppear() { showRestoreDbButton = shouldShowRestoreDbButton() } } - private func titleText(_ s: LocalizedStringKey) -> Text { - Text(s).font(.title) + private func titleText(_ s: LocalizedStringKey) -> some View { + Text(s).font(.largeTitle).bold().multilineTextAlignment(.center) } - private func fileNameText(_ f: String) -> Text { - Text("File: \((f as NSString).lastPathComponent)") + private func fileNameText(_ f: String, font: Font = .caption) -> Text { + Text("File: \((f as NSString).lastPathComponent)").font(font) } - private func migrationsText(_ ms: [String]) -> Text { - Text("Migrations: \(ms.joined(separator: ", "))") + private func migrationsText(_ ms: [String]) -> some View { + (Text("Migrations:").font(.subheadline) + Text(verbatim: "\n") + Text(ms.joined(separator: "\n")).font(.caption)) + .multilineTextAlignment(.center) + .padding(.horizontal, 25) } private func databaseKeyField(onSubmit: @escaping () -> Void) -> some View { PassphraseField(key: $dbKey, placeholder: "Enter passphrase…", valid: validKey(dbKey), onSubmit: onSubmit) + .padding(.vertical, 10) + .padding(.horizontal) + .background( + RoundedRectangle(cornerRadius: 10, style: .continuous) + .fill(Color(uiColor: .tertiarySystemFill)) + ) } private func saveAndOpenButton() -> some View { Button("Save passphrase and open chat") { saveAndRunChat() } + .buttonStyle(OnboardingButtonStyle(isDisabled: false)) } private func openChatButton() -> some View { Button("Open chat") { runChat() } + .buttonStyle(OnboardingButtonStyle(isDisabled: false)) } private func saveAndRunChat() { @@ -192,8 +241,9 @@ struct DatabaseErrorView: View { secondaryButton: .cancel() )) } label: { - Text("Restore database backup").foregroundColor(.red) + Text("Restore database backup") } + .buttonStyle(OnboardingButtonStyle(isDisabled: false)) } private func restoreDb() { @@ -208,6 +258,23 @@ struct DatabaseErrorView: View { )) } } + + private func errorView(_ s: Text? = nil) -> some View { + VStack(spacing: 35) { + Image(systemName: "exclamationmark.triangle.fill") + .resizable() + .frame(width: 50, height: 50) + .foregroundColor(.red) + + if let text = s { + text + .multilineTextAlignment(.center) + .font(.footnote) + } + } + .padding() + .frame(maxWidth: .infinity) + } } struct DatabaseErrorView_Previews: PreviewProvider { diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff index 310b5e8bb3..597041e163 100644 --- a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff @@ -4405,9 +4405,9 @@ This is your link for group %@! Миграцията е завършена No comment provided by engineer. - - Migrations: %@ - Миграции: %@ + + Migrations: + Миграции: No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/bn.xcloc/Localized Contents/bn.xliff b/apps/ios/SimpleX Localizations/bn.xcloc/Localized Contents/bn.xliff index d6fb9a40a4..f7630b9e1f 100644 --- a/apps/ios/SimpleX Localizations/bn.xcloc/Localized Contents/bn.xliff +++ b/apps/ios/SimpleX Localizations/bn.xcloc/Localized Contents/bn.xliff @@ -2235,8 +2235,8 @@ Migration is completed No comment provided by engineer. - - Migrations: %@ + + Migrations: No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff index 79cb15d1ae..e2e77572c5 100644 --- a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff @@ -4253,9 +4253,9 @@ This is your link for group %@! Přenesení dokončeno No comment provided by engineer. - - Migrations: %@ - Migrace: %@ + + Migrations: + Migrace: No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff index 743c08ed00..4d1508dce3 100644 --- a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff +++ b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff @@ -4558,9 +4558,9 @@ Das ist Ihr Link für die Gruppe %@! Die Migration wurde abgeschlossen No comment provided by engineer. - - Migrations: %@ - Migrationen: %@ + + Migrations: + Migrationen: No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/el.xcloc/Localized Contents/el.xliff b/apps/ios/SimpleX Localizations/el.xcloc/Localized Contents/el.xliff index d18eb4483c..9a112d12fa 100644 --- a/apps/ios/SimpleX Localizations/el.xcloc/Localized Contents/el.xliff +++ b/apps/ios/SimpleX Localizations/el.xcloc/Localized Contents/el.xliff @@ -2012,8 +2012,8 @@ Available in v5.1 Migration is completed No comment provided by engineer. - - Migrations: %@ + + Migrations: No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff index 5a2c41379b..9973cfeeba 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff +++ b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff @@ -4600,9 +4600,9 @@ This is your link for group %@! Migration is completed No comment provided by engineer. - - Migrations: %@ - Migrations: %@ + + Migrations: + Migrations: No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff index 59c4bd167f..6161340303 100644 --- a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff +++ b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff @@ -4558,9 +4558,9 @@ This is your link for group %@! Migración completada No comment provided by engineer. - - Migrations: %@ - Migraciones: %@ + + Migrations: + Migraciones: No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff index c41190e0f1..fa5b2967e3 100644 --- a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff @@ -4243,9 +4243,9 @@ This is your link for group %@! Siirto on valmis No comment provided by engineer. - - Migrations: %@ - Siirrot: %@ + + Migrations: + Siirrot: No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff index 3ed363cb10..91359b5e66 100644 --- a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff @@ -4523,9 +4523,9 @@ Voici votre lien pour le groupe %@ ! La migration est terminée No comment provided by engineer. - - Migrations: %@ - Migrations : %@ + + Migrations: + Migrations : No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/he.xcloc/Localized Contents/he.xliff b/apps/ios/SimpleX Localizations/he.xcloc/Localized Contents/he.xliff index 219812651a..813eebc01a 100644 --- a/apps/ios/SimpleX Localizations/he.xcloc/Localized Contents/he.xliff +++ b/apps/ios/SimpleX Localizations/he.xcloc/Localized Contents/he.xliff @@ -2497,9 +2497,9 @@ Available in v5.1 ההעברה הושלמה No comment provided by engineer. - - Migrations: %@ - העברות: %@ + + Migrations: + העברות: No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff index 81cece7794..80ec462622 100644 --- a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff @@ -4558,9 +4558,9 @@ Ez az Ön hivatkozása a(z) %@ csoporthoz! Az átköltöztetés befejeződött No comment provided by engineer. - - Migrations: %@ - Átköltöztetések: %@ + + Migrations: + Átköltöztetések: No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff index 3b75e36a86..5b0c2cdb99 100644 --- a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff +++ b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff @@ -4558,9 +4558,9 @@ Questo è il tuo link per il gruppo %@! La migrazione è completata No comment provided by engineer. - - Migrations: %@ - Migrazioni: %@ + + Migrations: + Migrazioni: No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff index 7f97220bc5..12a34e3569 100644 --- a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff +++ b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff @@ -4269,9 +4269,9 @@ This is your link for group %@! 移行が完了しました No comment provided by engineer. - - Migrations: %@ - 移行: %@ + + Migrations: + 移行 No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff index 06ab82cf2a..b7d4260354 100644 --- a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff @@ -4558,9 +4558,9 @@ Dit is jouw link voor groep %@! Migratie is voltooid No comment provided by engineer. - - Migrations: %@ - Migraties: %@ + + Migrations: + Migraties: No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff index 531d50f522..38e6c8991d 100644 --- a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff @@ -4548,9 +4548,9 @@ To jest twój link do grupy %@! Migracja została zakończona No comment provided by engineer. - - Migrations: %@ - Migracje: %@ + + Migrations: + Migracje: No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/pt.xcloc/Localized Contents/pt.xliff b/apps/ios/SimpleX Localizations/pt.xcloc/Localized Contents/pt.xliff index cdadd677f9..e20181e4f7 100644 --- a/apps/ios/SimpleX Localizations/pt.xcloc/Localized Contents/pt.xliff +++ b/apps/ios/SimpleX Localizations/pt.xcloc/Localized Contents/pt.xliff @@ -2115,8 +2115,8 @@ Available in v5.1 Migration is completed No comment provided by engineer. - - Migrations: %@ + + Migrations: No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff index 119f1650a0..558dd682f1 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff @@ -4558,9 +4558,9 @@ This is your link for group %@! Перемещение данных завершено No comment provided by engineer. - - Migrations: %@ - Миграции: %@ + + Migrations: + Миграции: No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff index e16565b6fa..6fc740ccea 100644 --- a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff +++ b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff @@ -4226,9 +4226,9 @@ This is your link for group %@! การโยกย้ายเสร็จสมบูรณ์ No comment provided by engineer. - - Migrations: %@ - การย้ายข้อมูล: %@ + + Migrations: + การย้ายข้อมูล No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff index 0b2149e9ce..9c9fe3e253 100644 --- a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff @@ -4558,9 +4558,9 @@ Bu senin grup için bağlantın %@! Geçiş tamamlandı No comment provided by engineer. - - Migrations: %@ - Geçişler: %@ + + Migrations: + Geçişler: No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff index 339f06687d..b641bbe8cf 100644 --- a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff @@ -4523,9 +4523,9 @@ This is your link for group %@! Міграцію завершено No comment provided by engineer. - - Migrations: %@ - Міграції: %@ + + Migrations: + Міграції: No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff index 3f48211025..1daae62a6d 100644 --- a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff +++ b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff @@ -4523,9 +4523,9 @@ This is your link for group %@! 迁移完成 No comment provided by engineer. - - Migrations: %@ - 迁移:%@ + + Migrations: + 迁移 No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/zh-Hant.xcloc/Localized Contents/zh-Hant.xliff b/apps/ios/SimpleX Localizations/zh-Hant.xcloc/Localized Contents/zh-Hant.xliff index 1c1ae53673..da4f843974 100644 --- a/apps/ios/SimpleX Localizations/zh-Hant.xcloc/Localized Contents/zh-Hant.xliff +++ b/apps/ios/SimpleX Localizations/zh-Hant.xcloc/Localized Contents/zh-Hant.xliff @@ -4781,8 +4781,8 @@ Available in v5.1 訊息 & 檔案 No comment provided by engineer. - - Migrations: %@ + + Migrations: 遷移:%@ No comment provided by engineer. diff --git a/apps/ios/bg.lproj/Localizable.strings b/apps/ios/bg.lproj/Localizable.strings index ccfd9a7b98..da890d4ecb 100644 --- a/apps/ios/bg.lproj/Localizable.strings +++ b/apps/ios/bg.lproj/Localizable.strings @@ -2510,7 +2510,7 @@ "Migration is completed" = "Миграцията е завършена"; /* No comment provided by engineer. */ -"Migrations: %@" = "Миграции: %@"; +"Migrations:" = "Миграции:"; /* time unit */ "minutes" = "минути"; diff --git a/apps/ios/cs.lproj/Localizable.strings b/apps/ios/cs.lproj/Localizable.strings index a00adef700..611be02606 100644 --- a/apps/ios/cs.lproj/Localizable.strings +++ b/apps/ios/cs.lproj/Localizable.strings @@ -2029,7 +2029,7 @@ "Migration is completed" = "Přenesení dokončeno"; /* No comment provided by engineer. */ -"Migrations: %@" = "Migrace: %@"; +"Migrations:" = "Migrace:"; /* time unit */ "minutes" = "minut"; diff --git a/apps/ios/de.lproj/Localizable.strings b/apps/ios/de.lproj/Localizable.strings index f526eaf7e1..8ea6a30716 100644 --- a/apps/ios/de.lproj/Localizable.strings +++ b/apps/ios/de.lproj/Localizable.strings @@ -2984,7 +2984,7 @@ "Migration is completed" = "Die Migration wurde abgeschlossen"; /* No comment provided by engineer. */ -"Migrations: %@" = "Migrationen: %@"; +"Migrations:" = "Migrationen:"; /* time unit */ "minutes" = "Minuten"; diff --git a/apps/ios/es.lproj/Localizable.strings b/apps/ios/es.lproj/Localizable.strings index c2f982d0d4..10b8bc317c 100644 --- a/apps/ios/es.lproj/Localizable.strings +++ b/apps/ios/es.lproj/Localizable.strings @@ -2984,7 +2984,7 @@ "Migration is completed" = "Migración completada"; /* No comment provided by engineer. */ -"Migrations: %@" = "Migraciones: %@"; +"Migrations:" = "Migraciones:"; /* time unit */ "minutes" = "minutos"; diff --git a/apps/ios/fi.lproj/Localizable.strings b/apps/ios/fi.lproj/Localizable.strings index 2faab1dbd9..081e8735a1 100644 --- a/apps/ios/fi.lproj/Localizable.strings +++ b/apps/ios/fi.lproj/Localizable.strings @@ -2005,7 +2005,7 @@ "Migration is completed" = "Siirto on valmis"; /* No comment provided by engineer. */ -"Migrations: %@" = "Siirrot: %@"; +"Migrations:" = "Siirrot:"; /* time unit */ "minutes" = "minuuttia"; diff --git a/apps/ios/fr.lproj/Localizable.strings b/apps/ios/fr.lproj/Localizable.strings index 92b66efb72..4b4a5aaf4d 100644 --- a/apps/ios/fr.lproj/Localizable.strings +++ b/apps/ios/fr.lproj/Localizable.strings @@ -2885,7 +2885,7 @@ "Migration is completed" = "La migration est terminée"; /* No comment provided by engineer. */ -"Migrations: %@" = "Migrations : %@"; +"Migrations:" = "Migrations :"; /* time unit */ "minutes" = "minutes"; diff --git a/apps/ios/hu.lproj/Localizable.strings b/apps/ios/hu.lproj/Localizable.strings index 5668b5367f..a68a9e11b1 100644 --- a/apps/ios/hu.lproj/Localizable.strings +++ b/apps/ios/hu.lproj/Localizable.strings @@ -2984,7 +2984,7 @@ "Migration is completed" = "Az átköltöztetés befejeződött"; /* No comment provided by engineer. */ -"Migrations: %@" = "Átköltöztetések: %@"; +"Migrations:" = "Átköltöztetések:"; /* time unit */ "minutes" = "perc"; diff --git a/apps/ios/it.lproj/Localizable.strings b/apps/ios/it.lproj/Localizable.strings index 2e2aea3f3c..43fd26e534 100644 --- a/apps/ios/it.lproj/Localizable.strings +++ b/apps/ios/it.lproj/Localizable.strings @@ -2984,7 +2984,7 @@ "Migration is completed" = "La migrazione è completata"; /* No comment provided by engineer. */ -"Migrations: %@" = "Migrazioni: %@"; +"Migrations:" = "Migrazioni:"; /* time unit */ "minutes" = "minuti"; diff --git a/apps/ios/ja.lproj/Localizable.strings b/apps/ios/ja.lproj/Localizable.strings index c2f2717b1b..93735ef2d1 100644 --- a/apps/ios/ja.lproj/Localizable.strings +++ b/apps/ios/ja.lproj/Localizable.strings @@ -2083,7 +2083,7 @@ "Migration is completed" = "移行が完了しました"; /* No comment provided by engineer. */ -"Migrations: %@" = "移行: %@"; +"Migrations:" = "移行"; /* time unit */ "minutes" = "分"; diff --git a/apps/ios/nl.lproj/Localizable.strings b/apps/ios/nl.lproj/Localizable.strings index e6320e6208..94cb3115a9 100644 --- a/apps/ios/nl.lproj/Localizable.strings +++ b/apps/ios/nl.lproj/Localizable.strings @@ -2984,7 +2984,7 @@ "Migration is completed" = "Migratie is voltooid"; /* No comment provided by engineer. */ -"Migrations: %@" = "Migraties: %@"; +"Migrations:" = "Migraties:"; /* time unit */ "minutes" = "minuten"; diff --git a/apps/ios/pl.lproj/Localizable.strings b/apps/ios/pl.lproj/Localizable.strings index 2dde086020..77c724a6b1 100644 --- a/apps/ios/pl.lproj/Localizable.strings +++ b/apps/ios/pl.lproj/Localizable.strings @@ -2957,7 +2957,7 @@ "Migration is completed" = "Migracja została zakończona"; /* No comment provided by engineer. */ -"Migrations: %@" = "Migracje: %@"; +"Migrations:" = "Migracje:"; /* time unit */ "minutes" = "minuty"; diff --git a/apps/ios/ru.lproj/Localizable.strings b/apps/ios/ru.lproj/Localizable.strings index 631280ae84..272484ac47 100644 --- a/apps/ios/ru.lproj/Localizable.strings +++ b/apps/ios/ru.lproj/Localizable.strings @@ -2984,7 +2984,7 @@ "Migration is completed" = "Перемещение данных завершено"; /* No comment provided by engineer. */ -"Migrations: %@" = "Миграции: %@"; +"Migrations:" = "Миграции:"; /* time unit */ "minutes" = "минут"; diff --git a/apps/ios/th.lproj/Localizable.strings b/apps/ios/th.lproj/Localizable.strings index d37dd725df..85295df87d 100644 --- a/apps/ios/th.lproj/Localizable.strings +++ b/apps/ios/th.lproj/Localizable.strings @@ -1948,7 +1948,7 @@ "Migration is completed" = "การโยกย้ายเสร็จสมบูรณ์"; /* No comment provided by engineer. */ -"Migrations: %@" = "การย้ายข้อมูล: %@"; +"Migrations:" = "การย้ายข้อมูล"; /* time unit */ "minutes" = "นาที"; diff --git a/apps/ios/tr.lproj/Localizable.strings b/apps/ios/tr.lproj/Localizable.strings index cc250808be..bbaa1a5657 100644 --- a/apps/ios/tr.lproj/Localizable.strings +++ b/apps/ios/tr.lproj/Localizable.strings @@ -2984,7 +2984,7 @@ "Migration is completed" = "Geçiş tamamlandı"; /* No comment provided by engineer. */ -"Migrations: %@" = "Geçişler: %@"; +"Migrations:" = "Geçişler:"; /* time unit */ "minutes" = "dakikalar"; diff --git a/apps/ios/uk.lproj/Localizable.strings b/apps/ios/uk.lproj/Localizable.strings index eb92d191c6..7f6a8bb677 100644 --- a/apps/ios/uk.lproj/Localizable.strings +++ b/apps/ios/uk.lproj/Localizable.strings @@ -2885,7 +2885,7 @@ "Migration is completed" = "Міграцію завершено"; /* No comment provided by engineer. */ -"Migrations: %@" = "Міграції: %@"; +"Migrations:" = "Міграції:"; /* time unit */ "minutes" = "хвилини"; diff --git a/apps/ios/zh-Hans.lproj/Localizable.strings b/apps/ios/zh-Hans.lproj/Localizable.strings index 89773a7481..a15b7d45fe 100644 --- a/apps/ios/zh-Hans.lproj/Localizable.strings +++ b/apps/ios/zh-Hans.lproj/Localizable.strings @@ -2885,7 +2885,7 @@ "Migration is completed" = "迁移完成"; /* No comment provided by engineer. */ -"Migrations: %@" = "迁移:%@"; +"Migrations:" = "迁移"; /* time unit */ "minutes" = "分钟"; From 096dec2c7b3a2d622e71643c9c6eb9d5e4b13e76 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Wed, 27 Nov 2024 23:42:02 +0000 Subject: [PATCH 078/167] ui: translations (#5264) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Translated using Weblate (Spanish) Currently translated at 100.0% (2089 of 2089 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/es/ * Translated using Weblate (Spanish) Currently translated at 100.0% (1843 of 1843 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/es/ * Translated using Weblate (Dutch) Currently translated at 100.0% (2089 of 2089 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/nl/ * Translated using Weblate (Dutch) Currently translated at 100.0% (1843 of 1843 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/nl/ * Translated using Weblate (Czech) Currently translated at 96.1% (2008 of 2089 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/cs/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2089 of 2089 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (1843 of 1843 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2089 of 2089 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (1843 of 1843 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hu/ * Translated using Weblate (Arabic) Currently translated at 100.0% (2089 of 2089 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ar/ * Translated using Weblate (Vietnamese) Currently translated at 44.0% (921 of 2089 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/vi/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (2089 of 2089 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/uk/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2089 of 2089 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Vietnamese) Currently translated at 44.8% (936 of 2089 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/vi/ * Translated using Weblate (Indonesian) Currently translated at 12.7% (267 of 2089 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/id/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2089 of 2089 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (1843 of 1843 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hu/ * Translated using Weblate (Vietnamese) Currently translated at 45.7% (956 of 2089 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/vi/ * Translated using Weblate (Korean) Currently translated at 46.2% (967 of 2089 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ko/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2089 of 2089 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (1843 of 1843 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hu/ * Translated using Weblate (Japanese) Currently translated at 89.4% (1869 of 2089 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ja/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2089 of 2089 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (1843 of 1843 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hu/ * Translated using Weblate (Vietnamese) Currently translated at 46.9% (980 of 2089 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/vi/ * Translated using Weblate (Indonesian) Currently translated at 12.8% (268 of 2089 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/id/ * Translated using Weblate (French) Currently translated at 100.0% (2089 of 2089 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/fr/ * Translated using Weblate (French) Currently translated at 100.0% (1843 of 1843 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/fr/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2089 of 2089 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (1843 of 1843 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hu/ * Translated using Weblate (Vietnamese) Currently translated at 51.9% (1086 of 2089 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/vi/ * Translated using Weblate (Vietnamese) Currently translated at 52.8% (1104 of 2089 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/vi/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2089 of 2089 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (1843 of 1843 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hu/ * Translated using Weblate (Ukrainian) Currently translated at 96.6% (1782 of 1843 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/uk/ * Translated using Weblate (Italian) Currently translated at 100.0% (2089 of 2089 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/it/ * Translated using Weblate (Italian) Currently translated at 100.0% (1843 of 1843 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/it/ * Translated using Weblate (Japanese) Currently translated at 92.3% (1929 of 2089 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ja/ * Translated using Weblate (Portuguese (Brazil)) Currently translated at 98.9% (2068 of 2089 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/pt_BR/ * Translated using Weblate (Portuguese (Brazil)) Currently translated at 50.0% (923 of 1843 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/pt_BR/ * Translated using Weblate (Greek) Currently translated at 19.0% (397 of 2089 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/el/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2089 of 2089 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (1843 of 1843 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hu/ * Translated using Weblate (Vietnamese) Currently translated at 55.2% (1155 of 2089 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/vi/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2089 of 2089 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (1843 of 1843 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hu/ * Translated using Weblate (Vietnamese) Currently translated at 56.2% (1175 of 2089 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/vi/ * Translated using Weblate (Korean) Currently translated at 51.1% (1068 of 2089 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ko/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2089 of 2089 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (1843 of 1843 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hu/ * Translated using Weblate (Korean) Currently translated at 60.8% (1271 of 2089 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ko/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2089 of 2089 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (1843 of 1843 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hu/ * Translated using Weblate (Vietnamese) Currently translated at 58.0% (1213 of 2089 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/vi/ * Translated using Weblate (Korean) Currently translated at 63.0% (1317 of 2089 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ko/ * Translated using Weblate (Indonesian) Currently translated at 16.1% (337 of 2089 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/id/ * Translated using Weblate (Japanese) Currently translated at 92.6% (1936 of 2089 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ja/ * Translated using Weblate (Korean) Currently translated at 66.7% (1394 of 2089 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ko/ * Translated using Weblate (Vietnamese) Currently translated at 59.8% (1251 of 2089 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/vi/ * Translated using Weblate (Indonesian) Currently translated at 54.6% (1141 of 2089 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/id/ * Translated using Weblate (Korean) Currently translated at 66.7% (1395 of 2089 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ko/ * Translated using Weblate (Indonesian) Currently translated at 61.0% (1275 of 2089 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/id/ * Translated using Weblate (Vietnamese) Currently translated at 60.8% (1271 of 2089 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/vi/ * Translated using Weblate (Korean) Currently translated at 67.6% (1414 of 2089 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ko/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2089 of 2089 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (1843 of 1843 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hu/ * Translated using Weblate (Vietnamese) Currently translated at 61.7% (1291 of 2089 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/vi/ * Translated using Weblate (Vietnamese) Currently translated at 62.6% (1308 of 2089 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/vi/ * Translated using Weblate (Lithuanian) Currently translated at 83.6% (1747 of 2089 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/lt/ * Translated using Weblate (Vietnamese) Currently translated at 62.7% (1310 of 2089 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/vi/ * Translated using Weblate (Indonesian) Currently translated at 61.2% (1279 of 2089 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/id/ * Translated using Weblate (Vietnamese) Currently translated at 63.6% (1330 of 2089 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/vi/ * Translated using Weblate (Vietnamese) Currently translated at 64.3% (1344 of 2089 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/vi/ * process localizations * ru * export localizations --------- Co-authored-by: No name Co-authored-by: M1K4 Co-authored-by: zenobit Co-authored-by: summoner001 Co-authored-by: jonnysemon Co-authored-by: tuananh-ng <158744840+tuananh-ng@users.noreply.github.com> Co-authored-by: Bezruchenko Simon Co-authored-by: billy appetie Co-authored-by: 장재원 Co-authored-by: Miyu Sakatsuki Co-authored-by: Ophiushi <41908476+ishi-sama@users.noreply.github.com> Co-authored-by: aquaticexpectancy Co-authored-by: Random Co-authored-by: Dampuzakura Co-authored-by: Luis Henrique Rodrigues Dos Santos Co-authored-by: Dinos B Co-authored-by: translatorforkr Co-authored-by: d4f5409d Co-authored-by: Rafi Co-authored-by: Vaclovas Intas --- .../es.xcloc/Localized Contents/es.xliff | 1 - .../fr.xcloc/Localized Contents/fr.xliff | 75 +- .../hu.xcloc/Localized Contents/hu.xliff | 156 +-- .../it.xcloc/Localized Contents/it.xliff | 2 +- .../Localized Contents/pt-BR.xliff | 84 ++ .../ru.xcloc/Localized Contents/ru.xliff | 2 +- .../uk.xcloc/Localized Contents/uk.xliff | 3 + apps/ios/bg.lproj/Localizable.strings | 144 +-- apps/ios/cs.lproj/Localizable.strings | 131 +-- apps/ios/de.lproj/Localizable.strings | 153 +-- apps/ios/es.lproj/Localizable.strings | 150 +-- apps/ios/fi.lproj/Localizable.strings | 131 +-- apps/ios/fr.lproj/Localizable.strings | 349 ++++-- apps/ios/hu.lproj/Localizable.strings | 307 ++--- apps/ios/it.lproj/Localizable.strings | 155 +-- apps/ios/ja.lproj/Localizable.strings | 131 +-- apps/ios/nl.lproj/Localizable.strings | 153 +-- apps/ios/pl.lproj/Localizable.strings | 153 +-- apps/ios/ru.lproj/Localizable.strings | 159 +-- apps/ios/th.lproj/Localizable.strings | 131 +-- apps/ios/tr.lproj/Localizable.strings | 153 +-- apps/ios/uk.lproj/Localizable.strings | 162 +-- apps/ios/zh-Hans.lproj/Localizable.strings | 153 +-- .../commonMain/resources/MR/ar/strings.xml | 4 +- .../commonMain/resources/MR/el/strings.xml | 155 +++ .../commonMain/resources/MR/fr/strings.xml | 71 +- .../commonMain/resources/MR/hu/strings.xml | 179 ++- .../commonMain/resources/MR/in/strings.xml | 1028 ++++++++++++++++- .../commonMain/resources/MR/it/strings.xml | 2 +- .../commonMain/resources/MR/ja/strings.xml | 89 +- .../commonMain/resources/MR/ko/strings.xml | 683 +++++++++-- .../commonMain/resources/MR/lt/strings.xml | 4 + .../resources/MR/pt-rBR/strings.xml | 1 + .../commonMain/resources/MR/uk/strings.xml | 10 + .../commonMain/resources/MR/vi/strings.xml | 453 ++++++++ 35 files changed, 3584 insertions(+), 2133 deletions(-) diff --git a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff index 6161340303..01e534f424 100644 --- a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff +++ b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff @@ -4823,7 +4823,6 @@ This is your link for group %@! No push server - No push server No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff index 91359b5e66..ecea1c6eb7 100644 --- a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff @@ -132,6 +132,7 @@ %1$@, %2$@ + %1$@, %2$@ format for date separator in chat @@ -156,18 +157,22 @@ %d file(s) are still being downloaded. + %d fichier(s) en cours de téléchargement. forward confirmation reason %d file(s) failed to download. + Le téléchargement de %d fichier(s) a échoué. forward confirmation reason %d file(s) were deleted. + Le(s) fichier(s) %d a(ont) été supprimé(s). forward confirmation reason %d file(s) were not downloaded. + Le(s) fichier(s) %d n'a (n'ont) pas été téléchargé(s). forward confirmation reason @@ -177,6 +182,7 @@ %d messages not forwarded + %d messages non transférés alert title @@ -954,6 +960,7 @@ App session + Session de l'app No comment provided by engineer. @@ -1063,6 +1070,7 @@ Auto-accept settings + Paramètres de réception automatique alert title @@ -1092,6 +1100,7 @@ Better calls + Appels améliorés No comment provided by engineer. @@ -1101,6 +1110,7 @@ Better message dates. + Meilleures dates de messages. No comment provided by engineer. @@ -1115,14 +1125,17 @@ Better notifications + Notifications améliorées No comment provided by engineer. Better security ✅ + Sécurité accrue ✅ No comment provided by engineer. Better user experience + Une meilleure expérience pour l'utilisateur No comment provided by engineer. @@ -1413,6 +1426,7 @@ Chat preferences were changed. + Les préférences de discussion ont été modifiées. alert message @@ -1864,6 +1878,7 @@ Il s'agit de votre propre lien unique ! Corner + Coin No comment provided by engineer. @@ -1996,6 +2011,7 @@ Il s'agit de votre propre lien unique ! Customizable message shape. + Forme des messages personnalisable. No comment provided by engineer. @@ -2295,6 +2311,7 @@ Il s'agit de votre propre lien unique ! Delete or moderate up to 200 messages. + Supprimer ou modérer jusqu'à 200 messages. No comment provided by engineer. @@ -2553,6 +2570,7 @@ Il s'agit de votre propre lien unique ! Do not use credentials with proxy. + Ne pas utiliser d'identifiants avec le proxy. No comment provided by engineer. @@ -2598,6 +2616,7 @@ Il s'agit de votre propre lien unique ! Download files + Télécharger les fichiers alert action @@ -2893,6 +2912,7 @@ Il s'agit de votre propre lien unique ! Error changing connection profile + Erreur lors du changement de profil de connexion No comment provided by engineer. @@ -2907,6 +2927,7 @@ Il s'agit de votre propre lien unique ! Error changing to incognito! + Erreur lors du passage en mode incognito ! No comment provided by engineer. @@ -3030,6 +3051,7 @@ Il s'agit de votre propre lien unique ! Error migrating settings + Erreur lors de la migration des paramètres No comment provided by engineer. @@ -3133,6 +3155,7 @@ Il s'agit de votre propre lien unique ! Error switching profile + Erreur lors du changement de profil No comment provided by engineer. @@ -3281,6 +3304,8 @@ Il s'agit de votre propre lien unique ! File errors: %@ + Erreurs de fichier : +%@ alert message @@ -3436,6 +3461,7 @@ Il s'agit de votre propre lien unique ! Forward %d message(s)? + Transférer %d message(s) ? alert title @@ -3445,14 +3471,17 @@ Il s'agit de votre propre lien unique ! Forward messages + Transférer les messages alert action Forward messages without files? + Transférer les messages sans les fichiers ? alert message Forward up to 20 messages at once. + Transférez jusqu'à 20 messages à la fois. No comment provided by engineer. @@ -3467,6 +3496,7 @@ Il s'agit de votre propre lien unique ! Forwarding %lld messages + Transfert des %lld messages No comment provided by engineer. @@ -3768,6 +3798,7 @@ Erreur : %2$@ IP address + Adresse IP No comment provided by engineer. @@ -3848,6 +3879,8 @@ Erreur : %2$@ Improved delivery, reduced traffic usage. More improvements are coming soon! + Amélioration de la distribution, réduction de l'utilisation du trafic. +D'autres améliorations sont à venir ! No comment provided by engineer. @@ -4402,6 +4435,7 @@ Voici votre lien pour le groupe %@ ! Message shape + Forme du message No comment provided by engineer. @@ -4456,6 +4490,7 @@ Voici votre lien pour le groupe %@ ! Messages were deleted after you selected them. + Les messages ont été supprimés après avoir été sélectionnés. alert message @@ -4627,10 +4662,12 @@ Voici votre lien pour le groupe %@ ! New SOCKS credentials will be used every time you start the app. + De nouveaux identifiants SOCKS seront utilisés chaque fois que vous démarrerez l'application. No comment provided by engineer. New SOCKS credentials will be used for each server. + De nouveaux identifiants SOCKS seront utilisées pour chaque serveur. No comment provided by engineer. @@ -4771,10 +4808,12 @@ Voici votre lien pour le groupe %@ ! No permission to record speech + Enregistrement des conversations non autorisé No comment provided by engineer. No permission to record video + Enregistrement de la vidéo non autorisé No comment provided by engineer. @@ -4825,6 +4864,7 @@ Voici votre lien pour le groupe %@ ! Nothing to forward! + Rien à transférer ! alert title @@ -5042,7 +5082,7 @@ Nécessite l'activation d'un VPN. Or show this code - Ou présenter ce code + Ou montrez ce code No comment provided by engineer. @@ -5057,6 +5097,8 @@ Nécessite l'activation d'un VPN. Other file errors: %@ + Autres erreurs de fichiers : +%@ alert message @@ -5096,6 +5138,7 @@ Nécessite l'activation d'un VPN. Password + Mot de passe No comment provided by engineer. @@ -5244,6 +5287,7 @@ Erreur : %@ Port + Port No comment provided by engineer. @@ -5434,6 +5478,7 @@ Activez-le dans les paramètres *Réseau et serveurs*. Proxy requires password + Le proxy est protégé par un mot de passe No comment provided by engineer. @@ -5658,6 +5703,7 @@ Activez-le dans les paramètres *Réseau et serveurs*. Remove archive? + Supprimer l'archive ? No comment provided by engineer. @@ -5850,6 +5896,7 @@ Activez-le dans les paramètres *Réseau et serveurs*. SOCKS proxy + proxy SOCKS No comment provided by engineer. @@ -5948,6 +5995,7 @@ Activez-le dans les paramètres *Réseau et serveurs*. Save your profile? + Sauvegarder votre profil ? alert title @@ -5972,6 +6020,7 @@ Activez-le dans les paramètres *Réseau et serveurs*. Saving %lld messages + Sauvegarde de %lld messages No comment provided by engineer. @@ -5981,7 +6030,7 @@ Activez-le dans les paramètres *Réseau et serveurs*. Scan / Paste link - Scanner / Coller le lien + Scanner / Coller un lien No comment provided by engineer. @@ -6056,6 +6105,7 @@ Activez-le dans les paramètres *Réseau et serveurs*. Select chat profile + Sélectionner un profil de discussion No comment provided by engineer. @@ -6270,6 +6320,7 @@ Activez-le dans les paramètres *Réseau et serveurs*. Server + Serveur No comment provided by engineer. @@ -6410,6 +6461,7 @@ Activez-le dans les paramètres *Réseau et serveurs*. Settings were changed. + Les paramètres ont été modifiés. alert message @@ -6462,11 +6514,12 @@ Activez-le dans les paramètres *Réseau et serveurs*. Share profile + Partager le profil No comment provided by engineer. Share this 1-time invite link - Partager ce lien d'invitation unique + Partagez ce lien d'invitation unique No comment provided by engineer. @@ -6609,6 +6662,7 @@ Activez-le dans les paramètres *Réseau et serveurs*. SimpleX protocols reviewed by Trail of Bits. + Protocoles SimpleX audité par Trail of Bits. No comment provided by engineer. @@ -6643,6 +6697,7 @@ Activez-le dans les paramètres *Réseau et serveurs*. Some app settings were not migrated. + Certains paramètres de l'application n'ont pas été migrés. No comment provided by engineer. @@ -6792,10 +6847,12 @@ Activez-le dans les paramètres *Réseau et serveurs*. Switch audio and video during the call. + Passer de l'audio à la vidéo pendant l'appel. No comment provided by engineer. Switch chat profile for 1-time invitations. + Changer de profil de chat pour les invitations à usage unique. No comment provided by engineer. @@ -6835,6 +6892,7 @@ Activez-le dans les paramètres *Réseau et serveurs*. Tail + Queue No comment provided by engineer. @@ -7042,6 +7100,7 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. The uploaded database archive will be permanently removed from the servers. + L'archive de la base de données envoyée sera définitivement supprimée des serveurs. No comment provided by engineer. @@ -7140,7 +7199,7 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. To connect, your contact can scan QR code or use the link in the app. - Pour se connecter, votre contact peut scanner le code QR ou utiliser le lien dans l'application. + Pour se connecter, votre contact peut scanner un code QR ou utiliser un lien dans l'app. No comment provided by engineer. @@ -7185,10 +7244,12 @@ Vous serez invité à confirmer l'authentification avant que cette fonction ne s To record speech please grant permission to use Microphone. + Si vous souhaitez enregistrer une conversation, veuillez autoriser l'utilisation du microphone. No comment provided by engineer. To record video please grant permission to use Camera. + Si vous souhaitez enregistrer une vidéo, veuillez autoriser l'utilisation de la caméra. No comment provided by engineer. @@ -7476,6 +7537,7 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Use SOCKS proxy + Utiliser un proxy SOCKS No comment provided by engineer. @@ -7562,6 +7624,7 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Username + Nom d'utilisateur No comment provided by engineer. @@ -8183,6 +8246,7 @@ Répéter la demande de connexion ? Your chat preferences + Vos préférences de discussion alert title @@ -8192,6 +8256,7 @@ Répéter la demande de connexion ? Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile. + Votre connexion a été déplacée vers %@ mais une erreur inattendue s'est produite lors de la redirection vers le profil. No comment provided by engineer. @@ -8211,6 +8276,7 @@ Répéter la demande de connexion ? Your credentials may be sent unencrypted. + Vos informations d'identification peuvent être envoyées non chiffrées. No comment provided by engineer. @@ -8250,6 +8316,7 @@ Répéter la demande de connexion ? Your profile was changed. If you save it, the updated profile will be sent to all your contacts. + Votre profil a été modifié. Si vous l'enregistrez, le profil mis à jour sera envoyé à tous vos contacts. alert message diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff index 80ec462622..bca18b73e6 100644 --- a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff @@ -247,7 +247,7 @@ %lld messages blocked by admin - %lld üzenetet letiltott az admin + %lld üzenetet letiltott az adminisztrátor No comment provided by engineer. @@ -542,13 +542,13 @@ A separate TCP connection will be used **for each chat profile you have in the app**. - A rendszer külön TCP-kapcsolatot fog használni **az alkalmazásban található minden csevegési profilhoz**. + **Az összes csevegési profiljához az alkalmazásban** külön TCP-kapcsolat (és SOCKS-hitelesítőadat) lesz használva. No comment provided by engineer. A separate TCP connection will be used **for each contact and group member**. **Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail. - **Minden egyes kapcsolathoz és csoporttaghoz** külön TCP-kapcsolat lesz használva. + **Az összes ismerőséhez és csoporttaghoz** külön TCP-kapcsolat (és SOCKS-hitelesítőadat) lesz használva. **Megjegyzés:** ha sok kapcsolata van, az akkumulátor-használat és az adatforgalom jelentősen megnövekedhet, és néhány kapcsolódási kísérlet sikertelen lehet. No comment provided by engineer. @@ -696,12 +696,12 @@ Admins can block a member for all. - Az adminok egy tagot mindenki számára letilthatnak. + Az adminisztrátorok egy tagot a csoport összes tagja számára letilthatnak. No comment provided by engineer. Admins can create the links to join groups. - Az adminok hivatkozásokat hozhatnak létre a csoportokhoz való kapcsolódáshoz. + Az adminisztrátorok hivatkozásokat hozhatnak létre a csoportokhoz való kapcsolódáshoz. No comment provided by engineer. @@ -716,27 +716,27 @@ All app data is deleted. - Minden alkalmazásadat törölve. + Az összes alkalmazásadat törölve. No comment provided by engineer. All chats and messages will be deleted - this cannot be undone! - Minden csevegés és üzenet törlésre kerül - ez a művelet nem vonható vissza! + Az összes csevegés és üzenet törlésre kerül - ez a művelet nem vonható vissza! No comment provided by engineer. All data is erased when it is entered. - A jelkód megadása után minden adat törlésre kerül. + A jelkód megadása után az összes adat törlésre kerül. No comment provided by engineer. All data is private to your device. - Minden adat biztonságban van az eszközén. + Az összes adat biztonságban van az eszközén. No comment provided by engineer. All group members will remain connected. - Minden csoporttag kapcsolódva marad. + Az összes csoporttag kapcsolatban marad. No comment provided by engineer. @@ -745,27 +745,27 @@ All messages will be deleted - this cannot be undone! - Minden üzenet törlésre kerül – ez a művelet nem vonható vissza! + Az összes üzenet törlésre kerül – ez a művelet nem vonható vissza! No comment provided by engineer. All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you. - Minden üzenet törlésre kerül - ez a művelet nem vonható vissza! Az üzenetek CSAK az Ön számára törlődnek. + Az összes üzenet törlésre kerül - ez a művelet nem vonható vissza! Az üzenetek CSAK az Ön számára törlődnek. No comment provided by engineer. All new messages from %@ will be hidden! - Minden új üzenet elrejtésre kerül tőle: %@! + Az összes új üzenet elrejtésre kerül tőle: %@! No comment provided by engineer. All profiles - Minden profil + Összes profil profile dropdown All your contacts will remain connected. - Minden ismerősével kapcsolatban marad. + Az összes ismerősével kapcsolatban marad. No comment provided by engineer. @@ -775,7 +775,7 @@ All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays. - Minden ismerőse, a beszélgetései és a fájljai biztonságosan titkosításra kerülnek, melyek részletekben feltöltődnek a beállított XFTP-közvetítő-kiszolgálóra. + Az összes ismerőse, -beszélgetése és -fájlja biztonságosan titkosításra kerülnek, melyek részletekben feltöltődnek a beállított XFTP-közvetítő-kiszolgálóra. No comment provided by engineer. @@ -945,7 +945,7 @@ App icon - Alkalmazás ikon + Alkalmazásikon No comment provided by engineer. @@ -1150,7 +1150,7 @@ Block for all - Letiltás mindenki számára + Letiltás az összes tag számára No comment provided by engineer. @@ -1165,7 +1165,7 @@ Block member for all? - Mindenki számára letiltja ezt a tagot? + Az összes tag számára letiltja ezt a tagot? No comment provided by engineer. @@ -1175,7 +1175,7 @@ Blocked by admin - Az admin letiltotta + Az adminisztrátor letiltotta No comment provided by engineer. @@ -1255,12 +1255,12 @@ Can't invite contact! - Ismerős meghívása nem lehetséges! + Nem lehet meghívni az ismerőst! No comment provided by engineer. Can't invite contacts! - Ismerősök meghívása nem lehetséges! + Nem lehet meghívni az ismerősöket! No comment provided by engineer. @@ -1605,7 +1605,7 @@ Confirm files from unknown servers. - Ismeretlen kiszolgálókról származó fájlok jóváhagyása. + Ismeretlen kiszolgálókról származó fájlok megerősítése. No comment provided by engineer. @@ -1694,12 +1694,12 @@ Ez az Ön egyszer használható hivatkozása! Connect with %@ - Kapcsolódás ezzel: %@ + Kapcsolódás a következővel: %@ No comment provided by engineer. Connected - Kapcsolódva + Kapcsolódott No comment provided by engineer. @@ -2176,7 +2176,7 @@ Ez az Ön egyszer használható hivatkozása! Delete all files - Minden fájl törlése + Az összes fájl törlése No comment provided by engineer. @@ -2241,12 +2241,12 @@ Ez az Ön egyszer használható hivatkozása! Delete files for all chat profiles - Fájlok törlése minden csevegési profilból + Fájlok törlése az összes csevegési profilból No comment provided by engineer. Delete for everyone - Törlés mindenkinél + Törlés az összes tagnál chat feature @@ -2485,7 +2485,7 @@ Ez az Ön egyszer használható hivatkozása! Disable for all - Letiltás mindenki számára + Letiltás az összes tag számára No comment provided by engineer. @@ -2699,7 +2699,7 @@ Ez az Ön egyszer használható hivatkozása! Enable for all - Engedélyezés mindenki számára + Engedélyezés az összes tag számára No comment provided by engineer. @@ -2744,7 +2744,7 @@ Ez az Ön egyszer használható hivatkozása! Enabled for - Engedélyezve + Számukra engedélyezve: No comment provided by engineer. @@ -3330,7 +3330,7 @@ Ez az Ön egyszer használható hivatkozása! File will be deleted from servers. - A fájl törölve lesz a kiszolgálóról. + A fájl törölve lesz a kiszolgálókról. No comment provided by engineer. @@ -3700,7 +3700,7 @@ Hiba: %2$@ Group will be deleted for all members - this cannot be undone! - A csoport törlésre kerül minden tag számára - ez a művelet nem vonható vissza! + A csoport törlésre kerül az összes tag számára - ez a művelet nem vonható vissza! No comment provided by engineer. @@ -3710,7 +3710,7 @@ Hiba: %2$@ Help - Segítség + Súgó No comment provided by engineer. @@ -3930,7 +3930,7 @@ További fejlesztések hamarosan! Incognito mode protects your privacy by using a new random profile for each contact. - Az inkognitómód védi személyes adatait azáltal, hogy minden ismerőshöz új véletlenszerű profilt használ. + Az inkognitómód védi személyes adatait azáltal, hogy az összes ismerőséhez új, véletlenszerű profilt használ. No comment provided by engineer. @@ -4160,7 +4160,7 @@ További fejlesztések hamarosan! Join your group? This is your link for group %@! Csatlakozik a csoportjához? -Ez az Ön hivatkozása a(z) %@ csoporthoz! +Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! No comment provided by engineer. @@ -4310,12 +4310,12 @@ Ez az Ön hivatkozása a(z) %@ csoporthoz! Mark deleted for everyone - Jelölje meg mindenki számára töröltként + Jelölje meg az összes tag számára töröltként No comment provided by engineer. Mark read - Olvasottnak jelölés + Megjelölés olvasottként No comment provided by engineer. @@ -4355,7 +4355,7 @@ Ez az Ön hivatkozása a(z) %@ csoporthoz! Member role will be changed to "%@". All group members will be notified. - A tag szerepköre meg fog változni erre: „%@”. A csoport minden tagja értesítést kap róla. + A tag szerepköre meg fog változni erre: „%@”. A csoportban az összes tag értesítve lesz. No comment provided by engineer. @@ -4667,7 +4667,7 @@ Ez az Ön hivatkozása a(z) %@ csoporthoz! New SOCKS credentials will be used for each server. - Minden egyes kiszolgálóhoz új SOCKS-hitelesítő-adatok legyenek használva. + Az összes kiszolgálóhoz új, SOCKS-hitelesítő-adatok legyenek használva. No comment provided by engineer. @@ -4770,7 +4770,7 @@ Ez az Ön hivatkozása a(z) %@ csoporthoz! No direct connection yet, message is forwarded by admin. - Még nincs közvetlen kapcsolat, az üzenetet az admin továbbítja. + Még nincs közvetlen kapcsolat, az üzenetet az adminisztrátor továbbítja. item status description @@ -4885,7 +4885,7 @@ Ez az Ön hivatkozása a(z) %@ csoporthoz! Now admins can: - delete members' messages. - disable members ("observer" role) - Most már az adminok is: + Most már az adminisztrátorok is: - törölhetik a tagok üzeneteit. - letilthatnak tagokat („megfigyelő” szerepkör) No comment provided by engineer. @@ -5220,7 +5220,7 @@ Minden további problémát osszon meg a fejlesztőkkel. Please check your network connection with %@ and try again. - Ellenőrizze a hálózati kapcsolatát a(z) %@ segítségével, és próbálja újra. + Ellenőrizze a hálózati kapcsolatát a következővel: %@, és próbálja újra. No comment provided by engineer. @@ -5242,7 +5242,7 @@ Hiba: %@ Please contact group admin. - Lépjen kapcsolatba a csoport adminnal. + Lépjen kapcsolatba a csoport adminisztrátorával. No comment provided by engineer. @@ -5517,7 +5517,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Read - Olvasd el + Olvasott swipe action @@ -5597,7 +5597,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Receiving file will be stopped. - A fájl fogadása leállt. + A fájl fogadása le fog állni. No comment provided by engineer. @@ -5622,7 +5622,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Reconnect - Újrakapcsolás + Újrakapcsolódás No comment provided by engineer. @@ -5632,12 +5632,12 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Reconnect all servers - Újrakapcsolódás minden kiszolgálóhoz + Újrakapcsolódás az összes kiszolgálóhoz No comment provided by engineer. Reconnect all servers? - Újrakapcsolódás minden kiszolgálóhoz? + Újrakapcsolódás az összes kiszolgálóhoz? No comment provided by engineer. @@ -5788,12 +5788,12 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Reset all statistics - Minden statisztika visszaállítása + Az összes statisztika visszaállítása No comment provided by engineer. Reset all statistics? - Minden statisztika visszaállítása? + Az összes statisztika visszaállítása? No comment provided by engineer. @@ -6070,7 +6070,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Search or paste SimpleX link - Keresés, vagy SimpleX-hivatkozás beillesztése + Keresés vagy SimpleX-hivatkozás beillesztése No comment provided by engineer. @@ -6230,17 +6230,17 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Sending delivery receipts will be enabled for all contacts in all visible chat profiles. - A kézbesítési jelentések küldése engedélyezésre kerül az összes látható csevegési profilban lévő minden ismerős számára. + A kézbesítési jelentések küldése engedélyezésre kerül az összes látható csevegési profilban lévő összes ismerőse számára. No comment provided by engineer. Sending delivery receipts will be enabled for all contacts. - A kézbesítési jelentés küldése minden ismerőse számára engedélyezésre kerül. + A kézbesítési jelentés küldése az összes ismerőse számára engedélyezésre kerül. No comment provided by engineer. Sending file will be stopped. - A fájl küldése leállt. + A fájl küldése le fog állni. No comment provided by engineer. @@ -6637,7 +6637,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. SimpleX group link - SimpleX csoporthivatkozás + SimpleX-csoporthivatkozás simplex link type @@ -6777,7 +6777,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped. - A csevegés megállítása a csevegési adatbázis exportálásához, importálásához vagy törléséhez. A csevegés megállítása alatt nem tud üzeneteket fogadni és küldeni. + A csevegés megállítása a csevegési adatbázis exportálásához, importálásához vagy törléséhez. A csevegés megállításakor nem tud üzeneteket fogadni és küldeni. No comment provided by engineer. @@ -7003,7 +7003,7 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. The code you scanned is not a SimpleX link QR code. - A beolvasott QR-kód nem egy SimpleX QR-kód hivatkozás. + A beolvasott QR-kód nem egy SimpleX-QR-kód-hivatkozás. No comment provided by engineer. @@ -7042,22 +7042,22 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. The message will be deleted for all members. - Az üzenet minden tag számára törlésre kerül. + Az üzenet az összes tag számára törlésre kerül. No comment provided by engineer. The message will be marked as moderated for all members. - Az üzenet minden tag számára moderáltként lesz megjelölve. + Az üzenet az összes tag számára moderáltként lesz megjelölve. No comment provided by engineer. The messages will be deleted for all members. - Az üzenetek minden tag számára törlésre kerülnek. + Az üzenetek az összes tag számára törlésre kerülnek. No comment provided by engineer. The messages will be marked as moderated for all members. - Az üzenetek moderáltként lesznek megjelölve minden tag számára. + Az üzenetek az összes tag számára moderáltként lesznek megjelölve. No comment provided by engineer. @@ -7114,7 +7114,7 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. These settings are for your current profile **%@**. - Ezek a beállítások a jelenlegi **%@** profiljára vonatkoznak. + Ezek a beállítások csak a jelenlegi (**%@**) profiljára vonatkoznak. No comment provided by engineer. @@ -7184,7 +7184,7 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. This setting applies to messages in your current chat profile **%@**. - Ez a beállítás a jelenlegi **%@** profiljában lévő üzenetekre érvényes. + Ez a beállítás csak a jelenlegi (**%@**) profiljában lévő üzenetekre vonatkozik. No comment provided by engineer. @@ -7218,7 +7218,7 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. To protect timezone, image/voice files use UTC. - Az időzóna védelme érdekében a kép-/hangfájlok UTC-t használnak. + Az időzóna védelmének érdekében a kép-/hangfájlok UTC-t használnak. No comment provided by engineer. @@ -7235,7 +7235,7 @@ A funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beáll To protect your privacy, SimpleX uses separate IDs for each of your contacts. - Az adatvédelem érdekében, a más csevegési platformokon megszokott felhasználó-azonosítók helyett, a SimpleX csak az üzenetek sorbaállításához használ azonosítókat, minden egyes ismerőshöz egy-egy különbözőt. + Az adatvédelem érdekében (a más csevegési platformokon megszokott felhasználó-azonosítók helyett) a SimpleX csak az üzenetek sorbaállításához használ azonosítókat, az összes ismerőséhez különbözőt. No comment provided by engineer. @@ -7347,7 +7347,7 @@ A funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beáll Unblock for all - Letiltás feloldása mindenki számára + Letiltás feloldása az összes tag számára No comment provided by engineer. @@ -7357,7 +7357,7 @@ A funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beáll Unblock member for all? - Mindenki számára feloldja a tag letiltását? + Az összes tag számára feloldja a tag letiltását? No comment provided by engineer. @@ -7807,7 +7807,7 @@ A kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapc What's new - Milyen újdonságok vannak + Újdonságok No comment provided by engineer. @@ -7906,7 +7906,7 @@ A kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapc You allow - Engedélyezte + Ön engedélyezi No comment provided by engineer. @@ -7931,7 +7931,7 @@ A kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapc You are already in group %@. - Már a(z) %@ csoport tagja. + Ön már a(z) %@ nevű csoport tagja. No comment provided by engineer. @@ -7958,7 +7958,7 @@ Csatlakozáskérés megismétlése? You are connected to the server used to receive messages from this contact. - Már kapcsolódott ahhoz a kiszolgálóhoz, amely az adott ismerősétől érkező üzenetek fogadására szolgál. + Ön már kapcsolódott ahhoz a kiszolgálóhoz, amely az adott ismerősétől érkező üzenetek fogadására szolgál. No comment provided by engineer. @@ -8380,12 +8380,12 @@ Kapcsolatkérés megismétlése? admin - admin + adminisztrátor member role admins - adminok + adminisztrátorok feature role @@ -8400,7 +8400,7 @@ Kapcsolatkérés megismétlése? all members - minden tag + összes tag feature role @@ -8450,7 +8450,7 @@ Kapcsolatkérés megismétlése? blocked by admin - letiltva az admin által + letiltva az adminisztrátor által marked deleted chat item preview text @@ -8525,7 +8525,7 @@ Kapcsolatkérés megismétlése? connected - kapcsolódva + kapcsolódott No comment provided by engineer. @@ -9260,7 +9260,7 @@ utoljára fogadott üzenet: %2$@ you are observer - megfigyelő szerep + Ön megfigyelő No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff index 5b0c2cdb99..acb46596ce 100644 --- a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff +++ b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff @@ -9115,7 +9115,7 @@ ultimo msg ricevuto: %2$@ set new profile picture - impostata nuova immagine del profilo + ha impostato una nuova immagine del profilo profile update event chat item diff --git a/apps/ios/SimpleX Localizations/pt-BR.xcloc/Localized Contents/pt-BR.xliff b/apps/ios/SimpleX Localizations/pt-BR.xcloc/Localized Contents/pt-BR.xliff index ffbaec1d96..40fc2cd4b3 100644 --- a/apps/ios/SimpleX Localizations/pt-BR.xcloc/Localized Contents/pt-BR.xliff +++ b/apps/ios/SimpleX Localizations/pt-BR.xcloc/Localized Contents/pt-BR.xliff @@ -5561,6 +5561,90 @@ Isso pode acontecer por causa de algum bug ou quando a conexão está comprometi Capacity exceeded - recipient did not receive previously sent messages. Capacidade excedida - o destinatário não recebeu as mensagens enviadas anteriormente. + + Chat migrated! + Conversa migrada! + + + Auto-accept settings + Aceitar automaticamente configurações + + + App encrypts new local files (except videos). + O aplicativo criptografa novos arquivos locais (exceto videos). + + + App session + Sessão do aplicativo + + + Acknowledged + Reconhecido + + + Acknowledgement errors + Erros conhecidos + + + Chat list + Lista de conversas + + + Chat database exported + Banco de dados da conversa exportado + + + Chat preferences were changed. + As preferências de bate-papo foram alteradas. + + + Chat theme + Tema da conversa + + + Better calls + Chamadas melhores + + + Better user experience + Melhor experiência do usuário + + + Allow downgrade + Permitir redução + + + Additional secondary + Secundária adicional + + + App data migration + Migração de dados do aplicativo + + + Archive and upload + Arquivar e enviar + + + Background + Fundo + + + Better message dates. + Datas de mensagens melhores. + + + Better notifications + Notificações melhores + + + Better security ✅ + Melhor segurança ✅ + + + Chat profile + Perfil da conversa + diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff index 558dd682f1..a36257c392 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff @@ -4823,7 +4823,7 @@ This is your link for group %@! No push server - Локальные + Без сервера нотификаций No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff index b641bbe8cf..c4beadaf66 100644 --- a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff @@ -156,14 +156,17 @@ %d file(s) are still being downloaded. + %их файл(ів) ще досі завантажуються. forward confirmation reason %d file(s) failed to download. + %их файлів не вийшло завантажити. forward confirmation reason %d file(s) were deleted. + %их файл(ів) було видалено. forward confirmation reason diff --git a/apps/ios/bg.lproj/Localizable.strings b/apps/ios/bg.lproj/Localizable.strings index da890d4ecb..5734fc58cc 100644 --- a/apps/ios/bg.lproj/Localizable.strings +++ b/apps/ios/bg.lproj/Localizable.strings @@ -1,15 +1,6 @@ /* No comment provided by engineer. */ "\n" = "\n"; -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - /* No comment provided by engineer. */ " (" = " ("; @@ -319,12 +310,6 @@ /* No comment provided by engineer. */ "Abort changing address?" = "Откажи смяна на адрес?"; -/* No comment provided by engineer. */ -"About SimpleX" = "За SimpleX"; - -/* No comment provided by engineer. */ -"About SimpleX address" = "Повече за SimpleX адреса"; - /* No comment provided by engineer. */ "About SimpleX Chat" = "За SimpleX Chat"; @@ -352,12 +337,6 @@ /* No comment provided by engineer. */ "Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." = "Добавете адрес към вашия профил, така че вашите контакти да могат да го споделят с други хора. Актуализацията на профила ще бъде изпратена до вашите контакти."; -/* No comment provided by engineer. */ -"Add contact" = "Добави контакт"; - -/* No comment provided by engineer. */ -"Add preset servers" = "Добави предварително зададени сървъри"; - /* No comment provided by engineer. */ "Add profile" = "Добави профил"; @@ -514,6 +493,9 @@ /* No comment provided by engineer. */ "Answer call" = "Отговор на повикване"; +/* No comment provided by engineer. */ +"Anybody can host servers." = "Протокол и код с отворен код – всеки може да оперира собствени сървъри."; + /* No comment provided by engineer. */ "App build: %@" = "Компилация на приложението: %@"; @@ -694,7 +676,8 @@ /* No comment provided by engineer. */ "Can't invite contacts!" = "Не може да поканят контактите!"; -/* alert button */ +/* alert action + alert button */ "Cancel" = "Отказ"; /* No comment provided by engineer. */ @@ -794,7 +777,7 @@ /* No comment provided by engineer. */ "Chats" = "Чатове"; -/* No comment provided by engineer. */ +/* alert title */ "Check server address and try again." = "Проверете адреса на сървъра и опитайте отново."; /* No comment provided by engineer. */ @@ -1016,9 +999,6 @@ /* No comment provided by engineer. */ "Create a group using a random profile." = "Създай група с автоматично генериран профилл."; -/* No comment provided by engineer. */ -"Create an address to let people connect with you." = "Създайте адрес, за да позволите на хората да се свързват с вас."; - /* server test step */ "Create file" = "Създай файл"; @@ -1160,7 +1140,8 @@ /* No comment provided by engineer. */ "default (yes)" = "по подразбиране (да)"; -/* chat item action +/* alert action + chat item action swipe action */ "Delete" = "Изтрий"; @@ -1678,9 +1659,6 @@ /* No comment provided by engineer. */ "Error joining group" = "Грешка при присъединяване към група"; -/* No comment provided by engineer. */ -"Error loading %@ servers" = "Грешка при зареждане на %@ сървъри"; - /* No comment provided by engineer. */ "Error opening chat" = "Грешка при отваряне на чата"; @@ -1690,9 +1668,6 @@ /* No comment provided by engineer. */ "Error removing member" = "Грешка при отстраняване на член"; -/* No comment provided by engineer. */ -"Error saving %@ servers" = "Грешка при запазване на %@ сървъра"; - /* No comment provided by engineer. */ "Error saving group profile" = "Грешка при запазване на профила на групата"; @@ -2026,9 +2001,6 @@ /* time unit */ "hours" = "часове"; -/* No comment provided by engineer. */ -"How it works" = "Как работи"; - /* No comment provided by engineer. */ "How SimpleX works" = "Как работи SimpleX"; @@ -2162,10 +2134,10 @@ "Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)" = "Инсталирайте [SimpleX Chat за терминал](https://github.com/simplex-chat/simplex-chat)"; /* No comment provided by engineer. */ -"Instant push notifications will be hidden!\n" = "Незабавните push известия ще бъдат скрити!\n"; +"Instant" = "Мигновено"; /* No comment provided by engineer. */ -"Instant" = "Мигновено"; +"Instant push notifications will be hidden!\n" = "Незабавните push известия ще бъдат скрити!\n"; /* No comment provided by engineer. */ "Interface" = "Интерфейс"; @@ -2200,7 +2172,7 @@ /* No comment provided by engineer. */ "Invalid response" = "Невалиден отговор"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid server address!" = "Невалиден адрес на сървъра!"; /* item status text */ @@ -2296,13 +2268,13 @@ /* No comment provided by engineer. */ "Joining group" = "Присъединяване към групата"; -/* No comment provided by engineer. */ +/* alert action */ "Keep" = "Запази"; /* No comment provided by engineer. */ "Keep the app open to use it from desktop" = "Дръжте приложението отворено, за да го използвате от настолното устройство"; -/* No comment provided by engineer. */ +/* alert title */ "Keep unused invitation?" = "Запази неизползваната покана за връзка?"; /* No comment provided by engineer. */ @@ -2359,9 +2331,6 @@ /* No comment provided by engineer. */ "Live messages" = "Съобщения на живо"; -/* No comment provided by engineer. */ -"No push server" = "Локално"; - /* No comment provided by engineer. */ "Local name" = "Локално име"; @@ -2374,24 +2343,15 @@ /* No comment provided by engineer. */ "Lock mode" = "Режим на заключване"; -/* No comment provided by engineer. */ -"Make a private connection" = "Добави поверителна връзка"; - /* No comment provided by engineer. */ "Make one message disappear" = "Накарайте едно съобщение да изчезне"; /* No comment provided by engineer. */ "Make profile private!" = "Направи профила поверителен!"; -/* No comment provided by engineer. */ -"Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." = "Уверете се, че %@ сървърните адреси са в правилен формат, разделени на редове и не се дублират (%@)."; - /* No comment provided by engineer. */ "Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "Уверете се, че адресите на WebRTC ICE сървъра са в правилен формат, разделени на редове и не са дублирани."; -/* No comment provided by engineer. */ -"Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" = "Много хора попитаха: *ако SimpleX няма потребителски идентификатори, как може да доставя съобщения?*"; - /* No comment provided by engineer. */ "Mark deleted for everyone" = "Маркирай като изтрито за всички"; @@ -2650,12 +2610,18 @@ /* No comment provided by engineer. */ "No permission to record voice message" = "Няма разрешение за запис на гласово съобщение"; +/* No comment provided by engineer. */ +"No push server" = "Локално"; + /* No comment provided by engineer. */ "No received or sent files" = "Няма получени или изпратени файлове"; /* copied message info in history */ "no text" = "няма текст"; +/* No comment provided by engineer. */ +"No user identifiers." = "Първата платформа без никакви потребителски идентификатори – поверителна по дизайн."; + /* No comment provided by engineer. */ "Not compatible!" = "Несъвместим!"; @@ -2772,12 +2738,6 @@ /* No comment provided by engineer. */ "Open Settings" = "Отвори настройки"; -/* authentication reason */ -"Open user profiles" = "Отвори потребителските профили"; - -/* No comment provided by engineer. */ -"Anybody can host servers." = "Протокол и код с отворен код – всеки може да оперира собствени сървъри."; - /* No comment provided by engineer. */ "Opening app…" = "Приложението се отваря…"; @@ -2838,9 +2798,6 @@ /* No comment provided by engineer. */ "peer-to-peer" = "peer-to-peer"; -/* No comment provided by engineer. */ -"You decide who can connect." = "Хората могат да се свържат с вас само чрез ликовете, които споделяте."; - /* No comment provided by engineer. */ "Periodic" = "Периодично"; @@ -2907,9 +2864,6 @@ /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Запазете последната чернова на съобщението с прикачени файлове."; -/* No comment provided by engineer. */ -"Preset server" = "Предварително зададен сървър"; - /* No comment provided by engineer. */ "Preset server address" = "Предварително зададен адрес на сървъра"; @@ -2940,7 +2894,7 @@ /* No comment provided by engineer. */ "Profile password" = "Профилна парола"; -/* No comment provided by engineer. */ +/* alert message */ "Profile update will be sent to your contacts." = "Актуализацията на профила ще бъде изпратена до вашите контакти."; /* No comment provided by engineer. */ @@ -3007,10 +2961,10 @@ "Read more" = "Прочетете още"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." = "Прочетете повече в [Ръководство за потребителя](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)."; +"Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." = "Прочетете повече в [Ръководство за потребителя](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)."; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." = "Прочетете повече в [Ръководство за потребителя](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)."; +"Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." = "Прочетете повече в [Ръководство за потребителя](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)."; /* No comment provided by engineer. */ "Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." = "Прочетете повече в [Ръководство на потребителя](https://simplex.chat/docs/guide/readme.html#connect-to-friends)."; @@ -3018,9 +2972,6 @@ /* No comment provided by engineer. */ "Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." = "Прочетете повече в нашето [GitHub хранилище](https://github.com/simplex-chat/simplex-chat#readme)."; -/* No comment provided by engineer. */ -"Read more in our GitHub repository." = "Прочетете повече в нашето хранилище в GitHub."; - /* No comment provided by engineer. */ "Receipts are disabled" = "Потвърждениeто за доставка е деактивирано"; @@ -3239,7 +3190,7 @@ /* No comment provided by engineer. */ "Save servers" = "Запази сървърите"; -/* No comment provided by engineer. */ +/* alert title */ "Save servers?" = "Запази сървърите?"; /* No comment provided by engineer. */ @@ -3350,9 +3301,6 @@ /* No comment provided by engineer. */ "Send notifications" = "Изпращай известия"; -/* No comment provided by engineer. */ -"Send notifications:" = "Изпратени известия:"; - /* No comment provided by engineer. */ "Send questions and ideas" = "Изпращайте въпроси и идеи"; @@ -3464,7 +3412,8 @@ /* No comment provided by engineer. */ "Shape profile images" = "Променете формата на профилните изображения"; -/* chat item action */ +/* alert action + chat item action */ "Share" = "Сподели"; /* No comment provided by engineer. */ @@ -3473,7 +3422,7 @@ /* No comment provided by engineer. */ "Share address" = "Сподели адрес"; -/* No comment provided by engineer. */ +/* alert title */ "Share address with contacts?" = "Сподели адреса с контактите?"; /* No comment provided by engineer. */ @@ -3605,10 +3554,10 @@ /* No comment provided by engineer. */ "Stop sending file?" = "Спри изпращането на файла?"; -/* No comment provided by engineer. */ +/* alert action */ "Stop sharing" = "Спри споделянето"; -/* No comment provided by engineer. */ +/* alert title */ "Stop sharing address?" = "Спри споделянето на адреса?"; /* authentication reason */ @@ -3677,7 +3626,7 @@ /* No comment provided by engineer. */ "Test servers" = "Тествай сървърите"; -/* No comment provided by engineer. */ +/* alert title */ "Tests failed!" = "Тестовете са неуспешни!"; /* No comment provided by engineer. */ @@ -3689,9 +3638,6 @@ /* No comment provided by engineer. */ "Thanks to the users – contribute via Weblate!" = "Благодарение на потребителите – допринесете през Weblate!"; -/* No comment provided by engineer. */ -"No user identifiers." = "Първата платформа без никакви потребителски идентификатори – поверителна по дизайн."; - /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "Приложението може да ви уведоми, когато получите съобщения или заявки за контакт - моля, отворете настройките, за да активирате."; @@ -3713,6 +3659,9 @@ /* No comment provided by engineer. */ "The encryption is working and the new encryption agreement is not required. It may result in connection errors!" = "Криптирането работи и новото споразумение за криптиране не е необходимо. Това може да доведе до грешки при свързване!"; +/* No comment provided by engineer. */ +"The future of messaging" = "Ново поколение поверителни съобщения"; + /* No comment provided by engineer. */ "The hash of the previous message is different." = "Хешът на предишното съобщение е различен."; @@ -3725,9 +3674,6 @@ /* No comment provided by engineer. */ "The message will be marked as moderated for all members." = "Съобщението ще бъде маркирано като модерирано за всички членове."; -/* No comment provided by engineer. */ -"The future of messaging" = "Ново поколение поверителни съобщения"; - /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "Старата база данни не бе премахната по време на миграцията, тя може да бъде изтрита."; @@ -3803,15 +3749,15 @@ /* No comment provided by engineer. */ "To make a new connection" = "За да направите нова връзка"; -/* No comment provided by engineer. */ -"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "За да се защити поверителността, вместо потребителски идентификатори, използвани от всички други платформи, SimpleX има идентификатори за опашки от съобщения, отделни за всеки от вашите контакти."; - /* No comment provided by engineer. */ "To protect timezone, image/voice files use UTC." = "За да не се разкрива часовата зона, файловете с изображения/глас използват UTC."; /* No comment provided by engineer. */ "To protect your information, turn on SimpleX Lock.\nYou will be prompted to complete authentication before this feature is enabled." = "За да защитите информацията си, включете SimpleX заключване.\nЩе бъдете подканени да извършите идентификация, преди тази функция да бъде активирана."; +/* No comment provided by engineer. */ +"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "За да се защити поверителността, вместо потребителски идентификатори, използвани от всички други платформи, SimpleX има идентификатори за опашки от съобщения, отделни за всеки от вашите контакти."; + /* No comment provided by engineer. */ "To record voice message please grant permission to use Microphone." = "За да запишете гласово съобщение, моля, дайте разрешение за използване на микрофон."; @@ -4127,9 +4073,6 @@ /* No comment provided by engineer. */ "When connecting audio and video calls." = "При свързване на аудио и видео разговори."; -/* No comment provided by engineer. */ -"When people request to connect, you can accept or reject it." = "Когато хората искат да се свържат с вас, можете да ги приемете или отхвърлите."; - /* No comment provided by engineer. */ "When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "Когато споделяте инкогнито профил с някого, този профил ще се използва за групите, в които той ви кани."; @@ -4247,9 +4190,6 @@ /* No comment provided by engineer. */ "You can share this address with your contacts to let them connect with **%@**." = "Можете да споделите този адрес с вашите контакти, за да им позволите да се свържат с **%@**."; -/* No comment provided by engineer. */ -"You can share your address as a link or QR code - anybody can connect to you." = "Можете да споделите адреса си като линк или QR код - всеки може да се свърже с вас."; - /* No comment provided by engineer. */ "You can start chat via app Settings / Database or by restarting the app" = "Можете да започнете чат през Настройки на приложението / База данни или като рестартирате приложението"; @@ -4259,7 +4199,7 @@ /* No comment provided by engineer. */ "You can use markdown to format messages:" = "Можете да използвате markdown за форматиране на съобщенията:"; -/* No comment provided by engineer. */ +/* alert message */ "You can view invitation link again in connection details." = "Можете да видите отново линкът за покана в подробностите за връзката."; /* No comment provided by engineer. */ @@ -4280,6 +4220,9 @@ /* No comment provided by engineer. */ "You could not be verified; please try again." = "Не можахте да бъдете потвърдени; Моля, опитайте отново."; +/* No comment provided by engineer. */ +"You decide who can connect." = "Хората могат да се свържат с вас само чрез ликовете, които споделяте."; + /* No comment provided by engineer. */ "You have already requested connection via this address!" = "Вече сте заявили връзка през този адрес!"; @@ -4361,9 +4304,6 @@ /* No comment provided by engineer. */ "You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed" = "Използвате инкогнито профил за тази група - за да се предотврати споделянето на основния ви профил, поканите на контакти не са разрешени"; -/* No comment provided by engineer. */ -"Your %@ servers" = "Вашите %@ сървъри"; - /* No comment provided by engineer. */ "Your calls" = "Вашите обаждания"; @@ -4415,9 +4355,6 @@ /* No comment provided by engineer. */ "Your random profile" = "Вашият автоматично генериран профил"; -/* No comment provided by engineer. */ -"Your server" = "Вашият сървър"; - /* No comment provided by engineer. */ "Your server address" = "Вашият адрес на сървъра"; @@ -4430,6 +4367,3 @@ /* No comment provided by engineer. */ "Your SMP servers" = "Вашите SMP сървъри"; -/* No comment provided by engineer. */ -"Your XFTP servers" = "Вашите XFTP сървъри"; - diff --git a/apps/ios/cs.lproj/Localizable.strings b/apps/ios/cs.lproj/Localizable.strings index 611be02606..462988855b 100644 --- a/apps/ios/cs.lproj/Localizable.strings +++ b/apps/ios/cs.lproj/Localizable.strings @@ -1,15 +1,6 @@ /* No comment provided by engineer. */ "\n" = "\n"; -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - /* No comment provided by engineer. */ " (" = " ("; @@ -271,12 +262,6 @@ /* No comment provided by engineer. */ "Abort changing address?" = "Přerušit změnu adresy?"; -/* No comment provided by engineer. */ -"About SimpleX" = "O SimpleX"; - -/* No comment provided by engineer. */ -"About SimpleX address" = "O SimpleX adrese"; - /* No comment provided by engineer. */ "About SimpleX Chat" = "O SimpleX chat"; @@ -304,9 +289,6 @@ /* No comment provided by engineer. */ "Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." = "Přidejte adresu do svého profilu, aby ji vaše kontakty mohly sdílet s dalšími lidmi. Aktualizace profilu bude zaslána vašim kontaktům."; -/* No comment provided by engineer. */ -"Add preset servers" = "Přidejte přednastavené servery"; - /* No comment provided by engineer. */ "Add profile" = "Přidat profil"; @@ -433,6 +415,9 @@ /* No comment provided by engineer. */ "Answer call" = "Přijmout hovor"; +/* No comment provided by engineer. */ +"Anybody can host servers." = "Servery může provozovat kdokoli."; + /* No comment provided by engineer. */ "App build: %@" = "Sestavení aplikace: %@"; @@ -559,7 +544,8 @@ /* No comment provided by engineer. */ "Can't invite contacts!" = "Nelze pozvat kontakty!"; -/* alert button */ +/* alert action + alert button */ "Cancel" = "Zrušit"; /* feature offered item */ @@ -647,7 +633,7 @@ /* No comment provided by engineer. */ "Chats" = "Chaty"; -/* No comment provided by engineer. */ +/* alert title */ "Check server address and try again." = "Zkontrolujte adresu serveru a zkuste to znovu."; /* No comment provided by engineer. */ @@ -812,9 +798,6 @@ /* No comment provided by engineer. */ "Create" = "Vytvořit"; -/* No comment provided by engineer. */ -"Create an address to let people connect with you." = "Vytvořit adresu, aby se s vámi lidé mohli spojit."; - /* server test step */ "Create file" = "Vytvořit soubor"; @@ -938,7 +921,8 @@ /* No comment provided by engineer. */ "default (yes)" = "výchozí (ano)"; -/* chat item action +/* alert action + chat item action swipe action */ "Delete" = "Smazat"; @@ -1377,18 +1361,12 @@ /* No comment provided by engineer. */ "Error joining group" = "Chyba při připojování ke skupině"; -/* No comment provided by engineer. */ -"Error loading %@ servers" = "Chyba načítání %@ serverů"; - /* alert title */ "Error receiving file" = "Chyba při příjmu souboru"; /* No comment provided by engineer. */ "Error removing member" = "Chyba při odebrání člena"; -/* No comment provided by engineer. */ -"Error saving %@ servers" = "Chyba při ukládání serverů %@"; - /* No comment provided by engineer. */ "Error saving group profile" = "Chyba při ukládání profilu skupiny"; @@ -1656,9 +1634,6 @@ /* time unit */ "hours" = "hodin"; -/* No comment provided by engineer. */ -"How it works" = "Jak to funguje"; - /* No comment provided by engineer. */ "How SimpleX works" = "Jak SimpleX funguje"; @@ -1768,10 +1743,10 @@ "Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)" = "Nainstalujte [SimpleX Chat pro terminál](https://github.com/simplex-chat/simplex-chat)"; /* No comment provided by engineer. */ -"Instant push notifications will be hidden!\n" = "Okamžitá oznámení budou skryta!\n"; +"Instant" = "Okamžitě"; /* No comment provided by engineer. */ -"Instant" = "Okamžitě"; +"Instant push notifications will be hidden!\n" = "Okamžitá oznámení budou skryta!\n"; /* No comment provided by engineer. */ "Interface" = "Rozhranní"; @@ -1788,7 +1763,7 @@ /* invalid chat item */ "invalid data" = "neplatné údaje"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid server address!" = "Neplatná adresa serveru!"; /* item status text */ @@ -1917,9 +1892,6 @@ /* No comment provided by engineer. */ "Live messages" = "Živé zprávy"; -/* No comment provided by engineer. */ -"No push server" = "Místní"; - /* No comment provided by engineer. */ "Local name" = "Místní název"; @@ -1932,24 +1904,15 @@ /* No comment provided by engineer. */ "Lock mode" = "Režim zámku"; -/* No comment provided by engineer. */ -"Make a private connection" = "Vytvořte si soukromé připojení"; - /* No comment provided by engineer. */ "Make one message disappear" = "Nechat jednu zprávu zmizet"; /* No comment provided by engineer. */ "Make profile private!" = "Změnit profil na soukromý!"; -/* No comment provided by engineer. */ -"Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." = "Ujistěte se, že adresy %@ serverů jsou ve správném formátu, oddělené řádky a nejsou duplicitní (%@)."; - /* No comment provided by engineer. */ "Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "Ujistěte se, že adresy serverů WebRTC ICE jsou ve správném formátu, oddělené na řádcích a nejsou duplicitní."; -/* No comment provided by engineer. */ -"Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" = "Mnoho lidí se ptalo: *Pokud SimpleX nemá žádné uživatelské identifikátory, jak může doručovat zprávy?*"; - /* No comment provided by engineer. */ "Mark deleted for everyone" = "Označit jako smazané pro všechny"; @@ -2154,12 +2117,18 @@ /* No comment provided by engineer. */ "No permission to record voice message" = "Nemáte oprávnění nahrávat hlasové zprávy"; +/* No comment provided by engineer. */ +"No push server" = "Místní"; + /* No comment provided by engineer. */ "No received or sent files" = "Žádné přijaté ani odeslané soubory"; /* copied message info in history */ "no text" = "žádný text"; +/* No comment provided by engineer. */ +"No user identifiers." = "Bez uživatelských identifikátorů"; + /* No comment provided by engineer. */ "Notifications" = "Oznámení"; @@ -2264,12 +2233,6 @@ /* No comment provided by engineer. */ "Open Settings" = "Otevřít nastavení"; -/* authentication reason */ -"Open user profiles" = "Otevřít uživatelské profily"; - -/* No comment provided by engineer. */ -"Anybody can host servers." = "Servery může provozovat kdokoli."; - /* member role */ "owner" = "vlastník"; @@ -2297,9 +2260,6 @@ /* No comment provided by engineer. */ "peer-to-peer" = "peer-to-peer"; -/* No comment provided by engineer. */ -"You decide who can connect." = "Lidé se s vámi mohou spojit pouze prostřednictvím odkazu, který sdílíte."; - /* No comment provided by engineer. */ "Periodic" = "Pravidelně"; @@ -2357,9 +2317,6 @@ /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Zachování posledního návrhu zprávy s přílohami."; -/* No comment provided by engineer. */ -"Preset server" = "Přednastavený server"; - /* No comment provided by engineer. */ "Preset server address" = "Přednastavená adresa serveru"; @@ -2384,7 +2341,7 @@ /* No comment provided by engineer. */ "Profile password" = "Heslo profilu"; -/* No comment provided by engineer. */ +/* alert message */ "Profile update will be sent to your contacts." = "Aktualizace profilu bude zaslána vašim kontaktům."; /* No comment provided by engineer. */ @@ -2447,9 +2404,6 @@ /* No comment provided by engineer. */ "Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." = "Přečtěte si více v našem [GitHub repozitáři](https://github.com/simplex-chat/simplex-chat#readme)."; -/* No comment provided by engineer. */ -"Read more in our GitHub repository." = "Další informace najdete v našem repozitáři GitHub."; - /* No comment provided by engineer. */ "Receipts are disabled" = "Informace o dodání jsou zakázány"; @@ -2635,7 +2589,7 @@ /* No comment provided by engineer. */ "Save servers" = "Uložit servery"; -/* No comment provided by engineer. */ +/* alert title */ "Save servers?" = "Uložit servery?"; /* No comment provided by engineer. */ @@ -2722,9 +2676,6 @@ /* No comment provided by engineer. */ "Send notifications" = "Odeslat oznámení"; -/* No comment provided by engineer. */ -"Send notifications:" = "Odeslat oznámení:"; - /* No comment provided by engineer. */ "Send questions and ideas" = "Zasílání otázek a nápadů"; @@ -2818,7 +2769,8 @@ /* No comment provided by engineer. */ "Settings" = "Nastavení"; -/* chat item action */ +/* alert action + chat item action */ "Share" = "Sdílet"; /* No comment provided by engineer. */ @@ -2827,7 +2779,7 @@ /* No comment provided by engineer. */ "Share address" = "Sdílet adresu"; -/* No comment provided by engineer. */ +/* alert title */ "Share address with contacts?" = "Sdílet adresu s kontakty?"; /* No comment provided by engineer. */ @@ -2935,10 +2887,10 @@ /* No comment provided by engineer. */ "Stop sending file?" = "Zastavit odesílání souboru?"; -/* No comment provided by engineer. */ +/* alert action */ "Stop sharing" = "Přestat sdílet"; -/* No comment provided by engineer. */ +/* alert title */ "Stop sharing address?" = "Přestat sdílet adresu?"; /* authentication reason */ @@ -2995,7 +2947,7 @@ /* No comment provided by engineer. */ "Test servers" = "Testovací servery"; -/* No comment provided by engineer. */ +/* alert title */ "Tests failed!" = "Testy selhaly!"; /* No comment provided by engineer. */ @@ -3007,9 +2959,6 @@ /* No comment provided by engineer. */ "Thanks to the users – contribute via Weblate!" = "Díky uživatelům - přispívejte prostřednictvím Weblate!"; -/* No comment provided by engineer. */ -"No user identifiers." = "Bez uživatelských identifikátorů"; - /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "Aplikace vás může upozornit na přijaté zprávy nebo žádosti o kontakt - povolte to v nastavení."; @@ -3028,6 +2977,9 @@ /* No comment provided by engineer. */ "The encryption is working and the new encryption agreement is not required. It may result in connection errors!" = "Šifrování funguje a nové povolení šifrování není vyžadováno. To může vyvolat chybu v připojení!"; +/* No comment provided by engineer. */ +"The future of messaging" = "Nová generace soukromých zpráv"; + /* No comment provided by engineer. */ "The hash of the previous message is different." = "Hash předchozí zprávy se liší."; @@ -3040,9 +2992,6 @@ /* No comment provided by engineer. */ "The message will be marked as moderated for all members." = "Zpráva bude pro všechny členy označena jako moderovaná."; -/* No comment provided by engineer. */ -"The future of messaging" = "Nová generace soukromých zpráv"; - /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "Stará databáze nebyla během přenášení odstraněna, lze ji smazat."; @@ -3094,15 +3043,15 @@ /* No comment provided by engineer. */ "To make a new connection" = "Vytvoření nového připojení"; -/* No comment provided by engineer. */ -"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Pro ochranu soukromí namísto ID uživatelů používaných všemi ostatními platformami má SimpleX identifikátory pro fronty zpráv, oddělené pro každý z vašich kontaktů."; - /* No comment provided by engineer. */ "To protect timezone, image/voice files use UTC." = "K ochraně časového pásma používají obrazové/hlasové soubory UTC."; /* No comment provided by engineer. */ "To protect your information, turn on SimpleX Lock.\nYou will be prompted to complete authentication before this feature is enabled." = "Chcete-li chránit své informace, zapněte zámek SimpleX Lock.\nPřed zapnutím této funkce budete vyzváni k dokončení ověření."; +/* No comment provided by engineer. */ +"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Pro ochranu soukromí namísto ID uživatelů používaných všemi ostatními platformami má SimpleX identifikátory pro fronty zpráv, oddělené pro každý z vašich kontaktů."; + /* No comment provided by engineer. */ "To record voice message please grant permission to use Microphone." = "Chcete-li nahrávat hlasové zprávy, udělte povolení k použití mikrofonu."; @@ -3328,9 +3277,6 @@ /* No comment provided by engineer. */ "When available" = "Když je k dispozici"; -/* No comment provided by engineer. */ -"When people request to connect, you can accept or reject it." = "Když někdo požádá o připojení, můžete žádost přijmout nebo odmítnout."; - /* No comment provided by engineer. */ "When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "Pokud s někým sdílíte inkognito profil, bude tento profil použit pro skupiny, do kterých vás pozve."; @@ -3397,9 +3343,6 @@ /* No comment provided by engineer. */ "You can share this address with your contacts to let them connect with **%@**." = "Tuto adresu můžete sdílet s vašimi kontakty, abyse se mohli spojit s **%@**."; -/* No comment provided by engineer. */ -"You can share your address as a link or QR code - anybody can connect to you." = "Můžete sdílet svou adresu jako odkaz nebo jako QR kód - kdokoli se k vám bude moci připojit."; - /* No comment provided by engineer. */ "You can start chat via app Settings / Database or by restarting the app" = "Chat můžete zahájit prostřednictvím aplikace Nastavení / Databáze nebo restartováním aplikace"; @@ -3427,6 +3370,9 @@ /* No comment provided by engineer. */ "You could not be verified; please try again." = "Nemohli jste být ověřeni; Zkuste to prosím znovu."; +/* No comment provided by engineer. */ +"You decide who can connect." = "Lidé se s vámi mohou spojit pouze prostřednictvím odkazu, který sdílíte."; + /* No comment provided by engineer. */ "You have to enter passphrase every time the app starts - it is not stored on the device." = "Musíte zadat přístupovou frázi při každém spuštění aplikace - není uložena v zařízení."; @@ -3493,9 +3439,6 @@ /* No comment provided by engineer. */ "You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed" = "Pro tuto skupinu používáte inkognito profil - abyste zabránili sdílení svého hlavního profilu, není pozvání kontaktů povoleno"; -/* No comment provided by engineer. */ -"Your %@ servers" = "Vaše servery %@"; - /* No comment provided by engineer. */ "Your calls" = "Vaše hovory"; @@ -3544,9 +3487,6 @@ /* No comment provided by engineer. */ "Your random profile" = "Váš náhodný profil"; -/* No comment provided by engineer. */ -"Your server" = "Váš server"; - /* No comment provided by engineer. */ "Your server address" = "Adresa vašeho serveru"; @@ -3559,6 +3499,3 @@ /* No comment provided by engineer. */ "Your SMP servers" = "Vaše servery SMP"; -/* No comment provided by engineer. */ -"Your XFTP servers" = "Vaše XFTP servery"; - diff --git a/apps/ios/de.lproj/Localizable.strings b/apps/ios/de.lproj/Localizable.strings index 8ea6a30716..312cab136a 100644 --- a/apps/ios/de.lproj/Localizable.strings +++ b/apps/ios/de.lproj/Localizable.strings @@ -1,15 +1,6 @@ /* No comment provided by engineer. */ "\n" = "\n"; -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - /* No comment provided by engineer. */ " (" = " ("; @@ -337,12 +328,6 @@ /* No comment provided by engineer. */ "Abort changing address?" = "Wechsel der Empfängeradresse beenden?"; -/* No comment provided by engineer. */ -"About SimpleX" = "Über SimpleX"; - -/* No comment provided by engineer. */ -"About SimpleX address" = "Über die SimpleX-Adresse"; - /* No comment provided by engineer. */ "About SimpleX Chat" = "Über SimpleX Chat"; @@ -382,12 +367,6 @@ /* No comment provided by engineer. */ "Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." = "Fügen Sie die Adresse Ihrem Profil hinzu, damit Ihre Kontakte sie mit anderen Personen teilen können. Es wird eine Profilaktualisierung an Ihre Kontakte gesendet."; -/* No comment provided by engineer. */ -"Add contact" = "Kontakt hinzufügen"; - -/* No comment provided by engineer. */ -"Add preset servers" = "Füge voreingestellte Server hinzu"; - /* No comment provided by engineer. */ "Add profile" = "Profil hinzufügen"; @@ -574,6 +553,9 @@ /* No comment provided by engineer. */ "Answer call" = "Anruf annehmen"; +/* No comment provided by engineer. */ +"Anybody can host servers." = "Jeder kann seine eigenen Server aufsetzen."; + /* No comment provided by engineer. */ "App build: %@" = "App Build: %@"; @@ -817,7 +799,8 @@ /* No comment provided by engineer. */ "Can't message member" = "Mitglied kann nicht benachrichtigt werden"; -/* alert button */ +/* alert action + alert button */ "Cancel" = "Abbrechen"; /* No comment provided by engineer. */ @@ -938,7 +921,7 @@ /* No comment provided by engineer. */ "Chats" = "Chats"; -/* No comment provided by engineer. */ +/* alert title */ "Check server address and try again." = "Überprüfen Sie die Serveradresse und versuchen Sie es nochmal."; /* No comment provided by engineer. */ @@ -1001,9 +984,6 @@ /* No comment provided by engineer. */ "Configure ICE servers" = "ICE-Server konfigurieren"; -/* No comment provided by engineer. */ -"Configured %@ servers" = "Konfigurierte %@ Server"; - /* No comment provided by engineer. */ "Confirm" = "Bestätigen"; @@ -1232,9 +1212,6 @@ /* No comment provided by engineer. */ "Create a group using a random profile." = "Erstellen Sie eine Gruppe mit einem zufälligen Profil."; -/* No comment provided by engineer. */ -"Create an address to let people connect with you." = "Erstellen Sie eine Adresse, damit sich Personen mit Ihnen verbinden können."; - /* server test step */ "Create file" = "Datei erstellen"; @@ -1397,7 +1374,8 @@ /* No comment provided by engineer. */ "default (yes)" = "Voreinstellung (Ja)"; -/* chat item action +/* alert action + chat item action swipe action */ "Delete" = "Löschen"; @@ -1996,9 +1974,6 @@ /* No comment provided by engineer. */ "Error joining group" = "Fehler beim Beitritt zur Gruppe"; -/* No comment provided by engineer. */ -"Error loading %@ servers" = "Fehler beim Laden von %@ Servern"; - /* No comment provided by engineer. */ "Error migrating settings" = "Fehler beim Migrieren der Einstellungen"; @@ -2020,9 +1995,6 @@ /* No comment provided by engineer. */ "Error resetting statistics" = "Fehler beim Zurücksetzen der Statistiken"; -/* No comment provided by engineer. */ -"Error saving %@ servers" = "Fehler beim Speichern der %@-Server"; - /* No comment provided by engineer. */ "Error saving group profile" = "Fehler beim Speichern des Gruppenprofils"; @@ -2425,9 +2397,6 @@ /* time unit */ "hours" = "Stunden"; -/* No comment provided by engineer. */ -"How it works" = "Wie es funktioniert"; - /* No comment provided by engineer. */ "How SimpleX works" = "Wie SimpleX funktioniert"; @@ -2570,10 +2539,10 @@ "Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)" = "Installieren Sie [SimpleX Chat als Terminalanwendung](https://github.com/simplex-chat/simplex-chat)"; /* No comment provided by engineer. */ -"Instant push notifications will be hidden!\n" = "Sofortige Push-Benachrichtigungen werden verborgen!\n"; +"Instant" = "Sofort"; /* No comment provided by engineer. */ -"Instant" = "Sofort"; +"Instant push notifications will be hidden!\n" = "Sofortige Push-Benachrichtigungen werden verborgen!\n"; /* No comment provided by engineer. */ "Interface" = "Schnittstelle"; @@ -2611,7 +2580,7 @@ /* No comment provided by engineer. */ "Invalid response" = "Ungültige Reaktion"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid server address!" = "Ungültige Serveradresse!"; /* item status text */ @@ -2716,7 +2685,7 @@ /* No comment provided by engineer. */ "Joining group" = "Der Gruppe beitreten"; -/* No comment provided by engineer. */ +/* alert action */ "Keep" = "Behalten"; /* No comment provided by engineer. */ @@ -2725,7 +2694,7 @@ /* No comment provided by engineer. */ "Keep the app open to use it from desktop" = "Die App muss geöffnet bleiben, um sie vom Desktop aus nutzen zu können"; -/* No comment provided by engineer. */ +/* alert title */ "Keep unused invitation?" = "Nicht genutzte Einladung behalten?"; /* No comment provided by engineer. */ @@ -2782,9 +2751,6 @@ /* No comment provided by engineer. */ "Live messages" = "Live Nachrichten"; -/* No comment provided by engineer. */ -"No push server" = "Lokal"; - /* No comment provided by engineer. */ "Local name" = "Lokaler Name"; @@ -2797,24 +2763,15 @@ /* No comment provided by engineer. */ "Lock mode" = "Sperr-Modus"; -/* No comment provided by engineer. */ -"Make a private connection" = "Stellen Sie eine private Verbindung her"; - /* No comment provided by engineer. */ "Make one message disappear" = "Eine verschwindende Nachricht verfassen"; /* No comment provided by engineer. */ "Make profile private!" = "Privates Profil erzeugen!"; -/* No comment provided by engineer. */ -"Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." = "Stellen Sie sicher, dass die %@-Server-Adressen das richtige Format haben, zeilenweise getrennt und nicht doppelt vorhanden sind (%@)."; - /* No comment provided by engineer. */ "Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "Stellen Sie sicher, dass die WebRTC ICE-Server Adressen das richtige Format haben, zeilenweise getrennt und nicht doppelt vorhanden sind."; -/* No comment provided by engineer. */ -"Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" = "Viele Menschen haben gefragt: *Wie kann SimpleX Nachrichten zustellen, wenn es keine Benutzerkennungen gibt?*"; - /* No comment provided by engineer. */ "Mark deleted for everyone" = "Für Alle als gelöscht markieren"; @@ -3154,12 +3111,18 @@ /* No comment provided by engineer. */ "No permission to record voice message" = "Keine Berechtigung für das Aufnehmen von Sprachnachrichten"; +/* No comment provided by engineer. */ +"No push server" = "Lokal"; + /* No comment provided by engineer. */ "No received or sent files" = "Keine empfangenen oder gesendeten Dateien"; /* copied message info in history */ "no text" = "Kein Text"; +/* No comment provided by engineer. */ +"No user identifiers." = "Keine Benutzerkennungen."; + /* No comment provided by engineer. */ "Not compatible!" = "Nicht kompatibel!"; @@ -3282,18 +3245,9 @@ /* authentication reason */ "Open migration to another device" = "Migration auf ein anderes Gerät öffnen"; -/* No comment provided by engineer. */ -"Open server settings" = "Server-Einstellungen öffnen"; - /* No comment provided by engineer. */ "Open Settings" = "Geräte-Einstellungen öffnen"; -/* authentication reason */ -"Open user profiles" = "Benutzerprofile öffnen"; - -/* No comment provided by engineer. */ -"Anybody can host servers." = "Jeder kann seine eigenen Server aufsetzen."; - /* No comment provided by engineer. */ "Opening app…" = "App wird geöffnet…"; @@ -3315,9 +3269,6 @@ /* No comment provided by engineer. */ "Other" = "Andere"; -/* No comment provided by engineer. */ -"Other %@ servers" = "Andere %@ Server"; - /* No comment provided by engineer. */ "other errors" = "Andere Fehler"; @@ -3372,9 +3323,6 @@ /* No comment provided by engineer. */ "Pending" = "Ausstehend"; -/* No comment provided by engineer. */ -"You decide who can connect." = "Sie entscheiden, wer sich mit Ihnen verbinden kann."; - /* No comment provided by engineer. */ "Periodic" = "Periodisch"; @@ -3453,9 +3401,6 @@ /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Den letzten Nachrichtenentwurf, auch mit seinen Anhängen, aufbewahren."; -/* No comment provided by engineer. */ -"Preset server" = "Voreingestellter Server"; - /* No comment provided by engineer. */ "Preset server address" = "Voreingestellte Serveradresse"; @@ -3504,7 +3449,7 @@ /* No comment provided by engineer. */ "Profile theme" = "Profil-Design"; -/* No comment provided by engineer. */ +/* alert message */ "Profile update will be sent to your contacts." = "Profil-Aktualisierung wird an Ihre Kontakte gesendet."; /* No comment provided by engineer. */ @@ -3589,10 +3534,10 @@ "Read more" = "Mehr erfahren"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." = "Mehr dazu in der [Benutzeranleitung](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses) lesen."; +"Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." = "Lesen Sie mehr dazu im [Benutzerhandbuch](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)."; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." = "Lesen Sie mehr dazu im [Benutzerhandbuch](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)."; +"Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." = "Mehr dazu in der [Benutzeranleitung](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses) lesen."; /* No comment provided by engineer. */ "Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." = "Mehr dazu in der [Benutzeranleitung](https://simplex.chat/docs/guide/readme.html#connect-to-friends) lesen."; @@ -3600,9 +3545,6 @@ /* No comment provided by engineer. */ "Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." = "Erfahren Sie in unserem [GitHub-Repository](https://github.com/simplex-chat/simplex-chat#readme) mehr dazu."; -/* No comment provided by engineer. */ -"Read more in our GitHub repository." = "Erfahren Sie in unserem GitHub-Repository mehr dazu."; - /* No comment provided by engineer. */ "Receipts are disabled" = "Bestätigungen sind deaktiviert"; @@ -3875,7 +3817,7 @@ /* No comment provided by engineer. */ "Save servers" = "Alle Server speichern"; -/* No comment provided by engineer. */ +/* alert title */ "Save servers?" = "Alle Server speichern?"; /* No comment provided by engineer. */ @@ -4028,9 +3970,6 @@ /* No comment provided by engineer. */ "Send notifications" = "Benachrichtigungen senden"; -/* No comment provided by engineer. */ -"Send notifications:" = "Benachrichtigungen senden:"; - /* No comment provided by engineer. */ "Send questions and ideas" = "Senden Sie Fragen und Ideen"; @@ -4193,7 +4132,8 @@ /* No comment provided by engineer. */ "Shape profile images" = "Form der Profil-Bilder"; -/* chat item action */ +/* alert action + chat item action */ "Share" = "Teilen"; /* No comment provided by engineer. */ @@ -4202,7 +4142,7 @@ /* No comment provided by engineer. */ "Share address" = "Adresse teilen"; -/* No comment provided by engineer. */ +/* alert title */ "Share address with contacts?" = "Die Adresse mit Kontakten teilen?"; /* No comment provided by engineer. */ @@ -4385,10 +4325,10 @@ /* No comment provided by engineer. */ "Stop sending file?" = "Das Senden der Datei beenden?"; -/* No comment provided by engineer. */ +/* alert action */ "Stop sharing" = "Teilen beenden"; -/* No comment provided by engineer. */ +/* alert title */ "Stop sharing address?" = "Das Teilen der Adresse beenden?"; /* authentication reason */ @@ -4484,7 +4424,7 @@ /* No comment provided by engineer. */ "Test servers" = "Teste alle Server"; -/* No comment provided by engineer. */ +/* alert title */ "Tests failed!" = "Tests sind fehlgeschlagen!"; /* No comment provided by engineer. */ @@ -4496,9 +4436,6 @@ /* No comment provided by engineer. */ "Thanks to the users – contribute via Weblate!" = "Dank der Nutzer - Tragen Sie per Weblate bei!"; -/* No comment provided by engineer. */ -"No user identifiers." = "Keine Benutzerkennungen."; - /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "Wenn sie Nachrichten oder Kontaktanfragen empfangen, kann Sie die App benachrichtigen - Um dies zu aktivieren, öffnen Sie bitte die Einstellungen."; @@ -4523,6 +4460,9 @@ /* No comment provided by engineer. */ "The encryption is working and the new encryption agreement is not required. It may result in connection errors!" = "Die Verschlüsselung funktioniert und ein neues Verschlüsselungsabkommen ist nicht erforderlich. Es kann zu Verbindungsfehlern kommen!"; +/* No comment provided by engineer. */ +"The future of messaging" = "Die nächste Generation von privatem Messaging"; + /* No comment provided by engineer. */ "The hash of the previous message is different." = "Der Hash der vorherigen Nachricht unterscheidet sich."; @@ -4541,9 +4481,6 @@ /* No comment provided by engineer. */ "The messages will be marked as moderated for all members." = "Die Nachrichten werden für alle Mitglieder als moderiert gekennzeichnet werden."; -/* No comment provided by engineer. */ -"The future of messaging" = "Die nächste Generation von privatem Messaging"; - /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "Die alte Datenbank wurde während der Migration nicht entfernt. Sie kann gelöscht werden."; @@ -4631,9 +4568,6 @@ /* No comment provided by engineer. */ "To make a new connection" = "Um eine Verbindung mit einem neuen Kontakt zu erstellen"; -/* No comment provided by engineer. */ -"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Zum Schutz Ihrer Privatsphäre verwendet SimpleX an Stelle von Benutzerkennungen, die von allen anderen Plattformen verwendet werden, Kennungen für Nachrichtenwarteschlangen, die für jeden Ihrer Kontakte individuell sind."; - /* No comment provided by engineer. */ "To protect timezone, image/voice files use UTC." = "Bild- und Sprachdateinamen enthalten UTC, um Informationen zur Zeitzone zu schützen."; @@ -4643,6 +4577,9 @@ /* No comment provided by engineer. */ "To protect your IP address, private routing uses your SMP servers to deliver messages." = "Zum Schutz Ihrer IP-Adresse, wird für die Nachrichten-Auslieferung privates Routing über Ihre konfigurierten SMP-Server genutzt."; +/* No comment provided by engineer. */ +"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Zum Schutz Ihrer Privatsphäre verwendet SimpleX an Stelle von Benutzerkennungen, die von allen anderen Plattformen verwendet werden, Kennungen für Nachrichtenwarteschlangen, die für jeden Ihrer Kontakte individuell sind."; + /* No comment provided by engineer. */ "To record speech please grant permission to use Microphone." = "Bitte erteilen Sie für Sprach-Aufnahmen die Genehmigung das Mikrofon zu nutzen."; @@ -5030,9 +4967,6 @@ /* No comment provided by engineer. */ "when IP hidden" = "Wenn die IP-Adresse versteckt ist"; -/* No comment provided by engineer. */ -"When people request to connect, you can accept or reject it." = "Wenn Personen eine Verbindung anfordern, können Sie diese annehmen oder ablehnen."; - /* No comment provided by engineer. */ "When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "Wenn Sie ein Inkognito-Profil mit Jemandem teilen, wird dieses Profil auch für die Gruppen verwendet, für die Sie von diesem Kontakt eingeladen werden."; @@ -5174,9 +5108,6 @@ /* No comment provided by engineer. */ "You can share this address with your contacts to let them connect with **%@**." = "Sie können diese Adresse mit Ihren Kontakten teilen, um sie mit **%@** verbinden zu lassen."; -/* No comment provided by engineer. */ -"You can share your address as a link or QR code - anybody can connect to you." = "Sie können Ihre Adresse als Link oder als QR-Code teilen – Jede Person kann sich darüber mit Ihnen verbinden."; - /* No comment provided by engineer. */ "You can start chat via app Settings / Database or by restarting the app" = "Sie können den Chat über die App-Einstellungen / Datenbank oder durch Neustart der App starten"; @@ -5189,7 +5120,7 @@ /* No comment provided by engineer. */ "You can use markdown to format messages:" = "Um Nachrichteninhalte zu formatieren, können Sie Markdowns verwenden:"; -/* No comment provided by engineer. */ +/* alert message */ "You can view invitation link again in connection details." = "Den Einladungslink können Sie in den Details der Verbindung nochmals sehen."; /* No comment provided by engineer. */ @@ -5210,6 +5141,9 @@ /* No comment provided by engineer. */ "You could not be verified; please try again." = "Sie konnten nicht überprüft werden; bitte versuchen Sie es erneut."; +/* No comment provided by engineer. */ +"You decide who can connect." = "Sie entscheiden, wer sich mit Ihnen verbinden kann."; + /* No comment provided by engineer. */ "You have already requested connection via this address!" = "Sie haben über diese Adresse bereits eine Verbindung beantragt!"; @@ -5300,9 +5234,6 @@ /* No comment provided by engineer. */ "You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed" = "Sie verwenden ein Inkognito-Profil für diese Gruppe. Um zu verhindern, dass Sie Ihr Hauptprofil teilen, ist in diesem Fall das Einladen von Kontakten nicht erlaubt"; -/* No comment provided by engineer. */ -"Your %@ servers" = "Ihre %@-Server"; - /* No comment provided by engineer. */ "Your calls" = "Anrufe"; @@ -5366,9 +5297,6 @@ /* No comment provided by engineer. */ "Your random profile" = "Ihr Zufallsprofil"; -/* No comment provided by engineer. */ -"Your server" = "Ihr Server"; - /* No comment provided by engineer. */ "Your server address" = "Ihre Serveradresse"; @@ -5381,6 +5309,3 @@ /* No comment provided by engineer. */ "Your SMP servers" = "Ihre SMP-Server"; -/* No comment provided by engineer. */ -"Your XFTP servers" = "Ihre XFTP-Server"; - diff --git a/apps/ios/es.lproj/Localizable.strings b/apps/ios/es.lproj/Localizable.strings index 10b8bc317c..9f775acb54 100644 --- a/apps/ios/es.lproj/Localizable.strings +++ b/apps/ios/es.lproj/Localizable.strings @@ -1,15 +1,6 @@ /* No comment provided by engineer. */ "\n" = "\n"; -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - /* No comment provided by engineer. */ " (" = " ("; @@ -337,12 +328,6 @@ /* No comment provided by engineer. */ "Abort changing address?" = "¿Cancelar el cambio de servidor?"; -/* No comment provided by engineer. */ -"About SimpleX" = "Acerca de SimpleX"; - -/* No comment provided by engineer. */ -"About SimpleX address" = "Acerca de la dirección SimpleX"; - /* No comment provided by engineer. */ "About SimpleX Chat" = "Sobre SimpleX Chat"; @@ -382,12 +367,6 @@ /* No comment provided by engineer. */ "Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." = "Añade la dirección a tu perfil para que tus contactos puedan compartirla con otros. La actualización del perfil se enviará a tus contactos."; -/* No comment provided by engineer. */ -"Add contact" = "Añadir contacto"; - -/* No comment provided by engineer. */ -"Add preset servers" = "Añadir servidores predefinidos"; - /* No comment provided by engineer. */ "Add profile" = "Añadir perfil"; @@ -574,6 +553,9 @@ /* No comment provided by engineer. */ "Answer call" = "Responder llamada"; +/* No comment provided by engineer. */ +"Anybody can host servers." = "Cualquiera puede alojar servidores."; + /* No comment provided by engineer. */ "App build: %@" = "Compilación app: %@"; @@ -817,7 +799,8 @@ /* No comment provided by engineer. */ "Can't message member" = "No se pueden enviar mensajes al miembro"; -/* alert button */ +/* alert action + alert button */ "Cancel" = "Cancelar"; /* No comment provided by engineer. */ @@ -938,7 +921,7 @@ /* No comment provided by engineer. */ "Chats" = "Chats"; -/* No comment provided by engineer. */ +/* alert title */ "Check server address and try again." = "Comprueba la dirección del servidor e inténtalo de nuevo."; /* No comment provided by engineer. */ @@ -1001,9 +984,6 @@ /* No comment provided by engineer. */ "Configure ICE servers" = "Configure servidores ICE"; -/* No comment provided by engineer. */ -"Configured %@ servers" = "%@ servidores configurados"; - /* No comment provided by engineer. */ "Confirm" = "Confirmar"; @@ -1232,9 +1212,6 @@ /* No comment provided by engineer. */ "Create a group using a random profile." = "Crear grupo usando perfil aleatorio."; -/* No comment provided by engineer. */ -"Create an address to let people connect with you." = "Crea una dirección para que otras personas puedan conectar contigo."; - /* server test step */ "Create file" = "Crear archivo"; @@ -1397,7 +1374,8 @@ /* No comment provided by engineer. */ "default (yes)" = "predeterminado (sí)"; -/* chat item action +/* alert action + chat item action swipe action */ "Delete" = "Eliminar"; @@ -1996,9 +1974,6 @@ /* No comment provided by engineer. */ "Error joining group" = "Error al unirte al grupo"; -/* No comment provided by engineer. */ -"Error loading %@ servers" = "Error al cargar servidores %@"; - /* No comment provided by engineer. */ "Error migrating settings" = "Error al migrar la configuración"; @@ -2020,9 +1995,6 @@ /* No comment provided by engineer. */ "Error resetting statistics" = "Error al restablecer las estadísticas"; -/* No comment provided by engineer. */ -"Error saving %@ servers" = "Error al guardar servidores %@"; - /* No comment provided by engineer. */ "Error saving group profile" = "Error al guardar perfil de grupo"; @@ -2425,9 +2397,6 @@ /* time unit */ "hours" = "horas"; -/* No comment provided by engineer. */ -"How it works" = "Cómo funciona"; - /* No comment provided by engineer. */ "How SimpleX works" = "Cómo funciona SimpleX"; @@ -2570,10 +2539,10 @@ "Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)" = "Instalar terminal para [SimpleX Chat](https://github.com/simplex-chat/simplex-chat)"; /* No comment provided by engineer. */ -"Instant push notifications will be hidden!\n" = "¡Las notificaciones automáticas estarán ocultas!\n"; +"Instant" = "Al instante"; /* No comment provided by engineer. */ -"Instant" = "Al instante"; +"Instant push notifications will be hidden!\n" = "¡Las notificaciones automáticas estarán ocultas!\n"; /* No comment provided by engineer. */ "Interface" = "Interfaz"; @@ -2611,7 +2580,7 @@ /* No comment provided by engineer. */ "Invalid response" = "Respuesta no válida"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid server address!" = "¡Dirección de servidor no válida!"; /* item status text */ @@ -2716,7 +2685,7 @@ /* No comment provided by engineer. */ "Joining group" = "Entrando al grupo"; -/* No comment provided by engineer. */ +/* alert action */ "Keep" = "Guardar"; /* No comment provided by engineer. */ @@ -2725,7 +2694,7 @@ /* No comment provided by engineer. */ "Keep the app open to use it from desktop" = "Mantén la aplicación abierta para usarla desde el ordenador"; -/* No comment provided by engineer. */ +/* alert title */ "Keep unused invitation?" = "¿Guardar invitación no usada?"; /* No comment provided by engineer. */ @@ -2782,9 +2751,6 @@ /* No comment provided by engineer. */ "Live messages" = "Mensajes en vivo"; -/* No comment provided by engineer. */ -"No push server" = "No push server"; - /* No comment provided by engineer. */ "Local name" = "Nombre local"; @@ -2797,24 +2763,15 @@ /* No comment provided by engineer. */ "Lock mode" = "Modo bloqueo"; -/* No comment provided by engineer. */ -"Make a private connection" = "Establecer una conexión privada"; - /* No comment provided by engineer. */ "Make one message disappear" = "Escribir un mensaje temporal"; /* No comment provided by engineer. */ "Make profile private!" = "¡Hacer perfil privado!"; -/* No comment provided by engineer. */ -"Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." = "Asegúrate de que las direcciones del servidor %@ tienen el formato correcto, están separadas por líneas y no duplicadas (%@)."; - /* No comment provided by engineer. */ "Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "Asegúrate de que las direcciones del servidor WebRTC ICE tienen el formato correcto, están separadas por líneas y no duplicadas."; -/* No comment provided by engineer. */ -"Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" = "Muchos se preguntarán: *si SimpleX no tiene identificadores de usuario, ¿cómo puede entregar los mensajes?*"; - /* No comment provided by engineer. */ "Mark deleted for everyone" = "Marcar como eliminado para todos"; @@ -3160,6 +3117,9 @@ /* copied message info in history */ "no text" = "sin texto"; +/* No comment provided by engineer. */ +"No user identifiers." = "Sin identificadores de usuario."; + /* No comment provided by engineer. */ "Not compatible!" = "¡No compatible!"; @@ -3282,18 +3242,9 @@ /* authentication reason */ "Open migration to another device" = "Abrir menú migración a otro dispositivo"; -/* No comment provided by engineer. */ -"Open server settings" = "Abrir configuración del servidor"; - /* No comment provided by engineer. */ "Open Settings" = "Abrir Configuración"; -/* authentication reason */ -"Open user profiles" = "Abrir perfil de usuario"; - -/* No comment provided by engineer. */ -"Anybody can host servers." = "Cualquiera puede alojar servidores."; - /* No comment provided by engineer. */ "Opening app…" = "Iniciando aplicación…"; @@ -3315,9 +3266,6 @@ /* No comment provided by engineer. */ "Other" = "Otro"; -/* No comment provided by engineer. */ -"Other %@ servers" = "Otros servidores %@"; - /* No comment provided by engineer. */ "other errors" = "otros errores"; @@ -3372,9 +3320,6 @@ /* No comment provided by engineer. */ "Pending" = "Pendientes"; -/* No comment provided by engineer. */ -"You decide who can connect." = "Tu decides quién se conecta."; - /* No comment provided by engineer. */ "Periodic" = "Periódicamente"; @@ -3453,9 +3398,6 @@ /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Conserva el último borrador del mensaje con los datos adjuntos."; -/* No comment provided by engineer. */ -"Preset server" = "Servidor predefinido"; - /* No comment provided by engineer. */ "Preset server address" = "Dirección del servidor predefinida"; @@ -3504,7 +3446,7 @@ /* No comment provided by engineer. */ "Profile theme" = "Tema del perfil"; -/* No comment provided by engineer. */ +/* alert message */ "Profile update will be sent to your contacts." = "La actualización del perfil se enviará a tus contactos."; /* No comment provided by engineer. */ @@ -3589,10 +3531,10 @@ "Read more" = "Conoce más"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." = "Conoce más en el [Manual del Usuario](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)."; +"Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." = "Conoce más en la [Guía del Usuario](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)."; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." = "Conoce más en la [Guía del Usuario](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)."; +"Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." = "Conoce más en el [Manual del Usuario](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)."; /* No comment provided by engineer. */ "Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." = "Conoce más en el [Manual del Usuario](https://simplex.chat/docs/guide/readme.html#connect-to-friends)."; @@ -3600,9 +3542,6 @@ /* No comment provided by engineer. */ "Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." = "Conoce más en nuestro [repositorio GitHub](https://github.com/simplex-chat/simplex-chat#readme)."; -/* No comment provided by engineer. */ -"Read more in our GitHub repository." = "Conoce más en nuestro repositorio GitHub."; - /* No comment provided by engineer. */ "Receipts are disabled" = "Las confirmaciones están desactivadas"; @@ -3875,7 +3814,7 @@ /* No comment provided by engineer. */ "Save servers" = "Guardar servidores"; -/* No comment provided by engineer. */ +/* alert title */ "Save servers?" = "¿Guardar servidores?"; /* No comment provided by engineer. */ @@ -4028,9 +3967,6 @@ /* No comment provided by engineer. */ "Send notifications" = "Enviar notificaciones"; -/* No comment provided by engineer. */ -"Send notifications:" = "Enviar notificaciones:"; - /* No comment provided by engineer. */ "Send questions and ideas" = "Consultas y sugerencias"; @@ -4193,7 +4129,8 @@ /* No comment provided by engineer. */ "Shape profile images" = "Dar forma a las imágenes de perfil"; -/* chat item action */ +/* alert action + chat item action */ "Share" = "Compartir"; /* No comment provided by engineer. */ @@ -4202,7 +4139,7 @@ /* No comment provided by engineer. */ "Share address" = "Compartir dirección"; -/* No comment provided by engineer. */ +/* alert title */ "Share address with contacts?" = "¿Compartir la dirección con los contactos?"; /* No comment provided by engineer. */ @@ -4385,10 +4322,10 @@ /* No comment provided by engineer. */ "Stop sending file?" = "¿Dejar de enviar el archivo?"; -/* No comment provided by engineer. */ +/* alert action */ "Stop sharing" = "Dejar de compartir"; -/* No comment provided by engineer. */ +/* alert title */ "Stop sharing address?" = "¿Dejar de compartir la dirección?"; /* authentication reason */ @@ -4484,7 +4421,7 @@ /* No comment provided by engineer. */ "Test servers" = "Probar servidores"; -/* No comment provided by engineer. */ +/* alert title */ "Tests failed!" = "¡Pruebas no superadas!"; /* No comment provided by engineer. */ @@ -4496,9 +4433,6 @@ /* No comment provided by engineer. */ "Thanks to the users – contribute via Weblate!" = "¡Nuestro agradecimiento a todos los colaboradores! Puedes contribuir a través de Weblate"; -/* No comment provided by engineer. */ -"No user identifiers." = "Sin identificadores de usuario."; - /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "La aplicación puede notificarte cuando recibas mensajes o solicitudes de contacto: por favor, abre la configuración para activarlo."; @@ -4523,6 +4457,9 @@ /* No comment provided by engineer. */ "The encryption is working and the new encryption agreement is not required. It may result in connection errors!" = "El cifrado funciona y un cifrado nuevo no es necesario. ¡Podría dar lugar a errores de conexión!"; +/* No comment provided by engineer. */ +"The future of messaging" = "La nueva generación de mensajería privada"; + /* No comment provided by engineer. */ "The hash of the previous message is different." = "El hash del mensaje anterior es diferente."; @@ -4541,9 +4478,6 @@ /* No comment provided by engineer. */ "The messages will be marked as moderated for all members." = "Los mensajes serán marcados como moderados para todos los miembros."; -/* No comment provided by engineer. */ -"The future of messaging" = "La nueva generación de mensajería privada"; - /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "La base de datos antigua no se eliminó durante la migración, puede eliminarse."; @@ -4631,9 +4565,6 @@ /* No comment provided by engineer. */ "To make a new connection" = "Para hacer una conexión nueva"; -/* No comment provided by engineer. */ -"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Para proteger tu privacidad, en lugar de los identificadores de usuario que usan el resto de plataformas, SimpleX dispone de identificadores para las colas de mensajes, independientes para cada uno de tus contactos."; - /* No comment provided by engineer. */ "To protect timezone, image/voice files use UTC." = "Para proteger la zona horaria, los archivos de imagen/voz usan la hora UTC."; @@ -4643,6 +4574,9 @@ /* No comment provided by engineer. */ "To protect your IP address, private routing uses your SMP servers to deliver messages." = "Para proteger tu dirección IP, el enrutamiento privado usa tu lista de servidores SMP para enviar mensajes."; +/* No comment provided by engineer. */ +"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Para proteger tu privacidad, en lugar de los identificadores de usuario que usan el resto de plataformas, SimpleX dispone de identificadores para las colas de mensajes, independientes para cada uno de tus contactos."; + /* No comment provided by engineer. */ "To record speech please grant permission to use Microphone." = "Para grabación de voz, por favor concede el permiso para usar el micrófono."; @@ -5030,9 +4964,6 @@ /* No comment provided by engineer. */ "when IP hidden" = "con IP oculta"; -/* No comment provided by engineer. */ -"When people request to connect, you can accept or reject it." = "Cuando alguien solicite conectarse podrás aceptar o rechazar la solicitud."; - /* No comment provided by engineer. */ "When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "Cuando compartes un perfil incógnito con alguien, este perfil también se usará para los grupos a los que te inviten."; @@ -5174,9 +5105,6 @@ /* No comment provided by engineer. */ "You can share this address with your contacts to let them connect with **%@**." = "Puedes compartir esta dirección con tus contactos para que puedan conectar con **%@**."; -/* No comment provided by engineer. */ -"You can share your address as a link or QR code - anybody can connect to you." = "Puedes compartir tu dirección como enlace o código QR para que cualquiera pueda conectarse contigo."; - /* No comment provided by engineer. */ "You can start chat via app Settings / Database or by restarting the app" = "Puede iniciar Chat a través de la Configuración / Base de datos de la aplicación o reiniciando la aplicación"; @@ -5189,7 +5117,7 @@ /* No comment provided by engineer. */ "You can use markdown to format messages:" = "Puedes usar la sintaxis markdown para dar formato a tus mensajes:"; -/* No comment provided by engineer. */ +/* alert message */ "You can view invitation link again in connection details." = "Podrás ver el enlace de invitación en detalles de conexión."; /* No comment provided by engineer. */ @@ -5210,6 +5138,9 @@ /* No comment provided by engineer. */ "You could not be verified; please try again." = "No has podido ser autenticado. Inténtalo de nuevo."; +/* No comment provided by engineer. */ +"You decide who can connect." = "Tu decides quién se conecta."; + /* No comment provided by engineer. */ "You have already requested connection via this address!" = "¡Ya has solicitado la conexión mediante esta dirección!"; @@ -5300,9 +5231,6 @@ /* No comment provided by engineer. */ "You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed" = "Estás usando un perfil incógnito en este grupo. Para evitar descubrir tu perfil principal no se permite invitar contactos"; -/* No comment provided by engineer. */ -"Your %@ servers" = "Mis servidores %@"; - /* No comment provided by engineer. */ "Your calls" = "Llamadas"; @@ -5366,9 +5294,6 @@ /* No comment provided by engineer. */ "Your random profile" = "Tu perfil aleatorio"; -/* No comment provided by engineer. */ -"Your server" = "Tu servidor"; - /* No comment provided by engineer. */ "Your server address" = "Dirección del servidor"; @@ -5381,6 +5306,3 @@ /* No comment provided by engineer. */ "Your SMP servers" = "Servidores SMP"; -/* No comment provided by engineer. */ -"Your XFTP servers" = "Servidores XFTP"; - diff --git a/apps/ios/fi.lproj/Localizable.strings b/apps/ios/fi.lproj/Localizable.strings index 081e8735a1..f927dbdabb 100644 --- a/apps/ios/fi.lproj/Localizable.strings +++ b/apps/ios/fi.lproj/Localizable.strings @@ -1,15 +1,6 @@ /* No comment provided by engineer. */ "\n" = "\n"; -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - /* No comment provided by engineer. */ " (" = " ("; @@ -262,12 +253,6 @@ /* No comment provided by engineer. */ "Abort changing address?" = "Keskeytä osoitteenvaihto?"; -/* No comment provided by engineer. */ -"About SimpleX" = "Tietoja SimpleX:stä"; - -/* No comment provided by engineer. */ -"About SimpleX address" = "Tietoja SimpleX osoitteesta"; - /* No comment provided by engineer. */ "About SimpleX Chat" = "Tietoja SimpleX Chatistä"; @@ -295,9 +280,6 @@ /* No comment provided by engineer. */ "Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." = "Lisää osoite profiiliisi, jotta kontaktisi voivat jakaa sen muiden kanssa. Profiilipäivitys lähetetään kontakteillesi."; -/* No comment provided by engineer. */ -"Add preset servers" = "Lisää esiasetettuja palvelimia"; - /* No comment provided by engineer. */ "Add profile" = "Lisää profiili"; @@ -424,6 +406,9 @@ /* No comment provided by engineer. */ "Answer call" = "Vastaa puheluun"; +/* No comment provided by engineer. */ +"Anybody can host servers." = "Avoimen lähdekoodin protokolla ja koodi - kuka tahansa voi käyttää palvelimia."; + /* No comment provided by engineer. */ "App build: %@" = "Sovellusversio: %@"; @@ -544,7 +529,8 @@ /* No comment provided by engineer. */ "Can't invite contacts!" = "Kontakteja ei voi kutsua!"; -/* alert button */ +/* alert action + alert button */ "Cancel" = "Peruuta"; /* feature offered item */ @@ -632,7 +618,7 @@ /* No comment provided by engineer. */ "Chats" = "Keskustelut"; -/* No comment provided by engineer. */ +/* alert title */ "Check server address and try again." = "Tarkista palvelimen osoite ja yritä uudelleen."; /* No comment provided by engineer. */ @@ -794,9 +780,6 @@ /* No comment provided by engineer. */ "Create" = "Luo"; -/* No comment provided by engineer. */ -"Create an address to let people connect with you." = "Luo osoite, jolla ihmiset voivat ottaa sinuun yhteyttä."; - /* server test step */ "Create file" = "Luo tiedosto"; @@ -920,7 +903,8 @@ /* No comment provided by engineer. */ "default (yes)" = "oletusarvo (kyllä)"; -/* chat item action +/* alert action + chat item action swipe action */ "Delete" = "Poista"; @@ -1353,18 +1337,12 @@ /* No comment provided by engineer. */ "Error joining group" = "Virhe ryhmään liittymisessä"; -/* No comment provided by engineer. */ -"Error loading %@ servers" = "Virhe %@-palvelimien lataamisessa"; - /* alert title */ "Error receiving file" = "Virhe tiedoston vastaanottamisessa"; /* No comment provided by engineer. */ "Error removing member" = "Virhe poistettaessa jäsentä"; -/* No comment provided by engineer. */ -"Error saving %@ servers" = "Virhe %@ palvelimien tallentamisessa"; - /* No comment provided by engineer. */ "Error saving group profile" = "Virhe ryhmäprofiilin tallentamisessa"; @@ -1632,9 +1610,6 @@ /* time unit */ "hours" = "tuntia"; -/* No comment provided by engineer. */ -"How it works" = "Kuinka se toimii"; - /* No comment provided by engineer. */ "How SimpleX works" = "Miten SimpleX toimii"; @@ -1744,10 +1719,10 @@ "Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)" = "Asenna [SimpleX Chat terminaalille](https://github.com/simplex-chat/simplex-chat)"; /* No comment provided by engineer. */ -"Instant push notifications will be hidden!\n" = "Välittömät push-ilmoitukset ovat piilossa!\n"; +"Instant" = "Heti"; /* No comment provided by engineer. */ -"Instant" = "Heti"; +"Instant push notifications will be hidden!\n" = "Välittömät push-ilmoitukset ovat piilossa!\n"; /* No comment provided by engineer. */ "Interface" = "Käyttöliittymä"; @@ -1764,7 +1739,7 @@ /* invalid chat item */ "invalid data" = "virheelliset tiedot"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid server address!" = "Virheellinen palvelinosoite!"; /* item status text */ @@ -1893,9 +1868,6 @@ /* No comment provided by engineer. */ "Live messages" = "Live-viestit"; -/* No comment provided by engineer. */ -"No push server" = "Paikallinen"; - /* No comment provided by engineer. */ "Local name" = "Paikallinen nimi"; @@ -1908,24 +1880,15 @@ /* No comment provided by engineer. */ "Lock mode" = "Lukitustila"; -/* No comment provided by engineer. */ -"Make a private connection" = "Luo yksityinen yhteys"; - /* No comment provided by engineer. */ "Make one message disappear" = "Hävitä yksi viesti"; /* No comment provided by engineer. */ "Make profile private!" = "Tee profiilista yksityinen!"; -/* No comment provided by engineer. */ -"Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." = "Varmista, että %@-palvelinosoitteet ovat oikeassa muodossa, että ne on erotettu toisistaan riveittäin ja että ne eivät ole päällekkäisiä (%@)."; - /* No comment provided by engineer. */ "Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "Varmista, että WebRTC ICE -palvelinosoitteet ovat oikeassa muodossa, rivieroteltuina ja että ne eivät ole päällekkäisiä."; -/* No comment provided by engineer. */ -"Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" = "Monet ihmiset kysyivät: *Jos SimpleX:llä ei ole käyttäjätunnuksia, miten se voi toimittaa viestejä?*"; - /* No comment provided by engineer. */ "Mark deleted for everyone" = "Merkitse poistetuksi kaikilta"; @@ -2127,12 +2090,18 @@ /* No comment provided by engineer. */ "No permission to record voice message" = "Ei lupaa ääniviestin tallentamiseen"; +/* No comment provided by engineer. */ +"No push server" = "Paikallinen"; + /* No comment provided by engineer. */ "No received or sent files" = "Ei vastaanotettuja tai lähetettyjä tiedostoja"; /* copied message info in history */ "no text" = "ei tekstiä"; +/* No comment provided by engineer. */ +"No user identifiers." = "Ensimmäinen alusta ilman käyttäjätunnisteita – suunniteltu yksityiseksi."; + /* No comment provided by engineer. */ "Notifications" = "Ilmoitukset"; @@ -2234,12 +2203,6 @@ /* No comment provided by engineer. */ "Open Settings" = "Avaa Asetukset"; -/* authentication reason */ -"Open user profiles" = "Avaa käyttäjäprofiilit"; - -/* No comment provided by engineer. */ -"Anybody can host servers." = "Avoimen lähdekoodin protokolla ja koodi - kuka tahansa voi käyttää palvelimia."; - /* member role */ "owner" = "omistaja"; @@ -2267,9 +2230,6 @@ /* No comment provided by engineer. */ "peer-to-peer" = "vertais"; -/* No comment provided by engineer. */ -"You decide who can connect." = "Kimin bağlanabileceğine siz karar verirsiniz."; - /* No comment provided by engineer. */ "Periodic" = "Ajoittain"; @@ -2327,9 +2287,6 @@ /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Säilytä viimeinen viestiluonnos liitteineen."; -/* No comment provided by engineer. */ -"Preset server" = "Esiasetettu palvelin"; - /* No comment provided by engineer. */ "Preset server address" = "Esiasetettu palvelimen osoite"; @@ -2354,7 +2311,7 @@ /* No comment provided by engineer. */ "Profile password" = "Profiilin salasana"; -/* No comment provided by engineer. */ +/* alert message */ "Profile update will be sent to your contacts." = "Profiilipäivitys lähetetään kontakteillesi."; /* No comment provided by engineer. */ @@ -2417,9 +2374,6 @@ /* No comment provided by engineer. */ "Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." = "Lue lisää [GitHub-arkistosta](https://github.com/simplex-chat/simplex-chat#readme)."; -/* No comment provided by engineer. */ -"Read more in our GitHub repository." = "Lue lisää GitHub-tietovarastostamme."; - /* No comment provided by engineer. */ "Receipts are disabled" = "Kuittaukset pois käytöstä"; @@ -2605,7 +2559,7 @@ /* No comment provided by engineer. */ "Save servers" = "Tallenna palvelimet"; -/* No comment provided by engineer. */ +/* alert title */ "Save servers?" = "Tallenna palvelimet?"; /* No comment provided by engineer. */ @@ -2686,9 +2640,6 @@ /* No comment provided by engineer. */ "Send notifications" = "Lähetys ilmoitukset"; -/* No comment provided by engineer. */ -"Send notifications:" = "Lähetys ilmoitukset:"; - /* No comment provided by engineer. */ "Send questions and ideas" = "Lähetä kysymyksiä ja ideoita"; @@ -2782,7 +2733,8 @@ /* No comment provided by engineer. */ "Settings" = "Asetukset"; -/* chat item action */ +/* alert action + chat item action */ "Share" = "Jaa"; /* No comment provided by engineer. */ @@ -2791,7 +2743,7 @@ /* No comment provided by engineer. */ "Share address" = "Jaa osoite"; -/* No comment provided by engineer. */ +/* alert title */ "Share address with contacts?" = "Jaa osoite kontakteille?"; /* No comment provided by engineer. */ @@ -2896,10 +2848,10 @@ /* No comment provided by engineer. */ "Stop sending file?" = "Lopeta tiedoston lähettäminen?"; -/* No comment provided by engineer. */ +/* alert action */ "Stop sharing" = "Lopeta jakaminen"; -/* No comment provided by engineer. */ +/* alert title */ "Stop sharing address?" = "Lopeta osoitteen jakaminen?"; /* authentication reason */ @@ -2956,7 +2908,7 @@ /* No comment provided by engineer. */ "Test servers" = "Testipalvelimet"; -/* No comment provided by engineer. */ +/* alert title */ "Tests failed!" = "Testit epäonnistuivat!"; /* No comment provided by engineer. */ @@ -2968,9 +2920,6 @@ /* No comment provided by engineer. */ "Thanks to the users – contribute via Weblate!" = "Kiitokset käyttäjille – osallistu Weblaten kautta!"; -/* No comment provided by engineer. */ -"No user identifiers." = "Ensimmäinen alusta ilman käyttäjätunnisteita – suunniteltu yksityiseksi."; - /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "Sovellus voi ilmoittaa sinulle, kun saat viestejä tai yhteydenottopyyntöjä - avaa asetukset ottaaksesi ne käyttöön."; @@ -2989,6 +2938,9 @@ /* No comment provided by engineer. */ "The encryption is working and the new encryption agreement is not required. It may result in connection errors!" = "Salaus toimii ja uutta salaussopimusta ei tarvita. Tämä voi johtaa yhteysvirheisiin!"; +/* No comment provided by engineer. */ +"The future of messaging" = "Seuraavan sukupolven yksityisviestit"; + /* No comment provided by engineer. */ "The hash of the previous message is different." = "Edellisen viestin tarkiste on erilainen."; @@ -3001,9 +2953,6 @@ /* No comment provided by engineer. */ "The message will be marked as moderated for all members." = "Viesti merkitään moderoiduksi kaikille jäsenille."; -/* No comment provided by engineer. */ -"The future of messaging" = "Seuraavan sukupolven yksityisviestit"; - /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "Vanhaa tietokantaa ei poistettu siirron aikana, se voidaan kuitenkin poistaa."; @@ -3055,15 +3004,15 @@ /* No comment provided by engineer. */ "To make a new connection" = "Uuden yhteyden luominen"; -/* No comment provided by engineer. */ -"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Yksityisyyden suojaamiseksi kaikkien muiden alustojen käyttämien käyttäjätunnusten sijaan SimpleX käyttää viestijonojen tunnisteita, jotka ovat kaikille kontakteille erillisiä."; - /* No comment provided by engineer. */ "To protect timezone, image/voice files use UTC." = "Aikavyöhykkeen suojaamiseksi kuva-/äänitiedostot käyttävät UTC:tä."; /* No comment provided by engineer. */ "To protect your information, turn on SimpleX Lock.\nYou will be prompted to complete authentication before this feature is enabled." = "Suojaa tietosi ottamalla SimpleX Lock käyttöön.\nSinua kehotetaan suorittamaan todennus loppuun, ennen kuin tämä ominaisuus otetaan käyttöön."; +/* No comment provided by engineer. */ +"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Yksityisyyden suojaamiseksi kaikkien muiden alustojen käyttämien käyttäjätunnusten sijaan SimpleX käyttää viestijonojen tunnisteita, jotka ovat kaikille kontakteille erillisiä."; + /* No comment provided by engineer. */ "To record voice message please grant permission to use Microphone." = "Jos haluat nauhoittaa ääniviestin, anna lupa käyttää mikrofonia."; @@ -3286,9 +3235,6 @@ /* No comment provided by engineer. */ "When available" = "Kun saatavilla"; -/* No comment provided by engineer. */ -"When people request to connect, you can accept or reject it." = "Kun ihmiset pyytävät yhteyden muodostamista, voit hyväksyä tai hylätä sen."; - /* No comment provided by engineer. */ "When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "Kun jaat inkognitoprofiilin jonkun kanssa, tätä profiilia käytetään ryhmissä, joihin tämä sinut kutsuu."; @@ -3355,9 +3301,6 @@ /* No comment provided by engineer. */ "You can share this address with your contacts to let them connect with **%@**." = "Voit jakaa tämän osoitteen kontaktiesi kanssa, jotta ne voivat muodostaa yhteyden **%@** kanssa."; -/* No comment provided by engineer. */ -"You can share your address as a link or QR code - anybody can connect to you." = "Voit jakaa osoitteesi linkkinä tai QR-koodina - kuka tahansa voi muodostaa yhteyden sinuun."; - /* No comment provided by engineer. */ "You can start chat via app Settings / Database or by restarting the app" = "Voit aloittaa keskustelun sovelluksen Asetukset / Tietokanta kautta tai käynnistämällä sovelluksen uudelleen"; @@ -3385,6 +3328,9 @@ /* No comment provided by engineer. */ "You could not be verified; please try again." = "Sinua ei voitu todentaa; yritä uudelleen."; +/* No comment provided by engineer. */ +"You decide who can connect." = "Kimin bağlanabileceğine siz karar verirsiniz."; + /* No comment provided by engineer. */ "You have to enter passphrase every time the app starts - it is not stored on the device." = "Sinun on annettava tunnuslause aina, kun sovellus käynnistyy - sitä ei tallenneta laitteeseen."; @@ -3451,9 +3397,6 @@ /* No comment provided by engineer. */ "You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed" = "Käytät tässä ryhmässä incognito-profiilia. Kontaktien kutsuminen ei ole sallittua, jotta pääprofiilisi ei tule jaetuksi"; -/* No comment provided by engineer. */ -"Your %@ servers" = "%@-palvelimesi"; - /* No comment provided by engineer. */ "Your calls" = "Puhelusi"; @@ -3502,9 +3445,6 @@ /* No comment provided by engineer. */ "Your random profile" = "Satunnainen profiilisi"; -/* No comment provided by engineer. */ -"Your server" = "Palvelimesi"; - /* No comment provided by engineer. */ "Your server address" = "Palvelimesi osoite"; @@ -3517,6 +3457,3 @@ /* No comment provided by engineer. */ "Your SMP servers" = "SMP-palvelimesi"; -/* No comment provided by engineer. */ -"Your XFTP servers" = "XFTP-palvelimesi"; - diff --git a/apps/ios/fr.lproj/Localizable.strings b/apps/ios/fr.lproj/Localizable.strings index 4b4a5aaf4d..239d425973 100644 --- a/apps/ios/fr.lproj/Localizable.strings +++ b/apps/ios/fr.lproj/Localizable.strings @@ -1,15 +1,6 @@ /* No comment provided by engineer. */ "\n" = "\n"; -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - /* No comment provided by engineer. */ " (" = " ("; @@ -157,6 +148,9 @@ /* notification title */ "%@ wants to connect!" = "%@ veut se connecter !"; +/* format for date separator in chat */ +"%@, %@" = "%1$@, %2$@"; + /* No comment provided by engineer. */ "%@, %@ and %lld members" = "%@, %@ et %lld membres"; @@ -169,9 +163,24 @@ /* time interval */ "%d days" = "%d jours"; +/* forward confirmation reason */ +"%d file(s) are still being downloaded." = "%d fichier(s) en cours de téléchargement."; + +/* forward confirmation reason */ +"%d file(s) failed to download." = "Le téléchargement de %d fichier(s) a échoué."; + +/* forward confirmation reason */ +"%d file(s) were deleted." = "Le(s) fichier(s) %d a(ont) été supprimé(s)."; + +/* forward confirmation reason */ +"%d file(s) were not downloaded." = "Le(s) fichier(s) %d n'a (n'ont) pas été téléchargé(s)."; + /* time interval */ "%d hours" = "%d heures"; +/* alert title */ +"%d messages not forwarded" = "%d messages non transférés"; + /* time interval */ "%d min" = "%d min"; @@ -319,12 +328,6 @@ /* No comment provided by engineer. */ "Abort changing address?" = "Abandonner le changement d'adresse ?"; -/* No comment provided by engineer. */ -"About SimpleX" = "À propos de SimpleX"; - -/* No comment provided by engineer. */ -"About SimpleX address" = "À propos de l'adresse SimpleX"; - /* No comment provided by engineer. */ "About SimpleX Chat" = "À propos de SimpleX Chat"; @@ -364,12 +367,6 @@ /* No comment provided by engineer. */ "Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." = "Ajoutez une adresse à votre profil, afin que vos contacts puissent la partager avec d'autres personnes. La mise à jour du profil sera envoyée à vos contacts."; -/* No comment provided by engineer. */ -"Add contact" = "Ajouter le contact"; - -/* No comment provided by engineer. */ -"Add preset servers" = "Ajouter des serveurs prédéfinis"; - /* No comment provided by engineer. */ "Add profile" = "Ajouter un profil"; @@ -556,6 +553,9 @@ /* No comment provided by engineer. */ "Answer call" = "Répondre à l'appel"; +/* No comment provided by engineer. */ +"Anybody can host servers." = "N'importe qui peut heberger un serveur."; + /* No comment provided by engineer. */ "App build: %@" = "Build de l'app : %@"; @@ -574,6 +574,9 @@ /* No comment provided by engineer. */ "App passcode is replaced with self-destruct passcode." = "Le code d'accès de l'application est remplacé par un code d'autodestruction."; +/* No comment provided by engineer. */ +"App session" = "Session de l'app"; + /* No comment provided by engineer. */ "App version" = "Version de l'app"; @@ -646,6 +649,9 @@ /* No comment provided by engineer. */ "Auto-accept images" = "Images auto-acceptées"; +/* alert title */ +"Auto-accept settings" = "Paramètres de réception automatique"; + /* No comment provided by engineer. */ "Back" = "Retour"; @@ -667,15 +673,30 @@ /* No comment provided by engineer. */ "Bad message ID" = "Mauvais ID de message"; +/* No comment provided by engineer. */ +"Better calls" = "Appels améliorés"; + /* No comment provided by engineer. */ "Better groups" = "Des groupes plus performants"; +/* No comment provided by engineer. */ +"Better message dates." = "Meilleures dates de messages."; + /* No comment provided by engineer. */ "Better messages" = "Meilleurs messages"; /* No comment provided by engineer. */ "Better networking" = "Meilleure gestion de réseau"; +/* No comment provided by engineer. */ +"Better notifications" = "Notifications améliorées"; + +/* No comment provided by engineer. */ +"Better security ✅" = "Sécurité accrue ✅"; + +/* No comment provided by engineer. */ +"Better user experience" = "Une meilleure expérience pour l'utilisateur"; + /* No comment provided by engineer. */ "Black" = "Noir"; @@ -778,7 +799,8 @@ /* No comment provided by engineer. */ "Can't message member" = "Impossible d'envoyer un message à ce membre"; -/* alert button */ +/* alert action + alert button */ "Cancel" = "Annuler"; /* No comment provided by engineer. */ @@ -887,6 +909,9 @@ /* No comment provided by engineer. */ "Chat preferences" = "Préférences de chat"; +/* alert message */ +"Chat preferences were changed." = "Les préférences de discussion ont été modifiées."; + /* No comment provided by engineer. */ "Chat profile" = "Profil d'utilisateur"; @@ -896,7 +921,7 @@ /* No comment provided by engineer. */ "Chats" = "Discussions"; -/* No comment provided by engineer. */ +/* alert title */ "Check server address and try again." = "Vérifiez l'adresse du serveur et réessayez."; /* No comment provided by engineer. */ @@ -959,9 +984,6 @@ /* No comment provided by engineer. */ "Configure ICE servers" = "Configurer les serveurs ICE"; -/* No comment provided by engineer. */ -"Configured %@ servers" = "%@ serveurs configurés"; - /* No comment provided by engineer. */ "Confirm" = "Confirmer"; @@ -1178,6 +1200,9 @@ /* No comment provided by engineer. */ "Core version: v%@" = "Version du cœur : v%@"; +/* No comment provided by engineer. */ +"Corner" = "Coin"; + /* No comment provided by engineer. */ "Correct name to %@?" = "Corriger le nom pour %@ ?"; @@ -1187,9 +1212,6 @@ /* No comment provided by engineer. */ "Create a group using a random profile." = "Création de groupes via un profil aléatoire."; -/* No comment provided by engineer. */ -"Create an address to let people connect with you." = "Vous pouvez créer une adresse pour permettre aux autres utilisateurs de vous contacter."; - /* server test step */ "Create file" = "Créer un fichier"; @@ -1259,6 +1281,9 @@ /* No comment provided by engineer. */ "Custom time" = "Délai personnalisé"; +/* No comment provided by engineer. */ +"Customizable message shape." = "Forme des messages personnalisable."; + /* No comment provided by engineer. */ "Customize theme" = "Personnaliser le thème"; @@ -1349,7 +1374,8 @@ /* No comment provided by engineer. */ "default (yes)" = "par défaut (oui)"; -/* chat item action +/* alert action + chat item action swipe action */ "Delete" = "Supprimer"; @@ -1449,6 +1475,9 @@ /* No comment provided by engineer. */ "Delete old database?" = "Supprimer l'ancienne base de données ?"; +/* No comment provided by engineer. */ +"Delete or moderate up to 200 messages." = "Supprimer ou modérer jusqu'à 200 messages."; + /* No comment provided by engineer. */ "Delete pending connection?" = "Supprimer la connexion en attente ?"; @@ -1611,6 +1640,9 @@ /* No comment provided by engineer. */ "Do NOT send messages directly, even if your or destination server does not support private routing." = "Ne pas envoyer de messages directement, même si votre serveur ou le serveur de destination ne prend pas en charge le routage privé."; +/* No comment provided by engineer. */ +"Do not use credentials with proxy." = "Ne pas utiliser d'identifiants avec le proxy."; + /* No comment provided by engineer. */ "Do NOT use private routing." = "Ne pas utiliser de routage privé."; @@ -1642,6 +1674,9 @@ /* server test step */ "Download file" = "Télécharger le fichier"; +/* alert action */ +"Download files" = "Télécharger les fichiers"; + /* No comment provided by engineer. */ "Downloaded" = "Téléchargé"; @@ -1858,12 +1893,18 @@ /* No comment provided by engineer. */ "Error changing address" = "Erreur de changement d'adresse"; +/* No comment provided by engineer. */ +"Error changing connection profile" = "Erreur lors du changement de profil de connexion"; + /* No comment provided by engineer. */ "Error changing role" = "Erreur lors du changement de rôle"; /* No comment provided by engineer. */ "Error changing setting" = "Erreur de changement de paramètre"; +/* No comment provided by engineer. */ +"Error changing to incognito!" = "Erreur lors du passage en mode incognito !"; + /* No comment provided by engineer. */ "Error connecting to forwarding server %@. Please try later." = "Erreur de connexion au serveur de redirection %@. Veuillez réessayer plus tard."; @@ -1934,7 +1975,7 @@ "Error joining group" = "Erreur lors de la liaison avec le groupe"; /* No comment provided by engineer. */ -"Error loading %@ servers" = "Erreur lors du chargement des serveurs %@"; +"Error migrating settings" = "Erreur lors de la migration des paramètres"; /* No comment provided by engineer. */ "Error opening chat" = "Erreur lors de l'ouverture du chat"; @@ -1954,9 +1995,6 @@ /* No comment provided by engineer. */ "Error resetting statistics" = "Erreur de réinitialisation des statistiques"; -/* No comment provided by engineer. */ -"Error saving %@ servers" = "Erreur lors de la sauvegarde des serveurs %@"; - /* No comment provided by engineer. */ "Error saving group profile" = "Erreur lors de la sauvegarde du profil de groupe"; @@ -1996,6 +2034,9 @@ /* No comment provided by engineer. */ "Error stopping chat" = "Erreur lors de l'arrêt du chat"; +/* No comment provided by engineer. */ +"Error switching profile" = "Erreur lors du changement de profil"; + /* alertTitle */ "Error switching profile!" = "Erreur lors du changement de profil !"; @@ -2083,6 +2124,9 @@ /* No comment provided by engineer. */ "File error" = "Erreur de fichier"; +/* alert message */ +"File errors:\n%@" = "Erreurs de fichier :\n%@"; + /* file error text */ "File not found - most likely file was deleted or cancelled." = "Fichier introuvable - le fichier a probablement été supprimé ou annulé."; @@ -2164,9 +2208,21 @@ /* chat item action */ "Forward" = "Transférer"; +/* alert title */ +"Forward %d message(s)?" = "Transférer %d message(s) ?"; + /* No comment provided by engineer. */ "Forward and save messages" = "Transférer et sauvegarder des messages"; +/* alert action */ +"Forward messages" = "Transférer les messages"; + +/* alert message */ +"Forward messages without files?" = "Transférer les messages sans les fichiers ?"; + +/* No comment provided by engineer. */ +"Forward up to 20 messages at once." = "Transférez jusqu'à 20 messages à la fois."; + /* No comment provided by engineer. */ "forwarded" = "transféré"; @@ -2176,6 +2232,9 @@ /* No comment provided by engineer. */ "Forwarded from" = "Transféré depuis"; +/* No comment provided by engineer. */ +"Forwarding %lld messages" = "Transfert des %lld messages"; + /* No comment provided by engineer. */ "Forwarding server %@ failed to connect to destination server %@. Please try later." = "Le serveur de redirection %@ n'a pas réussi à se connecter au serveur de destination %@. Veuillez réessayer plus tard."; @@ -2338,9 +2397,6 @@ /* time unit */ "hours" = "heures"; -/* No comment provided by engineer. */ -"How it works" = "Comment ça fonctionne"; - /* No comment provided by engineer. */ "How SimpleX works" = "Comment SimpleX fonctionne"; @@ -2404,6 +2460,9 @@ /* No comment provided by engineer. */ "Importing archive" = "Importation de l'archive"; +/* No comment provided by engineer. */ +"Improved delivery, reduced traffic usage.\nMore improvements are coming soon!" = "Amélioration de la distribution, réduction de l'utilisation du trafic.\nD'autres améliorations sont à venir !"; + /* No comment provided by engineer. */ "Improved message delivery" = "Amélioration de la transmission des messages"; @@ -2480,10 +2539,10 @@ "Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)" = "Installer [SimpleX Chat pour terminal](https://github.com/simplex-chat/simplex-chat)"; /* No comment provided by engineer. */ -"Instant push notifications will be hidden!\n" = "Les notifications push instantanées vont être cachées !\n"; +"Instant" = "Instantané"; /* No comment provided by engineer. */ -"Instant" = "Instantané"; +"Instant push notifications will be hidden!\n" = "Les notifications push instantanées vont être cachées !\n"; /* No comment provided by engineer. */ "Interface" = "Interface"; @@ -2521,7 +2580,7 @@ /* No comment provided by engineer. */ "Invalid response" = "Réponse invalide"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid server address!" = "Adresse de serveur invalide !"; /* item status text */ @@ -2563,6 +2622,9 @@ /* No comment provided by engineer. */ "iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications." = "La keychain d'iOS sera utilisée pour stocker en toute sécurité la phrase secrète après le redémarrage de l'app ou la modification de la phrase secrète - il permettra de recevoir les notifications push."; +/* No comment provided by engineer. */ +"IP address" = "Adresse IP"; + /* No comment provided by engineer. */ "Irreversible message deletion" = "Suppression irréversible des messages"; @@ -2623,7 +2685,7 @@ /* No comment provided by engineer. */ "Joining group" = "Entrain de rejoindre le groupe"; -/* No comment provided by engineer. */ +/* alert action */ "Keep" = "Conserver"; /* No comment provided by engineer. */ @@ -2632,7 +2694,7 @@ /* No comment provided by engineer. */ "Keep the app open to use it from desktop" = "Garder l'application ouverte pour l'utiliser depuis le bureau"; -/* No comment provided by engineer. */ +/* alert title */ "Keep unused invitation?" = "Conserver l'invitation inutilisée ?"; /* No comment provided by engineer. */ @@ -2689,9 +2751,6 @@ /* No comment provided by engineer. */ "Live messages" = "Messages dynamiques"; -/* No comment provided by engineer. */ -"No push server" = "No push server"; - /* No comment provided by engineer. */ "Local name" = "Nom local"; @@ -2704,24 +2763,15 @@ /* No comment provided by engineer. */ "Lock mode" = "Mode de verrouillage"; -/* No comment provided by engineer. */ -"Make a private connection" = "Établir une connexion privée"; - /* No comment provided by engineer. */ "Make one message disappear" = "Rendre un message éphémère"; /* No comment provided by engineer. */ "Make profile private!" = "Rendre un profil privé !"; -/* No comment provided by engineer. */ -"Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." = "Assurez-vous que les adresses des serveurs %@ sont au bon format et ne sont pas dupliquées, un par ligne (%@)."; - /* No comment provided by engineer. */ "Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "Assurez-vous que les adresses des serveurs WebRTC ICE sont au bon format et ne sont pas dupliquées, un par ligne."; -/* No comment provided by engineer. */ -"Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" = "Beaucoup se demandent : *si SimpleX n'a pas d'identifiant d'utilisateur, comment peut-il délivrer des messages ?*"; - /* No comment provided by engineer. */ "Mark deleted for everyone" = "Marquer comme supprimé pour tout le monde"; @@ -2815,6 +2865,9 @@ /* No comment provided by engineer. */ "Message servers" = "Serveurs de messages"; +/* No comment provided by engineer. */ +"Message shape" = "Forme du message"; + /* No comment provided by engineer. */ "Message source remains private." = "La source du message reste privée."; @@ -2845,6 +2898,9 @@ /* No comment provided by engineer. */ "Messages sent" = "Messages envoyés"; +/* alert message */ +"Messages were deleted after you selected them." = "Les messages ont été supprimés après avoir été sélectionnés."; + /* No comment provided by engineer. */ "Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery." = "Les messages, fichiers et appels sont protégés par un chiffrement **de bout en bout** avec une confidentialité persistante, une répudiation et une récupération en cas d'effraction."; @@ -2998,6 +3054,12 @@ /* No comment provided by engineer. */ "New passphrase…" = "Nouvelle phrase secrète…"; +/* No comment provided by engineer. */ +"New SOCKS credentials will be used every time you start the app." = "De nouveaux identifiants SOCKS seront utilisés chaque fois que vous démarrerez l'application."; + +/* No comment provided by engineer. */ +"New SOCKS credentials will be used for each server." = "De nouveaux identifiants SOCKS seront utilisées pour chaque serveur."; + /* pref value */ "no" = "non"; @@ -3040,21 +3102,36 @@ /* No comment provided by engineer. */ "No network connection" = "Pas de connexion au réseau"; +/* No comment provided by engineer. */ +"No permission to record speech" = "Enregistrement des conversations non autorisé"; + +/* No comment provided by engineer. */ +"No permission to record video" = "Enregistrement de la vidéo non autorisé"; + /* No comment provided by engineer. */ "No permission to record voice message" = "Pas l'autorisation d'enregistrer un message vocal"; +/* No comment provided by engineer. */ +"No push server" = "No push server"; + /* No comment provided by engineer. */ "No received or sent files" = "Aucun fichier reçu ou envoyé"; /* copied message info in history */ "no text" = "aucun texte"; +/* No comment provided by engineer. */ +"No user identifiers." = "Aucun identifiant d'utilisateur."; + /* No comment provided by engineer. */ "Not compatible!" = "Non compatible !"; /* No comment provided by engineer. */ "Nothing selected" = "Aucune sélection"; +/* alert title */ +"Nothing to forward!" = "Rien à transférer !"; + /* No comment provided by engineer. */ "Notifications" = "Notifications"; @@ -3168,18 +3245,9 @@ /* authentication reason */ "Open migration to another device" = "Ouvrir le transfert vers un autre appareil"; -/* No comment provided by engineer. */ -"Open server settings" = "Ouvrir les paramètres du serveur"; - /* No comment provided by engineer. */ "Open Settings" = "Ouvrir les Paramètres"; -/* authentication reason */ -"Open user profiles" = "Ouvrir les profils d'utilisateurs"; - -/* No comment provided by engineer. */ -"Anybody can host servers." = "N\'importe qui peut heberger un serveur."; - /* No comment provided by engineer. */ "Opening app…" = "Ouverture de l'app…"; @@ -3193,7 +3261,7 @@ "Or securely share this file link" = "Ou partagez en toute sécurité le lien de ce fichier"; /* No comment provided by engineer. */ -"Or show this code" = "Ou présenter ce code"; +"Or show this code" = "Ou montrez ce code"; /* No comment provided by engineer. */ "other" = "autre"; @@ -3201,12 +3269,12 @@ /* No comment provided by engineer. */ "Other" = "Autres"; -/* No comment provided by engineer. */ -"Other %@ servers" = "Autres serveurs %@"; - /* No comment provided by engineer. */ "other errors" = "autres erreurs"; +/* alert message */ +"Other file errors:\n%@" = "Autres erreurs de fichiers :\n%@"; + /* member role */ "owner" = "propriétaire"; @@ -3228,6 +3296,9 @@ /* No comment provided by engineer. */ "Passcode set!" = "Code d'accès défini !"; +/* No comment provided by engineer. */ +"Password" = "Mot de passe"; + /* No comment provided by engineer. */ "Password to show" = "Mot de passe à entrer"; @@ -3252,9 +3323,6 @@ /* No comment provided by engineer. */ "Pending" = "En attente"; -/* No comment provided by engineer. */ -"You decide who can connect." = "Vous choisissez qui peut se connecter."; - /* No comment provided by engineer. */ "Periodic" = "Périodique"; @@ -3324,15 +3392,15 @@ /* No comment provided by engineer. */ "Polish interface" = "Interface en polonais"; +/* No comment provided by engineer. */ +"Port" = "Port"; + /* server test error */ "Possibly, certificate fingerprint in server address is incorrect" = "Il est possible que l'empreinte du certificat dans l'adresse du serveur soit incorrecte"; /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Conserver le brouillon du dernier message, avec les pièces jointes."; -/* No comment provided by engineer. */ -"Preset server" = "Serveur prédéfini"; - /* No comment provided by engineer. */ "Preset server address" = "Adresse du serveur prédéfinie"; @@ -3381,7 +3449,7 @@ /* No comment provided by engineer. */ "Profile theme" = "Thème de profil"; -/* No comment provided by engineer. */ +/* alert message */ "Profile update will be sent to your contacts." = "La mise à jour du profil sera envoyée à vos contacts."; /* No comment provided by engineer. */ @@ -3435,6 +3503,9 @@ /* No comment provided by engineer. */ "Proxied servers" = "Serveurs routés via des proxy"; +/* No comment provided by engineer. */ +"Proxy requires password" = "Le proxy est protégé par un mot de passe"; + /* No comment provided by engineer. */ "Push notifications" = "Notifications push"; @@ -3463,10 +3534,10 @@ "Read more" = "En savoir plus"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." = "Pour en savoir plus, consultez le [Guide de l'utilisateur](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)."; +"Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." = "Pour en savoir plus, consultez le [Guide de l'utilisateur](https ://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)."; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." = "Pour en savoir plus, consultez le [Guide de l'utilisateur](https ://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)."; +"Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." = "Pour en savoir plus, consultez le [Guide de l'utilisateur](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)."; /* No comment provided by engineer. */ "Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." = "Pour en savoir plus, consultez le [Guide de l'utilisateur](https://simplex.chat/docs/guide/readme.html#connect-to-friends)."; @@ -3474,9 +3545,6 @@ /* No comment provided by engineer. */ "Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." = "Pour en savoir plus, consultez notre [dépôt GitHub](https://github.com/simplex-chat/simplex-chat#readme)."; -/* No comment provided by engineer. */ -"Read more in our GitHub repository." = "Plus d'informations sur notre GitHub."; - /* No comment provided by engineer. */ "Receipts are disabled" = "Les accusés de réception sont désactivés"; @@ -3580,6 +3648,9 @@ /* No comment provided by engineer. */ "Remove" = "Supprimer"; +/* No comment provided by engineer. */ +"Remove archive?" = "Supprimer l'archive ?"; + /* No comment provided by engineer. */ "Remove image" = "Enlever l'image"; @@ -3746,12 +3817,15 @@ /* No comment provided by engineer. */ "Save servers" = "Enregistrer les serveurs"; -/* No comment provided by engineer. */ +/* alert title */ "Save servers?" = "Enregistrer les serveurs ?"; /* No comment provided by engineer. */ "Save welcome message?" = "Enregistrer le message d'accueil ?"; +/* alert title */ +"Save your profile?" = "Sauvegarder votre profil ?"; + /* No comment provided by engineer. */ "saved" = "enregistré"; @@ -3770,11 +3844,14 @@ /* No comment provided by engineer. */ "Saved WebRTC ICE servers will be removed" = "Les serveurs WebRTC ICE sauvegardés seront supprimés"; +/* No comment provided by engineer. */ +"Saving %lld messages" = "Sauvegarde de %lld messages"; + /* No comment provided by engineer. */ "Scale" = "Échelle"; /* No comment provided by engineer. */ -"Scan / Paste link" = "Scanner / Coller le lien"; +"Scan / Paste link" = "Scanner / Coller un lien"; /* No comment provided by engineer. */ "Scan code" = "Scanner le code"; @@ -3833,6 +3910,9 @@ /* chat item action */ "Select" = "Choisir"; +/* No comment provided by engineer. */ +"Select chat profile" = "Sélectionner un profil de discussion"; + /* No comment provided by engineer. */ "Selected %lld" = "%lld sélectionné(s)"; @@ -3890,9 +3970,6 @@ /* No comment provided by engineer. */ "Send notifications" = "Envoi de notifications"; -/* No comment provided by engineer. */ -"Send notifications:" = "Envoi de notifications :"; - /* No comment provided by engineer. */ "Send questions and ideas" = "Envoyez vos questions et idées"; @@ -3965,6 +4042,9 @@ /* No comment provided by engineer. */ "Sent via proxy" = "Envoyé via le proxy"; +/* No comment provided by engineer. */ +"Server" = "Serveur"; + /* No comment provided by engineer. */ "Server address" = "Adresse du serveur"; @@ -4046,10 +4126,14 @@ /* No comment provided by engineer. */ "Settings" = "Paramètres"; +/* alert message */ +"Settings were changed." = "Les paramètres ont été modifiés."; + /* No comment provided by engineer. */ "Shape profile images" = "Images de profil modelable"; -/* chat item action */ +/* alert action + chat item action */ "Share" = "Partager"; /* No comment provided by engineer. */ @@ -4058,7 +4142,7 @@ /* No comment provided by engineer. */ "Share address" = "Partager l'adresse"; -/* No comment provided by engineer. */ +/* alert title */ "Share address with contacts?" = "Partager l'adresse avec vos contacts ?"; /* No comment provided by engineer. */ @@ -4068,7 +4152,10 @@ "Share link" = "Partager le lien"; /* No comment provided by engineer. */ -"Share this 1-time invite link" = "Partager ce lien d'invitation unique"; +"Share profile" = "Partager le profil"; + +/* No comment provided by engineer. */ +"Share this 1-time invite link" = "Partagez ce lien d'invitation unique"; /* No comment provided by engineer. */ "Share to SimpleX" = "Partager sur SimpleX"; @@ -4148,6 +4235,9 @@ /* simplex link type */ "SimpleX one-time invitation" = "Invitation unique SimpleX"; +/* No comment provided by engineer. */ +"SimpleX protocols reviewed by Trail of Bits." = "Protocoles SimpleX audité par Trail of Bits."; + /* No comment provided by engineer. */ "Simplified incognito mode" = "Mode incognito simplifié"; @@ -4166,9 +4256,15 @@ /* No comment provided by engineer. */ "SMP server" = "Serveur SMP"; +/* No comment provided by engineer. */ +"SOCKS proxy" = "proxy SOCKS"; + /* blur media */ "Soft" = "Léger"; +/* No comment provided by engineer. */ +"Some app settings were not migrated." = "Certains paramètres de l'application n'ont pas été migrés."; + /* No comment provided by engineer. */ "Some file(s) were not exported:" = "Certains fichiers n'ont pas été exportés :"; @@ -4229,10 +4325,10 @@ /* No comment provided by engineer. */ "Stop sending file?" = "Arrêter l'envoi du fichier ?"; -/* No comment provided by engineer. */ +/* alert action */ "Stop sharing" = "Cesser le partage"; -/* No comment provided by engineer. */ +/* alert title */ "Stop sharing address?" = "Cesser le partage d'adresse ?"; /* authentication reason */ @@ -4262,12 +4358,21 @@ /* No comment provided by engineer. */ "Support SimpleX Chat" = "Supporter SimpleX Chat"; +/* No comment provided by engineer. */ +"Switch audio and video during the call." = "Passer de l'audio à la vidéo pendant l'appel."; + +/* No comment provided by engineer. */ +"Switch chat profile for 1-time invitations." = "Changer de profil de chat pour les invitations à usage unique."; + /* No comment provided by engineer. */ "System" = "Système"; /* No comment provided by engineer. */ "System authentication" = "Authentification du système"; +/* No comment provided by engineer. */ +"Tail" = "Queue"; + /* No comment provided by engineer. */ "Take picture" = "Prendre une photo"; @@ -4319,7 +4424,7 @@ /* No comment provided by engineer. */ "Test servers" = "Tester les serveurs"; -/* No comment provided by engineer. */ +/* alert title */ "Tests failed!" = "Échec des tests !"; /* No comment provided by engineer. */ @@ -4331,9 +4436,6 @@ /* No comment provided by engineer. */ "Thanks to the users – contribute via Weblate!" = "Merci aux utilisateurs - contribuez via Weblate !"; -/* No comment provided by engineer. */ -"No user identifiers." = "Aucun identifiant d\'utilisateur."; - /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "L'application peut vous avertir lorsque vous recevez des messages ou des demandes de contact - veuillez ouvrir les paramètres pour les activer."; @@ -4358,6 +4460,9 @@ /* No comment provided by engineer. */ "The encryption is working and the new encryption agreement is not required. It may result in connection errors!" = "Le chiffrement fonctionne et le nouvel accord de chiffrement n'est pas nécessaire. Cela peut provoquer des erreurs de connexion !"; +/* No comment provided by engineer. */ +"The future of messaging" = "La nouvelle génération de messagerie privée"; + /* No comment provided by engineer. */ "The hash of the previous message is different." = "Le hash du message précédent est différent."; @@ -4376,9 +4481,6 @@ /* No comment provided by engineer. */ "The messages will be marked as moderated for all members." = "Les messages seront marqués comme modérés pour tous les membres."; -/* No comment provided by engineer. */ -"The future of messaging" = "La nouvelle génération de messagerie privée"; - /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "L'ancienne base de données n'a pas été supprimée lors de la migration, elle peut être supprimée."; @@ -4397,6 +4499,9 @@ /* No comment provided by engineer. */ "The text you pasted is not a SimpleX link." = "Le texte collé n'est pas un lien SimpleX."; +/* No comment provided by engineer. */ +"The uploaded database archive will be permanently removed from the servers." = "L'archive de la base de données envoyée sera définitivement supprimée des serveurs."; + /* No comment provided by engineer. */ "Themes" = "Thèmes"; @@ -4455,7 +4560,7 @@ "To ask any questions and to receive updates:" = "Si vous avez des questions et que vous souhaitez des réponses :"; /* No comment provided by engineer. */ -"To connect, your contact can scan QR code or use the link in the app." = "Pour se connecter, votre contact peut scanner le code QR ou utiliser le lien dans l'application."; +"To connect, your contact can scan QR code or use the link in the app." = "Pour se connecter, votre contact peut scanner un code QR ou utiliser un lien dans l'app."; /* No comment provided by engineer. */ "To hide unwanted messages." = "Pour cacher les messages indésirables."; @@ -4463,9 +4568,6 @@ /* No comment provided by engineer. */ "To make a new connection" = "Pour établir une nouvelle connexion"; -/* No comment provided by engineer. */ -"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Pour protéger votre vie privée, au lieu d’IDs utilisés par toutes les autres plateformes, SimpleX a des IDs pour les queues de messages, distinctes pour chacun de vos contacts."; - /* No comment provided by engineer. */ "To protect timezone, image/voice files use UTC." = "Pour préserver le fuseau horaire, les fichiers image/voix utilisent le système UTC."; @@ -4475,6 +4577,15 @@ /* No comment provided by engineer. */ "To protect your IP address, private routing uses your SMP servers to deliver messages." = "Pour protéger votre adresse IP, le routage privé utilise vos serveurs SMP pour délivrer les messages."; +/* No comment provided by engineer. */ +"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Pour protéger votre vie privée, au lieu d’IDs utilisés par toutes les autres plateformes, SimpleX a des IDs pour les queues de messages, distinctes pour chacun de vos contacts."; + +/* No comment provided by engineer. */ +"To record speech please grant permission to use Microphone." = "Si vous souhaitez enregistrer une conversation, veuillez autoriser l'utilisation du microphone."; + +/* No comment provided by engineer. */ +"To record video please grant permission to use Camera." = "Si vous souhaitez enregistrer une vidéo, veuillez autoriser l'utilisation de la caméra."; + /* No comment provided by engineer. */ "To record voice message please grant permission to use Microphone." = "Pour enregistrer un message vocal, veuillez accorder la permission d'utiliser le microphone."; @@ -4691,6 +4802,9 @@ /* No comment provided by engineer. */ "Use SimpleX Chat servers?" = "Utiliser les serveurs SimpleX Chat ?"; +/* No comment provided by engineer. */ +"Use SOCKS proxy" = "Utiliser un proxy SOCKS"; + /* No comment provided by engineer. */ "Use the app while in the call." = "Utiliser l'application pendant l'appel."; @@ -4700,6 +4814,9 @@ /* No comment provided by engineer. */ "User selection" = "Sélection de l'utilisateur"; +/* No comment provided by engineer. */ +"Username" = "Nom d'utilisateur"; + /* No comment provided by engineer. */ "Using SimpleX Chat servers." = "Vous utilisez les serveurs SimpleX."; @@ -4850,9 +4967,6 @@ /* No comment provided by engineer. */ "when IP hidden" = "lorsque l'IP est masquée"; -/* No comment provided by engineer. */ -"When people request to connect, you can accept or reject it." = "Vous pouvez accepter ou refuser les demandes de contacts."; - /* No comment provided by engineer. */ "When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "Lorsque vous partagez un profil incognito avec quelqu'un, ce profil sera utilisé pour les groupes auxquels il vous invite."; @@ -4994,9 +5108,6 @@ /* No comment provided by engineer. */ "You can share this address with your contacts to let them connect with **%@**." = "Vous pouvez partager cette adresse avec vos contacts pour leur permettre de se connecter avec **%@**."; -/* No comment provided by engineer. */ -"You can share your address as a link or QR code - anybody can connect to you." = "Vous pouvez partager votre adresse sous la forme d'un lien ou d'un code QR - tout le monde peut l'utiliser pour vous contacter."; - /* No comment provided by engineer. */ "You can start chat via app Settings / Database or by restarting the app" = "Vous pouvez lancer le chat via Paramètres / Base de données ou en redémarrant l'app"; @@ -5009,7 +5120,7 @@ /* No comment provided by engineer. */ "You can use markdown to format messages:" = "Vous pouvez utiliser le format markdown pour mettre en forme les messages :"; -/* No comment provided by engineer. */ +/* alert message */ "You can view invitation link again in connection details." = "Vous pouvez à nouveau consulter le lien d'invitation dans les détails de la connexion."; /* No comment provided by engineer. */ @@ -5030,6 +5141,9 @@ /* No comment provided by engineer. */ "You could not be verified; please try again." = "Vous n'avez pas pu être vérifié·e ; veuillez réessayer."; +/* No comment provided by engineer. */ +"You decide who can connect." = "Vous choisissez qui peut se connecter."; + /* No comment provided by engineer. */ "You have already requested connection via this address!" = "Vous avez déjà demandé une connexion via cette adresse !"; @@ -5120,9 +5234,6 @@ /* No comment provided by engineer. */ "You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed" = "Vous utilisez un profil incognito pour ce groupe - pour éviter de partager votre profil principal ; inviter des contacts n'est pas possible"; -/* No comment provided by engineer. */ -"Your %@ servers" = "Vos serveurs %@"; - /* No comment provided by engineer. */ "Your calls" = "Vos appels"; @@ -5132,9 +5243,15 @@ /* No comment provided by engineer. */ "Your chat database is not encrypted - set passphrase to encrypt it." = "Votre base de données de chat n'est pas chiffrée - définisez une phrase secrète."; +/* alert title */ +"Your chat preferences" = "Vos préférences de discussion"; + /* No comment provided by engineer. */ "Your chat profiles" = "Vos profils de chat"; +/* No comment provided by engineer. */ +"Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." = "Votre connexion a été déplacée vers %@ mais une erreur inattendue s'est produite lors de la redirection vers le profil."; + /* No comment provided by engineer. */ "Your contact sent a file that is larger than currently supported maximum size (%@)." = "Votre contact a envoyé un fichier plus grand que la taille maximale supportée actuellement(%@)."; @@ -5144,6 +5261,9 @@ /* No comment provided by engineer. */ "Your contacts will remain connected." = "Vos contacts resteront connectés."; +/* No comment provided by engineer. */ +"Your credentials may be sent unencrypted." = "Vos informations d'identification peuvent être envoyées non chiffrées."; + /* No comment provided by engineer. */ "Your current chat database will be DELETED and REPLACED with the imported one." = "Votre base de données de chat actuelle va être SUPPRIMEE et REMPLACEE par celle importée."; @@ -5168,15 +5288,15 @@ /* No comment provided by engineer. */ "Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Votre profil est stocké sur votre appareil et est seulement partagé avec vos contacts. Les serveurs SimpleX ne peuvent pas voir votre profil."; +/* alert message */ +"Your profile was changed. If you save it, the updated profile will be sent to all your contacts." = "Votre profil a été modifié. Si vous l'enregistrez, le profil mis à jour sera envoyé à tous vos contacts."; + /* No comment provided by engineer. */ "Your profile, contacts and delivered messages are stored on your device." = "Votre profil, vos contacts et les messages reçus sont stockés sur votre appareil."; /* No comment provided by engineer. */ "Your random profile" = "Votre profil aléatoire"; -/* No comment provided by engineer. */ -"Your server" = "Votre serveur"; - /* No comment provided by engineer. */ "Your server address" = "Votre adresse de serveur"; @@ -5189,6 +5309,3 @@ /* No comment provided by engineer. */ "Your SMP servers" = "Vos serveurs SMP"; -/* No comment provided by engineer. */ -"Your XFTP servers" = "Vos serveurs XFTP"; - diff --git a/apps/ios/hu.lproj/Localizable.strings b/apps/ios/hu.lproj/Localizable.strings index a68a9e11b1..1704389267 100644 --- a/apps/ios/hu.lproj/Localizable.strings +++ b/apps/ios/hu.lproj/Localizable.strings @@ -1,15 +1,6 @@ /* No comment provided by engineer. */ "\n" = "\n"; -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - /* No comment provided by engineer. */ " (" = " ("; @@ -227,7 +218,7 @@ "%lld messages blocked" = "%lld üzenet letiltva"; /* No comment provided by engineer. */ -"%lld messages blocked by admin" = "%lld üzenetet letiltott az admin"; +"%lld messages blocked by admin" = "%lld üzenetet letiltott az adminisztrátor"; /* No comment provided by engineer. */ "%lld messages marked deleted" = "%lld törlésre megjelölt üzenet"; @@ -323,10 +314,10 @@ "A new random profile will be shared." = "Egy új, véletlenszerű profil kerül megosztásra."; /* No comment provided by engineer. */ -"A separate TCP connection will be used **for each chat profile you have in the app**." = "A rendszer külön TCP-kapcsolatot fog használni **az alkalmazásban található minden csevegési profilhoz**."; +"A separate TCP connection will be used **for each chat profile you have in the app**." = "**Az összes csevegési profiljához az alkalmazásban** külön TCP-kapcsolat (és SOCKS-hitelesítőadat) lesz használva."; /* No comment provided by engineer. */ -"A separate TCP connection will be used **for each contact and group member**.\n**Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail." = "**Minden egyes kapcsolathoz és csoporttaghoz** külön TCP-kapcsolat lesz használva.\n**Megjegyzés:** ha sok kapcsolata van, az akkumulátor-használat és az adatforgalom jelentősen megnövekedhet, és néhány kapcsolódási kísérlet sikertelen lehet."; +"A separate TCP connection will be used **for each contact and group member**.\n**Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail." = "**Az összes ismerőséhez és csoporttaghoz** külön TCP-kapcsolat (és SOCKS-hitelesítőadat) lesz használva.\n**Megjegyzés:** ha sok kapcsolata van, az akkumulátor-használat és az adatforgalom jelentősen megnövekedhet, és néhány kapcsolódási kísérlet sikertelen lehet."; /* No comment provided by engineer. */ "Abort" = "Megszakítás"; @@ -337,12 +328,6 @@ /* No comment provided by engineer. */ "Abort changing address?" = "Címváltoztatás megszakítása??"; -/* No comment provided by engineer. */ -"About SimpleX" = "A SimpleXről"; - -/* No comment provided by engineer. */ -"About SimpleX address" = "A SimpleX-címről"; - /* No comment provided by engineer. */ "About SimpleX Chat" = "A SimpleX Chatről"; @@ -382,12 +367,6 @@ /* No comment provided by engineer. */ "Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." = "Cím hozzáadása a profilhoz, hogy az ismerősei megoszthassák másokkal. A profilfrissítés elküldésre kerül az ismerősei számára."; -/* No comment provided by engineer. */ -"Add contact" = "Ismerős hozzáadása"; - -/* No comment provided by engineer. */ -"Add preset servers" = "Előre beállított kiszolgálók hozzáadása"; - /* No comment provided by engineer. */ "Add profile" = "Profil hozzáadása"; @@ -419,16 +398,16 @@ "Address change will be aborted. Old receiving address will be used." = "A cím módosítása megszakad. A régi fogadási cím kerül felhasználásra."; /* member role */ -"admin" = "admin"; +"admin" = "adminisztrátor"; /* feature role */ -"admins" = "adminok"; +"admins" = "adminisztrátorok"; /* No comment provided by engineer. */ -"Admins can block a member for all." = "Az adminok egy tagot mindenki számára letilthatnak."; +"Admins can block a member for all." = "Az adminisztrátorok egy tagot a csoport összes tagja számára letilthatnak."; /* No comment provided by engineer. */ -"Admins can create the links to join groups." = "Az adminok hivatkozásokat hozhatnak létre a csoportokhoz való kapcsolódáshoz."; +"Admins can create the links to join groups." = "Az adminisztrátorok hivatkozásokat hozhatnak létre a csoportokhoz való kapcsolódáshoz."; /* No comment provided by engineer. */ "Advanced network settings" = "Speciális hálózati beállítások"; @@ -443,43 +422,43 @@ "agreeing encryption…" = "titkosítás elfogadása…"; /* No comment provided by engineer. */ -"All app data is deleted." = "Minden alkalmazásadat törölve."; +"All app data is deleted." = "Az összes alkalmazásadat törölve."; /* No comment provided by engineer. */ -"All chats and messages will be deleted - this cannot be undone!" = "Minden csevegés és üzenet törlésre kerül - ez a művelet nem vonható vissza!"; +"All chats and messages will be deleted - this cannot be undone!" = "Az összes csevegés és üzenet törlésre kerül - ez a művelet nem vonható vissza!"; /* No comment provided by engineer. */ -"All data is erased when it is entered." = "A jelkód megadása után minden adat törlésre kerül."; +"All data is erased when it is entered." = "A jelkód megadása után az összes adat törlésre kerül."; /* No comment provided by engineer. */ -"All data is private to your device." = "Minden adat biztonságban van az eszközén."; +"All data is private to your device." = "Az összes adat biztonságban van az eszközén."; /* No comment provided by engineer. */ -"All group members will remain connected." = "Minden csoporttag kapcsolódva marad."; +"All group members will remain connected." = "Az összes csoporttag kapcsolatban marad."; /* feature role */ -"all members" = "minden tag"; +"all members" = "összes tag"; /* No comment provided by engineer. */ -"All messages will be deleted - this cannot be undone!" = "Minden üzenet törlésre kerül – ez a művelet nem vonható vissza!"; +"All messages will be deleted - this cannot be undone!" = "Az összes üzenet törlésre kerül – ez a művelet nem vonható vissza!"; /* No comment provided by engineer. */ -"All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." = "Minden üzenet törlésre kerül - ez a művelet nem vonható vissza! Az üzenetek CSAK az Ön számára törlődnek."; +"All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." = "Az összes üzenet törlésre kerül - ez a művelet nem vonható vissza! Az üzenetek CSAK az Ön számára törlődnek."; /* No comment provided by engineer. */ -"All new messages from %@ will be hidden!" = "Minden új üzenet elrejtésre kerül tőle: %@!"; +"All new messages from %@ will be hidden!" = "Az összes új üzenet elrejtésre kerül tőle: %@!"; /* profile dropdown */ -"All profiles" = "Minden profil"; +"All profiles" = "Összes profil"; /* No comment provided by engineer. */ -"All your contacts will remain connected." = "Minden ismerősével kapcsolatban marad."; +"All your contacts will remain connected." = "Az összes ismerősével kapcsolatban marad."; /* No comment provided by engineer. */ "All your contacts will remain connected. Profile update will be sent to your contacts." = "Az ismerőseivel kapcsolatban marad. A profil-változtatások frissítésre kerülnek az ismerősöknél."; /* No comment provided by engineer. */ -"All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." = "Minden ismerőse, a beszélgetései és a fájljai biztonságosan titkosításra kerülnek, melyek részletekben feltöltődnek a beállított XFTP-közvetítő-kiszolgálóra."; +"All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." = "Az összes ismerőse, -beszélgetése és -fájlja biztonságosan titkosításra kerülnek, melyek részletekben feltöltődnek a beállított XFTP-közvetítő-kiszolgálóra."; /* No comment provided by engineer. */ "Allow" = "Engedélyezés"; @@ -574,6 +553,9 @@ /* No comment provided by engineer. */ "Answer call" = "Hívás fogadása"; +/* No comment provided by engineer. */ +"Anybody can host servers." = "Bárki üzemeltethet kiszolgálókat."; + /* No comment provided by engineer. */ "App build: %@" = "Az alkalmazás build száma: %@"; @@ -584,7 +566,7 @@ "App encrypts new local files (except videos)." = "Az alkalmazás titkosítja a helyi fájlokat (a videók kivételével)."; /* No comment provided by engineer. */ -"App icon" = "Alkalmazás ikon"; +"App icon" = "Alkalmazásikon"; /* No comment provided by engineer. */ "App passcode" = "Alkalmazás jelkód"; @@ -722,7 +704,7 @@ "Block" = "Letiltás"; /* No comment provided by engineer. */ -"Block for all" = "Letiltás mindenki számára"; +"Block for all" = "Letiltás az összes tag számára"; /* No comment provided by engineer. */ "Block group members" = "Csoporttagok letiltása"; @@ -731,7 +713,7 @@ "Block member" = "Tag letiltása"; /* No comment provided by engineer. */ -"Block member for all?" = "Mindenki számára letiltja ezt a tagot?"; +"Block member for all?" = "Az összes tag számára letiltja ezt a tagot?"; /* No comment provided by engineer. */ "Block member?" = "Tag letiltása?"; @@ -743,10 +725,10 @@ "blocked %@" = "letiltotta %@-t"; /* marked deleted chat item preview text */ -"blocked by admin" = "letiltva az admin által"; +"blocked by admin" = "letiltva az adminisztrátor által"; /* No comment provided by engineer. */ -"Blocked by admin" = "Az admin letiltotta"; +"Blocked by admin" = "Az adminisztrátor letiltotta"; /* No comment provided by engineer. */ "Blur for better privacy." = "Elhomályosítás a jobb adatvédelemért."; @@ -809,15 +791,16 @@ "Can't call member" = "Nem lehet felhívni a tagot"; /* No comment provided by engineer. */ -"Can't invite contact!" = "Ismerős meghívása nem lehetséges!"; +"Can't invite contact!" = "Nem lehet meghívni az ismerőst!"; /* No comment provided by engineer. */ -"Can't invite contacts!" = "Ismerősök meghívása nem lehetséges!"; +"Can't invite contacts!" = "Nem lehet meghívni az ismerősöket!"; /* No comment provided by engineer. */ "Can't message member" = "Nem lehet üzenetet küldeni a tagnak"; -/* alert button */ +/* alert action + alert button */ "Cancel" = "Mégse"; /* No comment provided by engineer. */ @@ -938,7 +921,7 @@ /* No comment provided by engineer. */ "Chats" = "Csevegések"; -/* No comment provided by engineer. */ +/* alert title */ "Check server address and try again." = "Kiszolgáló címének ellenőrzése és újrapróbálkozás."; /* No comment provided by engineer. */ @@ -1001,9 +984,6 @@ /* No comment provided by engineer. */ "Configure ICE servers" = "ICE-kiszolgálók beállítása"; -/* No comment provided by engineer. */ -"Configured %@ servers" = "Beállított %@ kiszolgálók"; - /* No comment provided by engineer. */ "Confirm" = "Megerősítés"; @@ -1014,7 +994,7 @@ "Confirm database upgrades" = "Adatbázis fejlesztésének megerősítése"; /* No comment provided by engineer. */ -"Confirm files from unknown servers." = "Ismeretlen kiszolgálókról származó fájlok jóváhagyása."; +"Confirm files from unknown servers." = "Ismeretlen kiszolgálókról származó fájlok megerősítése."; /* No comment provided by engineer. */ "Confirm network settings" = "Hálózati beállítások megerősítése"; @@ -1071,13 +1051,13 @@ "Connect via one-time link" = "Kapcsolódás egyszer használható hivatkozáson keresztül"; /* No comment provided by engineer. */ -"Connect with %@" = "Kapcsolódás ezzel: %@"; +"Connect with %@" = "Kapcsolódás a következővel: %@"; /* No comment provided by engineer. */ -"connected" = "kapcsolódva"; +"connected" = "kapcsolódott"; /* No comment provided by engineer. */ -"Connected" = "Kapcsolódva"; +"Connected" = "Kapcsolódott"; /* No comment provided by engineer. */ "Connected desktop" = "Társított számítógép"; @@ -1232,9 +1212,6 @@ /* No comment provided by engineer. */ "Create a group using a random profile." = "Csoport létrehozása véletlenszerűen létrehozott profillal."; -/* No comment provided by engineer. */ -"Create an address to let people connect with you." = "Cím létrehozása, hogy az emberek kapcsolatba léphessenek Önnel."; - /* server test step */ "Create file" = "Fájl létrehozása"; @@ -1397,7 +1374,8 @@ /* No comment provided by engineer. */ "default (yes)" = "alapértelmezett (igen)"; -/* chat item action +/* alert action + chat item action swipe action */ "Delete" = "Törlés"; @@ -1417,7 +1395,7 @@ "Delete after" = "Törlés ennyi idő után"; /* No comment provided by engineer. */ -"Delete all files" = "Minden fájl törlése"; +"Delete all files" = "Az összes fájl törlése"; /* No comment provided by engineer. */ "Delete and notify contact" = "Törlés, és az ismerős értesítése"; @@ -1456,10 +1434,10 @@ "Delete files and media?" = "Fájlok és a médiatartalmak törlése?"; /* No comment provided by engineer. */ -"Delete files for all chat profiles" = "Fájlok törlése minden csevegési profilból"; +"Delete files for all chat profiles" = "Fájlok törlése az összes csevegési profilból"; /* chat feature */ -"Delete for everyone" = "Törlés mindenkinél"; +"Delete for everyone" = "Törlés az összes tagnál"; /* No comment provided by engineer. */ "Delete for me" = "Csak nálam"; @@ -1612,7 +1590,7 @@ "Disable (keep overrides)" = "Letiltás (felülírások megtartásával)"; /* No comment provided by engineer. */ -"Disable for all" = "Letiltás mindenki számára"; +"Disable for all" = "Letiltás az összes tag számára"; /* authentication reason */ "Disable SimpleX Lock" = "SimpleX-zár kikapcsolása"; @@ -1745,7 +1723,7 @@ "Enable camera access" = "Kamera hozzáférés engedélyezése"; /* No comment provided by engineer. */ -"Enable for all" = "Engedélyezés mindenki számára"; +"Enable for all" = "Engedélyezés az összes tag számára"; /* No comment provided by engineer. */ "Enable in direct chats (BETA)!" = "Engedélyezés a közvetlen csevegésekben (BÉTA)!"; @@ -1781,7 +1759,7 @@ "Enabled" = "Engedélyezve"; /* No comment provided by engineer. */ -"Enabled for" = "Engedélyezve"; +"Enabled for" = "Számukra engedélyezve:"; /* enabled status */ "enabled for contact" = "engedélyezve az ismerős számára"; @@ -1996,9 +1974,6 @@ /* No comment provided by engineer. */ "Error joining group" = "Hiba a csoporthoz való csatlakozáskor"; -/* No comment provided by engineer. */ -"Error loading %@ servers" = "Hiba a(z) %@ -kiszolgálók betöltésekor"; - /* No comment provided by engineer. */ "Error migrating settings" = "Hiba a beallítások átköltöztetésekor"; @@ -2020,9 +1995,6 @@ /* No comment provided by engineer. */ "Error resetting statistics" = "Hiba a statisztikák visszaállításakor"; -/* No comment provided by engineer. */ -"Error saving %@ servers" = "Hiba történt a(z) %@ -kiszolgálók mentésekor"; - /* No comment provided by engineer. */ "Error saving group profile" = "Hiba a csoportprofil mentésekor"; @@ -2168,7 +2140,7 @@ "File status: %@" = "Fájlállapot: %@"; /* No comment provided by engineer. */ -"File will be deleted from servers." = "A fájl törölve lesz a kiszolgálóról."; +"File will be deleted from servers." = "A fájl törölve lesz a kiszolgálókról."; /* No comment provided by engineer. */ "File will be received when your contact completes uploading it." = "A fájl akkor érkezik meg, amikor a küldője befejezte annak feltöltését."; @@ -2387,13 +2359,13 @@ "Group welcome message" = "A csoport üdvözlőüzenete"; /* No comment provided by engineer. */ -"Group will be deleted for all members - this cannot be undone!" = "A csoport törlésre kerül minden tag számára - ez a művelet nem vonható vissza!"; +"Group will be deleted for all members - this cannot be undone!" = "A csoport törlésre kerül az összes tag számára - ez a művelet nem vonható vissza!"; /* No comment provided by engineer. */ "Group will be deleted for you - this cannot be undone!" = "A csoport törlésre kerül az Ön számára - ez a művelet nem vonható vissza!"; /* No comment provided by engineer. */ -"Help" = "Segítség"; +"Help" = "Súgó"; /* No comment provided by engineer. */ "Hidden" = "Se név, se üzenet"; @@ -2425,9 +2397,6 @@ /* time unit */ "hours" = "óra"; -/* No comment provided by engineer. */ -"How it works" = "Hogyan működik"; - /* No comment provided by engineer. */ "How SimpleX works" = "Hogyan működik a SimpleX"; @@ -2525,7 +2494,7 @@ "Incognito mode" = "Inkognitómód"; /* No comment provided by engineer. */ -"Incognito mode protects your privacy by using a new random profile for each contact." = "Az inkognitómód védi személyes adatait azáltal, hogy minden ismerőshöz új véletlenszerű profilt használ."; +"Incognito mode protects your privacy by using a new random profile for each contact." = "Az inkognitómód védi személyes adatait azáltal, hogy az összes ismerőséhez új, véletlenszerű profilt használ."; /* chat list item description */ "incognito via contact address link" = "inkognitó a kapcsolattartási címhivatkozáson keresztül"; @@ -2570,10 +2539,10 @@ "Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)" = "A [SimpleX Chat terminálhoz] telepítése (https://github.com/simplex-chat/simplex-chat)"; /* No comment provided by engineer. */ -"Instant push notifications will be hidden!\n" = "Az azonnali push-értesítések elrejtésre kerülnek!\n"; +"Instant" = "Azonnal"; /* No comment provided by engineer. */ -"Instant" = "Azonnal"; +"Instant push notifications will be hidden!\n" = "Az azonnali push-értesítések elrejtésre kerülnek!\n"; /* No comment provided by engineer. */ "Interface" = "Felület"; @@ -2611,7 +2580,7 @@ /* No comment provided by engineer. */ "Invalid response" = "Érvénytelen válasz"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid server address!" = "Érvénytelen kiszolgálócím!"; /* item status text */ @@ -2711,12 +2680,12 @@ "Join with current profile" = "Csatlakozás a jelenlegi profillal"; /* No comment provided by engineer. */ -"Join your group?\nThis is your link for group %@!" = "Csatlakozik a csoportjához?\nEz az Ön hivatkozása a(z) %@ csoporthoz!"; +"Join your group?\nThis is your link for group %@!" = "Csatlakozik a csoportjához?\nEz az Ön hivatkozása a(z) %@ nevű csoporthoz!"; /* No comment provided by engineer. */ "Joining group" = "Csatlakozás a csoporthoz"; -/* No comment provided by engineer. */ +/* alert action */ "Keep" = "Megtart"; /* No comment provided by engineer. */ @@ -2725,7 +2694,7 @@ /* No comment provided by engineer. */ "Keep the app open to use it from desktop" = "A számítógépről való használathoz tartsd nyitva az alkalmazást"; -/* No comment provided by engineer. */ +/* alert title */ "Keep unused invitation?" = "Fel nem használt meghívó megtartása?"; /* No comment provided by engineer. */ @@ -2782,9 +2751,6 @@ /* No comment provided by engineer. */ "Live messages" = "Élő üzenetek"; -/* No comment provided by engineer. */ -"No push server" = "Helyi"; - /* No comment provided by engineer. */ "Local name" = "Helyi név"; @@ -2797,29 +2763,20 @@ /* No comment provided by engineer. */ "Lock mode" = "Zárolási mód"; -/* No comment provided by engineer. */ -"Make a private connection" = "Privát kapcsolat létrehozása"; - /* No comment provided by engineer. */ "Make one message disappear" = "Egy üzenet eltüntetése"; /* No comment provided by engineer. */ "Make profile private!" = "Tegye priváttá a profilját!"; -/* No comment provided by engineer. */ -"Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." = "Győződjön meg arról, hogy a(z) %@ kiszolgálócímek megfelelő formátumúak, sorszeparáltak és nincsenek duplikálva (%@)."; - /* No comment provided by engineer. */ "Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "Győződjön meg arról, hogy a WebRTC ICE-kiszolgáló címei megfelelő formátumúak, sorszeparáltak és nincsenek duplikálva."; /* No comment provided by engineer. */ -"Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" = "Sokan kérdezték: *ha a SimpleX Chatnek nincsenek felhasználó-azonosítói, akkor hogyan tud üzeneteket kézbesíteni?*"; +"Mark deleted for everyone" = "Jelölje meg az összes tag számára töröltként"; /* No comment provided by engineer. */ -"Mark deleted for everyone" = "Jelölje meg mindenki számára töröltként"; - -/* No comment provided by engineer. */ -"Mark read" = "Olvasottnak jelölés"; +"Mark read" = "Megjelölés olvasottként"; /* No comment provided by engineer. */ "Mark verified" = "Hitelesítés"; @@ -2855,7 +2812,7 @@ "Member inactive" = "Inaktív tag"; /* No comment provided by engineer. */ -"Member role will be changed to \"%@\". All group members will be notified." = "A tag szerepköre meg fog változni erre: „%@”. A csoport minden tagja értesítést kap róla."; +"Member role will be changed to \"%@\". All group members will be notified." = "A tag szerepköre meg fog változni erre: „%@”. A csoportban az összes tag értesítve lesz."; /* No comment provided by engineer. */ "Member role will be changed to \"%@\". The member will receive a new invitation." = "A tag szerepköre meg fog változni erre: „%@”. A tag új meghívást fog kapni."; @@ -3101,7 +3058,7 @@ "New SOCKS credentials will be used every time you start the app." = "Minden alkalommal, amikor elindítja az alkalmazást, új SOCKS-hitelesítő-adatokat fog használni."; /* No comment provided by engineer. */ -"New SOCKS credentials will be used for each server." = "Minden egyes kiszolgálóhoz új SOCKS-hitelesítő-adatok legyenek használva."; +"New SOCKS credentials will be used for each server." = "Az összes kiszolgálóhoz új, SOCKS-hitelesítő-adatok legyenek használva."; /* pref value */ "no" = "nem"; @@ -3125,7 +3082,7 @@ "No device token!" = "Nincs kiszüléktoken!"; /* item status description */ -"No direct connection yet, message is forwarded by admin." = "Még nincs közvetlen kapcsolat, az üzenetet az admin továbbítja."; +"No direct connection yet, message is forwarded by admin." = "Még nincs közvetlen kapcsolat, az üzenetet az adminisztrátor továbbítja."; /* No comment provided by engineer. */ "no e2e encryption" = "nincs e2e titkosítás"; @@ -3154,12 +3111,18 @@ /* No comment provided by engineer. */ "No permission to record voice message" = "Nincs engedély a hangüzenet rögzítésére"; +/* No comment provided by engineer. */ +"No push server" = "Helyi"; + /* No comment provided by engineer. */ "No received or sent files" = "Nincsenek fogadott vagy küldött fájlok"; /* copied message info in history */ "no text" = "nincs szöveg"; +/* No comment provided by engineer. */ +"No user identifiers." = "Nincsenek felhasználó-azonosítók."; + /* No comment provided by engineer. */ "Not compatible!" = "Nem kompatibilis!"; @@ -3176,7 +3139,7 @@ "Notifications are disabled!" = "Az értesítések le vannak tiltva!"; /* No comment provided by engineer. */ -"Now admins can:\n- delete members' messages.\n- disable members (\"observer\" role)" = "Most már az adminok is:\n- törölhetik a tagok üzeneteit.\n- letilthatnak tagokat („megfigyelő” szerepkör)"; +"Now admins can:\n- delete members' messages.\n- disable members (\"observer\" role)" = "Most már az adminisztrátorok is:\n- törölhetik a tagok üzeneteit.\n- letilthatnak tagokat („megfigyelő” szerepkör)"; /* member role */ "observer" = "megfigyelő"; @@ -3282,18 +3245,9 @@ /* authentication reason */ "Open migration to another device" = "Átköltöztetés megkezdése egy másik eszközre"; -/* No comment provided by engineer. */ -"Open server settings" = "Kiszolgáló-beállítások megnyitása"; - /* No comment provided by engineer. */ "Open Settings" = "Beállítások megnyitása"; -/* authentication reason */ -"Open user profiles" = "Felhasználó-profilok megnyitása"; - -/* No comment provided by engineer. */ -"Anybody can host servers." = "Bárki üzemeltethet kiszolgálókat."; - /* No comment provided by engineer. */ "Opening app…" = "Az alkalmazás megnyitása…"; @@ -3315,9 +3269,6 @@ /* No comment provided by engineer. */ "Other" = "További"; -/* No comment provided by engineer. */ -"Other %@ servers" = "További %@ kiszolgálók"; - /* No comment provided by engineer. */ "other errors" = "egyéb hibák"; @@ -3372,9 +3323,6 @@ /* No comment provided by engineer. */ "Pending" = "Függőben"; -/* No comment provided by engineer. */ -"You decide who can connect." = "Ön dönti el, hogy kivel beszélget."; - /* No comment provided by engineer. */ "Periodic" = "Rendszeresen"; @@ -3406,7 +3354,7 @@ "Please check that you used the correct link or ask your contact to send you another one." = "Ellenőrizze, hogy a megfelelő hivatkozást használta-e, vagy kérje meg az ismerősét, hogy küldjön egy másikat."; /* No comment provided by engineer. */ -"Please check your network connection with %@ and try again." = "Ellenőrizze a hálózati kapcsolatát a(z) %@ segítségével, és próbálja újra."; +"Please check your network connection with %@ and try again." = "Ellenőrizze a hálózati kapcsolatát a következővel: %@, és próbálja újra."; /* No comment provided by engineer. */ "Please check yours and your contact preferences." = "Ellenőrizze a saját- és az ismerőse beállításait."; @@ -3418,7 +3366,7 @@ "Please contact developers.\nError: %@" = "Lépjen kapcsolatba a fejlesztőkkel.\nHiba: %@"; /* No comment provided by engineer. */ -"Please contact group admin." = "Lépjen kapcsolatba a csoport adminnal."; +"Please contact group admin." = "Lépjen kapcsolatba a csoport adminisztrátorával."; /* No comment provided by engineer. */ "Please enter correct current passphrase." = "Adja meg a helyes, jelenlegi jelmondatát."; @@ -3453,9 +3401,6 @@ /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Az utolsó üzenet tervezetének megőrzése a mellékletekkel együtt."; -/* No comment provided by engineer. */ -"Preset server" = "Előre beállított kiszolgáló"; - /* No comment provided by engineer. */ "Preset server address" = "Előre beállított kiszolgáló címe"; @@ -3504,7 +3449,7 @@ /* No comment provided by engineer. */ "Profile theme" = "Profiltéma"; -/* No comment provided by engineer. */ +/* alert message */ "Profile update will be sent to your contacts." = "A profilfrissítés elküldésre került az ismerősök számára."; /* No comment provided by engineer. */ @@ -3583,16 +3528,16 @@ "React…" = "Reagálj…"; /* swipe action */ -"Read" = "Olvasd el"; +"Read" = "Olvasott"; /* No comment provided by engineer. */ "Read more" = "Tudjon meg többet"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." = "További információ a [Használati útmutatóban](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)."; +"Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." = "További információ a [Használati útmutatóban](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)."; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." = "További információ a [Használati útmutatóban](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)."; +"Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." = "További információ a [Használati útmutatóban](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)."; /* No comment provided by engineer. */ "Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." = "További információ a [Használati útmutatóban](https://simplex.chat/docs/guide/readme.html#connect-to-friends)."; @@ -3600,9 +3545,6 @@ /* No comment provided by engineer. */ "Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." = "További információ a [GitHub tárolóban](https://github.com/simplex-chat/simplex-chat#readme)."; -/* No comment provided by engineer. */ -"Read more in our GitHub repository." = "További információ a GitHub tárolónkban."; - /* No comment provided by engineer. */ "Receipts are disabled" = "A kézbesítési jelentések le vannak tiltva"; @@ -3640,7 +3582,7 @@ "Receiving address will be changed to a different server. Address change will complete after sender comes online." = "A fogadó cím egy másik kiszolgálóra változik. A címváltoztatás a feladó online állapotba kerülése után fejeződik be."; /* No comment provided by engineer. */ -"Receiving file will be stopped." = "A fájl fogadása leállt."; +"Receiving file will be stopped." = "A fájl fogadása le fog állni."; /* No comment provided by engineer. */ "Receiving via" = "Fogadás a"; @@ -3655,16 +3597,16 @@ "Recipients see updates as you type them." = "A címzettek a beírás közben látják a szövegváltozásokat."; /* No comment provided by engineer. */ -"Reconnect" = "Újrakapcsolás"; +"Reconnect" = "Újrakapcsolódás"; /* No comment provided by engineer. */ "Reconnect all connected servers to force message delivery. It uses additional traffic." = "Az összes kiszolgálóhoz való újrakapcsolódás az üzenetkézbesítési jelentések kikényszerítéséhez. Ez további adatforgalmat használ."; /* No comment provided by engineer. */ -"Reconnect all servers" = "Újrakapcsolódás minden kiszolgálóhoz"; +"Reconnect all servers" = "Újrakapcsolódás az összes kiszolgálóhoz"; /* No comment provided by engineer. */ -"Reconnect all servers?" = "Újrakapcsolódás minden kiszolgálóhoz?"; +"Reconnect all servers?" = "Újrakapcsolódás az összes kiszolgálóhoz?"; /* No comment provided by engineer. */ "Reconnect server to force message delivery. It uses additional traffic." = "A kiszolgálóhoz való újrakapcsolódás az üzenetkézbesítési jelentések kikényszerítéséhez. Ez további adatforgalmat használ."; @@ -3773,10 +3715,10 @@ "Reset all hints" = "Tippek visszaállítása"; /* No comment provided by engineer. */ -"Reset all statistics" = "Minden statisztika visszaállítása"; +"Reset all statistics" = "Az összes statisztika visszaállítása"; /* No comment provided by engineer. */ -"Reset all statistics?" = "Minden statisztika visszaállítása?"; +"Reset all statistics?" = "Az összes statisztika visszaállítása?"; /* No comment provided by engineer. */ "Reset colors" = "Színek visszaállítása"; @@ -3875,7 +3817,7 @@ /* No comment provided by engineer. */ "Save servers" = "Kiszolgálók mentése"; -/* No comment provided by engineer. */ +/* alert title */ "Save servers?" = "Kiszolgálók mentése?"; /* No comment provided by engineer. */ @@ -3936,7 +3878,7 @@ "Search bar accepts invitation links." = "A keresősáv elfogadja a meghívó-hivatkozásokat."; /* No comment provided by engineer. */ -"Search or paste SimpleX link" = "Keresés, vagy SimpleX-hivatkozás beillesztése"; +"Search or paste SimpleX link" = "Keresés vagy SimpleX-hivatkozás beillesztése"; /* network option */ "sec" = "mp"; @@ -4028,9 +3970,6 @@ /* No comment provided by engineer. */ "Send notifications" = "Értesítések küldése"; -/* No comment provided by engineer. */ -"Send notifications:" = "Értesítések küldése:"; - /* No comment provided by engineer. */ "Send questions and ideas" = "Ötletek és kérdések beküldése"; @@ -4050,13 +3989,13 @@ "Sender may have deleted the connection request." = "A küldő törölhette a kapcsolatkérést."; /* No comment provided by engineer. */ -"Sending delivery receipts will be enabled for all contacts in all visible chat profiles." = "A kézbesítési jelentések küldése engedélyezésre kerül az összes látható csevegési profilban lévő minden ismerős számára."; +"Sending delivery receipts will be enabled for all contacts in all visible chat profiles." = "A kézbesítési jelentések küldése engedélyezésre kerül az összes látható csevegési profilban lévő összes ismerőse számára."; /* No comment provided by engineer. */ -"Sending delivery receipts will be enabled for all contacts." = "A kézbesítési jelentés küldése minden ismerőse számára engedélyezésre kerül."; +"Sending delivery receipts will be enabled for all contacts." = "A kézbesítési jelentés küldése az összes ismerőse számára engedélyezésre kerül."; /* No comment provided by engineer. */ -"Sending file will be stopped." = "A fájl küldése leállt."; +"Sending file will be stopped." = "A fájl küldése le fog állni."; /* No comment provided by engineer. */ "Sending receipts is disabled for %lld contacts" = "A kézbesítési jelentések le vannak tiltva %lld ismerősnél"; @@ -4193,7 +4132,8 @@ /* No comment provided by engineer. */ "Shape profile images" = "Profilkép alakzat"; -/* chat item action */ +/* alert action + chat item action */ "Share" = "Megosztás"; /* No comment provided by engineer. */ @@ -4202,7 +4142,7 @@ /* No comment provided by engineer. */ "Share address" = "Cím megosztása"; -/* No comment provided by engineer. */ +/* alert title */ "Share address with contacts?" = "Megosztja a címet az ismerőseivel?"; /* No comment provided by engineer. */ @@ -4269,7 +4209,7 @@ "SimpleX encrypted message or connection event" = "SimpleX titkosított üzenet vagy kapcsolati esemény"; /* simplex link type */ -"SimpleX group link" = "SimpleX csoporthivatkozás"; +"SimpleX group link" = "SimpleX-csoporthivatkozás"; /* chat feature */ "SimpleX links" = "SimpleX-hivatkozások"; @@ -4371,7 +4311,7 @@ "Stop chat to enable database actions" = "Csevegés megállítása az adatbázis-műveletek engedélyezéséhez"; /* No comment provided by engineer. */ -"Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." = "A csevegés megállítása a csevegési adatbázis exportálásához, importálásához vagy törléséhez. A csevegés megállítása alatt nem tud üzeneteket fogadni és küldeni."; +"Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." = "A csevegés megállítása a csevegési adatbázis exportálásához, importálásához vagy törléséhez. A csevegés megállításakor nem tud üzeneteket fogadni és küldeni."; /* No comment provided by engineer. */ "Stop chat?" = "Csevegési szolgáltatás megállítása?"; @@ -4385,10 +4325,10 @@ /* No comment provided by engineer. */ "Stop sending file?" = "Fájlküldés megállítása?"; -/* No comment provided by engineer. */ +/* alert action */ "Stop sharing" = "Megosztás megállítása"; -/* No comment provided by engineer. */ +/* alert title */ "Stop sharing address?" = "Címmegosztás megállítása?"; /* authentication reason */ @@ -4484,7 +4424,7 @@ /* No comment provided by engineer. */ "Test servers" = "Kiszolgálók tesztelése"; -/* No comment provided by engineer. */ +/* alert title */ "Tests failed!" = "Sikertelen tesztek!"; /* No comment provided by engineer. */ @@ -4496,9 +4436,6 @@ /* No comment provided by engineer. */ "Thanks to the users – contribute via Weblate!" = "Köszönet a felhasználóknak - hozzájárulás a Weblate-en!"; -/* No comment provided by engineer. */ -"No user identifiers." = "Nincsenek felhasználó-azonosítók."; - /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "Az alkalmazás értesíteni fogja, amikor üzeneteket vagy kapcsolatkéréseket kap – beállítások megnyitása az engedélyezéshez."; @@ -4509,7 +4446,7 @@ "The attempt to change database passphrase was not completed." = "Az adatbázis jelmondatának megváltoztatására tett kísérlet nem fejeződött be."; /* No comment provided by engineer. */ -"The code you scanned is not a SimpleX link QR code." = "A beolvasott QR-kód nem egy SimpleX QR-kód hivatkozás."; +"The code you scanned is not a SimpleX link QR code." = "A beolvasott QR-kód nem egy SimpleX-QR-kód-hivatkozás."; /* No comment provided by engineer. */ "The connection you accepted will be cancelled!" = "Az Ön által elfogadott kérelem vissza lesz vonva!"; @@ -4523,6 +4460,9 @@ /* No comment provided by engineer. */ "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 future of messaging" = "A privát üzenetküldés következő generációja"; + /* No comment provided by engineer. */ "The hash of the previous message is different." = "Az előző üzenet hasító értéke különbözik."; @@ -4530,19 +4470,16 @@ "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ő."; /* No comment provided by engineer. */ -"The message will be deleted for all members." = "Az üzenet minden tag számára törlésre kerül."; +"The message will be deleted for all members." = "Az üzenet az összes tag számára törlésre kerül."; /* No comment provided by engineer. */ -"The message will be marked as moderated for all members." = "Az üzenet minden tag számára moderáltként lesz megjelölve."; +"The message will be marked as moderated for all members." = "Az üzenet az összes tag számára moderáltként lesz megjelölve."; /* No comment provided by engineer. */ -"The messages will be deleted for all members." = "Az üzenetek minden tag számára törlésre kerülnek."; +"The messages will be deleted for all members." = "Az üzenetek az összes tag számára törlésre kerülnek."; /* No comment provided by engineer. */ -"The messages will be marked as moderated for all members." = "Az üzenetek moderáltként lesznek megjelölve minden tag számára."; - -/* No comment provided by engineer. */ -"The future of messaging" = "A privát üzenetküldés következő generációja"; +"The messages will be marked as moderated for all members." = "Az üzenetek az összes tag számára moderáltként lesznek megjelölve."; /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "A régi adatbázis nem került eltávolításra az átköltöztetéskor, így törölhető."; @@ -4569,7 +4506,7 @@ "Themes" = "Témák"; /* No comment provided by engineer. */ -"These settings are for your current profile **%@**." = "Ezek a beállítások a jelenlegi **%@** profiljára vonatkoznak."; +"These settings are for your current profile **%@**." = "Ezek a beállítások csak a jelenlegi (**%@**) profiljára vonatkoznak."; /* No comment provided by engineer. */ "They can be overridden in contact and group settings." = "Ezek felülbírálhatók az ismerős- és csoportbeállításokban."; @@ -4614,7 +4551,7 @@ "This link was used with another mobile device, please create a new link on the desktop." = "Ezt a hivatkozást egy másik hordozható eszközön már használták, hozzon létre egy új hivatkozást a számítógépén."; /* No comment provided by engineer. */ -"This setting applies to messages in your current chat profile **%@**." = "Ez a beállítás a jelenlegi **%@** profiljában lévő üzenetekre érvényes."; +"This setting applies to messages in your current chat profile **%@**." = "Ez a beállítás csak a jelenlegi (**%@**) profiljában lévő üzenetekre vonatkozik."; /* No comment provided by engineer. */ "Title" = "Cím"; @@ -4632,10 +4569,7 @@ "To make a new connection" = "Új kapcsolat létrehozásához"; /* No comment provided by engineer. */ -"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Az adatvédelem érdekében, a más csevegési platformokon megszokott felhasználó-azonosítók helyett, a SimpleX csak az üzenetek sorbaállításához használ azonosítókat, minden egyes ismerőshöz egy-egy különbözőt."; - -/* No comment provided by engineer. */ -"To protect timezone, image/voice files use UTC." = "Az időzóna védelme érdekében a kép-/hangfájlok UTC-t használnak."; +"To protect timezone, image/voice files use UTC." = "Az időzóna védelmének érdekében a kép-/hangfájlok UTC-t használnak."; /* No comment provided by engineer. */ "To protect your information, turn on SimpleX Lock.\nYou will be prompted to complete authentication before this feature is enabled." = "A biztonsága érdekében kapcsolja be a SimpleX-zár funkciót.\nA funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beállítására az eszközén."; @@ -4643,6 +4577,9 @@ /* No comment provided by engineer. */ "To protect your IP address, private routing uses your SMP servers to deliver messages." = "Az IP-cím védelmének érdekében a privát útválasztás az SMP-kiszolgálókat használja az üzenetek kézbesítéséhez."; +/* No comment provided by engineer. */ +"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Az adatvédelem érdekében (a más csevegési platformokon megszokott felhasználó-azonosítók helyett) a SimpleX csak az üzenetek sorbaállításához használ azonosítókat, az összes ismerőséhez különbözőt."; + /* No comment provided by engineer. */ "To record speech please grant permission to use Microphone." = "A beszéd rögzítéséhez adjon engedélyt a Mikrofon használatára."; @@ -4701,13 +4638,13 @@ "Unblock" = "Feloldás"; /* No comment provided by engineer. */ -"Unblock for all" = "Letiltás feloldása mindenki számára"; +"Unblock for all" = "Letiltás feloldása az összes tag számára"; /* No comment provided by engineer. */ "Unblock member" = "Tag feloldása"; /* No comment provided by engineer. */ -"Unblock member for all?" = "Mindenki számára feloldja a tag letiltását?"; +"Unblock member for all?" = "Az összes tag számára feloldja a tag letiltását?"; /* No comment provided by engineer. */ "Unblock member?" = "Tag feloldása?"; @@ -5019,7 +4956,7 @@ "Welcome message is too long" = "Az üdvözlőüzenet túl hosszú"; /* No comment provided by engineer. */ -"What's new" = "Milyen újdonságok vannak"; +"What's new" = "Újdonságok"; /* No comment provided by engineer. */ "When available" = "Amikor elérhető"; @@ -5030,9 +4967,6 @@ /* No comment provided by engineer. */ "when IP hidden" = "ha az IP-cím rejtett"; -/* No comment provided by engineer. */ -"When people request to connect, you can accept or reject it." = "Amikor az emberek kapcsolatot kérnek, Ön elfogadhatja vagy elutasíthatja azokat."; - /* No comment provided by engineer. */ "When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "Inkognitóprofil megosztása esetén a rendszer azt a profilt fogja használni azokhoz a csoportokhoz, amelyekbe meghívást kapott."; @@ -5088,7 +5022,7 @@ "You accepted connection" = "Kapcsolat létrehozása"; /* No comment provided by engineer. */ -"You allow" = "Engedélyezte"; +"You allow" = "Ön engedélyezi"; /* No comment provided by engineer. */ "You already have a chat profile with the same display name. Please choose another name." = "Már van egy csevegési profil ugyanezzel a megjelenített névvel. Válasszon egy másik nevet."; @@ -5103,7 +5037,7 @@ "You are already connecting via this one-time link!" = "A kapcsolódás már folyamatban van ezen az egyszer használható hivatkozáson keresztül!"; /* No comment provided by engineer. */ -"You are already in group %@." = "Már a(z) %@ csoport tagja."; +"You are already in group %@." = "Ön már a(z) %@ nevű csoport tagja."; /* No comment provided by engineer. */ "You are already joining the group %@." = "A csatlakozás már folyamatban van a(z) %@ nevű csoporthoz."; @@ -5118,7 +5052,7 @@ "You are already joining the group!\nRepeat join request?" = "Csatlakozás folyamatban!\nCsatlakozáskérés megismétlése?"; /* No comment provided by engineer. */ -"You are connected to the server used to receive messages from this contact." = "Már kapcsolódott ahhoz a kiszolgálóhoz, amely az adott ismerősétől érkező üzenetek fogadására szolgál."; +"You are connected to the server used to receive messages from this contact." = "Ön már kapcsolódott ahhoz a kiszolgálóhoz, amely az adott ismerősétől érkező üzenetek fogadására szolgál."; /* No comment provided by engineer. */ "you are invited to group" = "meghívást kapott a csoportba"; @@ -5130,7 +5064,7 @@ "You are not connected to these servers. Private routing is used to deliver messages to them." = "Ön nem kapcsolódik ezekhez a kiszolgálókhoz. A privát útválasztás az üzenetek kézbesítésére szolgál."; /* No comment provided by engineer. */ -"you are observer" = "megfigyelő szerep"; +"you are observer" = "Ön megfigyelő"; /* snd group event chat item */ "you blocked %@" = "Ön letiltotta őt: %@"; @@ -5174,9 +5108,6 @@ /* No comment provided by engineer. */ "You can share this address with your contacts to let them connect with **%@**." = "Megoszthatja ezt a címet az ismerőseivel, hogy kapcsolatba léphessenek Önnel a(z) **%@** nevű profilján keresztül."; -/* No comment provided by engineer. */ -"You can share your address as a link or QR code - anybody can connect to you." = "Megoszthatja a címét egy hivatkozásként vagy QR-kódként – így bárki kapcsolódhat Önhöz."; - /* No comment provided by engineer. */ "You can start chat via app Settings / Database or by restarting the app" = "A csevegést az alkalmazás „Beállítások / Adatbázis” menüben vagy az alkalmazás újraindításával indíthatja el"; @@ -5189,7 +5120,7 @@ /* No comment provided by engineer. */ "You can use markdown to format messages:" = "Üzenetek formázása a szövegbe szúrt speciális karakterekkel:"; -/* No comment provided by engineer. */ +/* alert message */ "You can view invitation link again in connection details." = "A meghívó-hivatkozást újra megtekintheti a kapcsolat részleteinél."; /* No comment provided by engineer. */ @@ -5210,6 +5141,9 @@ /* No comment provided by engineer. */ "You could not be verified; please try again." = "Nem sikerült hitelesíteni; próbálja meg újra."; +/* No comment provided by engineer. */ +"You decide who can connect." = "Ön dönti el, hogy kivel beszélget."; + /* No comment provided by engineer. */ "You have already requested connection via this address!" = "Már küldött egy kapcsolatkérést ezen a címen keresztül!"; @@ -5300,9 +5234,6 @@ /* No comment provided by engineer. */ "You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed" = "Inkognitóprofilt használ ehhez a csoporthoz - fő profilja megosztásának elkerülése érdekében a meghívók küldése le van tiltva"; -/* No comment provided by engineer. */ -"Your %@ servers" = "%@ nevű profiljához tartozó kiszolgálók"; - /* No comment provided by engineer. */ "Your calls" = "Hívások"; @@ -5366,9 +5297,6 @@ /* No comment provided by engineer. */ "Your random profile" = "Véletlenszerű profil"; -/* No comment provided by engineer. */ -"Your server" = "Saját SMP-kiszolgáló"; - /* No comment provided by engineer. */ "Your server address" = "Saját SMP-kiszolgálójának címe"; @@ -5381,6 +5309,3 @@ /* No comment provided by engineer. */ "Your SMP servers" = "Saját SMP-kiszolgálók"; -/* No comment provided by engineer. */ -"Your XFTP servers" = "Saját XFTP-kiszolgálók"; - diff --git a/apps/ios/it.lproj/Localizable.strings b/apps/ios/it.lproj/Localizable.strings index 43fd26e534..c041228706 100644 --- a/apps/ios/it.lproj/Localizable.strings +++ b/apps/ios/it.lproj/Localizable.strings @@ -1,15 +1,6 @@ /* No comment provided by engineer. */ "\n" = "\n"; -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - /* No comment provided by engineer. */ " (" = " ("; @@ -337,12 +328,6 @@ /* No comment provided by engineer. */ "Abort changing address?" = "Interrompere il cambio di indirizzo?"; -/* No comment provided by engineer. */ -"About SimpleX" = "Riguardo SimpleX"; - -/* No comment provided by engineer. */ -"About SimpleX address" = "Info sull'indirizzo SimpleX"; - /* No comment provided by engineer. */ "About SimpleX Chat" = "Riguardo SimpleX Chat"; @@ -382,12 +367,6 @@ /* No comment provided by engineer. */ "Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." = "Aggiungi l'indirizzo al tuo profilo, in modo che i tuoi contatti possano condividerlo con altre persone. L'aggiornamento del profilo verrà inviato ai tuoi contatti."; -/* No comment provided by engineer. */ -"Add contact" = "Aggiungi contatto"; - -/* No comment provided by engineer. */ -"Add preset servers" = "Aggiungi server preimpostati"; - /* No comment provided by engineer. */ "Add profile" = "Aggiungi profilo"; @@ -574,6 +553,9 @@ /* No comment provided by engineer. */ "Answer call" = "Rispondi alla chiamata"; +/* No comment provided by engineer. */ +"Anybody can host servers." = "Chiunque può installare i server."; + /* No comment provided by engineer. */ "App build: %@" = "Build dell'app: %@"; @@ -817,7 +799,8 @@ /* No comment provided by engineer. */ "Can't message member" = "Impossibile inviare un messaggio al membro"; -/* alert button */ +/* alert action + alert button */ "Cancel" = "Annulla"; /* No comment provided by engineer. */ @@ -938,7 +921,7 @@ /* No comment provided by engineer. */ "Chats" = "Chat"; -/* No comment provided by engineer. */ +/* alert title */ "Check server address and try again." = "Controlla l'indirizzo del server e riprova."; /* No comment provided by engineer. */ @@ -1001,9 +984,6 @@ /* No comment provided by engineer. */ "Configure ICE servers" = "Configura server ICE"; -/* No comment provided by engineer. */ -"Configured %@ servers" = "Configurati %@ server"; - /* No comment provided by engineer. */ "Confirm" = "Conferma"; @@ -1232,9 +1212,6 @@ /* No comment provided by engineer. */ "Create a group using a random profile." = "Crea un gruppo usando un profilo casuale."; -/* No comment provided by engineer. */ -"Create an address to let people connect with you." = "Crea un indirizzo per consentire alle persone di connettersi con te."; - /* server test step */ "Create file" = "Crea file"; @@ -1397,7 +1374,8 @@ /* No comment provided by engineer. */ "default (yes)" = "predefinito (sì)"; -/* chat item action +/* alert action + chat item action swipe action */ "Delete" = "Elimina"; @@ -1996,9 +1974,6 @@ /* No comment provided by engineer. */ "Error joining group" = "Errore di ingresso nel gruppo"; -/* No comment provided by engineer. */ -"Error loading %@ servers" = "Errore nel caricamento dei server %@"; - /* No comment provided by engineer. */ "Error migrating settings" = "Errore nella migrazione delle impostazioni"; @@ -2020,9 +1995,6 @@ /* No comment provided by engineer. */ "Error resetting statistics" = "Errore di azzeramento statistiche"; -/* No comment provided by engineer. */ -"Error saving %@ servers" = "Errore nel salvataggio dei server %@"; - /* No comment provided by engineer. */ "Error saving group profile" = "Errore nel salvataggio del profilo del gruppo"; @@ -2425,9 +2397,6 @@ /* time unit */ "hours" = "ore"; -/* No comment provided by engineer. */ -"How it works" = "Come funziona"; - /* No comment provided by engineer. */ "How SimpleX works" = "Come funziona SimpleX"; @@ -2570,10 +2539,10 @@ "Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)" = "Installa [Simplex Chat per terminale](https://github.com/simplex-chat/simplex-chat)"; /* No comment provided by engineer. */ -"Instant push notifications will be hidden!\n" = "Le notifiche push istantanee saranno nascoste!\n"; +"Instant" = "Istantaneamente"; /* No comment provided by engineer. */ -"Instant" = "Istantaneamente"; +"Instant push notifications will be hidden!\n" = "Le notifiche push istantanee saranno nascoste!\n"; /* No comment provided by engineer. */ "Interface" = "Interfaccia"; @@ -2611,7 +2580,7 @@ /* No comment provided by engineer. */ "Invalid response" = "Risposta non valida"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid server address!" = "Indirizzo del server non valido!"; /* item status text */ @@ -2716,7 +2685,7 @@ /* No comment provided by engineer. */ "Joining group" = "Ingresso nel gruppo"; -/* No comment provided by engineer. */ +/* alert action */ "Keep" = "Tieni"; /* No comment provided by engineer. */ @@ -2725,7 +2694,7 @@ /* No comment provided by engineer. */ "Keep the app open to use it from desktop" = "Tieni aperta l'app per usarla dal desktop"; -/* No comment provided by engineer. */ +/* alert title */ "Keep unused invitation?" = "Tenere l'invito inutilizzato?"; /* No comment provided by engineer. */ @@ -2782,9 +2751,6 @@ /* No comment provided by engineer. */ "Live messages" = "Messaggi in diretta"; -/* No comment provided by engineer. */ -"No push server" = "Locale"; - /* No comment provided by engineer. */ "Local name" = "Nome locale"; @@ -2797,24 +2763,15 @@ /* No comment provided by engineer. */ "Lock mode" = "Modalità di blocco"; -/* No comment provided by engineer. */ -"Make a private connection" = "Crea una connessione privata"; - /* No comment provided by engineer. */ "Make one message disappear" = "Fai sparire un messaggio"; /* No comment provided by engineer. */ "Make profile private!" = "Rendi privato il profilo!"; -/* No comment provided by engineer. */ -"Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." = "Assicurati che gli indirizzi dei server %@ siano nel formato corretto, uno per riga e non doppi (%@)."; - /* No comment provided by engineer. */ "Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "Assicurati che gli indirizzi dei server WebRTC ICE siano nel formato corretto, uno per riga e non doppi."; -/* No comment provided by engineer. */ -"Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" = "Molte persone hanno chiesto: *se SimpleX non ha identificatori utente, come può recapitare i messaggi?*"; - /* No comment provided by engineer. */ "Mark deleted for everyone" = "Contrassegna eliminato per tutti"; @@ -3154,12 +3111,18 @@ /* No comment provided by engineer. */ "No permission to record voice message" = "Nessuna autorizzazione per registrare messaggi vocali"; +/* No comment provided by engineer. */ +"No push server" = "Locale"; + /* No comment provided by engineer. */ "No received or sent files" = "Nessun file ricevuto o inviato"; /* copied message info in history */ "no text" = "nessun testo"; +/* No comment provided by engineer. */ +"No user identifiers." = "Nessun identificatore utente."; + /* No comment provided by engineer. */ "Not compatible!" = "Non compatibile!"; @@ -3282,18 +3245,9 @@ /* authentication reason */ "Open migration to another device" = "Apri migrazione ad un altro dispositivo"; -/* No comment provided by engineer. */ -"Open server settings" = "Apri impostazioni server"; - /* No comment provided by engineer. */ "Open Settings" = "Apri le impostazioni"; -/* authentication reason */ -"Open user profiles" = "Apri i profili utente"; - -/* No comment provided by engineer. */ -"Anybody can host servers." = "Chiunque può installare i server."; - /* No comment provided by engineer. */ "Opening app…" = "Apertura dell'app…"; @@ -3315,9 +3269,6 @@ /* No comment provided by engineer. */ "Other" = "Altro"; -/* No comment provided by engineer. */ -"Other %@ servers" = "Altri %@ server"; - /* No comment provided by engineer. */ "other errors" = "altri errori"; @@ -3372,9 +3323,6 @@ /* No comment provided by engineer. */ "Pending" = "In attesa"; -/* No comment provided by engineer. */ -"You decide who can connect." = "Sei tu a decidere chi può connettersi."; - /* No comment provided by engineer. */ "Periodic" = "Periodicamente"; @@ -3453,9 +3401,6 @@ /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Conserva la bozza dell'ultimo messaggio, con gli allegati."; -/* No comment provided by engineer. */ -"Preset server" = "Server preimpostato"; - /* No comment provided by engineer. */ "Preset server address" = "Indirizzo server preimpostato"; @@ -3504,7 +3449,7 @@ /* No comment provided by engineer. */ "Profile theme" = "Tema del profilo"; -/* No comment provided by engineer. */ +/* alert message */ "Profile update will be sent to your contacts." = "L'aggiornamento del profilo verrà inviato ai tuoi contatti."; /* No comment provided by engineer. */ @@ -3589,10 +3534,10 @@ "Read more" = "Leggi tutto"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." = "Maggiori informazioni nella [Guida per l'utente](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)."; +"Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." = "Leggi di più nella [Guida utente](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)."; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." = "Leggi di più nella [Guida utente](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)."; +"Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." = "Maggiori informazioni nella [Guida per l'utente](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)."; /* No comment provided by engineer. */ "Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." = "Maggiori informazioni nella [Guida per l'utente](https://simplex.chat/docs/guide/readme.html#connect-to-friends)."; @@ -3600,9 +3545,6 @@ /* No comment provided by engineer. */ "Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." = "Maggiori informazioni nel nostro [repository GitHub](https://github.com/simplex-chat/simplex-chat#readme)."; -/* No comment provided by engineer. */ -"Read more in our GitHub repository." = "Maggiori informazioni nel nostro repository GitHub."; - /* No comment provided by engineer. */ "Receipts are disabled" = "Le ricevute sono disattivate"; @@ -3875,7 +3817,7 @@ /* No comment provided by engineer. */ "Save servers" = "Salva i server"; -/* No comment provided by engineer. */ +/* alert title */ "Save servers?" = "Salvare i server?"; /* No comment provided by engineer. */ @@ -4028,9 +3970,6 @@ /* No comment provided by engineer. */ "Send notifications" = "Invia notifiche"; -/* No comment provided by engineer. */ -"Send notifications:" = "Invia notifiche:"; - /* No comment provided by engineer. */ "Send questions and ideas" = "Invia domande e idee"; @@ -4167,7 +4106,7 @@ "set new contact address" = "impostato nuovo indirizzo di contatto"; /* profile update event chat item */ -"set new profile picture" = "impostata nuova immagine del profilo"; +"set new profile picture" = "ha impostato una nuova immagine del profilo"; /* No comment provided by engineer. */ "Set passcode" = "Imposta codice"; @@ -4193,7 +4132,8 @@ /* No comment provided by engineer. */ "Shape profile images" = "Forma delle immagini del profilo"; -/* chat item action */ +/* alert action + chat item action */ "Share" = "Condividi"; /* No comment provided by engineer. */ @@ -4202,7 +4142,7 @@ /* No comment provided by engineer. */ "Share address" = "Condividi indirizzo"; -/* No comment provided by engineer. */ +/* alert title */ "Share address with contacts?" = "Condividere l'indirizzo con i contatti?"; /* No comment provided by engineer. */ @@ -4385,10 +4325,10 @@ /* No comment provided by engineer. */ "Stop sending file?" = "Fermare l'invio del file?"; -/* No comment provided by engineer. */ +/* alert action */ "Stop sharing" = "Smetti di condividere"; -/* No comment provided by engineer. */ +/* alert title */ "Stop sharing address?" = "Smettere di condividere l'indirizzo?"; /* authentication reason */ @@ -4484,7 +4424,7 @@ /* No comment provided by engineer. */ "Test servers" = "Prova i server"; -/* No comment provided by engineer. */ +/* alert title */ "Tests failed!" = "Test falliti!"; /* No comment provided by engineer. */ @@ -4496,9 +4436,6 @@ /* No comment provided by engineer. */ "Thanks to the users – contribute via Weblate!" = "Grazie agli utenti – contribuite via Weblate!"; -/* No comment provided by engineer. */ -"No user identifiers." = "Nessun identificatore utente."; - /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "L'app può avvisarti quando ricevi messaggi o richieste di contatto: apri le impostazioni per attivare."; @@ -4523,6 +4460,9 @@ /* No comment provided by engineer. */ "The encryption is working and the new encryption agreement is not required. It may result in connection errors!" = "La crittografia funziona e il nuovo accordo sulla crittografia non è richiesto. Potrebbero verificarsi errori di connessione!"; +/* No comment provided by engineer. */ +"The future of messaging" = "La nuova generazione di messaggistica privata"; + /* No comment provided by engineer. */ "The hash of the previous message is different." = "L'hash del messaggio precedente è diverso."; @@ -4541,9 +4481,6 @@ /* No comment provided by engineer. */ "The messages will be marked as moderated for all members." = "I messaggi verranno contrassegnati come moderati per tutti i membri."; -/* No comment provided by engineer. */ -"The future of messaging" = "La nuova generazione di messaggistica privata"; - /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "Il database vecchio non è stato rimosso durante la migrazione, può essere eliminato."; @@ -4631,9 +4568,6 @@ /* No comment provided by engineer. */ "To make a new connection" = "Per creare una nuova connessione"; -/* No comment provided by engineer. */ -"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Per proteggere la privacy, invece degli ID utente utilizzati da tutte le altre piattaforme, SimpleX ha identificatori per le code di messaggi, separati per ciascuno dei tuoi contatti."; - /* No comment provided by engineer. */ "To protect timezone, image/voice files use UTC." = "Per proteggere il fuso orario, i file immagine/vocali usano UTC."; @@ -4643,6 +4577,9 @@ /* No comment provided by engineer. */ "To protect your IP address, private routing uses your SMP servers to deliver messages." = "Per proteggere il tuo indirizzo IP, l'instradamento privato usa i tuoi server SMP per consegnare i messaggi."; +/* No comment provided by engineer. */ +"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Per proteggere la privacy, invece degli ID utente utilizzati da tutte le altre piattaforme, SimpleX ha identificatori per le code di messaggi, separati per ciascuno dei tuoi contatti."; + /* No comment provided by engineer. */ "To record speech please grant permission to use Microphone." = "Per registrare l'audio, concedi l'autorizzazione di usare il microfono."; @@ -5030,9 +4967,6 @@ /* No comment provided by engineer. */ "when IP hidden" = "quando l'IP è nascosto"; -/* No comment provided by engineer. */ -"When people request to connect, you can accept or reject it." = "Quando le persone chiedono di connettersi, puoi accettare o rifiutare."; - /* No comment provided by engineer. */ "When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "Quando condividi un profilo in incognito con qualcuno, questo profilo verrà utilizzato per i gruppi a cui ti invitano."; @@ -5174,9 +5108,6 @@ /* No comment provided by engineer. */ "You can share this address with your contacts to let them connect with **%@**." = "Puoi condividere questo indirizzo con i tuoi contatti per consentire loro di connettersi con **%@**."; -/* No comment provided by engineer. */ -"You can share your address as a link or QR code - anybody can connect to you." = "Puoi condividere il tuo indirizzo come link o come codice QR: chiunque potrà connettersi a te."; - /* No comment provided by engineer. */ "You can start chat via app Settings / Database or by restarting the app" = "Puoi avviare la chat via Impostazioni / Database o riavviando l'app"; @@ -5189,7 +5120,7 @@ /* No comment provided by engineer. */ "You can use markdown to format messages:" = "Puoi usare il markdown per formattare i messaggi:"; -/* No comment provided by engineer. */ +/* alert message */ "You can view invitation link again in connection details." = "Puoi vedere di nuovo il link di invito nei dettagli di connessione."; /* No comment provided by engineer. */ @@ -5210,6 +5141,9 @@ /* No comment provided by engineer. */ "You could not be verified; please try again." = "Non è stato possibile verificarti, riprova."; +/* No comment provided by engineer. */ +"You decide who can connect." = "Sei tu a decidere chi può connettersi."; + /* No comment provided by engineer. */ "You have already requested connection via this address!" = "Hai già richiesto la connessione tramite questo indirizzo!"; @@ -5300,9 +5234,6 @@ /* No comment provided by engineer. */ "You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed" = "Stai usando un profilo in incognito per questo gruppo: per impedire la condivisione del tuo profilo principale non è consentito invitare contatti"; -/* No comment provided by engineer. */ -"Your %@ servers" = "I tuoi server %@"; - /* No comment provided by engineer. */ "Your calls" = "Le tue chiamate"; @@ -5366,9 +5297,6 @@ /* No comment provided by engineer. */ "Your random profile" = "Il tuo profilo casuale"; -/* No comment provided by engineer. */ -"Your server" = "Il tuo server"; - /* No comment provided by engineer. */ "Your server address" = "L'indirizzo del tuo server"; @@ -5381,6 +5309,3 @@ /* No comment provided by engineer. */ "Your SMP servers" = "I tuoi server SMP"; -/* No comment provided by engineer. */ -"Your XFTP servers" = "I tuoi server XFTP"; - diff --git a/apps/ios/ja.lproj/Localizable.strings b/apps/ios/ja.lproj/Localizable.strings index 93735ef2d1..d8756cc788 100644 --- a/apps/ios/ja.lproj/Localizable.strings +++ b/apps/ios/ja.lproj/Localizable.strings @@ -1,15 +1,6 @@ /* No comment provided by engineer. */ "\n" = "\n"; -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - /* No comment provided by engineer. */ " (" = " ("; @@ -313,12 +304,6 @@ /* No comment provided by engineer. */ "Abort changing address?" = "アドレス変更を中止しますか?"; -/* No comment provided by engineer. */ -"About SimpleX" = "SimpleXについて"; - -/* No comment provided by engineer. */ -"About SimpleX address" = "SimpleXアドレスについて"; - /* No comment provided by engineer. */ "About SimpleX Chat" = "SimpleX Chat について"; @@ -346,9 +331,6 @@ /* No comment provided by engineer. */ "Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." = "プロフィールにアドレスを追加し、連絡先があなたのアドレスを他の人と共有できるようにします。プロフィールの更新は連絡先に送信されます。"; -/* No comment provided by engineer. */ -"Add preset servers" = "既存サーバを追加"; - /* No comment provided by engineer. */ "Add profile" = "プロフィールを追加"; @@ -487,6 +469,9 @@ /* No comment provided by engineer. */ "Answer call" = "通話に応答"; +/* No comment provided by engineer. */ +"Anybody can host servers." = "プロトコル技術とコードはオープンソースで、どなたでもご自分のサーバを運用できます。"; + /* No comment provided by engineer. */ "App build: %@" = "アプリのビルド: %@"; @@ -616,7 +601,8 @@ /* No comment provided by engineer. */ "Can't invite contacts!" = "連絡先を招待できません!"; -/* alert button */ +/* alert action + alert button */ "Cancel" = "中止"; /* feature offered item */ @@ -704,7 +690,7 @@ /* No comment provided by engineer. */ "Chats" = "チャット"; -/* No comment provided by engineer. */ +/* alert title */ "Check server address and try again." = "サーバのアドレスを確認してから再度試してください。"; /* No comment provided by engineer. */ @@ -866,9 +852,6 @@ /* No comment provided by engineer. */ "Create" = "作成"; -/* No comment provided by engineer. */ -"Create an address to let people connect with you." = "人とつながるためのアドレスを作成する。"; - /* server test step */ "Create file" = "ファイルを作成"; @@ -992,7 +975,8 @@ /* No comment provided by engineer. */ "default (yes)" = "デフォルト(はい)"; -/* chat item action +/* alert action + chat item action swipe action */ "Delete" = "削除"; @@ -1428,18 +1412,12 @@ /* No comment provided by engineer. */ "Error joining group" = "グループ参加にエラー発生"; -/* No comment provided by engineer. */ -"Error loading %@ servers" = "%@ サーバーのロード中にエラーが発生"; - /* alert title */ "Error receiving file" = "ファイル受信にエラー発生"; /* No comment provided by engineer. */ "Error removing member" = "メンバー除名にエラー発生"; -/* No comment provided by engineer. */ -"Error saving %@ servers" = "%@ サーバの保存エラー"; - /* No comment provided by engineer. */ "Error saving group profile" = "グループのプロフィール保存にエラー発生"; @@ -1707,9 +1685,6 @@ /* time unit */ "hours" = "時間"; -/* No comment provided by engineer. */ -"How it works" = "技術の説明"; - /* No comment provided by engineer. */ "How SimpleX works" = "SimpleX の仕組み"; @@ -1819,10 +1794,10 @@ "Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)" = "インストール [ターミナル用SimpleX Chat](https://github.com/simplex-chat/simplex-chat)"; /* No comment provided by engineer. */ -"Instant push notifications will be hidden!\n" = "インスタントプッシュ通知は非表示になります!\n"; +"Instant" = "すぐに"; /* No comment provided by engineer. */ -"Instant" = "すぐに"; +"Instant push notifications will be hidden!\n" = "インスタントプッシュ通知は非表示になります!\n"; /* No comment provided by engineer. */ "Interface" = "インターフェース"; @@ -1839,7 +1814,7 @@ /* invalid chat item */ "invalid data" = "無効なデータ"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid server address!" = "無効なサーバアドレス!"; /* item status text */ @@ -1968,9 +1943,6 @@ /* No comment provided by engineer. */ "Live messages" = "ライブメッセージ"; -/* No comment provided by engineer. */ -"No push server" = "自分のみ"; - /* No comment provided by engineer. */ "Local name" = "ローカルネーム"; @@ -1983,24 +1955,15 @@ /* No comment provided by engineer. */ "Lock mode" = "ロックモード"; -/* No comment provided by engineer. */ -"Make a private connection" = "プライベートな接続をする"; - /* No comment provided by engineer. */ "Make one message disappear" = "メッセージを1つ消す"; /* No comment provided by engineer. */ "Make profile private!" = "プロフィールを非表示にできます!"; -/* No comment provided by engineer. */ -"Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." = "%@ サーバー アドレスが正しい形式で、行が区切られており、重複していないことを確認してください (%@)。"; - /* No comment provided by engineer. */ "Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "WebRTC ICEサーバのアドレスを正しく1行ずつに分けて、重複しないように、形式もご確認ください。"; -/* No comment provided by engineer. */ -"Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" = "多くの人が次のような質問をしました: *SimpleX にユーザー識別子がない場合、どうやってメッセージを配信できるのですか?*"; - /* No comment provided by engineer. */ "Mark deleted for everyone" = "全員に対して削除済みマークを付ける"; @@ -2208,12 +2171,18 @@ /* No comment provided by engineer. */ "No permission to record voice message" = "音声メッセージを録音する権限がありません"; +/* No comment provided by engineer. */ +"No push server" = "自分のみ"; + /* No comment provided by engineer. */ "No received or sent files" = "送受信済みのファイルがありません"; /* copied message info in history */ "no text" = "テキストなし"; +/* No comment provided by engineer. */ +"No user identifiers." = "世界初のユーザーIDのないプラットフォーム|設計も元からプライベート。"; + /* No comment provided by engineer. */ "Notifications" = "通知"; @@ -2318,12 +2287,6 @@ /* No comment provided by engineer. */ "Open Settings" = "設定を開く"; -/* authentication reason */ -"Open user profiles" = "ユーザープロフィールを開く"; - -/* No comment provided by engineer. */ -"Anybody can host servers." = "プロトコル技術とコードはオープンソースで、どなたでもご自分のサーバを運用できます。"; - /* member role */ "owner" = "オーナー"; @@ -2351,9 +2314,6 @@ /* No comment provided by engineer. */ "peer-to-peer" = "P2P"; -/* No comment provided by engineer. */ -"You decide who can connect." = "あなたと繋がることができるのは、あなたからリンクを頂いた方のみです。"; - /* No comment provided by engineer. */ "Periodic" = "定期的に"; @@ -2411,9 +2371,6 @@ /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "添付を含めて、下書きを保存する。"; -/* No comment provided by engineer. */ -"Preset server" = "プレセットサーバ"; - /* No comment provided by engineer. */ "Preset server address" = "プレセットサーバのアドレス"; @@ -2438,7 +2395,7 @@ /* No comment provided by engineer. */ "Profile password" = "プロフィールのパスワード"; -/* No comment provided by engineer. */ +/* alert message */ "Profile update will be sent to your contacts." = "連絡先にプロフィール更新のお知らせが届きます。"; /* No comment provided by engineer. */ @@ -2501,9 +2458,6 @@ /* No comment provided by engineer. */ "Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." = "詳しくは[GitHubリポジトリ](https://github.com/simplex-chat/simplex-chat#readme)をご覧ください。"; -/* No comment provided by engineer. */ -"Read more in our GitHub repository." = "GitHubリポジトリで詳細をご確認ください。"; - /* No comment provided by engineer. */ "received answer…" = "回答を受け取りました…"; @@ -2686,7 +2640,7 @@ /* No comment provided by engineer. */ "Save servers" = "サーバを保存"; -/* No comment provided by engineer. */ +/* alert title */ "Save servers?" = "サーバを保存しますか?"; /* No comment provided by engineer. */ @@ -2767,9 +2721,6 @@ /* No comment provided by engineer. */ "Send notifications" = "通知を送信する"; -/* No comment provided by engineer. */ -"Send notifications:" = "通知を送信する:"; - /* No comment provided by engineer. */ "Send questions and ideas" = "質問やアイデアを送る"; @@ -2842,7 +2793,8 @@ /* No comment provided by engineer. */ "Settings" = "設定"; -/* chat item action */ +/* alert action + chat item action */ "Share" = "共有する"; /* No comment provided by engineer. */ @@ -2851,7 +2803,7 @@ /* No comment provided by engineer. */ "Share address" = "アドレスを共有する"; -/* No comment provided by engineer. */ +/* alert title */ "Share address with contacts?" = "アドレスを連絡先と共有しますか?"; /* No comment provided by engineer. */ @@ -2959,10 +2911,10 @@ /* No comment provided by engineer. */ "Stop sending file?" = "ファイルの送信を停止しますか?"; -/* No comment provided by engineer. */ +/* alert action */ "Stop sharing" = "共有を停止"; -/* No comment provided by engineer. */ +/* alert title */ "Stop sharing address?" = "アドレスの共有を停止しますか?"; /* authentication reason */ @@ -3019,7 +2971,7 @@ /* No comment provided by engineer. */ "Test servers" = "テストサーバ"; -/* No comment provided by engineer. */ +/* alert title */ "Tests failed!" = "テストは失敗しました!"; /* No comment provided by engineer. */ @@ -3031,9 +2983,6 @@ /* No comment provided by engineer. */ "Thanks to the users – contribute via Weblate!" = "ユーザーに感謝します – Weblate 経由で貢献してください!"; -/* No comment provided by engineer. */ -"No user identifiers." = "世界初のユーザーIDのないプラットフォーム|設計も元からプライベート。"; - /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "アプリは、メッセージや連絡先のリクエストを受信したときに通知することができます - 設定を開いて有効にしてください。"; @@ -3052,6 +3001,9 @@ /* No comment provided by engineer. */ "The encryption is working and the new encryption agreement is not required. It may result in connection errors!" = "暗号化は機能しており、新しい暗号化への同意は必要ありません。接続エラーが発生する可能性があります!"; +/* No comment provided by engineer. */ +"The future of messaging" = "次世代のプライバシー・メッセンジャー"; + /* No comment provided by engineer. */ "The hash of the previous message is different." = "以前のメッセージとハッシュ値が異なります。"; @@ -3064,9 +3016,6 @@ /* No comment provided by engineer. */ "The message will be marked as moderated for all members." = "メッセージは、すべてのメンバーに対してモデレートされたものとして表示されます。"; -/* No comment provided by engineer. */ -"The future of messaging" = "次世代のプライバシー・メッセンジャー"; - /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "古いデータベースは移行時に削除されなかったので、削除することができます。"; @@ -3115,15 +3064,15 @@ /* No comment provided by engineer. */ "To make a new connection" = "新規に接続する場合"; -/* No comment provided by engineer. */ -"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "プライバシーを保護するために、SimpleX には、他のすべてのプラットフォームで使用されるユーザー ID の代わりに、連絡先ごとに個別のメッセージ キューの識別子があります。"; - /* No comment provided by engineer. */ "To protect timezone, image/voice files use UTC." = "時間帯を漏らさないために、画像と音声ファイルはUTCを使います。"; /* No comment provided by engineer. */ "To protect your information, turn on SimpleX Lock.\nYou will be prompted to complete authentication before this feature is enabled." = "あなたのデータを守るために、SimpleXロックをオンにしてください。\nオンにするには、認証ステップが行われます。"; +/* No comment provided by engineer. */ +"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "プライバシーを保護するために、SimpleX には、他のすべてのプラットフォームで使用されるユーザー ID の代わりに、連絡先ごとに個別のメッセージ キューの識別子があります。"; + /* No comment provided by engineer. */ "To record voice message please grant permission to use Microphone." = "音声メッセージを録音する場合は、マイクの使用を許可してください。"; @@ -3346,9 +3295,6 @@ /* No comment provided by engineer. */ "When available" = "利用可能時に"; -/* No comment provided by engineer. */ -"When people request to connect, you can accept or reject it." = "接続が要求されたら、それを受け入れるか拒否するかを選択できます。"; - /* No comment provided by engineer. */ "When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "連絡相手にシークレットモードのプロフィールを共有すると、その連絡相手に招待されたグループでも同じプロフィールが使われます。"; @@ -3415,9 +3361,6 @@ /* No comment provided by engineer. */ "You can share this address with your contacts to let them connect with **%@**." = "このアドレスを連絡先と共有して、**%@** に接続できるようにすることができます。"; -/* No comment provided by engineer. */ -"You can share your address as a link or QR code - anybody can connect to you." = "アドレスをリンクやQRコードとして共有することで、誰でも接続することができます。"; - /* No comment provided by engineer. */ "You can start chat via app Settings / Database or by restarting the app" = "アプリの設定/データベースから、またはアプリを再起動することでチャットを開始できます"; @@ -3445,6 +3388,9 @@ /* No comment provided by engineer. */ "You could not be verified; please try again." = "確認できませんでした。 もう一度お試しください。"; +/* No comment provided by engineer. */ +"You decide who can connect." = "あなたと繋がることができるのは、あなたからリンクを頂いた方のみです。"; + /* No comment provided by engineer. */ "You have to enter passphrase every time the app starts - it is not stored on the device." = "アプリ起動時にパスフレーズを入力しなければなりません。端末に保存されてません。"; @@ -3511,9 +3457,6 @@ /* No comment provided by engineer. */ "You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed" = "シークレットモードのプロフィールでこのグループに参加しています。メインのプロフィールを守るために、招待することができません"; -/* No comment provided by engineer. */ -"Your %@ servers" = "あなたの %@ サーバー"; - /* No comment provided by engineer. */ "Your calls" = "あなたの通話"; @@ -3562,9 +3505,6 @@ /* No comment provided by engineer. */ "Your random profile" = "あなたのランダム・プロフィール"; -/* No comment provided by engineer. */ -"Your server" = "あなたのサーバ"; - /* No comment provided by engineer. */ "Your server address" = "あなたのサーバアドレス"; @@ -3577,6 +3517,3 @@ /* No comment provided by engineer. */ "Your SMP servers" = "あなたのSMPサーバ"; -/* No comment provided by engineer. */ -"Your XFTP servers" = "あなたのXFTPサーバ"; - diff --git a/apps/ios/nl.lproj/Localizable.strings b/apps/ios/nl.lproj/Localizable.strings index 94cb3115a9..4729e50d46 100644 --- a/apps/ios/nl.lproj/Localizable.strings +++ b/apps/ios/nl.lproj/Localizable.strings @@ -1,15 +1,6 @@ /* No comment provided by engineer. */ "\n" = "\n"; -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - /* No comment provided by engineer. */ " (" = " ("; @@ -337,12 +328,6 @@ /* No comment provided by engineer. */ "Abort changing address?" = "Adres wijziging afbreken?"; -/* No comment provided by engineer. */ -"About SimpleX" = "Over SimpleX"; - -/* No comment provided by engineer. */ -"About SimpleX address" = "Over SimpleX adres"; - /* No comment provided by engineer. */ "About SimpleX Chat" = "Over SimpleX Chat"; @@ -382,12 +367,6 @@ /* No comment provided by engineer. */ "Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." = "Voeg een adres toe aan uw profiel, zodat uw contacten het met andere mensen kunnen delen. Profiel update wordt naar uw contacten verzonden."; -/* No comment provided by engineer. */ -"Add contact" = "Contact toevoegen"; - -/* No comment provided by engineer. */ -"Add preset servers" = "Vooraf ingestelde servers toevoegen"; - /* No comment provided by engineer. */ "Add profile" = "Profiel toevoegen"; @@ -574,6 +553,9 @@ /* No comment provided by engineer. */ "Answer call" = "Beantwoord oproep"; +/* No comment provided by engineer. */ +"Anybody can host servers." = "Iedereen kan servers hosten."; + /* No comment provided by engineer. */ "App build: %@" = "App build: %@"; @@ -817,7 +799,8 @@ /* No comment provided by engineer. */ "Can't message member" = "Kan geen bericht sturen naar lid"; -/* alert button */ +/* alert action + alert button */ "Cancel" = "Annuleren"; /* No comment provided by engineer. */ @@ -938,7 +921,7 @@ /* No comment provided by engineer. */ "Chats" = "Chats"; -/* No comment provided by engineer. */ +/* alert title */ "Check server address and try again." = "Controleer het server adres en probeer het opnieuw."; /* No comment provided by engineer. */ @@ -1001,9 +984,6 @@ /* No comment provided by engineer. */ "Configure ICE servers" = "ICE servers configureren"; -/* No comment provided by engineer. */ -"Configured %@ servers" = "%@ servers geconfigureerd"; - /* No comment provided by engineer. */ "Confirm" = "Bevestigen"; @@ -1232,9 +1212,6 @@ /* No comment provided by engineer. */ "Create a group using a random profile." = "Maak een groep met een willekeurig profiel."; -/* No comment provided by engineer. */ -"Create an address to let people connect with you." = "Maak een adres aan zodat mensen contact met je kunnen opnemen."; - /* server test step */ "Create file" = "Bestand maken"; @@ -1397,7 +1374,8 @@ /* No comment provided by engineer. */ "default (yes)" = "standaard (ja)"; -/* chat item action +/* alert action + chat item action swipe action */ "Delete" = "Verwijderen"; @@ -1996,9 +1974,6 @@ /* No comment provided by engineer. */ "Error joining group" = "Fout bij lid worden van groep"; -/* No comment provided by engineer. */ -"Error loading %@ servers" = "Fout bij het laden van %@ servers"; - /* No comment provided by engineer. */ "Error migrating settings" = "Fout bij migreren van instellingen"; @@ -2020,9 +1995,6 @@ /* No comment provided by engineer. */ "Error resetting statistics" = "Fout bij het resetten van statistieken"; -/* No comment provided by engineer. */ -"Error saving %@ servers" = "Fout bij opslaan van %@ servers"; - /* No comment provided by engineer. */ "Error saving group profile" = "Fout bij opslaan van groep profiel"; @@ -2425,9 +2397,6 @@ /* time unit */ "hours" = "uren"; -/* No comment provided by engineer. */ -"How it works" = "Hoe het werkt"; - /* No comment provided by engineer. */ "How SimpleX works" = "Hoe SimpleX werkt"; @@ -2570,10 +2539,10 @@ "Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)" = "Installeer [SimpleX Chat voor terminal](https://github.com/simplex-chat/simplex-chat)"; /* No comment provided by engineer. */ -"Instant push notifications will be hidden!\n" = "Directe push meldingen worden verborgen!\n"; +"Instant" = "Direct"; /* No comment provided by engineer. */ -"Instant" = "Direct"; +"Instant push notifications will be hidden!\n" = "Directe push meldingen worden verborgen!\n"; /* No comment provided by engineer. */ "Interface" = "Interface"; @@ -2611,7 +2580,7 @@ /* No comment provided by engineer. */ "Invalid response" = "Ongeldig antwoord"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid server address!" = "Ongeldig server adres!"; /* item status text */ @@ -2716,7 +2685,7 @@ /* No comment provided by engineer. */ "Joining group" = "Deel nemen aan groep"; -/* No comment provided by engineer. */ +/* alert action */ "Keep" = "Bewaar"; /* No comment provided by engineer. */ @@ -2725,7 +2694,7 @@ /* 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"; -/* No comment provided by engineer. */ +/* alert title */ "Keep unused invitation?" = "Ongebruikte uitnodiging bewaren?"; /* No comment provided by engineer. */ @@ -2782,9 +2751,6 @@ /* No comment provided by engineer. */ "Live messages" = "Live berichten"; -/* No comment provided by engineer. */ -"No push server" = "Lokaal"; - /* No comment provided by engineer. */ "Local name" = "Lokale naam"; @@ -2797,24 +2763,15 @@ /* No comment provided by engineer. */ "Lock mode" = "Vergrendeling modus"; -/* No comment provided by engineer. */ -"Make a private connection" = "Maak een privéverbinding"; - /* No comment provided by engineer. */ "Make one message disappear" = "Eén bericht laten verdwijnen"; /* No comment provided by engineer. */ "Make profile private!" = "Profiel privé maken!"; -/* No comment provided by engineer. */ -"Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." = "Zorg ervoor dat %@ server adressen de juiste indeling hebben, regel gescheiden zijn en niet gedupliceerd zijn (%@)."; - /* No comment provided by engineer. */ "Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "Zorg ervoor dat WebRTC ICE server adressen de juiste indeling hebben, regel gescheiden zijn en niet gedupliceerd zijn."; -/* No comment provided by engineer. */ -"Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" = "Veel mensen vroegen: *als SimpleX geen gebruikers-ID's heeft, hoe kan het dan berichten bezorgen?*"; - /* No comment provided by engineer. */ "Mark deleted for everyone" = "Markeer verwijderd voor iedereen"; @@ -3154,12 +3111,18 @@ /* No comment provided by engineer. */ "No permission to record voice message" = "Geen toestemming om spraakbericht op te nemen"; +/* No comment provided by engineer. */ +"No push server" = "Lokaal"; + /* No comment provided by engineer. */ "No received or sent files" = "Geen ontvangen of verzonden bestanden"; /* copied message info in history */ "no text" = "geen tekst"; +/* No comment provided by engineer. */ +"No user identifiers." = "Geen gebruikers-ID's."; + /* No comment provided by engineer. */ "Not compatible!" = "Niet compatibel!"; @@ -3282,18 +3245,9 @@ /* authentication reason */ "Open migration to another device" = "Open de migratie naar een ander apparaat"; -/* No comment provided by engineer. */ -"Open server settings" = "Server instellingen openen"; - /* No comment provided by engineer. */ "Open Settings" = "Open instellingen"; -/* authentication reason */ -"Open user profiles" = "Gebruikers profielen openen"; - -/* No comment provided by engineer. */ -"Anybody can host servers." = "Iedereen kan servers hosten."; - /* No comment provided by engineer. */ "Opening app…" = "App openen…"; @@ -3315,9 +3269,6 @@ /* No comment provided by engineer. */ "Other" = "Ander"; -/* No comment provided by engineer. */ -"Other %@ servers" = "Andere %@ servers"; - /* No comment provided by engineer. */ "other errors" = "overige fouten"; @@ -3372,9 +3323,6 @@ /* No comment provided by engineer. */ "Pending" = "in behandeling"; -/* No comment provided by engineer. */ -"You decide who can connect." = "Jij bepaalt wie er verbinding mag maken."; - /* No comment provided by engineer. */ "Periodic" = "Periodiek"; @@ -3453,9 +3401,6 @@ /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Bewaar het laatste berichtconcept, met bijlagen."; -/* No comment provided by engineer. */ -"Preset server" = "Vooraf ingestelde server"; - /* No comment provided by engineer. */ "Preset server address" = "Vooraf ingesteld server adres"; @@ -3504,7 +3449,7 @@ /* No comment provided by engineer. */ "Profile theme" = "Profiel thema"; -/* No comment provided by engineer. */ +/* alert message */ "Profile update will be sent to your contacts." = "Profiel update wordt naar uw contacten verzonden."; /* No comment provided by engineer. */ @@ -3589,10 +3534,10 @@ "Read more" = "Lees meer"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." = "Lees meer in de [Gebruikershandleiding](https://simplex.chat/docs/guide/app-settings.html#uw-simplex-contactadres)."; +"Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." = "Lees meer in de [Gebruikershandleiding](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)."; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." = "Lees meer in de [Gebruikershandleiding](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)."; +"Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." = "Lees meer in de [Gebruikershandleiding](https://simplex.chat/docs/guide/app-settings.html#uw-simplex-contactadres)."; /* No comment provided by engineer. */ "Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." = "Lees meer in de [Gebruikershandleiding](https://simplex.chat/docs/guide/readme.html#connect-to-friends)."; @@ -3600,9 +3545,6 @@ /* No comment provided by engineer. */ "Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." = "Lees meer in onze [GitHub-repository](https://github.com/simplex-chat/simplex-chat#readme)."; -/* No comment provided by engineer. */ -"Read more in our GitHub repository." = "Lees meer in onze GitHub repository."; - /* No comment provided by engineer. */ "Receipts are disabled" = "Bevestigingen zijn uitgeschakeld"; @@ -3875,7 +3817,7 @@ /* No comment provided by engineer. */ "Save servers" = "Servers opslaan"; -/* No comment provided by engineer. */ +/* alert title */ "Save servers?" = "Servers opslaan?"; /* No comment provided by engineer. */ @@ -4028,9 +3970,6 @@ /* No comment provided by engineer. */ "Send notifications" = "Meldingen verzenden"; -/* No comment provided by engineer. */ -"Send notifications:" = "Meldingen verzenden:"; - /* No comment provided by engineer. */ "Send questions and ideas" = "Stuur vragen en ideeën"; @@ -4193,7 +4132,8 @@ /* No comment provided by engineer. */ "Shape profile images" = "Vorm profiel afbeeldingen"; -/* chat item action */ +/* alert action + chat item action */ "Share" = "Deel"; /* No comment provided by engineer. */ @@ -4202,7 +4142,7 @@ /* No comment provided by engineer. */ "Share address" = "Adres delen"; -/* No comment provided by engineer. */ +/* alert title */ "Share address with contacts?" = "Adres delen met contacten?"; /* No comment provided by engineer. */ @@ -4385,10 +4325,10 @@ /* No comment provided by engineer. */ "Stop sending file?" = "Bestand verzenden stoppen?"; -/* No comment provided by engineer. */ +/* alert action */ "Stop sharing" = "Stop met delen"; -/* No comment provided by engineer. */ +/* alert title */ "Stop sharing address?" = "Stop met het delen van adres?"; /* authentication reason */ @@ -4484,7 +4424,7 @@ /* No comment provided by engineer. */ "Test servers" = "Servers testen"; -/* No comment provided by engineer. */ +/* alert title */ "Tests failed!" = "Testen mislukt!"; /* No comment provided by engineer. */ @@ -4496,9 +4436,6 @@ /* No comment provided by engineer. */ "Thanks to the users – contribute via Weblate!" = "Dank aan de gebruikers – draag bij via Weblate!"; -/* No comment provided by engineer. */ -"No user identifiers." = "Geen gebruikers-ID\'s."; - /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "De app kan u op de hoogte stellen wanneer u berichten of contact verzoeken ontvangt - open de instellingen om dit in te schakelen."; @@ -4523,6 +4460,9 @@ /* No comment provided by engineer. */ "The encryption is working and the new encryption agreement is not required. It may result in connection errors!" = "De versleuteling werkt en de nieuwe versleutelingsovereenkomst is niet vereist. Dit kan leiden tot verbindingsfouten!"; +/* No comment provided by engineer. */ +"The future of messaging" = "De volgende generatie privéberichten"; + /* No comment provided by engineer. */ "The hash of the previous message is different." = "De hash van het vorige bericht is anders."; @@ -4541,9 +4481,6 @@ /* No comment provided by engineer. */ "The messages will be marked as moderated for all members." = "De berichten worden voor alle leden als gemodereerd gemarkeerd."; -/* No comment provided by engineer. */ -"The future of messaging" = "De volgende generatie privéberichten"; - /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "De oude database is niet verwijderd tijdens de migratie, deze kan worden verwijderd."; @@ -4631,9 +4568,6 @@ /* No comment provided by engineer. */ "To make a new connection" = "Om een nieuwe verbinding te maken"; -/* No comment provided by engineer. */ -"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Om de privacy te beschermen, heeft SimpleX in plaats van gebruikers-ID's die door alle andere platforms worden gebruikt, ID's voor berichten wachtrijen, afzonderlijk voor elk van uw contacten."; - /* No comment provided by engineer. */ "To protect timezone, image/voice files use UTC." = "Om de tijdzone te beschermen, gebruiken afbeeldings-/spraakbestanden UTC."; @@ -4643,6 +4577,9 @@ /* No comment provided by engineer. */ "To protect your IP address, private routing uses your SMP servers to deliver messages." = "Om uw IP-adres te beschermen, gebruikt privéroutering uw SMP-servers om berichten te bezorgen."; +/* No comment provided by engineer. */ +"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Om de privacy te beschermen, heeft SimpleX in plaats van gebruikers-ID's die door alle andere platforms worden gebruikt, ID's voor berichten wachtrijen, afzonderlijk voor elk van uw contacten."; + /* No comment provided by engineer. */ "To record speech please grant permission to use Microphone." = "Geef toestemming om de microfoon te gebruiken om spraak op te nemen."; @@ -5030,9 +4967,6 @@ /* No comment provided by engineer. */ "when IP hidden" = "wanneer IP verborgen is"; -/* No comment provided by engineer. */ -"When people request to connect, you can accept or reject it." = "Wanneer mensen vragen om verbinding te maken, kunt u dit accepteren of weigeren."; - /* No comment provided by engineer. */ "When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "Wanneer je een incognito profiel met iemand deelt, wordt dit profiel gebruikt voor de groepen waarvoor ze je uitnodigen."; @@ -5174,9 +5108,6 @@ /* No comment provided by engineer. */ "You can share this address with your contacts to let them connect with **%@**." = "U kunt dit adres delen met uw contacten om hen verbinding te laten maken met **%@**."; -/* No comment provided by engineer. */ -"You can share your address as a link or QR code - anybody can connect to you." = "U kunt uw adres delen als een link of als een QR-code. Iedereen kan verbinding met u maken."; - /* No comment provided by engineer. */ "You can start chat via app Settings / Database or by restarting the app" = "U kunt de chat starten via app Instellingen / Database of door de app opnieuw op te starten"; @@ -5189,7 +5120,7 @@ /* No comment provided by engineer. */ "You can use markdown to format messages:" = "U kunt markdown gebruiken voor opmaak in berichten:"; -/* No comment provided by engineer. */ +/* alert message */ "You can view invitation link again in connection details." = "U kunt de uitnodigingslink opnieuw bekijken in de verbindingsdetails."; /* No comment provided by engineer. */ @@ -5210,6 +5141,9 @@ /* No comment provided by engineer. */ "You could not be verified; please try again." = "U kon niet worden geverifieerd; probeer het opnieuw."; +/* No comment provided by engineer. */ +"You decide who can connect." = "Jij bepaalt wie er verbinding mag maken."; + /* No comment provided by engineer. */ "You have already requested connection via this address!" = "U heeft al een verbinding aangevraagd via dit adres!"; @@ -5300,9 +5234,6 @@ /* No comment provided by engineer. */ "You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed" = "Je gebruikt een incognito profiel voor deze groep. Om te voorkomen dat je je hoofdprofiel deelt, is het niet toegestaan om contacten uit te nodigen"; -/* No comment provided by engineer. */ -"Your %@ servers" = "Uw %@ servers"; - /* No comment provided by engineer. */ "Your calls" = "Uw oproepen"; @@ -5366,9 +5297,6 @@ /* No comment provided by engineer. */ "Your random profile" = "Je willekeurige profiel"; -/* No comment provided by engineer. */ -"Your server" = "Uw server"; - /* No comment provided by engineer. */ "Your server address" = "Uw server adres"; @@ -5381,6 +5309,3 @@ /* No comment provided by engineer. */ "Your SMP servers" = "Uw SMP servers"; -/* No comment provided by engineer. */ -"Your XFTP servers" = "Uw XFTP servers"; - diff --git a/apps/ios/pl.lproj/Localizable.strings b/apps/ios/pl.lproj/Localizable.strings index 77c724a6b1..644ba366f6 100644 --- a/apps/ios/pl.lproj/Localizable.strings +++ b/apps/ios/pl.lproj/Localizable.strings @@ -1,15 +1,6 @@ /* No comment provided by engineer. */ "\n" = "\n"; -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - /* No comment provided by engineer. */ " (" = " ("; @@ -337,12 +328,6 @@ /* No comment provided by engineer. */ "Abort changing address?" = "Przerwać zmianę adresu?"; -/* No comment provided by engineer. */ -"About SimpleX" = "O SimpleX"; - -/* No comment provided by engineer. */ -"About SimpleX address" = "O adresie SimpleX"; - /* No comment provided by engineer. */ "About SimpleX Chat" = "O SimpleX Chat"; @@ -382,12 +367,6 @@ /* No comment provided by engineer. */ "Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." = "Dodaj adres do swojego profilu, aby Twoje kontakty mogły go udostępnić innym osobom. Aktualizacja profilu zostanie wysłana do Twoich kontaktów."; -/* No comment provided by engineer. */ -"Add contact" = "Dodaj kontakt"; - -/* No comment provided by engineer. */ -"Add preset servers" = "Dodaj gotowe serwery"; - /* No comment provided by engineer. */ "Add profile" = "Dodaj profil"; @@ -574,6 +553,9 @@ /* No comment provided by engineer. */ "Answer call" = "Odbierz połączenie"; +/* No comment provided by engineer. */ +"Anybody can host servers." = "Każdy może hostować serwery."; + /* No comment provided by engineer. */ "App build: %@" = "Kompilacja aplikacji: %@"; @@ -802,7 +784,8 @@ /* No comment provided by engineer. */ "Can't message member" = "Nie można wysłać wiadomości do członka"; -/* alert button */ +/* alert action + alert button */ "Cancel" = "Anuluj"; /* No comment provided by engineer. */ @@ -923,7 +906,7 @@ /* No comment provided by engineer. */ "Chats" = "Czaty"; -/* No comment provided by engineer. */ +/* alert title */ "Check server address and try again." = "Sprawdź adres serwera i spróbuj ponownie."; /* No comment provided by engineer. */ @@ -986,9 +969,6 @@ /* No comment provided by engineer. */ "Configure ICE servers" = "Skonfiguruj serwery ICE"; -/* No comment provided by engineer. */ -"Configured %@ servers" = "Skonfigurowano %@ serwerów"; - /* No comment provided by engineer. */ "Confirm" = "Potwierdź"; @@ -1217,9 +1197,6 @@ /* No comment provided by engineer. */ "Create a group using a random profile." = "Utwórz grupę używając losowego profilu."; -/* No comment provided by engineer. */ -"Create an address to let people connect with you." = "Utwórz adres, aby ludzie mogli się z Tobą połączyć."; - /* server test step */ "Create file" = "Utwórz plik"; @@ -1379,7 +1356,8 @@ /* No comment provided by engineer. */ "default (yes)" = "domyślnie (tak)"; -/* chat item action +/* alert action + chat item action swipe action */ "Delete" = "Usuń"; @@ -1975,9 +1953,6 @@ /* No comment provided by engineer. */ "Error joining group" = "Błąd dołączenia do grupy"; -/* No comment provided by engineer. */ -"Error loading %@ servers" = "Błąd ładowania %@ serwerów"; - /* No comment provided by engineer. */ "Error migrating settings" = "Błąd migracji ustawień"; @@ -1999,9 +1974,6 @@ /* No comment provided by engineer. */ "Error resetting statistics" = "Błąd resetowania statystyk"; -/* No comment provided by engineer. */ -"Error saving %@ servers" = "Błąd zapisu %@ serwerów"; - /* No comment provided by engineer. */ "Error saving group profile" = "Błąd zapisu profilu grupy"; @@ -2401,9 +2373,6 @@ /* time unit */ "hours" = "godziny"; -/* No comment provided by engineer. */ -"How it works" = "Jak to działa"; - /* No comment provided by engineer. */ "How SimpleX works" = "Jak działa SimpleX"; @@ -2543,10 +2512,10 @@ "Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)" = "Zainstaluj [SimpleX Chat na terminal](https://github.com/simplex-chat/simplex-chat)"; /* No comment provided by engineer. */ -"Instant push notifications will be hidden!\n" = "Natychmiastowe powiadomienia push będą ukryte!\n"; +"Instant" = "Natychmiastowo"; /* No comment provided by engineer. */ -"Instant" = "Natychmiastowo"; +"Instant push notifications will be hidden!\n" = "Natychmiastowe powiadomienia push będą ukryte!\n"; /* No comment provided by engineer. */ "Interface" = "Interfejs"; @@ -2584,7 +2553,7 @@ /* No comment provided by engineer. */ "Invalid response" = "Nieprawidłowa odpowiedź"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid server address!" = "Nieprawidłowy adres serwera!"; /* item status text */ @@ -2689,7 +2658,7 @@ /* No comment provided by engineer. */ "Joining group" = "Dołączanie do grupy"; -/* No comment provided by engineer. */ +/* alert action */ "Keep" = "Zachowaj"; /* No comment provided by engineer. */ @@ -2698,7 +2667,7 @@ /* No comment provided by engineer. */ "Keep the app open to use it from desktop" = "Zostaw aplikację otwartą i używaj ją z komputera"; -/* No comment provided by engineer. */ +/* alert title */ "Keep unused invitation?" = "Zachować nieużyte zaproszenie?"; /* No comment provided by engineer. */ @@ -2755,9 +2724,6 @@ /* No comment provided by engineer. */ "Live messages" = "Wiadomości na żywo"; -/* No comment provided by engineer. */ -"No push server" = "Lokalnie"; - /* No comment provided by engineer. */ "Local name" = "Nazwa lokalna"; @@ -2770,24 +2736,15 @@ /* No comment provided by engineer. */ "Lock mode" = "Tryb blokady"; -/* No comment provided by engineer. */ -"Make a private connection" = "Nawiąż prywatne połączenie"; - /* No comment provided by engineer. */ "Make one message disappear" = "Spraw, aby jedna wiadomość zniknęła"; /* No comment provided by engineer. */ "Make profile private!" = "Ustaw profil jako prywatny!"; -/* No comment provided by engineer. */ -"Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." = "Upewnij się, że adresy serwerów %@ są w poprawnym formacie, rozdzielone liniami i nie są zduplikowane (%@)."; - /* No comment provided by engineer. */ "Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "Upewnij się, że adresy serwerów WebRTC ICE są w poprawnym formacie, rozdzielone liniami i nie są zduplikowane."; -/* No comment provided by engineer. */ -"Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" = "Wiele osób pytało: *jeśli SimpleX nie ma identyfikatora użytkownika, jak może dostarczać wiadomości?*"; - /* No comment provided by engineer. */ "Mark deleted for everyone" = "Oznacz jako usunięty dla wszystkich"; @@ -3127,12 +3084,18 @@ /* No comment provided by engineer. */ "No permission to record voice message" = "Brak uprawnień do nagrywania wiadomości głosowej"; +/* No comment provided by engineer. */ +"No push server" = "Lokalnie"; + /* No comment provided by engineer. */ "No received or sent files" = "Brak odebranych lub wysłanych plików"; /* copied message info in history */ "no text" = "brak tekstu"; +/* No comment provided by engineer. */ +"No user identifiers." = "Brak identyfikatorów użytkownika."; + /* No comment provided by engineer. */ "Not compatible!" = "Nie kompatybilny!"; @@ -3255,18 +3218,9 @@ /* authentication reason */ "Open migration to another device" = "Otwórz migrację na innym urządzeniu"; -/* No comment provided by engineer. */ -"Open server settings" = "Otwórz ustawienia serwera"; - /* No comment provided by engineer. */ "Open Settings" = "Otwórz Ustawienia"; -/* authentication reason */ -"Open user profiles" = "Otwórz profile użytkownika"; - -/* No comment provided by engineer. */ -"Anybody can host servers." = "Każdy może hostować serwery."; - /* No comment provided by engineer. */ "Opening app…" = "Otwieranie aplikacji…"; @@ -3288,9 +3242,6 @@ /* No comment provided by engineer. */ "Other" = "Inne"; -/* No comment provided by engineer. */ -"Other %@ servers" = "Inne %@ serwery"; - /* No comment provided by engineer. */ "other errors" = "inne błędy"; @@ -3345,9 +3296,6 @@ /* No comment provided by engineer. */ "Pending" = "Oczekujące"; -/* No comment provided by engineer. */ -"You decide who can connect." = "Ty decydujesz, kto może się połączyć."; - /* No comment provided by engineer. */ "Periodic" = "Okresowo"; @@ -3426,9 +3374,6 @@ /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Zachowaj ostatnią wersję roboczą wiadomości wraz z załącznikami."; -/* No comment provided by engineer. */ -"Preset server" = "Wstępnie ustawiony serwer"; - /* No comment provided by engineer. */ "Preset server address" = "Wstępnie ustawiony adres serwera"; @@ -3477,7 +3422,7 @@ /* No comment provided by engineer. */ "Profile theme" = "Motyw profilu"; -/* No comment provided by engineer. */ +/* alert message */ "Profile update will be sent to your contacts." = "Aktualizacja profilu zostanie wysłana do Twoich kontaktów."; /* No comment provided by engineer. */ @@ -3562,10 +3507,10 @@ "Read more" = "Przeczytaj więcej"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." = "Przeczytaj więcej w [Podręczniku Użytkownika](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)."; +"Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." = "Przeczytaj więcej w [Poradniku Użytkownika](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)."; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." = "Przeczytaj więcej w [Poradniku Użytkownika](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)."; +"Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." = "Przeczytaj więcej w [Podręczniku Użytkownika](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)."; /* No comment provided by engineer. */ "Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." = "Przeczytaj więcej w [Podręczniku Użytkownika](https://simplex.chat/docs/guide/readme.html#connect-to-friends)."; @@ -3573,9 +3518,6 @@ /* No comment provided by engineer. */ "Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." = "Przeczytaj więcej na naszym [repozytorium GitHub](https://github.com/simplex-chat/simplex-chat#readme)."; -/* No comment provided by engineer. */ -"Read more in our GitHub repository." = "Przeczytaj więcej na naszym repozytorium GitHub."; - /* No comment provided by engineer. */ "Receipts are disabled" = "Potwierdzenia są wyłączone"; @@ -3848,7 +3790,7 @@ /* No comment provided by engineer. */ "Save servers" = "Zapisz serwery"; -/* No comment provided by engineer. */ +/* alert title */ "Save servers?" = "Zapisać serwery?"; /* No comment provided by engineer. */ @@ -4001,9 +3943,6 @@ /* No comment provided by engineer. */ "Send notifications" = "Wyślij powiadomienia"; -/* No comment provided by engineer. */ -"Send notifications:" = "Wyślij powiadomienia:"; - /* No comment provided by engineer. */ "Send questions and ideas" = "Wyślij pytania i pomysły"; @@ -4166,7 +4105,8 @@ /* No comment provided by engineer. */ "Shape profile images" = "Kształtuj obrazy profilowe"; -/* chat item action */ +/* alert action + chat item action */ "Share" = "Udostępnij"; /* No comment provided by engineer. */ @@ -4175,7 +4115,7 @@ /* No comment provided by engineer. */ "Share address" = "Udostępnij adres"; -/* No comment provided by engineer. */ +/* alert title */ "Share address with contacts?" = "Udostępnić adres kontaktom?"; /* No comment provided by engineer. */ @@ -4355,10 +4295,10 @@ /* No comment provided by engineer. */ "Stop sending file?" = "Przestać wysyłać plik?"; -/* No comment provided by engineer. */ +/* alert action */ "Stop sharing" = "Przestań udostępniać"; -/* No comment provided by engineer. */ +/* alert title */ "Stop sharing address?" = "Przestać udostępniać adres?"; /* authentication reason */ @@ -4448,7 +4388,7 @@ /* No comment provided by engineer. */ "Test servers" = "Przetestuj serwery"; -/* No comment provided by engineer. */ +/* alert title */ "Tests failed!" = "Testy nie powiodły się!"; /* No comment provided by engineer. */ @@ -4460,9 +4400,6 @@ /* No comment provided by engineer. */ "Thanks to the users – contribute via Weblate!" = "Podziękowania dla użytkowników - wkład za pośrednictwem Weblate!"; -/* No comment provided by engineer. */ -"No user identifiers." = "Brak identyfikatorów użytkownika."; - /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "Aplikacja może powiadamiać Cię, gdy otrzymujesz wiadomości lub prośby o kontakt — otwórz ustawienia, aby włączyć."; @@ -4487,6 +4424,9 @@ /* No comment provided by engineer. */ "The encryption is working and the new encryption agreement is not required. It may result in connection errors!" = "Szyfrowanie działa, a nowe uzgodnienie szyfrowania nie jest wymagane. Może to spowodować błędy w połączeniu!"; +/* No comment provided by engineer. */ +"The future of messaging" = "Następna generacja prywatnych wiadomości"; + /* No comment provided by engineer. */ "The hash of the previous message is different." = "Hash poprzedniej wiadomości jest inny."; @@ -4505,9 +4445,6 @@ /* 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 future of messaging" = "Następna generacja prywatnych wiadomości"; - /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "Stara baza danych nie została usunięta podczas migracji, można ją usunąć."; @@ -4595,9 +4532,6 @@ /* No comment provided by engineer. */ "To make a new connection" = "Aby nawiązać nowe połączenie"; -/* No comment provided by engineer. */ -"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Aby chronić prywatność, zamiast identyfikatorów użytkowników używanych przez wszystkie inne platformy, SimpleX ma identyfikatory dla kolejek wiadomości, oddzielne dla każdego z Twoich kontaktów."; - /* No comment provided by engineer. */ "To protect timezone, image/voice files use UTC." = "Aby chronić strefę czasową, pliki obrazów/głosów używają UTC."; @@ -4607,6 +4541,9 @@ /* No comment provided by engineer. */ "To protect your IP address, private routing uses your SMP servers to deliver messages." = "Aby chronić Twój adres IP, prywatne trasowanie używa Twoich serwerów SMP, aby dostarczyć wiadomości."; +/* No comment provided by engineer. */ +"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Aby chronić prywatność, zamiast identyfikatorów użytkowników używanych przez wszystkie inne platformy, SimpleX ma identyfikatory dla kolejek wiadomości, oddzielne dla każdego z Twoich kontaktów."; + /* No comment provided by engineer. */ "To record speech please grant permission to use Microphone." = "Aby nagrać rozmowę, proszę zezwolić na użycie Mikrofonu."; @@ -4994,9 +4931,6 @@ /* No comment provided by engineer. */ "when IP hidden" = "gdy IP ukryty"; -/* No comment provided by engineer. */ -"When people request to connect, you can accept or reject it." = "Kiedy ludzie proszą o połączenie, możesz je zaakceptować lub odrzucić."; - /* No comment provided by engineer. */ "When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "Gdy udostępnisz komuś profil incognito, będzie on używany w grupach, do których Cię zaprosi."; @@ -5138,9 +5072,6 @@ /* No comment provided by engineer. */ "You can share this address with your contacts to let them connect with **%@**." = "Możesz udostępnić ten adres Twoim kontaktom, aby umożliwić im połączenie z **%@**."; -/* No comment provided by engineer. */ -"You can share your address as a link or QR code - anybody can connect to you." = "Możesz udostępnić swój adres jako link lub jako kod QR - każdy będzie mógł się z Tobą połączyć."; - /* 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"; @@ -5153,7 +5084,7 @@ /* No comment provided by engineer. */ "You can use markdown to format messages:" = "Możesz używać markdown do formatowania wiadomości:"; -/* No comment provided by engineer. */ +/* alert message */ "You can view invitation link again in connection details." = "Możesz zobaczyć link zaproszenia ponownie w szczegółach połączenia."; /* No comment provided by engineer. */ @@ -5174,6 +5105,9 @@ /* No comment provided by engineer. */ "You could not be verified; please try again." = "Nie można zweryfikować użytkownika; proszę spróbować ponownie."; +/* No comment provided by engineer. */ +"You decide who can connect." = "Ty decydujesz, kto może się połączyć."; + /* No comment provided by engineer. */ "You have already requested connection via this address!" = "Już prosiłeś o połączenie na ten adres!"; @@ -5264,9 +5198,6 @@ /* No comment provided by engineer. */ "You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed" = "Używasz profilu incognito dla tej grupy - aby zapobiec udostępnianiu głównego profilu zapraszanie kontaktów jest zabronione"; -/* No comment provided by engineer. */ -"Your %@ servers" = "Twoje serwery %@"; - /* No comment provided by engineer. */ "Your calls" = "Twoje połączenia"; @@ -5330,9 +5261,6 @@ /* No comment provided by engineer. */ "Your random profile" = "Twój losowy profil"; -/* No comment provided by engineer. */ -"Your server" = "Twój serwer"; - /* No comment provided by engineer. */ "Your server address" = "Twój adres serwera"; @@ -5345,6 +5273,3 @@ /* No comment provided by engineer. */ "Your SMP servers" = "Twoje serwery SMP"; -/* No comment provided by engineer. */ -"Your XFTP servers" = "Twoje serwery XFTP"; - diff --git a/apps/ios/ru.lproj/Localizable.strings b/apps/ios/ru.lproj/Localizable.strings index 272484ac47..536bbb62a8 100644 --- a/apps/ios/ru.lproj/Localizable.strings +++ b/apps/ios/ru.lproj/Localizable.strings @@ -1,15 +1,6 @@ /* No comment provided by engineer. */ "\n" = "\n"; -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - /* No comment provided by engineer. */ " (" = " ("; @@ -337,12 +328,6 @@ /* No comment provided by engineer. */ "Abort changing address?" = "Прекратить изменение адреса?"; -/* No comment provided by engineer. */ -"About SimpleX" = "О SimpleX"; - -/* No comment provided by engineer. */ -"About SimpleX address" = "Об адресе SimpleX"; - /* No comment provided by engineer. */ "About SimpleX Chat" = "Информация о SimpleX Chat"; @@ -382,12 +367,6 @@ /* No comment provided by engineer. */ "Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." = "Добавьте адрес в свой профиль, чтобы Ваши контакты могли поделиться им. Профиль будет отправлен Вашим контактам."; -/* No comment provided by engineer. */ -"Add contact" = "Добавить контакт"; - -/* No comment provided by engineer. */ -"Add preset servers" = "Добавить серверы по умолчанию"; - /* No comment provided by engineer. */ "Add profile" = "Добавить профиль"; @@ -460,6 +439,9 @@ /* feature role */ "all members" = "все члены"; +/* No comment provided by engineer. */ +"All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages." = "Все сообщения и файлы отправляются с **end-to-end шифрованием**, с постквантовой безопасностью в прямых разговорах."; + /* No comment provided by engineer. */ "All messages will be deleted - this cannot be undone!" = "Все сообщения будут удалены - это нельзя отменить!"; @@ -574,6 +556,9 @@ /* No comment provided by engineer. */ "Answer call" = "Принять звонок"; +/* No comment provided by engineer. */ +"Anybody can host servers." = "Кто угодно может запустить сервер."; + /* No comment provided by engineer. */ "App build: %@" = "Сборка приложения: %@"; @@ -817,7 +802,8 @@ /* No comment provided by engineer. */ "Can't message member" = "Не удается написать члену группы"; -/* alert button */ +/* alert action + alert button */ "Cancel" = "Отменить"; /* No comment provided by engineer. */ @@ -938,7 +924,7 @@ /* No comment provided by engineer. */ "Chats" = "Чаты"; -/* No comment provided by engineer. */ +/* alert title */ "Check server address and try again." = "Проверьте адрес сервера и попробуйте снова."; /* No comment provided by engineer. */ @@ -1001,9 +987,6 @@ /* No comment provided by engineer. */ "Configure ICE servers" = "Настройка ICE серверов"; -/* No comment provided by engineer. */ -"Configured %@ servers" = "Настроенные %@ серверы"; - /* No comment provided by engineer. */ "Confirm" = "Подтвердить"; @@ -1232,9 +1215,6 @@ /* No comment provided by engineer. */ "Create a group using a random profile." = "Создайте группу, используя случайный профиль."; -/* No comment provided by engineer. */ -"Create an address to let people connect with you." = "Создайте адрес, чтобы можно было соединиться с вами."; - /* server test step */ "Create file" = "Создание файла"; @@ -1397,7 +1377,8 @@ /* No comment provided by engineer. */ "default (yes)" = "по умолчанию (да)"; -/* chat item action +/* alert action + chat item action swipe action */ "Delete" = "Удалить"; @@ -1996,9 +1977,6 @@ /* No comment provided by engineer. */ "Error joining group" = "Ошибка при вступлении в группу"; -/* No comment provided by engineer. */ -"Error loading %@ servers" = "Ошибка загрузки %@ серверов"; - /* No comment provided by engineer. */ "Error migrating settings" = "Ошибка миграции настроек"; @@ -2020,9 +1998,6 @@ /* No comment provided by engineer. */ "Error resetting statistics" = "Ошибка сброса статистики"; -/* No comment provided by engineer. */ -"Error saving %@ servers" = "Ошибка при сохранении %@ серверов"; - /* No comment provided by engineer. */ "Error saving group profile" = "Ошибка при сохранении профиля группы"; @@ -2425,9 +2400,6 @@ /* time unit */ "hours" = "часов"; -/* No comment provided by engineer. */ -"How it works" = "Как это работает"; - /* No comment provided by engineer. */ "How SimpleX works" = "Как SimpleX работает"; @@ -2570,10 +2542,10 @@ "Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)" = "[SimpleX Chat для терминала](https://github.com/simplex-chat/simplex-chat)"; /* No comment provided by engineer. */ -"Instant push notifications will be hidden!\n" = "Мгновенные уведомления будут скрыты!\n"; +"Instant" = "Мгновенно"; /* No comment provided by engineer. */ -"Instant" = "Мгновенно"; +"Instant push notifications will be hidden!\n" = "Мгновенные уведомления будут скрыты!\n"; /* No comment provided by engineer. */ "Interface" = "Интерфейс"; @@ -2611,7 +2583,7 @@ /* No comment provided by engineer. */ "Invalid response" = "Ошибка ответа"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid server address!" = "Ошибка в адресе сервера!"; /* item status text */ @@ -2716,7 +2688,7 @@ /* No comment provided by engineer. */ "Joining group" = "Вступление в группу"; -/* No comment provided by engineer. */ +/* alert action */ "Keep" = "Оставить"; /* No comment provided by engineer. */ @@ -2725,7 +2697,7 @@ /* No comment provided by engineer. */ "Keep the app open to use it from desktop" = "Оставьте приложение открытым, чтобы использовать его с компьютера"; -/* No comment provided by engineer. */ +/* alert title */ "Keep unused invitation?" = "Оставить неиспользованное приглашение?"; /* No comment provided by engineer. */ @@ -2782,9 +2754,6 @@ /* No comment provided by engineer. */ "Live messages" = "\"Живые\" сообщения"; -/* No comment provided by engineer. */ -"No push server" = "Локальные"; - /* No comment provided by engineer. */ "Local name" = "Локальное имя"; @@ -2797,24 +2766,15 @@ /* No comment provided by engineer. */ "Lock mode" = "Режим блокировки"; -/* No comment provided by engineer. */ -"Make a private connection" = "Добавьте контакт"; - /* No comment provided by engineer. */ "Make one message disappear" = "Одно исчезающее сообщение"; /* No comment provided by engineer. */ "Make profile private!" = "Сделайте профиль скрытым!"; -/* No comment provided by engineer. */ -"Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." = "Пожалуйста, проверьте, что адреса %@ серверов имеют правильный формат, каждый адрес на отдельной строке и не повторяется (%@)."; - /* No comment provided by engineer. */ "Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "Пожалуйста, проверьте, что адреса WebRTC ICE серверов имеют правильный формат, каждый адрес на отдельной строке и не повторяется."; -/* No comment provided by engineer. */ -"Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" = "Много пользователей спросили: *как SimpleX доставляет сообщения без идентификаторов пользователей?*"; - /* No comment provided by engineer. */ "Mark deleted for everyone" = "Пометить как удаленное для всех"; @@ -3154,12 +3114,18 @@ /* No comment provided by engineer. */ "No permission to record voice message" = "Нет разрешения для записи голосового сообщения"; +/* No comment provided by engineer. */ +"No push server" = "Без сервера нотификаций"; + /* No comment provided by engineer. */ "No received or sent files" = "Нет полученных или отправленных файлов"; /* copied message info in history */ "no text" = "нет текста"; +/* No comment provided by engineer. */ +"No user identifiers." = "Без идентификаторов пользователей."; + /* No comment provided by engineer. */ "Not compatible!" = "Несовместимая версия!"; @@ -3225,9 +3191,6 @@ /* No comment provided by engineer. */ "Only client devices store user profiles, contacts, groups, and messages." = "Только пользовательские устройства хранят контакты, группы и сообщения."; -/* No comment provided by engineer. */ -"All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages." = "Все сообщения и файлы отправляются с **end-to-end шифрованием**, с постквантовой безопасностью в прямых разговорах."; - /* No comment provided by engineer. */ "Only delete conversation" = "Удалить только разговор"; @@ -3285,18 +3248,9 @@ /* authentication reason */ "Open migration to another device" = "Открытие миграции на другое устройство"; -/* No comment provided by engineer. */ -"Open server settings" = "Открыть настройки серверов"; - /* No comment provided by engineer. */ "Open Settings" = "Открыть Настройки"; -/* authentication reason */ -"Open user profiles" = "Открыть профили пользователя"; - -/* No comment provided by engineer. */ -"Anybody can host servers." = "Кто угодно может запустить сервер."; - /* No comment provided by engineer. */ "Opening app…" = "Приложение отрывается…"; @@ -3318,9 +3272,6 @@ /* No comment provided by engineer. */ "Other" = "Другaя сеть"; -/* No comment provided by engineer. */ -"Other %@ servers" = "Другие %@ серверы"; - /* No comment provided by engineer. */ "other errors" = "другие ошибки"; @@ -3375,9 +3326,6 @@ /* No comment provided by engineer. */ "Pending" = "В ожидании"; -/* No comment provided by engineer. */ -"You decide who can connect." = "Вы определяете, кто может соединиться."; - /* No comment provided by engineer. */ "Periodic" = "Периодически"; @@ -3456,9 +3404,6 @@ /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Сохранить последний черновик, вместе с вложениями."; -/* No comment provided by engineer. */ -"Preset server" = "Сервер по умолчанию"; - /* No comment provided by engineer. */ "Preset server address" = "Адрес сервера по умолчанию"; @@ -3507,7 +3452,7 @@ /* No comment provided by engineer. */ "Profile theme" = "Тема профиля"; -/* No comment provided by engineer. */ +/* alert message */ "Profile update will be sent to your contacts." = "Обновлённый профиль будет отправлен Вашим контактам."; /* No comment provided by engineer. */ @@ -3592,10 +3537,10 @@ "Read more" = "Узнать больше"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." = "Узнать больше в [Руководстве пользователя](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)."; +"Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." = "Дополнительная информация в [Руководстве пользователя](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)."; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." = "Дополнительная информация в [Руководстве пользователя](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)."; +"Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." = "Узнать больше в [Руководстве пользователя](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)."; /* No comment provided by engineer. */ "Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." = "Узнать больше в [Руководстве пользователя](https://simplex.chat/docs/guide/readme.html#connect-to-friends)."; @@ -3603,9 +3548,6 @@ /* No comment provided by engineer. */ "Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." = "Узнайте больше из нашего [GitHub репозитория](https://github.com/simplex-chat/simplex-chat#readme)."; -/* No comment provided by engineer. */ -"Read more in our GitHub repository." = "Узнайте больше из нашего GitHub репозитория."; - /* No comment provided by engineer. */ "Receipts are disabled" = "Отчёты о доставке выключены"; @@ -3878,7 +3820,7 @@ /* No comment provided by engineer. */ "Save servers" = "Сохранить серверы"; -/* No comment provided by engineer. */ +/* alert title */ "Save servers?" = "Сохранить серверы?"; /* No comment provided by engineer. */ @@ -4031,9 +3973,6 @@ /* No comment provided by engineer. */ "Send notifications" = "Отправлять уведомления"; -/* No comment provided by engineer. */ -"Send notifications:" = "Отправлять уведомления:"; - /* No comment provided by engineer. */ "Send questions and ideas" = "Отправьте вопросы и идеи"; @@ -4196,7 +4135,8 @@ /* No comment provided by engineer. */ "Shape profile images" = "Форма картинок профилей"; -/* chat item action */ +/* alert action + chat item action */ "Share" = "Поделиться"; /* No comment provided by engineer. */ @@ -4205,7 +4145,7 @@ /* No comment provided by engineer. */ "Share address" = "Поделиться адресом"; -/* No comment provided by engineer. */ +/* alert title */ "Share address with contacts?" = "Поделиться адресом с контактами?"; /* No comment provided by engineer. */ @@ -4388,10 +4328,10 @@ /* No comment provided by engineer. */ "Stop sending file?" = "Остановить отправку файла?"; -/* No comment provided by engineer. */ +/* alert action */ "Stop sharing" = "Прекратить делиться"; -/* No comment provided by engineer. */ +/* alert title */ "Stop sharing address?" = "Прекратить делиться адресом?"; /* authentication reason */ @@ -4487,7 +4427,7 @@ /* No comment provided by engineer. */ "Test servers" = "Тестировать серверы"; -/* No comment provided by engineer. */ +/* alert title */ "Tests failed!" = "Ошибка тестов!"; /* No comment provided by engineer. */ @@ -4499,9 +4439,6 @@ /* No comment provided by engineer. */ "Thanks to the users – contribute via Weblate!" = "Благодаря пользователям – добавьте переводы через Weblate!"; -/* No comment provided by engineer. */ -"No user identifiers." = "Без идентификаторов пользователей."; - /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "Приложение может посылать Вам уведомления о сообщениях и запросах на соединение - уведомления можно включить в Настройках."; @@ -4526,6 +4463,9 @@ /* No comment provided by engineer. */ "The encryption is working and the new encryption agreement is not required. It may result in connection errors!" = "Шифрование работает, и новое соглашение не требуется. Это может привести к ошибкам соединения!"; +/* No comment provided by engineer. */ +"The future of messaging" = "Будущее коммуникаций"; + /* No comment provided by engineer. */ "The hash of the previous message is different." = "Хэш предыдущего сообщения отличается."; @@ -4544,9 +4484,6 @@ /* No comment provided by engineer. */ "The messages will be marked as moderated for all members." = "Сообщения будут помечены как удаленные для всех членов группы."; -/* No comment provided by engineer. */ -"The future of messaging" = "Будущее коммуникаций"; - /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "Предыдущая версия данных чата не удалена при перемещении, её можно удалить."; @@ -4634,9 +4571,6 @@ /* No comment provided by engineer. */ "To make a new connection" = "Чтобы соединиться"; -/* No comment provided by engineer. */ -"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Чтобы защитить Вашу конфиденциальность, SimpleX использует разные идентификаторы для каждого Вашeго контакта."; - /* No comment provided by engineer. */ "To protect timezone, image/voice files use UTC." = "Чтобы защитить Ваш часовой пояс, файлы картинок и голосовых сообщений используют UTC."; @@ -4646,6 +4580,9 @@ /* No comment provided by engineer. */ "To protect your IP address, private routing uses your SMP servers to deliver messages." = "Чтобы защитить ваш IP адрес, приложение использует Ваши SMP серверы для конфиденциальной доставки сообщений."; +/* No comment provided by engineer. */ +"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Чтобы защитить Вашу конфиденциальность, SimpleX использует разные идентификаторы для каждого Вашeго контакта."; + /* No comment provided by engineer. */ "To record speech please grant permission to use Microphone." = "Для записи речи, пожалуйста, дайте разрешение на использование микрофона."; @@ -5033,9 +4970,6 @@ /* No comment provided by engineer. */ "when IP hidden" = "когда IP защищен"; -/* No comment provided by engineer. */ -"When people request to connect, you can accept or reject it." = "Когда Вы получите запрос на соединение, Вы можете принять или отклонить его."; - /* No comment provided by engineer. */ "When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "Когда Вы соединены с контактом инкогнито, тот же самый инкогнито профиль будет использоваться для групп с этим контактом."; @@ -5177,9 +5111,6 @@ /* No comment provided by engineer. */ "You can share this address with your contacts to let them connect with **%@**." = "Вы можете поделиться этим адресом с Вашими контактами, чтобы они могли соединиться с **%@**."; -/* No comment provided by engineer. */ -"You can share your address as a link or QR code - anybody can connect to you." = "Вы можете использовать Ваш адрес как ссылку или как QR код - кто угодно сможет соединиться с Вами."; - /* No comment provided by engineer. */ "You can start chat via app Settings / Database or by restarting the app" = "Вы можете запустить чат через Настройки приложения или перезапустив приложение."; @@ -5192,7 +5123,7 @@ /* No comment provided by engineer. */ "You can use markdown to format messages:" = "Вы можете форматировать сообщения:"; -/* No comment provided by engineer. */ +/* alert message */ "You can view invitation link again in connection details." = "Вы можете увидеть ссылку-приглашение снова открыв соединение."; /* No comment provided by engineer. */ @@ -5213,6 +5144,9 @@ /* No comment provided by engineer. */ "You could not be verified; please try again." = "Верификация не удалась; пожалуйста, попробуйте ещё раз."; +/* No comment provided by engineer. */ +"You decide who can connect." = "Вы определяете, кто может соединиться."; + /* No comment provided by engineer. */ "You have already requested connection via this address!" = "Вы уже запросили соединение через этот адрес!"; @@ -5303,9 +5237,6 @@ /* No comment provided by engineer. */ "You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed" = "Вы используете инкогнито профиль для этой группы - чтобы предотвратить раскрытие Вашего основного профиля, приглашать контакты не разрешено"; -/* No comment provided by engineer. */ -"Your %@ servers" = "Ваши %@ серверы"; - /* No comment provided by engineer. */ "Your calls" = "Ваши звонки"; @@ -5369,9 +5300,6 @@ /* No comment provided by engineer. */ "Your random profile" = "Случайный профиль"; -/* No comment provided by engineer. */ -"Your server" = "Ваш сервер"; - /* No comment provided by engineer. */ "Your server address" = "Адрес Вашего сервера"; @@ -5384,6 +5312,3 @@ /* No comment provided by engineer. */ "Your SMP servers" = "Ваши SMP серверы"; -/* No comment provided by engineer. */ -"Your XFTP servers" = "Ваши XFTP серверы"; - diff --git a/apps/ios/th.lproj/Localizable.strings b/apps/ios/th.lproj/Localizable.strings index 85295df87d..b50986cf17 100644 --- a/apps/ios/th.lproj/Localizable.strings +++ b/apps/ios/th.lproj/Localizable.strings @@ -1,15 +1,6 @@ /* No comment provided by engineer. */ "\n" = "\n"; -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - /* No comment provided by engineer. */ " (" = " ("; @@ -241,12 +232,6 @@ /* No comment provided by engineer. */ "Abort changing address?" = "ยกเลิกการเปลี่ยนที่อยู่?"; -/* No comment provided by engineer. */ -"About SimpleX" = "เกี่ยวกับ SimpleX"; - -/* No comment provided by engineer. */ -"About SimpleX address" = "เกี่ยวกับที่อยู่ SimpleX"; - /* No comment provided by engineer. */ "About SimpleX Chat" = "เกี่ยวกับ SimpleX Chat"; @@ -271,9 +256,6 @@ /* No comment provided by engineer. */ "Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." = "เพิ่มที่อยู่ลงในโปรไฟล์ของคุณ เพื่อให้ผู้ติดต่อของคุณสามารถแชร์กับผู้อื่นได้ การอัปเดตโปรไฟล์จะถูกส่งไปยังผู้ติดต่อของคุณ"; -/* No comment provided by engineer. */ -"Add preset servers" = "เพิ่มเซิร์ฟเวอร์ที่ตั้งไว้ล่วงหน้า"; - /* No comment provided by engineer. */ "Add profile" = "เพิ่มโปรไฟล์"; @@ -400,6 +382,9 @@ /* No comment provided by engineer. */ "Answer call" = "รับสาย"; +/* No comment provided by engineer. */ +"Anybody can host servers." = "โปรโตคอลและโค้ดโอเพ่นซอร์ส – ใคร ๆ ก็สามารถเปิดใช้เซิร์ฟเวอร์ได้"; + /* No comment provided by engineer. */ "App build: %@" = "รุ่นแอป: %@"; @@ -520,7 +505,8 @@ /* No comment provided by engineer. */ "Can't invite contacts!" = "ไม่สามารถเชิญผู้ติดต่อได้!"; -/* alert button */ +/* alert action + alert button */ "Cancel" = "ยกเลิก"; /* feature offered item */ @@ -608,7 +594,7 @@ /* No comment provided by engineer. */ "Chats" = "แชท"; -/* No comment provided by engineer. */ +/* alert title */ "Check server address and try again." = "ตรวจสอบที่อยู่เซิร์ฟเวอร์แล้วลองอีกครั้ง"; /* No comment provided by engineer. */ @@ -764,9 +750,6 @@ /* No comment provided by engineer. */ "Create" = "สร้าง"; -/* No comment provided by engineer. */ -"Create an address to let people connect with you." = "สร้างที่อยู่เพื่อให้ผู้อื่นเชื่อมต่อกับคุณ"; - /* server test step */ "Create file" = "สร้างไฟล์"; @@ -887,7 +870,8 @@ /* No comment provided by engineer. */ "default (yes)" = "ค่าเริ่มต้น (ใช่)"; -/* chat item action +/* alert action + chat item action swipe action */ "Delete" = "ลบ"; @@ -1305,18 +1289,12 @@ /* No comment provided by engineer. */ "Error joining group" = "เกิดข้อผิดพลาดในการเข้าร่วมกลุ่ม"; -/* No comment provided by engineer. */ -"Error loading %@ servers" = "โหลดเซิร์ฟเวอร์ %@ ผิดพลาด"; - /* alert title */ "Error receiving file" = "เกิดข้อผิดพลาดในการรับไฟล์"; /* No comment provided by engineer. */ "Error removing member" = "เกิดข้อผิดพลาดในการลบสมาชิก"; -/* No comment provided by engineer. */ -"Error saving %@ servers" = "เกิดข้อผิดพลาดในการบันทึกเซิร์ฟเวอร์ %@"; - /* No comment provided by engineer. */ "Error saving group profile" = "เกิดข้อผิดพลาดในการบันทึกโปรไฟล์กลุ่ม"; @@ -1581,9 +1559,6 @@ /* time unit */ "hours" = "ชั่วโมง"; -/* No comment provided by engineer. */ -"How it works" = "มันทำงานอย่างไร"; - /* No comment provided by engineer. */ "How SimpleX works" = "วิธีการ SimpleX ทํางานอย่างไร"; @@ -1690,10 +1665,10 @@ "Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)" = "ติดตั้ง [SimpleX Chat สำหรับเทอร์มินัล](https://github.com/simplex-chat/simplex-chat)"; /* No comment provided by engineer. */ -"Instant push notifications will be hidden!\n" = "การแจ้งเตือนโดยทันทีจะถูกซ่อน!\n"; +"Instant" = "ทันที"; /* No comment provided by engineer. */ -"Instant" = "ทันที"; +"Instant push notifications will be hidden!\n" = "การแจ้งเตือนโดยทันทีจะถูกซ่อน!\n"; /* No comment provided by engineer. */ "Interface" = "อินเตอร์เฟซ"; @@ -1710,7 +1685,7 @@ /* invalid chat item */ "invalid data" = "ข้อมูลไม่ถูกต้อง"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid server address!" = "ที่อยู่เซิร์ฟเวอร์ไม่ถูกต้อง!"; /* No comment provided by engineer. */ @@ -1836,9 +1811,6 @@ /* No comment provided by engineer. */ "Live messages" = "ข้อความสด"; -/* No comment provided by engineer. */ -"No push server" = "ในเครื่อง"; - /* No comment provided by engineer. */ "Local name" = "ชื่อภายในเครื่องเท่านั้น"; @@ -1851,24 +1823,15 @@ /* No comment provided by engineer. */ "Lock mode" = "โหมดล็อค"; -/* No comment provided by engineer. */ -"Make a private connection" = "สร้างการเชื่อมต่อแบบส่วนตัว"; - /* No comment provided by engineer. */ "Make one message disappear" = "ทำให้ข้อความหายไปหนึ่งข้อความ"; /* No comment provided by engineer. */ "Make profile private!" = "ทำให้โปรไฟล์เป็นส่วนตัว!"; -/* No comment provided by engineer. */ -"Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." = "ตรวจสอบให้แน่ใจว่าที่อยู่เซิร์ฟเวอร์ %@ อยู่ในรูปแบบที่ถูกต้อง แยกบรรทัดและไม่ซ้ำกัน (%@)"; - /* No comment provided by engineer. */ "Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "ตรวจสอบให้แน่ใจว่าที่อยู่เซิร์ฟเวอร์ WebRTC ICE อยู่ในรูปแบบที่ถูกต้อง แยกบรรทัดและไม่ซ้ำกัน"; -/* No comment provided by engineer. */ -"Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" = "หลายคนถามว่า: *หาก SimpleX ไม่มีตัวระบุผู้ใช้ จะส่งข้อความได้อย่างไร?*"; - /* No comment provided by engineer. */ "Mark deleted for everyone" = "ทำเครื่องหมายว่าลบแล้วสำหรับทุกคน"; @@ -2064,12 +2027,18 @@ /* No comment provided by engineer. */ "No permission to record voice message" = "ไม่อนุญาตให้บันทึกข้อความเสียง"; +/* No comment provided by engineer. */ +"No push server" = "ในเครื่อง"; + /* No comment provided by engineer. */ "No received or sent files" = "ไม่มีไฟล์ที่ได้รับหรือส่ง"; /* copied message info in history */ "no text" = "ไม่มีข้อความ"; +/* No comment provided by engineer. */ +"No user identifiers." = "แพลตฟอร์มแรกที่ไม่มีตัวระบุผู้ใช้ - ถูกออกแบบให้เป็นส่วนตัว"; + /* No comment provided by engineer. */ "Notifications" = "การแจ้งเตือน"; @@ -2171,12 +2140,6 @@ /* No comment provided by engineer. */ "Open Settings" = "เปิดการตั้งค่า"; -/* authentication reason */ -"Open user profiles" = "เปิดโปรไฟล์ผู้ใช้"; - -/* No comment provided by engineer. */ -"Anybody can host servers." = "โปรโตคอลและโค้ดโอเพ่นซอร์ส – ใคร ๆ ก็สามารถเปิดใช้เซิร์ฟเวอร์ได้"; - /* member role */ "owner" = "เจ้าของ"; @@ -2204,9 +2167,6 @@ /* No comment provided by engineer. */ "peer-to-peer" = "เพื่อนต่อเพื่อน"; -/* No comment provided by engineer. */ -"You decide who can connect." = "ผู้คนสามารถเชื่อมต่อกับคุณผ่านลิงก์ที่คุณแบ่งปันเท่านั้น"; - /* No comment provided by engineer. */ "Periodic" = "เป็นระยะๆ"; @@ -2264,9 +2224,6 @@ /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "เก็บข้อความที่ร่างไว้ล่าสุดพร้อมไฟล์แนบ"; -/* No comment provided by engineer. */ -"Preset server" = "เซิร์ฟเวอร์ที่ตั้งไว้ล่วงหน้า"; - /* No comment provided by engineer. */ "Preset server address" = "ที่อยู่เซิร์ฟเวอร์ที่ตั้งไว้ล่วงหน้า"; @@ -2291,7 +2248,7 @@ /* No comment provided by engineer. */ "Profile password" = "รหัสผ่านโปรไฟล์"; -/* No comment provided by engineer. */ +/* alert message */ "Profile update will be sent to your contacts." = "การอัปเดตโปรไฟล์จะถูกส่งไปยังผู้ติดต่อของคุณ"; /* No comment provided by engineer. */ @@ -2354,9 +2311,6 @@ /* No comment provided by engineer. */ "Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." = "อ่านเพิ่มเติมใน[พื้นที่เก็บข้อมูล GitHub](https://github.com/simplex-chat/simplex-chat#readme)"; -/* No comment provided by engineer. */ -"Read more in our GitHub repository." = "อ่านเพิ่มเติมในที่เก็บ GitHub ของเรา"; - /* No comment provided by engineer. */ "received answer…" = "ได้รับคำตอบ…"; @@ -2536,7 +2490,7 @@ /* No comment provided by engineer. */ "Save servers" = "บันทึกเซิร์ฟเวอร์"; -/* No comment provided by engineer. */ +/* alert title */ "Save servers?" = "บันทึกเซิร์ฟเวอร์?"; /* No comment provided by engineer. */ @@ -2617,9 +2571,6 @@ /* No comment provided by engineer. */ "Send notifications" = "ส่งการแจ้งเตือน"; -/* No comment provided by engineer. */ -"Send notifications:" = "ส่งการแจ้งเตือน:"; - /* No comment provided by engineer. */ "Send questions and ideas" = "ส่งคําถามและความคิด"; @@ -2707,7 +2658,8 @@ /* No comment provided by engineer. */ "Settings" = "การตั้งค่า"; -/* chat item action */ +/* alert action + chat item action */ "Share" = "แชร์"; /* No comment provided by engineer. */ @@ -2716,7 +2668,7 @@ /* No comment provided by engineer. */ "Share address" = "แชร์ที่อยู่"; -/* No comment provided by engineer. */ +/* alert title */ "Share address with contacts?" = "แชร์ที่อยู่กับผู้ติดต่อ?"; /* No comment provided by engineer. */ @@ -2815,10 +2767,10 @@ /* No comment provided by engineer. */ "Stop sending file?" = "หยุดส่งไฟล์ไหม?"; -/* No comment provided by engineer. */ +/* alert action */ "Stop sharing" = "หยุดแชร์"; -/* No comment provided by engineer. */ +/* alert title */ "Stop sharing address?" = "หยุดแชร์ที่อยู่ไหม?"; /* authentication reason */ @@ -2875,7 +2827,7 @@ /* No comment provided by engineer. */ "Test servers" = "เซิร์ฟเวอร์ทดสอบ"; -/* No comment provided by engineer. */ +/* alert title */ "Tests failed!" = "การทดสอบล้มเหลว!"; /* No comment provided by engineer. */ @@ -2887,9 +2839,6 @@ /* No comment provided by engineer. */ "Thanks to the users – contribute via Weblate!" = "ขอบคุณผู้ใช้ – มีส่วนร่วมผ่าน Weblate!"; -/* No comment provided by engineer. */ -"No user identifiers." = "แพลตฟอร์มแรกที่ไม่มีตัวระบุผู้ใช้ - ถูกออกแบบให้เป็นส่วนตัว"; - /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "แอปสามารถแจ้งให้คุณทราบเมื่อคุณได้รับข้อความหรือคำขอติดต่อ - โปรดเปิดการตั้งค่าเพื่อเปิดใช้งาน"; @@ -2908,6 +2857,9 @@ /* No comment provided by engineer. */ "The encryption is working and the new encryption agreement is not required. It may result in connection errors!" = "encryption กำลังทำงานและไม่จำเป็นต้องใช้ข้อตกลง encryption ใหม่ อาจทำให้การเชื่อมต่อผิดพลาดได้!"; +/* No comment provided by engineer. */ +"The future of messaging" = "การส่งข้อความส่วนตัวรุ่นต่อไป"; + /* No comment provided by engineer. */ "The hash of the previous message is different." = "แฮชของข้อความก่อนหน้านี้แตกต่างกัน"; @@ -2920,9 +2872,6 @@ /* No comment provided by engineer. */ "The message will be marked as moderated for all members." = "ข้อความจะถูกทำเครื่องหมายว่ากลั่นกรองสำหรับสมาชิกทุกคน"; -/* No comment provided by engineer. */ -"The future of messaging" = "การส่งข้อความส่วนตัวรุ่นต่อไป"; - /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "ฐานข้อมูลเก่าไม่ได้ถูกลบในระหว่างการย้ายข้อมูล แต่สามารถลบได้"; @@ -2968,15 +2917,15 @@ /* No comment provided by engineer. */ "To make a new connection" = "เพื่อสร้างการเชื่อมต่อใหม่"; -/* No comment provided by engineer. */ -"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "เพื่อปกป้องความเป็นส่วนตัว แทนที่จะใช้ ID ผู้ใช้เหมือนที่แพลตฟอร์มอื่นๆใช้ SimpleX มีตัวระบุสำหรับคิวข้อความ โดยแยกจากกันสำหรับผู้ติดต่อแต่ละราย"; - /* No comment provided by engineer. */ "To protect timezone, image/voice files use UTC." = "ไฟล์ภาพ/เสียงใช้ UTC เพื่อป้องกันเขตเวลา"; /* No comment provided by engineer. */ "To protect your information, turn on SimpleX Lock.\nYou will be prompted to complete authentication before this feature is enabled." = "เพื่อปกป้องข้อมูลของคุณ ให้เปิด SimpleX Lock\nคุณจะได้รับแจ้งให้ยืนยันตัวตนให้เสร็จสมบูรณ์ก่อนที่จะเปิดใช้งานคุณลักษณะนี้"; +/* No comment provided by engineer. */ +"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "เพื่อปกป้องความเป็นส่วนตัว แทนที่จะใช้ ID ผู้ใช้เหมือนที่แพลตฟอร์มอื่นๆใช้ SimpleX มีตัวระบุสำหรับคิวข้อความ โดยแยกจากกันสำหรับผู้ติดต่อแต่ละราย"; + /* No comment provided by engineer. */ "To record voice message please grant permission to use Microphone." = "ในการบันทึกข้อความเสียง โปรดให้สิทธิ์ในการใช้ไมโครโฟน"; @@ -3193,9 +3142,6 @@ /* No comment provided by engineer. */ "When available" = "เมื่อพร้อมใช้งาน"; -/* No comment provided by engineer. */ -"When people request to connect, you can accept or reject it." = "เมื่อมีคนขอเชื่อมต่อ คุณสามารถยอมรับหรือปฏิเสธได้"; - /* No comment provided by engineer. */ "When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "เมื่อคุณแชร์โปรไฟล์ที่ไม่ระบุตัวตนกับใครสักคน โปรไฟล์นี้จะใช้สำหรับกลุ่มที่พวกเขาเชิญคุณ"; @@ -3262,9 +3208,6 @@ /* No comment provided by engineer. */ "You can share this address with your contacts to let them connect with **%@**." = "คุณสามารถแบ่งปันที่อยู่นี้กับผู้ติดต่อของคุณเพื่อให้พวกเขาเชื่อมต่อกับ **%@**"; -/* No comment provided by engineer. */ -"You can share your address as a link or QR code - anybody can connect to you." = "คุณสามารถแชร์ที่อยู่ของคุณเป็นลิงก์หรือรหัสคิวอาร์ - ใคร ๆ ก็สามารถเชื่อมต่อกับคุณได้"; - /* No comment provided by engineer. */ "You can start chat via app Settings / Database or by restarting the app" = "คุณสามารถเริ่มแชทผ่านการตั้งค่าแอป / ฐานข้อมูล หรือโดยการรีสตาร์ทแอป"; @@ -3292,6 +3235,9 @@ /* No comment provided by engineer. */ "You could not be verified; please try again." = "เราไม่สามารถตรวจสอบคุณได้ กรุณาลองอีกครั้ง."; +/* No comment provided by engineer. */ +"You decide who can connect." = "ผู้คนสามารถเชื่อมต่อกับคุณผ่านลิงก์ที่คุณแบ่งปันเท่านั้น"; + /* No comment provided by engineer. */ "You have to enter passphrase every time the app starts - it is not stored on the device." = "คุณต้องใส่รหัสผ่านทุกครั้งที่เริ่มแอป - รหัสผ่านไม่ได้จัดเก็บไว้ในอุปกรณ์"; @@ -3355,9 +3301,6 @@ /* No comment provided by engineer. */ "You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed" = "คุณกำลังใช้โปรไฟล์ที่ไม่ระบุตัวตนสำหรับกลุ่มนี้ - ไม่อนุญาตให้เชิญผู้ติดต่อเพื่อป้องกันการแชร์โปรไฟล์หลักของคุณ"; -/* No comment provided by engineer. */ -"Your %@ servers" = "เซิร์ฟเวอร์ %@ ของคุณ"; - /* No comment provided by engineer. */ "Your calls" = "การโทรของคุณ"; @@ -3403,9 +3346,6 @@ /* No comment provided by engineer. */ "Your random profile" = "โปรไฟล์แบบสุ่มของคุณ"; -/* No comment provided by engineer. */ -"Your server" = "เซิร์ฟเวอร์ของคุณ"; - /* No comment provided by engineer. */ "Your server address" = "ที่อยู่เซิร์ฟเวอร์ของคุณ"; @@ -3418,6 +3358,3 @@ /* No comment provided by engineer. */ "Your SMP servers" = "เซิร์ฟเวอร์ SMP ของคุณ"; -/* No comment provided by engineer. */ -"Your XFTP servers" = "เซิร์ฟเวอร์ XFTP ของคุณ"; - diff --git a/apps/ios/tr.lproj/Localizable.strings b/apps/ios/tr.lproj/Localizable.strings index bbaa1a5657..1583f01cda 100644 --- a/apps/ios/tr.lproj/Localizable.strings +++ b/apps/ios/tr.lproj/Localizable.strings @@ -1,15 +1,6 @@ /* No comment provided by engineer. */ "\n" = "\n"; -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - /* No comment provided by engineer. */ " (" = " ("; @@ -337,12 +328,6 @@ /* No comment provided by engineer. */ "Abort changing address?" = "Adres değişimi iptal edilsin mi?"; -/* No comment provided by engineer. */ -"About SimpleX" = "SimpleX Hakkında"; - -/* No comment provided by engineer. */ -"About SimpleX address" = "SimpleX Chat adresi hakkında"; - /* No comment provided by engineer. */ "About SimpleX Chat" = "SimpleX Chat hakkında"; @@ -382,12 +367,6 @@ /* No comment provided by engineer. */ "Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." = "Kişilerinizin başkalarıyla paylaşabilmesi için profilinize adres ekleyin. Profil güncellemesi kişilerinize gönderilecek."; -/* No comment provided by engineer. */ -"Add contact" = "Kişi ekle"; - -/* No comment provided by engineer. */ -"Add preset servers" = "Önceden ayarlanmış sunucu ekle"; - /* No comment provided by engineer. */ "Add profile" = "Profil ekle"; @@ -574,6 +553,9 @@ /* No comment provided by engineer. */ "Answer call" = "Aramayı cevapla"; +/* No comment provided by engineer. */ +"Anybody can host servers." = "Açık kaynak protokolü ve kodu - herhangi biri sunucuları çalıştırabilir."; + /* No comment provided by engineer. */ "App build: %@" = "Uygulama sürümü: %@"; @@ -817,7 +799,8 @@ /* No comment provided by engineer. */ "Can't message member" = "Üyeye mesaj gönderilemiyor"; -/* alert button */ +/* alert action + alert button */ "Cancel" = "İptal et"; /* No comment provided by engineer. */ @@ -938,7 +921,7 @@ /* No comment provided by engineer. */ "Chats" = "Sohbetler"; -/* No comment provided by engineer. */ +/* alert title */ "Check server address and try again." = "Sunucu adresini kontrol edip tekrar deneyin."; /* No comment provided by engineer. */ @@ -1001,9 +984,6 @@ /* No comment provided by engineer. */ "Configure ICE servers" = "ICE sunucularını ayarla"; -/* No comment provided by engineer. */ -"Configured %@ servers" = "Yapılandırılmış %@ sunucuları"; - /* No comment provided by engineer. */ "Confirm" = "Onayla"; @@ -1232,9 +1212,6 @@ /* No comment provided by engineer. */ "Create a group using a random profile." = "Rasgele profil kullanarak grup oluştur."; -/* No comment provided by engineer. */ -"Create an address to let people connect with you." = "İnsanların seninle bağlanması için bir adres oluştur."; - /* server test step */ "Create file" = "Dosya oluştur"; @@ -1397,7 +1374,8 @@ /* No comment provided by engineer. */ "default (yes)" = "varsayılan (evet)"; -/* chat item action +/* alert action + chat item action swipe action */ "Delete" = "Sil"; @@ -1996,9 +1974,6 @@ /* No comment provided by engineer. */ "Error joining group" = "Gruba katılırken hata oluştu"; -/* No comment provided by engineer. */ -"Error loading %@ servers" = "%@ sunucuları yüklenirken hata oluştu"; - /* No comment provided by engineer. */ "Error migrating settings" = "Ayarlar taşınırken hata oluştu"; @@ -2020,9 +1995,6 @@ /* No comment provided by engineer. */ "Error resetting statistics" = "Hata istatistikler sıfırlanıyor"; -/* No comment provided by engineer. */ -"Error saving %@ servers" = "%@ sunucuları kaydedilirken sorun oluştu"; - /* No comment provided by engineer. */ "Error saving group profile" = "Grup profili kaydedilirken sorun oluştu"; @@ -2425,9 +2397,6 @@ /* time unit */ "hours" = "saat"; -/* No comment provided by engineer. */ -"How it works" = "Nasıl çalışıyor"; - /* No comment provided by engineer. */ "How SimpleX works" = "SimpleX nasıl çalışır"; @@ -2570,10 +2539,10 @@ "Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)" = "[Terminal için SimpleX Chat]i indir(https://github.com/simplex-chat/simplex-chat)"; /* No comment provided by engineer. */ -"Instant push notifications will be hidden!\n" = "Anlık bildirimler gizlenecek!\n"; +"Instant" = "Anında"; /* No comment provided by engineer. */ -"Instant" = "Anında"; +"Instant push notifications will be hidden!\n" = "Anlık bildirimler gizlenecek!\n"; /* No comment provided by engineer. */ "Interface" = "Arayüz"; @@ -2611,7 +2580,7 @@ /* No comment provided by engineer. */ "Invalid response" = "Geçersiz yanıt"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid server address!" = "Geçersiz sunucu adresi!"; /* item status text */ @@ -2716,7 +2685,7 @@ /* No comment provided by engineer. */ "Joining group" = "Gruba katılınıyor"; -/* No comment provided by engineer. */ +/* alert action */ "Keep" = "Tut"; /* No comment provided by engineer. */ @@ -2725,7 +2694,7 @@ /* No comment provided by engineer. */ "Keep the app open to use it from desktop" = "Bilgisayardan kullanmak için uygulamayı açık tut"; -/* No comment provided by engineer. */ +/* alert title */ "Keep unused invitation?" = "Kullanılmamış davet tutulsun mu?"; /* No comment provided by engineer. */ @@ -2782,9 +2751,6 @@ /* No comment provided by engineer. */ "Live messages" = "Canlı mesajlar"; -/* No comment provided by engineer. */ -"No push server" = "Yerel"; - /* No comment provided by engineer. */ "Local name" = "Yerel isim"; @@ -2797,24 +2763,15 @@ /* No comment provided by engineer. */ "Lock mode" = "Kilit modu"; -/* No comment provided by engineer. */ -"Make a private connection" = "Gizli bir bağlantı oluştur"; - /* No comment provided by engineer. */ "Make one message disappear" = "Bir mesajın kaybolmasını sağlayın"; /* No comment provided by engineer. */ "Make profile private!" = "Profili gizli yap!"; -/* No comment provided by engineer. */ -"Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." = "%@ sunucu adreslerinin doğru formatta olduğundan, satır ayrımı yapıldığından ve yinelenmediğinden (%@) emin olun."; - /* No comment provided by engineer. */ "Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "WebRTC ICE sunucu adreslerinin doğru formatta olduğundan, satırlara ayrıldığından ve yinelenmediğinden emin olun."; -/* No comment provided by engineer. */ -"Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" = "Çoğu kişi sordu: *eğer SimpleX'in hiç kullanıcı tanımlayıcıları yok, o zaman mesajları nasıl gönderebiliyor?*"; - /* No comment provided by engineer. */ "Mark deleted for everyone" = "Herkes için silinmiş olarak işaretle"; @@ -3154,12 +3111,18 @@ /* No comment provided by engineer. */ "No permission to record voice message" = "Sesli mesaj kaydetmek için izin yok"; +/* No comment provided by engineer. */ +"No push server" = "Yerel"; + /* No comment provided by engineer. */ "No received or sent files" = "Hiç alınmış veya gönderilmiş dosya yok"; /* copied message info in history */ "no text" = "metin yok"; +/* No comment provided by engineer. */ +"No user identifiers." = "Herhangi bir kullanıcı tanımlayıcısı yok."; + /* No comment provided by engineer. */ "Not compatible!" = "Uyumlu değil!"; @@ -3282,18 +3245,9 @@ /* authentication reason */ "Open migration to another device" = "Başka bir cihaza açık geçiş"; -/* No comment provided by engineer. */ -"Open server settings" = "Sunucu ayarlarını aç"; - /* No comment provided by engineer. */ "Open Settings" = "Ayarları aç"; -/* authentication reason */ -"Open user profiles" = "Kullanıcı profillerini aç"; - -/* No comment provided by engineer. */ -"Anybody can host servers." = "Açık kaynak protokolü ve kodu - herhangi biri sunucuları çalıştırabilir."; - /* No comment provided by engineer. */ "Opening app…" = "Uygulama açılıyor…"; @@ -3315,9 +3269,6 @@ /* No comment provided by engineer. */ "Other" = "Diğer"; -/* No comment provided by engineer. */ -"Other %@ servers" = "Diğer %@ sunucuları"; - /* No comment provided by engineer. */ "other errors" = "diğer hatalar"; @@ -3372,9 +3323,6 @@ /* No comment provided by engineer. */ "Pending" = "Bekleniyor"; -/* No comment provided by engineer. */ -"You decide who can connect." = "Kimin bağlanabileceğine siz karar verirsiniz."; - /* No comment provided by engineer. */ "Periodic" = "Periyodik olarak"; @@ -3453,9 +3401,6 @@ /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Son mesaj taslağını ekleriyle birlikte koru."; -/* No comment provided by engineer. */ -"Preset server" = "Ön ayarlı sunucu"; - /* No comment provided by engineer. */ "Preset server address" = "Ön ayarlı sunucu adresi"; @@ -3504,7 +3449,7 @@ /* No comment provided by engineer. */ "Profile theme" = "Profil teması"; -/* No comment provided by engineer. */ +/* alert message */ "Profile update will be sent to your contacts." = "Profil güncellemesi kişilerinize gönderilecektir."; /* No comment provided by engineer. */ @@ -3589,10 +3534,10 @@ "Read more" = "Dahasını oku"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." = "[Kullanıcı Rehberi]nde daha fazlasını okuyun(https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)."; +"Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." = "[Kullanıcı Rehberi]nde daha fazlasını okuyun(https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)."; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." = "[Kullanıcı Rehberi]nde daha fazlasını okuyun(https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)."; +"Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." = "[Kullanıcı Rehberi]nde daha fazlasını okuyun(https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)."; /* No comment provided by engineer. */ "Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." = "[Kullanıcı Rehberi]nde daha fazlasını okuyun(https://simplex.chat/docs/guide/readme.html#connect-to-friends)."; @@ -3600,9 +3545,6 @@ /* No comment provided by engineer. */ "Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." = "[GitHub deposu]nda daha fazlasını okuyun(https://github.com/simplex-chat/simplex-chat#readme)."; -/* No comment provided by engineer. */ -"Read more in our GitHub repository." = "Daha fazlasını GitHub depomuzdan oku."; - /* No comment provided by engineer. */ "Receipts are disabled" = "Alıcılar devre dışı bırakıldı"; @@ -3875,7 +3817,7 @@ /* No comment provided by engineer. */ "Save servers" = "Sunucuları kaydet"; -/* No comment provided by engineer. */ +/* alert title */ "Save servers?" = "Sunucular kaydedilsin mi?"; /* No comment provided by engineer. */ @@ -4028,9 +3970,6 @@ /* No comment provided by engineer. */ "Send notifications" = "Bildirimler gönder"; -/* No comment provided by engineer. */ -"Send notifications:" = "Bildirimler gönder:"; - /* No comment provided by engineer. */ "Send questions and ideas" = "Fikirler ve sorular gönderin"; @@ -4193,7 +4132,8 @@ /* No comment provided by engineer. */ "Shape profile images" = "Profil resimlerini şekillendir"; -/* chat item action */ +/* alert action + chat item action */ "Share" = "Paylaş"; /* No comment provided by engineer. */ @@ -4202,7 +4142,7 @@ /* No comment provided by engineer. */ "Share address" = "Adresi paylaş"; -/* No comment provided by engineer. */ +/* alert title */ "Share address with contacts?" = "Kişilerle adres paylaşılsın mı?"; /* No comment provided by engineer. */ @@ -4385,10 +4325,10 @@ /* No comment provided by engineer. */ "Stop sending file?" = "Dosya gönderimi durdurulsun mu?"; -/* No comment provided by engineer. */ +/* alert action */ "Stop sharing" = "Paylaşmayı durdur"; -/* No comment provided by engineer. */ +/* alert title */ "Stop sharing address?" = "Adresi paylaşmak durdurulsun mu?"; /* authentication reason */ @@ -4484,7 +4424,7 @@ /* No comment provided by engineer. */ "Test servers" = "Sunucuları test et"; -/* No comment provided by engineer. */ +/* alert title */ "Tests failed!" = "Testler başarısız oldu!"; /* No comment provided by engineer. */ @@ -4496,9 +4436,6 @@ /* No comment provided by engineer. */ "Thanks to the users – contribute via Weblate!" = "Kullanıcılar için teşekkürler - Weblate aracılığıyla katkıda bulun!"; -/* No comment provided by engineer. */ -"No user identifiers." = "Herhangi bir kullanıcı tanımlayıcısı yok."; - /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "Uygulama, mesaj veya iletişim isteği aldığınızda sizi bilgilendirebilir - etkinleştirmek için lütfen ayarları açın."; @@ -4523,6 +4460,9 @@ /* No comment provided by engineer. */ "The encryption is working and the new encryption agreement is not required. It may result in connection errors!" = "Şifreleme çalışıyor ve yeni şifreleme anlaşması gerekli değil. Bağlantı hatalarına neden olabilir!"; +/* No comment provided by engineer. */ +"The future of messaging" = "Gizli mesajlaşmanın yeni nesli"; + /* No comment provided by engineer. */ "The hash of the previous message is different." = "Önceki mesajın hash'i farklı."; @@ -4541,9 +4481,6 @@ /* No comment provided by engineer. */ "The messages will be marked as moderated for all members." = "Mesajlar tüm üyeler için moderasyonlu olarak işaretlenecektir."; -/* No comment provided by engineer. */ -"The future of messaging" = "Gizli mesajlaşmanın yeni nesli"; - /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "Eski veritabanı geçiş sırasında kaldırılmadı, silinebilir."; @@ -4631,9 +4568,6 @@ /* No comment provided by engineer. */ "To make a new connection" = "Yeni bir bağlantı oluşturmak için"; -/* No comment provided by engineer. */ -"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Gizliliği korumak için, diğer tüm platformlar gibi kullanıcı kimliği kullanmak yerine, SimpleX mesaj kuyrukları için kişilerinizin her biri için ayrı tanımlayıcılara sahiptir."; - /* No comment provided by engineer. */ "To protect timezone, image/voice files use UTC." = "Zaman bölgesini korumak için,fotoğraf/ses dosyaları UTC kullanır."; @@ -4643,6 +4577,9 @@ /* No comment provided by engineer. */ "To protect your IP address, private routing uses your SMP servers to deliver messages." = "IP adresinizi korumak için,gizli yönlendirme mesajları iletmek için SMP sunucularınızı kullanır."; +/* No comment provided by engineer. */ +"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Gizliliği korumak için, diğer tüm platformlar gibi kullanıcı kimliği kullanmak yerine, SimpleX mesaj kuyrukları için kişilerinizin her biri için ayrı tanımlayıcılara sahiptir."; + /* No comment provided by engineer. */ "To record speech please grant permission to use Microphone." = "Konuşmayı kaydetmek için lütfen Mikrofon kullanma izni verin."; @@ -5030,9 +4967,6 @@ /* No comment provided by engineer. */ "when IP hidden" = "IP gizliyken"; -/* No comment provided by engineer. */ -"When people request to connect, you can accept or reject it." = "İnsanlar bağlantı talebinde bulunduğunda, kabul edebilir veya reddedebilirsiniz."; - /* No comment provided by engineer. */ "When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "Biriyle gizli bir profil paylaştığınızda, bu profil sizi davet ettikleri gruplar için kullanılacaktır."; @@ -5174,9 +5108,6 @@ /* No comment provided by engineer. */ "You can share this address with your contacts to let them connect with **%@**." = "Bu adresi kişilerinizle paylaşarak onların **%@** ile bağlantı kurmasını sağlayabilirsiniz."; -/* No comment provided by engineer. */ -"You can share your address as a link or QR code - anybody can connect to you." = "Adresinizi bir bağlantı veya QR kodu olarak paylaşabilirsiniz - herkes size bağlanabilir."; - /* No comment provided by engineer. */ "You can start chat via app Settings / Database or by restarting the app" = "Sohbeti uygulamada Ayarlar / Veritabanı üzerinden veya uygulamayı yeniden başlatarak başlatabilirsiniz"; @@ -5189,7 +5120,7 @@ /* No comment provided by engineer. */ "You can use markdown to format messages:" = "Mesajları biçimlendirmek için markdown kullanabilirsiniz:"; -/* No comment provided by engineer. */ +/* alert message */ "You can view invitation link again in connection details." = "Bağlantı detaylarından davet bağlantısını yeniden görüntüleyebilirsin."; /* No comment provided by engineer. */ @@ -5210,6 +5141,9 @@ /* No comment provided by engineer. */ "You could not be verified; please try again." = "Doğrulanamadınız; lütfen tekrar deneyin."; +/* No comment provided by engineer. */ +"You decide who can connect." = "Kimin bağlanabileceğine siz karar verirsiniz."; + /* No comment provided by engineer. */ "You have already requested connection via this address!" = "Bu adres üzerinden zaten bağlantı talebinde bulundunuz!"; @@ -5300,9 +5234,6 @@ /* No comment provided by engineer. */ "You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed" = "Bu grup için gizli bir profil kullanıyorsunuz - ana profilinizi paylaşmayı önlemek için kişileri davet etmeye izin verilmiyor"; -/* No comment provided by engineer. */ -"Your %@ servers" = "%@ sunucularınız"; - /* No comment provided by engineer. */ "Your calls" = "Aramaların"; @@ -5366,9 +5297,6 @@ /* No comment provided by engineer. */ "Your random profile" = "Rasgele profiliniz"; -/* No comment provided by engineer. */ -"Your server" = "Sunucunuz"; - /* No comment provided by engineer. */ "Your server address" = "Sunucu adresiniz"; @@ -5381,6 +5309,3 @@ /* No comment provided by engineer. */ "Your SMP servers" = "SMP sunucularınız"; -/* No comment provided by engineer. */ -"Your XFTP servers" = "XFTP sunucularınız"; - diff --git a/apps/ios/uk.lproj/Localizable.strings b/apps/ios/uk.lproj/Localizable.strings index 7f6a8bb677..01a3196c03 100644 --- a/apps/ios/uk.lproj/Localizable.strings +++ b/apps/ios/uk.lproj/Localizable.strings @@ -1,15 +1,6 @@ /* No comment provided by engineer. */ "\n" = "\n"; -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - /* No comment provided by engineer. */ " (" = " ("; @@ -169,6 +160,15 @@ /* time interval */ "%d days" = "%d днів"; +/* forward confirmation reason */ +"%d file(s) are still being downloaded." = "%их файл(ів) ще досі завантажуються."; + +/* forward confirmation reason */ +"%d file(s) failed to download." = "%их файлів не вийшло завантажити."; + +/* forward confirmation reason */ +"%d file(s) were deleted." = "%их файл(ів) було видалено."; + /* time interval */ "%d hours" = "%d годин"; @@ -319,12 +319,6 @@ /* No comment provided by engineer. */ "Abort changing address?" = "Скасувати зміну адреси?"; -/* No comment provided by engineer. */ -"About SimpleX" = "Про SimpleX"; - -/* No comment provided by engineer. */ -"About SimpleX address" = "Про адресу SimpleX"; - /* No comment provided by engineer. */ "About SimpleX Chat" = "Про чат SimpleX"; @@ -364,12 +358,6 @@ /* No comment provided by engineer. */ "Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." = "Додайте адресу до свого профілю, щоб ваші контакти могли поділитися нею з іншими людьми. Повідомлення про оновлення профілю буде надіслано вашим контактам."; -/* No comment provided by engineer. */ -"Add contact" = "Додати контакт"; - -/* No comment provided by engineer. */ -"Add preset servers" = "Додавання попередньо встановлених серверів"; - /* No comment provided by engineer. */ "Add profile" = "Додати профіль"; @@ -556,6 +544,9 @@ /* No comment provided by engineer. */ "Answer call" = "Відповісти на дзвінок"; +/* No comment provided by engineer. */ +"Anybody can host servers." = "Кожен може хостити сервери."; + /* No comment provided by engineer. */ "App build: %@" = "Збірка програми: %@"; @@ -778,7 +769,8 @@ /* No comment provided by engineer. */ "Can't message member" = "Не можу надіслати повідомлення користувачеві"; -/* alert button */ +/* alert action + alert button */ "Cancel" = "Скасувати"; /* No comment provided by engineer. */ @@ -896,7 +888,7 @@ /* No comment provided by engineer. */ "Chats" = "Чати"; -/* No comment provided by engineer. */ +/* alert title */ "Check server address and try again." = "Перевірте адресу сервера та спробуйте ще раз."; /* No comment provided by engineer. */ @@ -959,9 +951,6 @@ /* No comment provided by engineer. */ "Configure ICE servers" = "Налаштування серверів ICE"; -/* No comment provided by engineer. */ -"Configured %@ servers" = "Налаштовані сервери %@"; - /* No comment provided by engineer. */ "Confirm" = "Підтвердити"; @@ -1187,9 +1176,6 @@ /* No comment provided by engineer. */ "Create a group using a random profile." = "Створіть групу, використовуючи випадковий профіль."; -/* No comment provided by engineer. */ -"Create an address to let people connect with you." = "Створіть адресу, щоб люди могли з вами зв'язатися."; - /* server test step */ "Create file" = "Створити файл"; @@ -1349,7 +1335,8 @@ /* No comment provided by engineer. */ "default (yes)" = "за замовчуванням (так)"; -/* chat item action +/* alert action + chat item action swipe action */ "Delete" = "Видалити"; @@ -1933,9 +1920,6 @@ /* No comment provided by engineer. */ "Error joining group" = "Помилка приєднання до групи"; -/* No comment provided by engineer. */ -"Error loading %@ servers" = "Помилка завантаження %@ серверів"; - /* No comment provided by engineer. */ "Error opening chat" = "Помилка відкриття чату"; @@ -1954,9 +1938,6 @@ /* No comment provided by engineer. */ "Error resetting statistics" = "Статистика скидання помилок"; -/* No comment provided by engineer. */ -"Error saving %@ servers" = "Помилка збереження %@ серверів"; - /* No comment provided by engineer. */ "Error saving group profile" = "Помилка збереження профілю групи"; @@ -2338,9 +2319,6 @@ /* time unit */ "hours" = "години"; -/* No comment provided by engineer. */ -"How it works" = "Як це працює"; - /* No comment provided by engineer. */ "How SimpleX works" = "Як працює SimpleX"; @@ -2480,10 +2458,10 @@ "Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)" = "Встановіть [SimpleX Chat для терміналу](https://github.com/simplex-chat/simplex-chat)"; /* No comment provided by engineer. */ -"Instant push notifications will be hidden!\n" = "Миттєві пуш-сповіщення будуть приховані!\n"; +"Instant" = "Миттєво"; /* No comment provided by engineer. */ -"Instant" = "Миттєво"; +"Instant push notifications will be hidden!\n" = "Миттєві пуш-сповіщення будуть приховані!\n"; /* No comment provided by engineer. */ "Interface" = "Інтерфейс"; @@ -2521,7 +2499,7 @@ /* No comment provided by engineer. */ "Invalid response" = "Неправильна відповідь"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid server address!" = "Неправильна адреса сервера!"; /* item status text */ @@ -2623,7 +2601,7 @@ /* No comment provided by engineer. */ "Joining group" = "Приєднання до групи"; -/* No comment provided by engineer. */ +/* alert action */ "Keep" = "Тримай"; /* No comment provided by engineer. */ @@ -2632,7 +2610,7 @@ /* No comment provided by engineer. */ "Keep the app open to use it from desktop" = "Тримайте додаток відкритим, щоб використовувати його з робочого столу"; -/* No comment provided by engineer. */ +/* alert title */ "Keep unused invitation?" = "Зберігати невикористані запрошення?"; /* No comment provided by engineer. */ @@ -2689,9 +2667,6 @@ /* No comment provided by engineer. */ "Live messages" = "Живі повідомлення"; -/* No comment provided by engineer. */ -"No push server" = "Локально"; - /* No comment provided by engineer. */ "Local name" = "Місцева назва"; @@ -2704,24 +2679,15 @@ /* No comment provided by engineer. */ "Lock mode" = "Режим блокування"; -/* No comment provided by engineer. */ -"Make a private connection" = "Створіть приватне з'єднання"; - /* No comment provided by engineer. */ "Make one message disappear" = "Зробити так, щоб одне повідомлення зникло"; /* No comment provided by engineer. */ "Make profile private!" = "Зробіть профіль приватним!"; -/* No comment provided by engineer. */ -"Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." = "Переконайтеся, що адреси серверів %@ мають правильний формат, розділені рядками і не дублюються (%@)."; - /* No comment provided by engineer. */ "Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "Переконайтеся, що адреси серверів WebRTC ICE мають правильний формат, розділені рядками і не дублюються."; -/* No comment provided by engineer. */ -"Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" = "Багато людей запитували: *якщо SimpleX не має ідентифікаторів користувачів, як він може доставляти повідомлення?*"; - /* No comment provided by engineer. */ "Mark deleted for everyone" = "Позначити видалено для всіх"; @@ -3043,12 +3009,18 @@ /* No comment provided by engineer. */ "No permission to record voice message" = "Немає дозволу на запис голосового повідомлення"; +/* No comment provided by engineer. */ +"No push server" = "Локально"; + /* No comment provided by engineer. */ "No received or sent files" = "Немає отриманих або відправлених файлів"; /* copied message info in history */ "no text" = "без тексту"; +/* No comment provided by engineer. */ +"No user identifiers." = "Ніяких ідентифікаторів користувачів."; + /* No comment provided by engineer. */ "Not compatible!" = "Не сумісні!"; @@ -3168,18 +3140,9 @@ /* authentication reason */ "Open migration to another device" = "Відкрита міграція на інший пристрій"; -/* No comment provided by engineer. */ -"Open server settings" = "Відкрити налаштування сервера"; - /* No comment provided by engineer. */ "Open Settings" = "Відкрийте Налаштування"; -/* authentication reason */ -"Open user profiles" = "Відкрити профілі користувачів"; - -/* No comment provided by engineer. */ -"Anybody can host servers." = "Кожен може хостити сервери."; - /* No comment provided by engineer. */ "Opening app…" = "Відкриваємо програму…"; @@ -3201,9 +3164,6 @@ /* No comment provided by engineer. */ "Other" = "Інше"; -/* No comment provided by engineer. */ -"Other %@ servers" = "Інші сервери %@"; - /* No comment provided by engineer. */ "other errors" = "інші помилки"; @@ -3252,9 +3212,6 @@ /* No comment provided by engineer. */ "Pending" = "В очікуванні"; -/* No comment provided by engineer. */ -"You decide who can connect." = "Ви вирішуєте, хто може під\'єднатися."; - /* No comment provided by engineer. */ "Periodic" = "Періодично"; @@ -3330,9 +3287,6 @@ /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Зберегти чернетку останнього повідомлення з вкладеннями."; -/* No comment provided by engineer. */ -"Preset server" = "Попередньо встановлений сервер"; - /* No comment provided by engineer. */ "Preset server address" = "Попередньо встановлена адреса сервера"; @@ -3381,7 +3335,7 @@ /* No comment provided by engineer. */ "Profile theme" = "Тема профілю"; -/* No comment provided by engineer. */ +/* alert message */ "Profile update will be sent to your contacts." = "Оновлення профілю буде надіслано вашим контактам."; /* No comment provided by engineer. */ @@ -3463,10 +3417,10 @@ "Read more" = "Читати далі"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." = "Читайте більше в [Посібнику користувача](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)."; +"Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." = "Читайте більше в [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)."; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." = "Читайте більше в [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)."; +"Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." = "Читайте більше в [Посібнику користувача](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)."; /* No comment provided by engineer. */ "Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." = "Читайте більше в [Посібнику користувача](https://simplex.chat/docs/guide/readme.html#connect-to-friends)."; @@ -3474,9 +3428,6 @@ /* No comment provided by engineer. */ "Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." = "Читайте більше в нашому [GitHub репозиторії](https://github.com/simplex-chat/simplex-chat#readme)."; -/* No comment provided by engineer. */ -"Read more in our GitHub repository." = "Читайте більше в нашому репозиторії на GitHub."; - /* No comment provided by engineer. */ "Receipts are disabled" = "Підтвердження виключені"; @@ -3746,7 +3697,7 @@ /* No comment provided by engineer. */ "Save servers" = "Зберегти сервери"; -/* No comment provided by engineer. */ +/* alert title */ "Save servers?" = "Зберегти сервери?"; /* No comment provided by engineer. */ @@ -3890,9 +3841,6 @@ /* No comment provided by engineer. */ "Send notifications" = "Надсилати сповіщення"; -/* No comment provided by engineer. */ -"Send notifications:" = "Надсилати сповіщення:"; - /* No comment provided by engineer. */ "Send questions and ideas" = "Надсилайте запитання та ідеї"; @@ -4049,7 +3997,8 @@ /* No comment provided by engineer. */ "Shape profile images" = "Сформуйте зображення профілю"; -/* chat item action */ +/* alert action + chat item action */ "Share" = "Поділіться"; /* No comment provided by engineer. */ @@ -4058,7 +4007,7 @@ /* No comment provided by engineer. */ "Share address" = "Поділитися адресою"; -/* No comment provided by engineer. */ +/* alert title */ "Share address with contacts?" = "Поділіться адресою з контактами?"; /* No comment provided by engineer. */ @@ -4229,10 +4178,10 @@ /* No comment provided by engineer. */ "Stop sending file?" = "Припинити надсилання файлу?"; -/* No comment provided by engineer. */ +/* alert action */ "Stop sharing" = "Припиніть ділитися"; -/* No comment provided by engineer. */ +/* alert title */ "Stop sharing address?" = "Припинити ділитися адресою?"; /* authentication reason */ @@ -4319,7 +4268,7 @@ /* No comment provided by engineer. */ "Test servers" = "Тестові сервери"; -/* No comment provided by engineer. */ +/* alert title */ "Tests failed!" = "Тести не пройшли!"; /* No comment provided by engineer. */ @@ -4331,9 +4280,6 @@ /* No comment provided by engineer. */ "Thanks to the users – contribute via Weblate!" = "Дякуємо користувачам - зробіть свій внесок через Weblate!"; -/* No comment provided by engineer. */ -"No user identifiers." = "Ніяких ідентифікаторів користувачів."; - /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "Додаток може сповіщати вас, коли ви отримуєте повідомлення або запити на контакт - будь ласка, відкрийте налаштування, щоб увімкнути цю функцію."; @@ -4358,6 +4304,9 @@ /* No comment provided by engineer. */ "The encryption is working and the new encryption agreement is not required. It may result in connection errors!" = "Шифрування працює і нова угода про шифрування не потрібна. Це може призвести до помилок з'єднання!"; +/* No comment provided by engineer. */ +"The future of messaging" = "Наступне покоління приватних повідомлень"; + /* No comment provided by engineer. */ "The hash of the previous message is different." = "Хеш попереднього повідомлення відрізняється."; @@ -4376,9 +4325,6 @@ /* No comment provided by engineer. */ "The messages will be marked as moderated for all members." = "Повідомлення будуть позначені як модеровані для всіх учасників."; -/* No comment provided by engineer. */ -"The future of messaging" = "Наступне покоління приватних повідомлень"; - /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "Стара база даних не була видалена під час міграції, її можна видалити."; @@ -4463,9 +4409,6 @@ /* No comment provided by engineer. */ "To make a new connection" = "Щоб створити нове з'єднання"; -/* No comment provided by engineer. */ -"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Щоб захистити конфіденційність, замість ідентифікаторів користувачів, які використовуються на всіх інших платформах, SimpleX має ідентифікатори для черг повідомлень, окремі для кожного з ваших контактів."; - /* No comment provided by engineer. */ "To protect timezone, image/voice files use UTC." = "Для захисту часового поясу у файлах зображень/голосу використовується UTC."; @@ -4475,6 +4418,9 @@ /* No comment provided by engineer. */ "To protect your IP address, private routing uses your SMP servers to deliver messages." = "Щоб захистити вашу IP-адресу, приватна маршрутизація використовує ваші SMP-сервери для доставки повідомлень."; +/* No comment provided by engineer. */ +"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Щоб захистити конфіденційність, замість ідентифікаторів користувачів, які використовуються на всіх інших платформах, SimpleX має ідентифікатори для черг повідомлень, окремі для кожного з ваших контактів."; + /* No comment provided by engineer. */ "To record voice message please grant permission to use Microphone." = "Щоб записати голосове повідомлення, будь ласка, надайте дозвіл на використання мікрофону."; @@ -4850,9 +4796,6 @@ /* No comment provided by engineer. */ "when IP hidden" = "коли IP приховано"; -/* No comment provided by engineer. */ -"When people request to connect, you can accept or reject it." = "Коли люди звертаються із запитом на підключення, ви можете прийняти або відхилити його."; - /* No comment provided by engineer. */ "When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "Коли ви ділитеся з кимось своїм профілем інкогніто, цей профіль буде використовуватися для груп, до яких вас запрошують."; @@ -4994,9 +4937,6 @@ /* No comment provided by engineer. */ "You can share this address with your contacts to let them connect with **%@**." = "Ви можете поділитися цією адресою зі своїми контактами, щоб вони могли зв'язатися з **%@**."; -/* No comment provided by engineer. */ -"You can share your address as a link or QR code - anybody can connect to you." = "Ви можете поділитися своєю адресою у вигляді посилання або QR-коду - будь-хто зможе зв'язатися з вами."; - /* No comment provided by engineer. */ "You can start chat via app Settings / Database or by restarting the app" = "Запустити чат можна через Налаштування програми / База даних або перезапустивши програму"; @@ -5009,7 +4949,7 @@ /* No comment provided by engineer. */ "You can use markdown to format messages:" = "Ви можете використовувати розмітку для форматування повідомлень:"; -/* No comment provided by engineer. */ +/* alert message */ "You can view invitation link again in connection details." = "Ви можете переглянути посилання на запрошення ще раз у деталях підключення."; /* No comment provided by engineer. */ @@ -5030,6 +4970,9 @@ /* No comment provided by engineer. */ "You could not be verified; please try again." = "Вас не вдалося верифікувати, спробуйте ще раз."; +/* No comment provided by engineer. */ +"You decide who can connect." = "Ви вирішуєте, хто може під'єднатися."; + /* No comment provided by engineer. */ "You have already requested connection via this address!" = "Ви вже надсилали запит на підключення за цією адресою!"; @@ -5120,9 +5063,6 @@ /* No comment provided by engineer. */ "You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed" = "Ви використовуєте профіль інкогніто для цієї групи - щоб запобігти поширенню вашого основного профілю, запрошення контактів заборонено"; -/* No comment provided by engineer. */ -"Your %@ servers" = "Ваші сервери %@"; - /* No comment provided by engineer. */ "Your calls" = "Твої дзвінки"; @@ -5174,9 +5114,6 @@ /* No comment provided by engineer. */ "Your random profile" = "Ваш випадковий профіль"; -/* No comment provided by engineer. */ -"Your server" = "Ваш сервер"; - /* No comment provided by engineer. */ "Your server address" = "Адреса вашого сервера"; @@ -5189,6 +5126,3 @@ /* No comment provided by engineer. */ "Your SMP servers" = "Ваші SMP-сервери"; -/* No comment provided by engineer. */ -"Your XFTP servers" = "Ваші XFTP-сервери"; - diff --git a/apps/ios/zh-Hans.lproj/Localizable.strings b/apps/ios/zh-Hans.lproj/Localizable.strings index a15b7d45fe..ba8dcd6e2c 100644 --- a/apps/ios/zh-Hans.lproj/Localizable.strings +++ b/apps/ios/zh-Hans.lproj/Localizable.strings @@ -1,15 +1,6 @@ /* No comment provided by engineer. */ "\n" = "\n"; -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - -/* No comment provided by engineer. */ -" " = " "; - /* No comment provided by engineer. */ " (" = " ("; @@ -319,12 +310,6 @@ /* No comment provided by engineer. */ "Abort changing address?" = "中止地址更改?"; -/* No comment provided by engineer. */ -"About SimpleX" = "关于SimpleX"; - -/* No comment provided by engineer. */ -"About SimpleX address" = "关于 SimpleX 地址"; - /* No comment provided by engineer. */ "About SimpleX Chat" = "关于SimpleX Chat"; @@ -364,12 +349,6 @@ /* No comment provided by engineer. */ "Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." = "将地址添加到您的个人资料,以便您的联系人可以与其他人共享。个人资料更新将发送给您的联系人。"; -/* No comment provided by engineer. */ -"Add contact" = "添加联系人"; - -/* No comment provided by engineer. */ -"Add preset servers" = "添加预设服务器"; - /* No comment provided by engineer. */ "Add profile" = "添加个人资料"; @@ -556,6 +535,9 @@ /* No comment provided by engineer. */ "Answer call" = "接听来电"; +/* No comment provided by engineer. */ +"Anybody can host servers." = "任何人都可以托管服务器。"; + /* No comment provided by engineer. */ "App build: %@" = "应用程序构建:%@"; @@ -778,7 +760,8 @@ /* No comment provided by engineer. */ "Can't message member" = "无法向成员发送消息"; -/* alert button */ +/* alert action + alert button */ "Cancel" = "取消"; /* No comment provided by engineer. */ @@ -896,7 +879,7 @@ /* No comment provided by engineer. */ "Chats" = "聊天"; -/* No comment provided by engineer. */ +/* alert title */ "Check server address and try again." = "检查服务器地址并再试一次。"; /* No comment provided by engineer. */ @@ -959,9 +942,6 @@ /* No comment provided by engineer. */ "Configure ICE servers" = "配置 ICE 服务器"; -/* No comment provided by engineer. */ -"Configured %@ servers" = "已配置 %@ 服务器"; - /* No comment provided by engineer. */ "Confirm" = "确认"; @@ -1187,9 +1167,6 @@ /* No comment provided by engineer. */ "Create a group using a random profile." = "使用随机身份创建群组."; -/* No comment provided by engineer. */ -"Create an address to let people connect with you." = "创建一个地址,让人们与您联系。"; - /* server test step */ "Create file" = "创建文件"; @@ -1349,7 +1326,8 @@ /* No comment provided by engineer. */ "default (yes)" = "默认 (是)"; -/* chat item action +/* alert action + chat item action swipe action */ "Delete" = "删除"; @@ -1933,9 +1911,6 @@ /* No comment provided by engineer. */ "Error joining group" = "加入群组错误"; -/* No comment provided by engineer. */ -"Error loading %@ servers" = "加载 %@ 服务器错误"; - /* No comment provided by engineer. */ "Error opening chat" = "打开聊天时出错"; @@ -1954,9 +1929,6 @@ /* No comment provided by engineer. */ "Error resetting statistics" = "重置统计信息时出错"; -/* No comment provided by engineer. */ -"Error saving %@ servers" = "保存 %@ 服务器错误"; - /* No comment provided by engineer. */ "Error saving group profile" = "保存群组资料错误"; @@ -2338,9 +2310,6 @@ /* time unit */ "hours" = "小时"; -/* No comment provided by engineer. */ -"How it works" = "工作原理"; - /* No comment provided by engineer. */ "How SimpleX works" = "SimpleX的工作原理"; @@ -2480,10 +2449,10 @@ "Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)" = "安装[用于终端的 SimpleX Chat](https://github.com/simplex-chat/simplex-chat)"; /* No comment provided by engineer. */ -"Instant push notifications will be hidden!\n" = "即时推送通知将被隐藏!\n"; +"Instant" = "即时"; /* No comment provided by engineer. */ -"Instant" = "即时"; +"Instant push notifications will be hidden!\n" = "即时推送通知将被隐藏!\n"; /* No comment provided by engineer. */ "Interface" = "界面"; @@ -2521,7 +2490,7 @@ /* No comment provided by engineer. */ "Invalid response" = "无效的响应"; -/* No comment provided by engineer. */ +/* alert title */ "Invalid server address!" = "无效的服务器地址!"; /* item status text */ @@ -2623,7 +2592,7 @@ /* No comment provided by engineer. */ "Joining group" = "加入群组中"; -/* No comment provided by engineer. */ +/* alert action */ "Keep" = "保留"; /* No comment provided by engineer. */ @@ -2632,7 +2601,7 @@ /* No comment provided by engineer. */ "Keep the app open to use it from desktop" = "保持应用程序打开状态以从桌面使用它"; -/* No comment provided by engineer. */ +/* alert title */ "Keep unused invitation?" = "保留未使用的邀请吗?"; /* No comment provided by engineer. */ @@ -2689,9 +2658,6 @@ /* No comment provided by engineer. */ "Live messages" = "实时消息"; -/* No comment provided by engineer. */ -"No push server" = "本地"; - /* No comment provided by engineer. */ "Local name" = "本地名称"; @@ -2704,24 +2670,15 @@ /* No comment provided by engineer. */ "Lock mode" = "锁定模式"; -/* No comment provided by engineer. */ -"Make a private connection" = "建立私密连接"; - /* No comment provided by engineer. */ "Make one message disappear" = "使一条消息消失"; /* No comment provided by engineer. */ "Make profile private!" = "将个人资料设为私密!"; -/* No comment provided by engineer. */ -"Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." = "请确保 %@服 务器地址格式正确,每行一个地址并且不重复 (%@)。"; - /* No comment provided by engineer. */ "Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "确保 WebRTC ICE 服务器地址格式正确、每行分开且不重复。"; -/* No comment provided by engineer. */ -"Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" = "许多人问: *如果SimpleX没有用户标识符,它怎么传递信息?*"; - /* No comment provided by engineer. */ "Mark deleted for everyone" = "标记为所有人已删除"; @@ -3043,12 +3000,18 @@ /* No comment provided by engineer. */ "No permission to record voice message" = "没有录制语音消息的权限"; +/* No comment provided by engineer. */ +"No push server" = "本地"; + /* No comment provided by engineer. */ "No received or sent files" = "未收到或发送文件"; /* copied message info in history */ "no text" = "无文本"; +/* No comment provided by engineer. */ +"No user identifiers." = "没有用户标识符。"; + /* No comment provided by engineer. */ "Not compatible!" = "不兼容!"; @@ -3168,18 +3131,9 @@ /* authentication reason */ "Open migration to another device" = "打开迁移到另一台设备"; -/* No comment provided by engineer. */ -"Open server settings" = "打开服务器设置"; - /* No comment provided by engineer. */ "Open Settings" = "打开设置"; -/* authentication reason */ -"Open user profiles" = "打开用户个人资料"; - -/* No comment provided by engineer. */ -"Anybody can host servers." = "任何人都可以托管服务器。"; - /* No comment provided by engineer. */ "Opening app…" = "正在打开应用程序…"; @@ -3201,9 +3155,6 @@ /* No comment provided by engineer. */ "Other" = "其他"; -/* No comment provided by engineer. */ -"Other %@ servers" = "其他 %@ 服务器"; - /* No comment provided by engineer. */ "other errors" = "其他错误"; @@ -3252,9 +3203,6 @@ /* No comment provided by engineer. */ "Pending" = "待定"; -/* No comment provided by engineer. */ -"You decide who can connect." = "你决定谁可以连接。"; - /* No comment provided by engineer. */ "Periodic" = "定期"; @@ -3330,9 +3278,6 @@ /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "保留最后的消息草稿及其附件。"; -/* No comment provided by engineer. */ -"Preset server" = "预设服务器"; - /* No comment provided by engineer. */ "Preset server address" = "预设服务器地址"; @@ -3381,7 +3326,7 @@ /* No comment provided by engineer. */ "Profile theme" = "个人资料主题"; -/* No comment provided by engineer. */ +/* alert message */ "Profile update will be sent to your contacts." = "个人资料更新将被发送给您的联系人。"; /* No comment provided by engineer. */ @@ -3463,10 +3408,10 @@ "Read more" = "阅读更多"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." = "在 [用户指南](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses) 中阅读更多内容。"; +"Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." = "阅读更多[User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)。"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." = "阅读更多[User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)。"; +"Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses)." = "在 [用户指南](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses) 中阅读更多内容。"; /* No comment provided by engineer. */ "Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." = "在 [用户指南](https://simplex.chat/docs/guide/readme.html#connect-to-friends) 中阅读更多内容。"; @@ -3474,9 +3419,6 @@ /* No comment provided by engineer. */ "Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." = "在我们的 [GitHub 仓库](https://github.com/simplex-chat/simplex-chat#readme) 中阅读更多信息。"; -/* No comment provided by engineer. */ -"Read more in our GitHub repository." = "在我们的 GitHub 仓库中阅读更多内容。"; - /* No comment provided by engineer. */ "Receipts are disabled" = "回执已禁用"; @@ -3746,7 +3688,7 @@ /* No comment provided by engineer. */ "Save servers" = "保存服务器"; -/* No comment provided by engineer. */ +/* alert title */ "Save servers?" = "保存服务器?"; /* No comment provided by engineer. */ @@ -3890,9 +3832,6 @@ /* No comment provided by engineer. */ "Send notifications" = "发送通知"; -/* No comment provided by engineer. */ -"Send notifications:" = "发送通知:"; - /* No comment provided by engineer. */ "Send questions and ideas" = "发送问题和想法"; @@ -4049,7 +3988,8 @@ /* No comment provided by engineer. */ "Shape profile images" = "改变个人资料图形状"; -/* chat item action */ +/* alert action + chat item action */ "Share" = "分享"; /* No comment provided by engineer. */ @@ -4058,7 +3998,7 @@ /* No comment provided by engineer. */ "Share address" = "分享地址"; -/* No comment provided by engineer. */ +/* alert title */ "Share address with contacts?" = "与联系人分享地址?"; /* No comment provided by engineer. */ @@ -4229,10 +4169,10 @@ /* No comment provided by engineer. */ "Stop sending file?" = "停止发送文件?"; -/* No comment provided by engineer. */ +/* alert action */ "Stop sharing" = "停止分享"; -/* No comment provided by engineer. */ +/* alert title */ "Stop sharing address?" = "停止分享地址?"; /* authentication reason */ @@ -4319,7 +4259,7 @@ /* No comment provided by engineer. */ "Test servers" = "测试服务器"; -/* No comment provided by engineer. */ +/* alert title */ "Tests failed!" = "测试失败!"; /* No comment provided by engineer. */ @@ -4331,9 +4271,6 @@ /* No comment provided by engineer. */ "Thanks to the users – contribute via Weblate!" = "感谢用户——通过 Weblate 做出贡献!"; -/* No comment provided by engineer. */ -"No user identifiers." = "没有用户标识符。"; - /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "该应用可以在您收到消息或联系人请求时通知您——请打开设置以启用通知。"; @@ -4358,6 +4295,9 @@ /* No comment provided by engineer. */ "The encryption is working and the new encryption agreement is not required. It may result in connection errors!" = "加密正在运行,不需要新的加密协议。这可能会导致连接错误!"; +/* No comment provided by engineer. */ +"The future of messaging" = "下一代私密通讯软件"; + /* No comment provided by engineer. */ "The hash of the previous message is different." = "上一条消息的散列不同。"; @@ -4376,9 +4316,6 @@ /* No comment provided by engineer. */ "The messages will be marked as moderated for all members." = "对于所有成员,这些消息将被标记为已审核。"; -/* No comment provided by engineer. */ -"The future of messaging" = "下一代私密通讯软件"; - /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "旧数据库在迁移过程中没有被移除,可以删除。"; @@ -4463,9 +4400,6 @@ /* No comment provided by engineer. */ "To make a new connection" = "建立新连接"; -/* No comment provided by engineer. */ -"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "为了保护隐私,SimpleX使用针对消息队列的标识符,而不是所有其他平台使用的用户ID,每个联系人都有独立的标识符。"; - /* No comment provided by engineer. */ "To protect timezone, image/voice files use UTC." = "为了保护时区,图像/语音文件使用 UTC。"; @@ -4475,6 +4409,9 @@ /* No comment provided by engineer. */ "To protect your IP address, private routing uses your SMP servers to deliver messages." = "为了保护您的 IP 地址,私有路由使用您的 SMP 服务器来传递邮件。"; +/* No comment provided by engineer. */ +"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "为了保护隐私,SimpleX使用针对消息队列的标识符,而不是所有其他平台使用的用户ID,每个联系人都有独立的标识符。"; + /* No comment provided by engineer. */ "To record voice message please grant permission to use Microphone." = "请授权使用麦克风以录制语音消息。"; @@ -4850,9 +4787,6 @@ /* No comment provided by engineer. */ "when IP hidden" = "当 IP 隐藏时"; -/* No comment provided by engineer. */ -"When people request to connect, you can accept or reject it." = "当人们请求连接时,您可以接受或拒绝它。"; - /* No comment provided by engineer. */ "When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "当您与某人共享隐身聊天资料时,该资料将用于他们邀请您加入的群组。"; @@ -4994,9 +4928,6 @@ /* No comment provided by engineer. */ "You can share this address with your contacts to let them connect with **%@**." = "您可以与您的联系人分享该地址,让他们与 **%@** 联系。"; -/* No comment provided by engineer. */ -"You can share your address as a link or QR code - anybody can connect to you." = "您可以将您的地址作为链接或二维码共享——任何人都可以连接到您。"; - /* No comment provided by engineer. */ "You can start chat via app Settings / Database or by restarting the app" = "您可以通过应用程序设置/数据库或重新启动应用程序开始聊天"; @@ -5009,7 +4940,7 @@ /* No comment provided by engineer. */ "You can use markdown to format messages:" = "您可以使用 markdown 来编排消息格式:"; -/* No comment provided by engineer. */ +/* alert message */ "You can view invitation link again in connection details." = "您可以在连接详情中再次查看邀请链接。"; /* No comment provided by engineer. */ @@ -5030,6 +4961,9 @@ /* No comment provided by engineer. */ "You could not be verified; please try again." = "您的身份无法验证,请再试一次。"; +/* No comment provided by engineer. */ +"You decide who can connect." = "你决定谁可以连接。"; + /* No comment provided by engineer. */ "You have already requested connection via this address!" = "你已经请求通过此地址进行连接!"; @@ -5120,9 +5054,6 @@ /* No comment provided by engineer. */ "You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed" = "您正在为该群组使用隐身个人资料——为防止共享您的主要个人资料,不允许邀请联系人"; -/* No comment provided by engineer. */ -"Your %@ servers" = "您的 %@ 服务器"; - /* No comment provided by engineer. */ "Your calls" = "您的通话"; @@ -5174,9 +5105,6 @@ /* No comment provided by engineer. */ "Your random profile" = "您的随机资料"; -/* No comment provided by engineer. */ -"Your server" = "您的服务器"; - /* No comment provided by engineer. */ "Your server address" = "您的服务器地址"; @@ -5189,6 +5117,3 @@ /* No comment provided by engineer. */ "Your SMP servers" = "您的 SMP 服务器"; -/* No comment provided by engineer. */ -"Your XFTP servers" = "您的 XFTP 服务器"; - diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml index 147d79002b..d9d86634b1 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml @@ -2047,7 +2047,7 @@ استخدم التطبيق بيد واحدة. صُدرت قاعدة بيانات الدردشة التحكم في شبكتك - حذف ما يصل إلى 20 رسالة في وقت واحد. + حذف ما يصل إلى 20 رسالة في آن واحد. لم يتم تصدير بعض الملفات يحمي عنوان IP الخاص بك واتصالاتك. اتصال TCP @@ -2119,7 +2119,7 @@ شكل الرسالة قابل للتخصيص. تبديل الصوت والفيديو أثناء المكالمة. حذف أو إشراف ما يصل إلى 200 رسالة. - حوّل ما يصل إلى 20 رسالة آن واحد. + حوّل ما يصل إلى 20 رسالة في آن واحد. مكالمات أفضل تواريخ أفضل للرسائل. أمان أفضل ✅ diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/el/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/el/strings.xml index cb4185ccb7..4b1d1b8838 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/el/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/el/strings.xml @@ -243,4 +243,159 @@ Προσθέστε τη διεύθυνση στο προφίλ σας, έτσι ώστε οι επαφές σας να μπορούν να τη μοιραστούν με άλλα άτομα. Το ενημέρωμένο προφίλ θα σταλεί στις επαφές σας. διαχειριστές Λάθη αναγνώρισης + Προειδοποίηση: το αρχείο θα διαγραφεί.]]> + Υπέρβαση χωρητικότητας - ο παραλήπτης δεν έλαβε μηνύματα που στάλθηκαν προηγουμένως. + αποκλεισμένος από τον διαχειριστή + Συνομιλίες + όλα τα μέλη + Όλες οι επαφές σας θα παραμείνουν ενεργές. Το ανανεωμένο προφίλ σας θα αποσταλεί στις επαφές σας. + Να χρησιμοποιείται πάντα ιδιωτική δρομολόγηση. + Ένα κενό προφίλ συνομιλίας με το παρεχόμενο όνομα δημιουργείται και η εφαρμογή ανοίγει ως συνήθως. + Η βάση δεδομένων της συνομιλίας διαγράφηκε + Απενεργοποίηση ήχου + Eνεργοποίηση ήχου + Κακό μήνυμα hash + Θάμπωση των μέσων + ΒΑΣΗ ΔΕΔΟΜΕΝΩΝ ΣΥΝΟΜΙΛΙΑΣ + Το Android Keystore χρησιμοποιείται για την ασφαλή αποθήκευση της φράσης πρόσβασης - επιτρέπει την υπηρεσία ειδοποιήσεων να λειτουργεί. + αποκλεισμένος + Αποκλεισμένος από τον διαχειριστή + Δεν είναι δυνατή η κλήση επαφής + Θέμα εφαρμογής + Εφαρμογή σε + Η εφαρμογή κρυπτογραφεί νέα τοπικά αρχεία (εκτός απο βίντεο). + Καλύτερες ομάδες + Γίνεται ήδη συμμετοχή στην ομάδα! + Αρχειοθέτηση και αποστολή + %1$d διαφορετικό/κα σφάλμα/τα αρχείου/ων. + Η υπηρεσία παρασκηνίου λειτουργεί πάντα - οι ειδοποιήσεις θα εμφανίζονται μόλις τα μηνύματα είναι διαθέσιμα. + %1$d αρχείο/α ακόμα κατεβαίνουν. + %1$d αρχείο/α απέτυχε/χαν να παραληφθεί/ουν + %1$d αρχείο/α διαγράφηκε/καν. + %1$d αρχείο/α δεν κατέβηκε/καν. + %1$s μήνυμα/τα δεν προωθήθηκε/καν + Προφίλ συνομιλίας + για κάθε προφίλ συνομιλίας που έχετε στην εφαρμογή.]]> + Παρακαλώ σημειώστε: οι αναμεταδότες μηνυμάτων και αρχείων συνδέονται μέσω διακομιστή μεσολάβησης SOCKS. Οι κλήσεις και οι προεπισκοπήσεις συνδέσμων αποστολής χρησιμοποιούν άμεση σύνδεση.]]> + Πάντα + Η ενημέρωση της εφαρμογής κατεβαίνει + Έλεγχος για ενημερώσεις + Οποιοσδήποτε μπορεί να φιλοξενήσει διακομιστές. + κλήση ήχου (χωρίς κρυπτογράφηση e2e) + Κλήσεις στην οθόνη κλειδώματος: + Κλήση ήχου + \'Εκδοση Εφαρμογής: %s + Απαγορεύονται οι κλήσεις ήχου/βίντεο. + Αποκλεισμός μελών ομάδας + Τα chunks διαγράφηκαν + Όλα τα δεδομένα διαγράφονται κατά την εισαγωγή. + Αρχειοθετημένες επαφές + Ακύρωση μεταφοράς + Χρώματα συνομιλίας + ΒΑΣΗ ΔΕΔΟΜΕΝΩΝ ΣΥΝΟΜΙΛΙΑΣ + Η συνομιλία εκτελείται + Παρακαλώ σημειώστε: ΔΕΝ θα μπορείτε να ανακτήσετε ή να αλλάξετε τη φράση πρόσβασης εάν τη χάσετε.]]> + Αποκλεισμός για όλους + Και εσείς και η επαφή σας μπορείτε να προσθέστε αντιδράσεις μηνυμάτων. + Και εσείς και η επαφή σας μπορείτε να κάνετε κλήσεις. + Επιτρέψτε την αποστολή συνδέσμων SimpleX. + Αραβικά, Βουλγαρικά, Φινλανδικά, Εβραϊκά, Ταϊλανδέζικα και Ουκρανικά - χάρη στους χρήστες και το Weblate. + Μεταφορά δεδομένων εφαρμογής + Θάμπωμα για καλύτερη ιδιωτικότητα. + Η συνομιλία έχει μεταφερθεί! + Αρχειοθέτηση της βάσης δεδομένων + Όλες οι επαφές, συζητήσεις και αρχεία θα κρυπτογραφηθούν με ασφάλεια και θα μεταφορτωθούν σε διαμορφωμένα κομμάτια αναμετάδοσης XFTP. + Κινητή τηλεφωνία + Δημιουργία ομάδας : για την δημιουργίας νέας ομάδας.]]> + Ελέγξτε τη σύνδεσή σας στο διαδίκτυο και δοκιμάστε ξανά + Συζήτηση με τους προγραμματιστές + Ζήτησε να λάβει το βίντεο + Δεν είναι δυνατή η αποστολή μηνυμάτων στο μέλος της ομάδας + Αλλαγή λειτουργίας κλειδώματος + αποκλεισμένος %s + άλλαξε η διεύθυνση για εσάς + και %d άλλες εκδηλώσεις + Μαύρο + Πρόσθετο δευτερεύον + Και εσείς και η επαφή σας μπορείτε να διαγράψετε απεσταλμένα μηνύματα χωρίς ανατροπή. (24 ώρες) + Και εσείς και η επαφή σας μπορείτε να στείλετε ηχητικά μηνύματα. + Η συνομιλία σταμάτησε + Η συνομιλία έχει διακοπεί. Εάν χρησιμοποιήσατε ήδη αυτήν τη βάση δεδομένων σε άλλη συσκευή, θα πρέπει να τη μεταφέρετε πίσω προτού ξεκινήσετε τη συνομιλία. + Η λειτουργία βελτιστοποίησης της μπαταρίας είναι ενεργή, η υπηρεσία παρασκηνίου και τα περιοδικά αιτήματα για νέα μηνύματα θα απενεργοποιηθούν. Μπορείτε να τα ενεργοποιήσετε ξανά μέσω των ρυθμίσεων. + σύνδεσμος μιας χρήσης + Κλήσεις ήχου & βίντεο + Κλήσεις ήχου/βίντεο + Κωδικός εφαρμογής + Συνεδρία εφαρμογής + Η συνομιλία σταμάτησε + Έλεγχος για ενημερώσεις + Κινεζική και Ισπανική διεπαφή + Καλύτερες ημερομηνίες μηνυμάτων + Bluetooth + έντονο + Κονσόλα συνομιλίας + Παρακαλώ σημειώστε: η χρήση της ίδιας βάσης δεδομένων σε δύο συσκευές θα διακόψει την αποκρυπτογράφηση των μηνυμάτων από τις συνδέσεις σας, ως προστασία ασφαλείας.]]> + Χρησιμοποιεί περισσότερη μπαταρία! Η εφαρμογή εκτελείται πάντα στο παρασκήνιο - οι ειδοποιήσεις εμφανίζονται αμέσως.]]> + Η βάση δεδομένων της συνομιλίας εξάχθηκε + κλήση + Κακή διεύθυνση Desktop + Μεταφορά απο άλλη συσκευή στη νέα συσκευή και σαρώστε τον κωδικό QR.]]> + Με προφίλ συνομιλίας (προεπιλογή) ή μέσω σύνδεσης (BETA). + Κάμερα και μικρόφωνο + 6 νέες γλώσσες διεπαφής + Καλό για την μπαταρία. Η εφαρμογή ελέγχει για την παραλαβή μηνυμάτων κάθε 10 λεπτά. Ενδέχεται να χάσετε κλήσεις ή επείγοντα μηνύματα.]]> + Επισύναψη + Διακοπή αλλαγής διεύθυνσης; + Επιλέξτε ένα αρχείο + Όλα τα νέα μηνύνματα απο %s θα αποκρυφθούν! + Δεν είναι δυνατή η λήψη του αρχείου + Πιστοποίηση + Όλα τα μηνύματα θα διαγραφούν - αυτή η ενέργεια δεν μπορεί να αντιστραφεί! + Ελέγχει νέα μηνύματα κάθε 10 λεπτά για έως και 1 λεπτό + Η εφαρμογή μπορεί να λαμβάνει ειδοποιήσεις μόνο όταν εκτελείται, καμία υπηρεσία δεν θα ξεκινήσει στο παρασκήνιο + Μπορεί να απενεργοποιηθεί μέσω των ρυθμίσεων – οι ειδοποιήσεις θα εξακολουθούν να εμφανίζονται ενώ η εφαρμογή εκτελείται.]]> + Επιτρέψτε τις επαφές σας να χρησιμοποιούν αντιδράσεις μηνυμάτων. + Και εσείς και η επαφή σας μπορείτε να στείλετε μηνύματα που εξαφανίζονται. + Κάμερα μη διαθέσιμη + Ελέγξτε την διεύθυνση του διακομιστή και δοκιμάστε ξανά. + Επιτρέψτε αντιδράσεις μηνυμάτων εφόσον οι επαφές σας το επιτρέπουν. + %1$d μήνυμα/τα παραλήφθηκε/καν. + Κλήσεις απογορευμένες! + Δεν είναι δυνατή η αποστολή μηνύματος + Η κλήση έχει ήδη τερματιστεί! + Ο κωδικός πρόσβασης της εφαρμογής αντικαθίσταται με κωδικό πρόσβασης αυτοκαταστροφής. + Το Android Keystore θα χρησιμοποιηθεί για την ασφαλή αποθήκευση της φράσης πρόσβασης μετά την επανεκκίνηση της εφαρμογής ή την αλλαγή της φράσης πρόσβασης - θα επιτρέπει τη λήψη ειδοποιήσεων. + Δεν είναι δυνατή η πρόσβαση στο Keystore για αποθήκευση του κωδικού πρόσβασης της βάσης δεδομένων + Αποκλεισμός μέλους + Αποκλεισμός μέλους; + Προτιμήσεις συνομιλίας + Καλύτερα μηνύματα + Εφαρμογή + Συνέναιση υποβάθμισης + Κάμερα + κλήση ήχου + Αρχειοθετήστε τις επαφές για να συνομιλήσετε αργότερα. + Όλα τα προφίλ + %1$d μηνύμα/τα παραλείφθηκε/καν + κακό μήνυμα hash + κακό αναγνωριστικό μηνύματος + Απάντηση κλήσης + Κακό αναγνωριστικό μηνύματος + ΣΥΝΟΜΙΛΙΕΣ + Η βάση δεδεδομένων της συνομιλίας εισάχθηκε + "συμφωνία κρυπτογράφησης για %s…" + Να επιτραπούν οι κλήσεις; + Αποκλεισμός μέλους για όλους; + Κλήσεις ήχου και βίντεο + προσπάθειες + Θέμα συνομιλίας + Καλύτερη ασφάλεια✅ + Καλύτερη εμπειρία χρήστη + Δεν είναι δυνατή η κλήση μέλους ομάδας + Ζήτησε να λάβει την εικόνα + για κάθε επαφή και μέλος ομάδας .\nΛάβετε υπόψη: εάν έχετε πολλές συνδέσεις, η κατανάλωση της μπαταρίας και της κυκλοφορίας μπορεί να είναι σημαντικά υψηλότερη και ορισμένες συνδέσεις μπορεί να αποτύχουν.]]> + Προσθήκη επαφής : για να δημιουργήσετε έναν νέο σύνδεσμο πρόσκλησης ή να συνδεθείτε μέσω ενός συνδέσμου που λάβατε.]]> + Καλύτερο για τη ζωή της μπαταρίας . Θα λαμβάνετε ειδοποιήσεις μόνο όταν εκτελείται η εφαρμογή (ΧΩΡΙΣ υπηρεσία παρασκηνίου).]]> + Beta + Καλύτερες κλήσεις \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml index 553f70e31a..8c88c1ea67 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml @@ -711,7 +711,7 @@ Envoyer un message dynamique Envoyez un message dynamique - il sera mis à jour pour le⸱s destinataire⸱s au fur et à mesure que vous le tapez Envoyer - Le rôle sera changé pour «%s». Le membre va recevoir une nouvelle invitation. + Son rôle est désormais %s. Le membre recevra une nouvelle invitation. LIVE Inviter des membres Vous pouvez partager un lien ou un code QR - n\'importe qui pourra rejoindre le groupe. Vous ne perdrez pas les membres du groupe si vous le supprimez par la suite. @@ -722,7 +722,7 @@ Seuls les propriétaires du groupe peuvent modifier les préférences du groupe. POUR TERMINAL Changer le rôle du groupe \? - Le rôle sera changé pour «%s». Les membres du groupe seront notifiés. + Son rôle est désormais %s. Tous les membres du groupe en seront informés. Contact vérifié⸱e Effacer %d contact·s sélectionné·e·s @@ -1163,7 +1163,7 @@ La mise à jour du profil sera envoyée à vos contacts. Guide de l\'utilisateur.]]> Enregistrer les paramètres de validation automatique - Pour se connecter, votre contact peut scanner le code QR ou utiliser le lien dans l\'application. + Pour se connecter, votre contact peut scanner un code QR ou utiliser un lien dans l\'app. Le code d\'accès de l\'application est remplacé par un code d\'autodestruction. Activer l\'autodestruction Un profil de chat vierge portant le nom fourni est créé et l\'application s\'ouvre normalement. @@ -1528,14 +1528,14 @@ Envoi des 100 derniers messages aux nouveaux membres. Ajouter un contact : pour créer un nouveau lien d\'invitation ou se connecter via un lien que vous avez reçu.]]> Ne pas envoyer d\'historique aux nouveaux membres. - Ou présenter ce code + Ou montrez ce code Les 100 derniers messages sont envoyés aux nouveaux membres. Le code scanné n\'est pas un code QR de lien SimpleX. Le texte collé n\'est pas un lien SimpleX. Autoriser l\'accès à la caméra Vous pouvez à nouveau consulter le lien d\'invitation dans les détails de la connexion. Conserver l\'invitation inutilisée ? - Partager ce lien d\'invitation unique + Partagez ce lien d\'invitation unique Créer un groupe : pour créer un nouveau groupe.]]> Historique visible Code d\'accès à l\'app @@ -1544,7 +1544,7 @@ Création d\'un lien… Ou scanner le code QR Code QR invalide - Ajouter le contact + Ajouter un contact Appuyez pour scanner Conserver Appuyez pour coller le lien @@ -1883,7 +1883,7 @@ Téléchargement %s (%s) Erreur de reconnexion au serveur inactif - Scanner / Coller le lien + Scanner / Coller un lien Le message peut être transmis plus tard si le membre devient actif. Reconnecter tous les serveurs connectés pour forcer la livraison des messages. Cette méthode utilise du trafic supplémentaire. Sessions de transport @@ -2069,4 +2069,61 @@ Connexion TCP Certains fichiers n\'ont pas été exportés Vous pouvez migrer la base de données exportée. + %1$d erreur(s) de fichier :\n%2$s + %1$d autre(s) erreur(s) de fichier. + Erreur lors du transfert de messages + %1$d fichier(s) est(sont) en cours de téléchargement. + %1$s messages non transférés + Télécharger + Transfert de messages… + Les messages ont été supprimés après avoir été sélectionnés. + Erreur lors du changement de profil + Sélectionner un profil de discussion + Partager le profil + Votre connexion a été déplacée vers %s mais une erreur inattendue s\'est produite lors de la redirection vers le profil. + Ne pas utiliser d\'identifiants avec le proxy. + Erreur lors de l\'enregistrement du proxy + Mot de passe + Authentification proxy + Utilisez des identifiants de proxy différents pour chaque connexion. + Vos informations d\'identification peuvent être envoyées non chiffrées. + Le téléchargement de %1$d fichier(s) a échoué. + %1$d fichier(s) a(ont) été supprimé(s). + Sécurité accrue ✅ + Une meilleure expérience pour l\'utilisateur + %1$d fichier(s) n\'a (n\'ont) pas été téléchargé(s). + Session de l\'app + Meilleures dates de messages. + Transférer %1$s message(s) ? + Transfert de %1$s messages + Assurez-vous que la configuration du proxy est correcte. + Transférer les messages sans les fichiers ? + De nouveaux identifiants SOCKS seront utilisés chaque fois que vous démarrerez l\'application. + Rien à transférer ! + Ouvrez Safari Paramètres / Sites web / Microphone, puis choisissez Autoriser pour localhost. + Sauvegarde de %1$s messages + L\'archive de la base de données envoyée sera définitivement supprimée des serveurs. + Utilisez des identifiants de proxy différents pour chaque profil. + Utiliser des identifiants aléatoires + Nom d\'utilisateur + Les messages seront supprimés - il n\'est pas possible de revenir en arrière ! + BASE DE DONNÉES DU CHAT + Mode système + Serveur + De nouveaux identifiants SOCKS seront utilisées pour chaque serveur. + Erreur lors de l\'initialisation de WebView. Assurez-vous que WebView est installé et que l\'architecture supportée est arm64.\nErreur : %s + Son muet + Coin + Forme du message + Queue + Cliquez sur le bouton info près du champ d\'adresse pour autoriser l\'utilisation du microphone. + Pour passer des appels, autorisez l\'utilisation de votre microphone. Mettez fin à l\'appel et essayez d\'appeler à nouveau. + Supprimer l\'archive ? + Appels améliorés + Forme des messages personnalisable. + Supprimer ou modérer jusqu\'à 200 messages. + Transférez jusqu\'à 20 messages à la fois. + Protocoles SimpleX audité par Trail of Bits. + Passer de l\'audio à la vidéo pendant l\'appel. + Changer de profil de chat pour les invitations à usage unique. \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml index 5edf8e786f..6fe624d621 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml @@ -21,7 +21,7 @@ A SimpleXről Kiemelés fogadott hívás - Hozzáférés a kiszolgálókhoz SOCKS proxy segítségével a %d porton? A proxyt el kell indítani, mielőtt engedélyezné ezt az opciót. + Hozzáférés a kiszolgálókhoz SOCKS proxyn keresztül a(z) %d porton? A proxyt el kell indítani, mielőtt engedélyezné ezt az opciót. Elfogadás Elfogadás gombra fent, majd: @@ -38,10 +38,9 @@ %s visszavonva Előre beállított kiszolgálók hozzáadása A hívások kezdeményezése le van tiltva ebben a csevegésben. - Minden egyes kapcsolathoz és csoporttaghoz külön TCP-kapcsolat (és SOCKS-hitelesítőadat) lesz használva. -\nMegjegyzés: ha sok kapcsolata van, az akkumulátor-használat és az adatforgalom jelentősen megnövekedhet, és néhány kapcsolódási kísérlet sikertelen lehet. + Az összes ismerőséhez és csoporttaghoz külön TCP-kapcsolat (és SOCKS-hitelesítőadat) lesz használva.\nMegjegyzés: ha sok kapcsolata van, az akkumulátor-használat és az adatforgalom jelentősen megnövekedhet, és néhány kapcsolódási kísérlet sikertelen lehet.]]> hivatkozás előnézetének visszavonása - Minden egyes kapcsolathoz és csoporttaghoz külön TCP-kapcsolat (és SOCKS-hitelesítőadat) lesz használva.]]> + Az összes csevegési profiljához az alkalmazásban külön TCP-kapcsolat (és SOCKS-hitelesítőadat) lesz használva.]]> Mindkét fél küldhet eltűnő üzeneteket. Az Android Keystore-t a jelmondat biztonságos tárolására használják - lehetővé teszi az értesítési szolgáltatás működését. Hibás az üzenet hasító értéke @@ -71,7 +70,7 @@ \nElérhető a v5.1-ben" Mindkét fél véglegesen törölheti az elküldött üzeneteket. (24 óra) Továbbfejlesztett csoportok - Minden üzenet törlésre kerül - ez a művelet nem vonható vissza! Az üzenetek CSAK az Ön számára törlődnek. + Az összes üzenet törlésre kerül - ez a művelet nem vonható vissza! Az üzenetek CSAK az Ön számára törlődnek. Hívás befejeződött HÍVÁSOK és további %d esemény @@ -87,10 +86,10 @@ Az üzenetreakciók küldése csak abban az esetben van engedélyezve, ha az ismerőse is engedélyezi. Vissza Kikapcsolható a beállításokban – az értesítések továbbra is megjelenítésre kerülnek amíg az alkalmazás fut.]]> - Az adminok hivatkozásokat hozhatnak létre a csoportokhoz való csatlakozáshoz. + Az adminisztrátorok hivatkozásokat hozhatnak létre a csoportokhoz való csatlakozáshoz. Hívások a zárolási képernyőn: titkosítás elfogadása… - Ismerős meghívása nem lehetséges! + Nem lehet meghívni az ismerőst! téves üzenet ID Kapcsolatkérések automatikus elfogadása Megjegyzés: NEM fogja tudni helyreállítani, vagy megváltoztatni a jelmondatot abban az esetben, ha elveszíti.]]> @@ -99,18 +98,18 @@ Hozzáadás egy másik eszközhöz Az üzenetreakciók küldése engedélyezve van. Fájlelőnézet visszavonása - Minden csoporttag kapcsolatban marad. + Az összes csoporttag kapcsolatban marad. Több akkumulátort használ! Az alkalmazás mindig fut a háttérben - az értesítések azonnal megjelennek.]]> Letiltás - admin + adminisztrátor Fénykép előnézet visszavonása - A jelkód megadása után minden adat törlésre kerül. + A jelkód megadása után az összes adat törlésre kerül. Felkérték a videó fogadására Letiltás Még néhány dolog Hitelesítés visszavonva A fájlok- és a médiatartalmak küldése engedélyezve van. - Minden csevegés és üzenet törlésre kerül - ez a művelet nem vonható vissza! + Az összes csevegés és üzenet törlésre kerül - ez a művelet nem vonható vissza! hanghívás félkövér Az alkalmazás jelkód helyettesítésre kerül egy önmegsemmisítő jelkóddal. @@ -120,14 +119,14 @@ mindig A hívás már befejeződött! Engedélyezés - Minden ismerősével kapcsolatban marad. + Az összes ismerősével kapcsolatban marad. Élő csevegési üzenet visszavonása Az üzenetek végleges törlése csak abban az esetben van engedélyezve, ha az ismerőse is engedélyezi. (24 óra) Hang- és videóhívások hibás az üzenet hasító értéke Mindig fut Az Android Keystore biztonságosan fogja tárolni a jelmondatot az alkalmazás újraindítása, vagy a jelmondat megváltoztatás után - lehetővé teszi az értesítések fogadását. - Minden alkalmazásadat törölve. + Az összes alkalmazásadat törölve. Legjobb akkumulátoridő. Csak akkor kap értesítéseket, amikor az alkalmazás meg van nyitva. (NINCS háttérszolgáltatás.)]]> Megjelenés Az akkumulátor-optimalizálás aktív, ez kikapcsolja a háttérszolgáltatást és az új üzenetek rendszeres lekérdezését. A beállításokban újraengedélyezheti. @@ -151,7 +150,7 @@ hívás folyamatban Képek automatikus elfogadása A hívások kezdeményezése engedélyezve van az ismerősei számára. - ALKALMAZÁS IKON + ALKALMAZÁSIKON Kiszolgáló hozzáadása QR-kód beolvasásával. Az eltűnő üzenetek küldése engedélyezve van. Az eltűnő üzenetek küldése csak abban az esetben van engedélyezve, ha az ismerőse is engedélyezi. @@ -162,7 +161,7 @@ Mindkét fél küldhet üzenetreakciókat. Mindkét fél tud hívásokat kezdeményezni. Sikertelen hitelesítés - Minden %s által írt új üzenet elrejtésre kerül! + Az összes %s által írt új üzenet elrejtésre kerül! Alkalmazás verzió: v%s A hívások kezdeményezése csak abban az esetben van engedélyezve, ha az ismerőse is engedélyezi. Kiszolgáló hozzáadása @@ -220,7 +219,7 @@ Kapcsolat Név helyesbítése erre: %s? Időtúllépés kapcsolódáskor - Kapcsolódás %1$s által? + Kapcsolódás a következővel: %1$s? Létrehozás Ismerős beállításai Kapcsolat @@ -230,7 +229,7 @@ Ismerős engedélyezi Rejtett név: Társítás számítógéppel - Környezeti ikon + Szövegkörnyezeti ikon Kapcsolódás egy hivatkozáson keresztül Ismerősök Kapcsolódási hiba @@ -255,12 +254,12 @@ Törölve ekkor: %s Törölve ekkor: Kínai és spanyol kezelőfelület - Ismerősök meghívása nem lehetséges! + Nem lehet meghívni az ismerősöket! A csevegés leállt Sötét Profil létrehozása törölt csoport - Törlés mindenkinél + Törlés az összes tagnál Hivatkozás létrehozása Csevegési beállítások Csevegési archívum @@ -327,7 +326,7 @@ kapcsolódás (bejelentve) Csoporthivatkozás létrehozása Csevegési konzol - Fájlok törlése minden csevegési profilból + Fájlok törlése az összes csevegési profilból Sorbaállítás törlése Ismerős törlése Létrehozva ekkor: %1$s @@ -363,8 +362,8 @@ Kiszolgáló törlése Az eszközön nincs beállítva a képernyőzár. A SimpleX-zár ki van kapcsolva. Letiltás - Letiltás minden csoport számára - Engedélyezés minden csoport számára + Letiltás az összes csoport számára + Engedélyezés az összes csoport számára engedélyezve az ismerős számára Az eltűnő üzenetek küldése le van tiltva ebben a csoportban. Cím törlése @@ -384,7 +383,7 @@ Az adatbázis-jelmondat eltér a Keystore-ban lévőtől. Közvetlen üzenetek E-mail - Letiltás mindenki számára + Letiltás az összes tag számára Fejlesztői eszközök Adatbázis-jelmondat %d nap @@ -427,7 +426,7 @@ Törlés, és az ismerős értesítése letiltva %d másodperc - Minden fájl törlése + Az összes fájl törlése Az adatbázis titkosításra kerül. Adatbázis-jelmondat és -exportálás Az adatbázis titkosításra kerül és a jelmondat a Keystore-ban lesz tárolva. @@ -462,7 +461,7 @@ %d fájl %s összméretben A csevegés megnyitásához adja meg az adatbázis jelmondatát. %dnap - Engedélyezés mindenki számára + Engedélyezés az összes tag számára A kézbesítési jelentések le vannak tiltva! Kibontás Hiba az üzenet küldésekor @@ -528,11 +527,11 @@ Kézbesítési jelentések engedélyezése? Hiba a csoportprofil mentésekor hiba - A fájl törölve lesz a kiszolgálóról. + A fájl törölve lesz a kiszolgálókról. Akkor is, ha le van tiltva a beszélgetésben. Gyorsabb csatlakozás és megbízhatóbb üzenetkézbesítés. Zárolás engedélyezése - SEGÍTSÉG + SÚGÓ Teljesen decentralizált - csak a tagok számára látható. Fájl: %s Hívás befejezése @@ -587,7 +586,7 @@ Hiba A csoportmeghívó már nem érvényes, a küldője eltávolította. A csoport teljes neve: - segítség + súgó Önmegsemmisítő jelkód engedélyezése KÍSÉRLETI Hiba a cím megváltoztatásának megszakításakor @@ -604,7 +603,7 @@ Tovább csökkentett akkumulátor-használat Hiba a csevegés megállításakor titkosítás rendben %s számára - A csoport törlésre kerül minden tag számára - ez a művelet nem vonható vissza! + A csoport törlésre kerül az összes tag számára - ez a művelet nem vonható vissza! Titkosítás javítása az adatmentések helyreállítása után. Hiba a csevegési adatbázis törlésekor Teljes hivatkozás @@ -697,7 +696,7 @@ Nem fogadott hívás Világos Az üzenet törlésre kerül - ez a művelet nem vonható vissza! - Markdown segítség + Markdown súgó Rejtett üzenet Régi adatbázis-archívum Speciális beállítások @@ -728,9 +727,7 @@ Helytelen biztonsági kód! Ez akkor fordulhat elő, ha Ön vagy az ismerőse régi adatbázis biztonsági mentést használt. Új számítógép-alkalmazás! - Most már az adminok is: -\n- törölhetik a tagok üzeneteit. -\n- letilthatnak tagokat (megfigyelő szerepkör) + Most már az adminisztrátorok is:\n- törölhetik a tagok üzeneteit.\n- letilthatnak tagokat (megfigyelő szerepkör) meghívta őt: %1$s Az üzenetreakciók küldése le van tiltva ebben a csoportban. Nem @@ -740,7 +737,7 @@ Új tag szerepköre Kikapcsolva Érvénytelen hivatkozás! - Újdonságok a %s verzióban + Újdonságok a(z) %s verzióban Érvénytelen kiszolgálócím! k soha @@ -809,9 +806,9 @@ Csak a csoporttulajdonosok módosíthatják a csoportbeállításokat. Nincsenek előzmények Érvénytelen QR-kód - Olvasottnak jelölés + Megjelölés olvasottként ÉLŐ - Olvasatlannak jelölés + Megjelölés olvasatlanként Több Bejelentkezés hitelesítőadatokkal érvénytelen üzenet formátum @@ -821,7 +818,7 @@ (ez az eszköz: v%s)]]> ajánlott %s Csoport elhagyása - Minden %s által írt üzenet megjelenik! + Az összes %s által írt üzenet megjelenik! Ha a SimpleX Chatnek nincs felhasználó-azonosítója, hogyan lehet mégis üzeneteket küldeni?]]> Ez akkor fordulhat elő, ha:\n1. Az üzenetek 2 nap után, vagy a kiszolgálón 30 nap után lejártak.\n2. Az üzenet visszafejtése sikertelen volt, mert Ön, vagy az ismerőse régebbi adatbázis biztonsági mentést használt.\n3. A kapcsolat sérült. megfigyelő @@ -872,7 +869,7 @@ Bejövő hanghívás Kulcstartóhiba Csatlakozik a csoporthoz? - Az inkognitómód védi személyes adatait azáltal, hogy minden ismerőshöz új véletlenszerű profilt használ. + Az inkognitómód védi személyes adatait azáltal, hogy az összes ismerőséhez új, véletlenszerű profilt használ. - stabilabb üzenetkézbesítés.\n- picit továbbfejlesztett csoportok.\n- és még sok más! Üzenetreakciók Nincs társított hordozható eszköz @@ -922,7 +919,7 @@ Egyszer használható hivatkozás megosztása Hiba az adatbázis visszaállításakor %s és %s - Engedélyezve + Ön engedélyezi Csökkentett akkumulátor-használat Mentés és az ismerősök értesítése Előnézet @@ -956,7 +953,7 @@ A kapcsolódáshoz az ismerőse beolvashatja a QR-kódot, vagy használhatja az alkalmazásban található hivatkozást. visszaigazolás fogadása… Biztonsági kód beolvasása az ismerősének alkalmazásából. - Lépjen kapcsolatba a csoport adminnal. + Lépjen kapcsolatba a csoport adminisztrátorával. Videó bekapcsolva Profilnév: Beillesztés @@ -970,7 +967,7 @@ Cím Üzenet elküldése Adatbázismentés visszaállítása - Visszavon + Visszavonás Kérje meg az ismerősét, hogy engedélyezze a hangüzenetek küldését. egyszer használható hivatkozást osztott meg A hivatkozás megnyitása a böngészőben gyengítheti az adatvédelmet és a biztonságot. A megbízhatatlan SimpleX-hivatkozások pirossal vannak kiemelve. @@ -1142,7 +1139,7 @@ Adatbázismentés visszaállítása? Üzenetek fogadása… %s és %s kapcsolódott - megfigyelő szerep + Ön megfigyelő Port Jelkód beállítása Újdonságok @@ -1281,7 +1278,7 @@ Videók és fájlok 1Gb méretig TCP kapcsolat időtúllépése A(z) %1$s nevű profiljának SimpleX-címe megosztásra fog kerülni. - Ön már kapcsolódva van ehhez: %1$s. + Ön már kapcsolódott a következőhöz: %1$s. Jelenlegi csevegési adatbázis TÖRLÉSRE és FELCSERÉLÉSRE kerül az importált által! \nEz a művelet nem vonható vissza - profiljai, ismerősei, csevegési üzenetei és fájljai véglegesen törölve lesznek. Ötletek és javaslatok @@ -1312,14 +1309,14 @@ Az ismerősei továbbra is kapcsolódva maradnak. A kiszolgálónak engedélyre van szüksége a várólisták létrehozásához, ellenőrizze jelszavát Az adatbázis nem működik megfelelően. Koppintson ide a további információkért - A fájl küldése leállt. + A fájl küldése le fog állni. Kapcsolódási kísérlet ahhoz a kiszolgálóhoz, amely az adott ismerősétől érkező üzenetek fogadására szolgál. Nem sikerült hitelesíteni; próbálja meg újra. - Az üzenet minden tag számára moderáltként lesz megjelölve. + Az üzenet az összes tag számára moderáltként lesz megjelölve. Értesítések fogadásához adja meg az adatbázis jelmondatát A teszt a(z) %s lépésnél sikertelen volt. Az alkalmazás elindításához vagy 30 másodpercnyi háttérben töltött idő után, az alkalmazáshoz való visszatéréshez hitelesítésre lesz szükség. - Az üzenet minden tag számára törlésre kerül. + Az üzenet az összes tag számára törlésre kerül. A videó nem dekódolható. Próbálja ki egy másik videóval, vagy lépjen kapcsolatba a fejlesztőkkel. Ez a szöveg a „Beállításokban” érhető el A profilja elküldésre kerül az ismerőse számára, akitől ezt a hivatkozást kapta. @@ -1332,7 +1329,7 @@ A biztonsága érdekében kapcsolja be a SimpleX-zár funkciót. \nA funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beállítására az eszközén. A videó akkor érkezik meg, amikor a küldője elérhető lesz, várjon, vagy ellenőrizze később! - Hálózati kapcsolat ellenőrzése a következővel: %1$s, és próbálja újra. + Ellenőrizze a hálózati kapcsolatát a következővel: %1$s, és próbálja újra. A SimpleX-zár az „Adatvédelem és biztonság” menüben kapcsolható be. Az alkalmazás összeomlott Ellenőrizze, hogy a megfelelő hivatkozást használta-e, vagy kérje meg az ismerősét, hogy küldjön egy másikat. @@ -1340,20 +1337,20 @@ Érvénytelen fájl elérési útvonalat osztott meg. Jelentse a problémát az alkalmazás fejlesztőinek. Már van egy csevegési profil ugyanezzel a megjelenített névvel. Válasszon egy másik nevet. Kapcsolódási kísérlet ahhoz a kiszolgálóhoz, amely az adott ismerősétől érkező üzenetek fogadására szolgál (hiba: %1$s). - A fájl fogadása leállt. + A fájl fogadása le fog állni. Ne felejtse el, vagy tárolja biztonságosan – az elveszett jelszót nem lehet visszaállítani! A videó akkor érkezik meg, amikor a küldője befejezte annak feltöltését. egyszer használható hivatkozást osztott meg inkognitóban - Már kapcsolódott ahhoz a kiszolgálóhoz, amely az adott ismerősétől érkező üzenetek fogadására szolgál. + Ön már kapcsolódott ahhoz a kiszolgálóhoz, amely az adott ismerősétől érkező üzenetek fogadására szolgál. Később engedélyezheti a „Beállításokban” Akkor lesz kapcsolódva a csoporthoz, amikor a csoport tulajdonosának eszköze online lesz, várjon, vagy ellenőrizze később! különböző átköltöztetés az alkalmazásban/adatbázisban: %s / %s - %1$s.]]> + %1$s.]]> Profil felfedése Ez nem egy érvényes kapcsolattartási hivatkozás! A végpontok közötti titkosítás hitelesítéséhez hasonlítsa össze (vagy olvassa be a QR-kódot) az ismerőse eszközén lévő kóddal. A csevegési adatbázis legfrissebb verzióját CSAK egy eszközön kell használnia, ellenkező esetben előfordulhat, hogy az üzeneteket nem fogja megkapni valamennyi ismerősétől. - Ez a beállítás a jelenlegi csevegési profilban lévő üzenetekre érvényes + Ez a beállítás csak a jelenlegi csevegési profiljában lévő üzenetekre vonatkozik Meghívást kapott a csoportba. Csatlakozzon, hogy kapcsolatba léphessen a csoport tagjaival. Ez a csoport már nem létezik. A csatlakozás már folyamatban van a csoporthoz ezen a hivatkozáson keresztül. @@ -1386,7 +1383,7 @@ A profilja az eszközén van tárolva és csak az ismerőseivel kerül megosztásra. A SimpleX-kiszolgálók nem láthatják a profilját. Ön megváltoztatta %s szerepkörét erre: %s Csoportmeghívó elutasítva - Az adatvédelem érdekében, a más csevegési platformokon megszokott felhasználó-azonosítók helyett, a SimpleX csak az üzenetek sorbaállításához használ azonosítókat, minden egyes ismerőshöz egy-egy különbözőt. + Az adatvédelem érdekében (a más csevegési platformokon megszokott felhasználó-azonosítók helyett) a SimpleX csak az üzenetek sorbaállításához használ azonosítókat, az összes ismerőséhez különbözőt. (a megosztáshoz az ismerősével) Csoportmeghívó elküldve Átvitel-izoláció módjának frissítése? @@ -1401,7 +1398,7 @@ Fejlesztés és a csevegés megnyitása Engedélyeznie kell a hangüzenetek küldését az ismerőse számára, hogy hangüzeneteket küldhessenek egymásnak. fogadja az üzeneteket, ismerősöket – a kiszolgálók, amelyeket az üzenetküldéshez használ.]]> - %1$s csoport tagja.]]> + %1$s nevű csoport tagja.]]> cím megváltoztatva Az ismerősei engedélyezhetik a teljes üzenet törlést. A jelmondatot minden alkalommal meg kell adnia, amikor az alkalmazás elindul - nem az eszközön kerül tárolásra. @@ -1418,18 +1415,18 @@ A csevegési szolgáltatás elindítható a „Beállítások / Adatbázis” menüben vagy az alkalmazás újraindításával. Kód hitelesítése a hordozható eszközön Csatlakozott ehhez a csoporthoz. Kapcsolódás a meghívó csoporttaghoz. - a SimpleX Chat fejlesztőivel, ahol bármiről kérdezhet és értesülhet az újdonságokról.]]> + a SimpleX Chat fejlesztőivel, ahol bármiről kérdezhet és értesülhet a friss hírekről.]]> Nem kötelező üdvözlőüzenettel. Ismeretlen adatbázishiba: %s Elrejtheti vagy lenémíthatja a felhasználó-profiljait - koppintson (vagy számítógép-alkalmazásban kattintson) hosszan a profilra a felugró menühöz. Inkognitómód használata kapcsolódáskor. Megoszthat egy hivatkozást vagy QR-kódot - így bárki csatlakozhat a csoporthoz. Ha a csoport később törlésre kerül, akkor nem fogja elveszíteni annak tagjait. Csatlakozott ehhez a csoporthoz - %1$s csoporthoz!]]> + %1$s nevű csoporthoz!]]> A hangüzenetek küldése le van tiltva ebben a csevegésben. Ön irányítja csevegését! Kód hitelesítése a számítógépen - Az időzóna védelme érdekében a kép-/hangfájlok UTC-t használnak. + Az időzóna védelmének érdekében a kép-/hangfájlok UTC-t használnak. A kapcsolatkérés elküldésre kerül ezen csoporttag számára. Inkognitó-profil megosztása esetén a rendszer azt a profilt fogja használni azokhoz a csoportokhoz, amelyekbe meghívást kapott. Már küldött egy kapcsolatkérést ezen a címen keresztül! @@ -1437,7 +1434,7 @@ Amikor az emberek kapcsolatot kérnek, Ön elfogadhatja vagy elutasíthatja azokat. Megjelenítendő üzenet beállítása az új tagok számára! Köszönet a felhasználóknak - hozzájárulás a Weblate-en! - A kézbesítési jelentés küldése minden ismerőse számára engedélyezésre kerül. + A kézbesítési jelentés küldése az összes ismerőse számára engedélyezésre kerül. Protokoll időtúllépése KB-onként Az adatbázis-jelmondat megváltoztatására tett kísérlet nem fejeződött be. Ez a művelet nem vonható vissza - a kiválasztottnál korábban küldött és fogadott üzenetek törlésre kerülnek. Ez több percet is igénybe vehet. @@ -1448,7 +1445,7 @@ A kézbesítési jelentések engedélyezve vannak %d ismerősnél Küldés ezen keresztül: Köszönet a felhasználóknak - hozzájárulás a Weblate-en! - A kézbesítési jelentések küldése engedélyezésre kerül az összes látható csevegési profilban lévő minden ismerős számára. + A kézbesítési jelentések küldése engedélyezésre kerül az összes látható csevegési profilban lévő összes ismerőse számára. Bluetooth támogatás és további fejlesztések. Ez a funkció még nem támogatott. Próbálja meg a következő kiadásban. A bejegyzés frissítve: %s @@ -1465,7 +1462,7 @@ A közvetítő-kiszolgáló csak szükség esetén kerül használatra. Egy másik fél megfigyelheti az IP-címet. Rendszerhitelesítés helyetti beállítás. A fogadó cím egy másik kiszolgálóra változik. A címváltoztatás a feladó online állapotba kerülése után fejeződik be. - A csevegés megállítása a csevegési adatbázis exportálásához, importálásához vagy törléséhez. A csevegés megállítása alatt nem tud üzeneteket fogadni és küldeni. + A csevegés megállítása a csevegési adatbázis exportálásához, importálásához vagy törléséhez. A csevegés megállításakor nem tud üzeneteket fogadni és küldeni. Jelmondat mentése a Keystore-ba Köszönet a felhasználóknak - hozzájárulás a Weblate-en! Jelmondat mentése a beállításokban @@ -1476,7 +1473,7 @@ Az utolsó üzenet tervezetének megőrzése a mellékletekkel együtt. A mentett WebRTC ICE-kiszolgálók eltávolításra kerülnek. A kézbesítési jelentések engedélyezve vannak %d csoportban - A szerepkör meg fog változni erre: %s. A csoportban mindenki értesítve lesz. + A szerepkör meg fog változni erre: %s. A csoportban az összes tag értesítve lesz. Profil és kiszolgálókapcsolatok Egy üzenetküldő- és alkalmazásplatform, amely védi az adatait és biztonságát. A profil aktiválásához koppintson az ikonra. @@ -1505,14 +1502,14 @@ A jelmondat a beállításokban egyszerű szövegként van tárolva. Konzol megjelenítése új ablakban Az előző üzenet hasító értéke különbözik. - Ezek a beállítások a jelenlegi profiljára vonatkoznak + Ezek a beállítások csak a jelenlegi profiljára vonatkoznak Várjon, amíg a fájl betöltődik a társított hordozható eszközről GitHub tárolónkban.]]> hiba a tartalom megjelenítésekor hiba az üzenet megjelenítésekor Láthatóvá teheti a SimpleXbeli ismerősei számára a „Beállításokban”. Legfeljebb az utolsó 100 üzenet kerül elküldésre az új tagok számára. - A beolvasott QR-kód nem egy SimpleX QR-kód hivatkozás. + A beolvasott QR-kód nem egy SimpleX-QR-kód-hivatkozás. A beillesztett szöveg nem egy SimpleX-hivatkozás. A meghívó-hivatkozását újra megtekintheti a kapcsolat részleteinél. Csevegés indítása? @@ -1539,7 +1536,7 @@ Vagy QR-kód beolvasása Érvénytelen QR-kód Megtartás - Keresés, vagy SimpleX-hivatkozás beillesztése + Keresés vagy SimpleX-hivatkozás beillesztése Belső hibák megjelenítése Kritikus hiba Belső hiba @@ -1591,7 +1588,7 @@ Létrehozva ekkor: Mentett üzenet Megosztva ekkor: %s - Minden üzenet törlésre kerül – ez a művelet nem vonható vissza! + Az összes üzenet törlésre kerül – ez a művelet nem vonható vissza! Továbbfejlesztett üzenetkézbesítés Csatlakozás csoportos beszélgetésekhez Hivatkozás beillesztése a kapcsolódáshoz! @@ -1604,16 +1601,16 @@ feloldotta %s letiltását Ön feloldotta %s letiltását letiltva - letiltva az admin által - Letiltva az admin által + letiltva az adminisztrátor által + Letiltva az adminisztrátor által letiltotta őt: %s - Letiltás mindenki számára - Mindenki számára letiltja ezt a tagot? - %d üzenetet letiltott az admin - Letiltás feloldása mindenki számára - Mindenki számára feloldja a tag letiltását? + Letiltás az összes tag számára + Az összes tag számára letiltja ezt a tagot? + %d üzenetet letiltott az adminisztrátor + Letiltás feloldása az összes tag számára + Az összes tag számára feloldja a tag letiltását? Ön letiltotta őt: %s - Hiba a tag mindenki számára való letiltásakor + Hiba a tag az összes csoporttag számára való letiltásakor Az üzenet túl nagy Az üdvözlőüzenet túl hosszú Az adatbázis átköltöztetése folyamatban van. @@ -1629,8 +1626,8 @@ Archiválás és feltöltés Feltöltés megerősítése Hiba az adatbázis törlésekor - Az adminok egy tagot mindenki számára letilthatnak. - Minden ismerőse, a beszélgetései és a fájljai biztonságosan titkosításra kerülnek, melyek részletekben feltöltődnek a beállított XFTP-közvetítő-kiszolgálóra. + Az adminisztrátorok egy tagot a csoport összes tagja számára letilthatnak. + Az összes ismerőse, -beszélgetése és -fájlja biztonságosan titkosításra kerülnek, melyek részletekben feltöltődnek a beállított XFTP-közvetítő-kiszolgálóra. Alkalmazásadatok átköltöztetése Adatbázis archiválása Átköltöztetés visszavonása @@ -1718,14 +1715,14 @@ A SimpleX-hivatkozások küldése le van tiltva A csoport tagjai küldhetnek SimpleX-hivatkozásokat. tulajdonosok - adminok - minden tag + adminisztrátorok + összes tag SimpleX-hivatkozások A hangüzenetek küldése le van tiltva A SimpleX-hivatkozások küldése le van tiltva ebben a csoportban. A SimpleX-hivatkozások küldése le van tiltva A fájlok- és médiatartalmak nincsenek engedélyezve - A SimpleX hivatkozások küldése engedélyezve van. + A SimpleX-hivatkozások küldése engedélyezve van. Számukra engedélyezve: mentett mentve innen: %s @@ -1793,7 +1790,7 @@ Ismeretlen kiszolgálók! Tor vagy VPN nélkül az IP-címe látható lesz az XFTP-közvetítő-kiszolgálók számára: \n%1$s. - Minden színmód + Összes színmód Fekete Színmód Sötét @@ -1826,7 +1823,7 @@ Perzsa kezelőfelület Védje IP-címét az ismerősei által kiválasztott üzenet-közvetítő-kiszolgálókkal szemben. \nEngedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. - Ismeretlen kiszolgálókról származó fájlok jóváhagyása. + Ismeretlen kiszolgálókról származó fájlok megerősítése. Javított üzenetkézbesítés Alkalmazás témájának visszaállítása Tegye egyedivé a csevegéseit! @@ -1862,7 +1859,7 @@ Inaktív tag Továbbított üzenet Az üzenet később is kézbesíthető, ha a tag aktívvá válik. - Még nincs közvetlen kapcsolat, az üzenetet az admin továbbítja. + Még nincs közvetlen kapcsolat, az üzenetet az adminisztrátor továbbítja. Hivatkozás beolvasása / beillesztése Konfigurált SMP-kiszolgálók Egyéb SMP-kiszolgálók @@ -1874,17 +1871,17 @@ Kapcsolódás Hibák Függőben - Statisztikagyűjtés kezdete: %s.\nMinden adat biztonságban van az eszközén. + Statisztikagyűjtés kezdete: %s.\nAz összes adat biztonságban van az eszközén. Elküldött üzenetek Proxyzott kiszolgálók Újrakapcsolódás a kiszolgálókhoz? Újrakapcsolódás a kiszolgálóhoz? Hiba a kiszolgálóhoz való újrakapcsolódáskor - Újrakapcsolódás minden kiszolgálóhoz + Újrakapcsolódás az összes kiszolgálóhoz Hiba a statisztikák visszaállításakor Visszaállítás - Minden statisztika visszaállítása - Minden statisztika visszaállítása? + Az összes statisztika visszaállítása + Az összes statisztika visszaállítása? A kiszolgálók statisztikái visszaállnak - ez a művelet nem vonható vissza! Részletes statisztikák Letöltve @@ -1892,7 +1889,7 @@ egyéb Összes fogadott üzenet Üzenetfogadási hibák - Újrakapcsolás + Újrakapcsolódás Üzenetküldési hibák Közvetlenül küldött Összes elküldött üzenet @@ -1924,7 +1921,7 @@ Elkészült Kapcsolódott kiszolgálók Konfigurált XFTP-kiszolgálók - Kapcsolódva + Kapcsolódott Jelenlegi profil További részletek visszafejtési hibák @@ -2017,12 +2014,12 @@ A(z) %1$s nevű ismerősével folytatott beszélgetéseit továbbra is megtekintheti a csevegések listájában. Üzenet… Kiválasztás - Az üzenetek minden tag számára moderáltként lesznek megjelölve. + Az üzenetek az összes tag számára moderáltként lesznek megjelölve. Nincs kiválasztva semmi Az üzenetek törlésre lesznek jelölve. A címzett(ek) képes(ek) lesz(nek) felfedni ezt az üzenetet. Törli a tagok %d üzenetét? %d kiválasztva - Az üzenetek minden tag számára törlésre kerülnek. + Az üzenetek az összes tag számára törlésre kerülnek. Csevegési adatbázis exportálva Kapcsolatok- és kiszolgálók állapotának megjelenítése. Kapcsolódjon gyorsabban az ismerőseihez. @@ -2066,8 +2063,8 @@ Rendszerbeállítások használata Csevegési profil kiválasztása Ne használja a hitelesítőadatokat proxyval. - Különböző proxy-hitelesítőadatok használata minden egyes profilhoz. - Különböző proxy-hitelesítőadatok használata minden egyes kapcsolathoz. + Különböző proxy-hitelesítőadatok használata az összes profilhoz. + Különböző proxy-hitelesítőadatok használata az összes kapcsolathoz. Jelszó Felhasználónév A hitelesítőadatai titkosítatlanul is elküldhetők. @@ -2101,7 +2098,7 @@ Kiszolgáló Minden alkalommal, amikor elindítja az alkalmazást, új SOCKS-hitelesítő-adatokat fog használni. Alkalmazás munkamenete - Minden egyes kiszolgálóhoz új SOCKS-hitelesítő-adatok legyenek használva. + Az összes kiszolgálóhoz új, SOCKS-hitelesítő-adatok legyenek használva. Kattintson a címmező melletti info gombra a mikrofon használatának engedélyezéséhez. Nyissa meg a Safari Beállítások / Weboldalak / Mikrofon menüt, majd válassza a helyi kiszolgálók engedélyezése lehetőséget. Hívások kezdeményezéséhez engedélyezze a mikrofon használatát. Fejezze be a hívást, és próbálja meg a hívást újra. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml index 9c256b4a4b..29c6430839 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml @@ -12,7 +12,7 @@ Tambah kontak Tentang SimpleX %1$d pesan gagal terdekripsi. - Tolong laporkan hal ini ke pengembang. + Mohon laporkan ke pengembang. 1 bulan 1 minggu Terima @@ -52,7 +52,7 @@ Batalkan Kamera tebal - menelepon… + memanggil… Bluetooth Panggilan diakhiri Ubah @@ -112,7 +112,7 @@ Tampilan macet Anda membagikan lokasi file yang tidak valid. Laporkan masalah ini ke pengembang aplikasi. error - Tuatan 1 kali pakai + Tautan sekali %1$d pesan yang terlewati %1$d pesan yang dilewati %s tidak didukung. Harap pastikan kamu menggunakan versi yang sama pada kedua perangkat.]]> @@ -170,7 +170,7 @@ Host Onion tidak akan digunakan. Selalu gunakan perutean pribadi. Pemberitahuan akan berhenti bekerja sampai kamu meluncurkan ulang aplikasi - Semua kontak kamu akan tetap terhubung. Pembaruan profil akan dikirim ke kontak kamu. + Semua kontak Anda akan tetap terhubung. Pembaruan profil akan dikirim ke kontak Anda. Tidak ada enkripsi ujung-ujung Kode sandi baru Mati @@ -263,4 +263,1024 @@ Izinkan untuk mengirim file dan media. Semua kontak kamu akan tetap terhubung. %1$d berkas telah dihapus. + Corak tambahan + Corak tambahan 2 + %1$s pesan tidak diteruskan + Koneksi aktif + admin + %1$d berkas galat :\n%2$s + Balas + Bagikan + Salin + Edit + Info + Tersimpan + Cari + Diteruskan + Disimpan dari + Perluas + Sedang + Hapus pesan? + Hapus %d pesan? + Pesan akan dihapus - Tindakan ini tidak dapat dibatalkan! + %1$d berkas masih diunduh. + Jawab panggilan + Pesan terkirim + Pesan diterima + Anggota tidak aktif + Pesan diteruskan + Cabut berkas? + Terlalu banyak gambar! + cari + panggilan + PENGATURAN + Untuk semua orang + Hentikan berkas + Cabut berkas + Mengirim berkas akan dihentikan. + Berhenti + Pengaturan + Berhenti kirim berkas? + Penerimaan berkas akan dihentikan. + Gambar disimpan ke Galeri + video + Kontak + %d pesan dihapus + dihapus + Mencoba terhubung ke server untuk menerima pesan dari kontak ini. + disimpan + diundang untuk terhubung + Deskripsi + Nama tampilan duplikat! + Mengirim + Diteruskan dari + Berhenti menerima berkas? + Tarik + Berkas akan dihapus dari server. + terkirim + Teruskan + Unduh + diedit + Obrolan + Selamat Datang! + Chat dengan pengembang + Setel nama kontak… + Pengaturan + Simpan + Kirim pesan langsung untuk terhubung + Berkas disimpan + Alamat server tujuan %1$s tidak kompatibel dengan pengaturan server penerusan %2$s. + Riwayat + Hapus + Perlihat + Sembunyikan + Grup kecil (maks 20) + Pilih + Hapus untuk saya + Gagal meneruskan pesan + %1$d berkas lainnya gagal. + Kontak sudah ada + menghubungkan… + kirim pesan langsung + Anda tidak memiliki obrolan + Memuat obrolan… + Ketuk untuk Hubungkan + Terhubung dengan %1$s? + Teruskan %1$s pesan? + Tidak ada yang diteruskan! + Teruskan pesan tanpa berkas? + Pesan dihapus setelah Anda memilihnya. + %1$d berkas tidak diunduh. + Unduh + Teruskan pesan… + Meneruskan %1$s pesan + Tautan SimpleX tidak diizinkan + Berkas dan media tidak diizinkan + Video + Menunggu video + Berkas tidak ditemukan + Tolak + Bagikan profil + Pilih profil obrolan + Kontak arsipan + Kode yang Anda pindai bukan kode QR tautan SimpleX. + Untuk memverifikasi enkripsi end-to-end dengan kontak Anda, bandingkan (atau pindai) kode pada perangkat Anda. + Kontak Anda + Pindai kode + Tandai terverifikasi + %s telah terverifikasi + %s belum terverifikasi + Bersihkan verifikasi + Konsol obrolan + Server pesan + Server SMP + Beri nilai aplikasi + Bintang di GitHub + Gunakan server SimpleX Chat? + Server SMP Anda + Server XFTP + Kredensial Anda mungkin dikirim tidak terenkripsi. + Username + Server XFTP Anda + Gunakan server SimpleX Chat. + Gunakan kredensial proxy yang berbeda untuk setiap koneksi. + Kata sandi + port %d + Host + Port + Gunakan proxy SOCKS? + Akses server melalui proxy SOCKS pada port %d? Proxy harus dimulai sebelum mengaktifkan opsi ini. + Gunakan koneksi Internet langsung? + Pastikan konfigurasi proxy sudah benar. + Gagal simpan proxy + Saat tersedia + Diperlukan + Jika Anda konfirmasi, server perpesanan akan dapat melihat alamat IP Anda, dan penyedia Anda - server mana yang Anda hubungkan. + Profil obrolan + Koneksi + Server tak dikenal + Kirim pesan secara langsung ketika alamat IP dilindungi dan server Anda atau tujuan tidak mendukung routing pribadi. + Kirim pesan secara langsung ketika server Anda atau server tujuan tidak mendukung routing pribadi. + Tampilkan status pesan + Terima otomatis + Undang teman + Buat + Nama tidak valid! + Buat profil + Masukkan nama Anda: + rahasia + miring + berwarna + menghubung panggilan… + panggilan berakhir %1$s + panggilan berlangsung + berakhir + Headphone + Kesalahan saat menginisialisasi WebView. Perbarui sistem Anda ke versi baru. Mohon hubungi pengembang.\nKesalahan: %s + Anda pilih siapa yang dapat terhubung. + Kebal terhadap spam + Buat profil Anda + Buat koneksi pribadi + Lewati + Panggilan Anda + Lihat + Server relai hanya digunakan jika diperlukan. Pihak lain dapat mengamati alamat IP Anda. + Speaker mati + Video nyala + Suara mati + Suara nyala + Speaker nyala + Balik kamera + Panggilan berlangsung + Menghubungkan panggilan + Pesan yang terlewati + Hash dari pesan sebelumnya berbeda. + Privasi & keamanan + Enkripsi berkas lokal + Terima gambar otomatis + Lindungi layar aplikasi + Lindungi alamat IP + Lihat pesan terakhir + Kirim pratinjau tautan + Draf pesan + Konfirmasi kode sandi + Kunci setelah + Kirim + Kode sandi salah + Kode sandi aplikasi + Kode sandi + %s detik + Simpan arsip + Ketuk untuk gabung + diblokir %s + Buka + enkripsi aman + status tidak diketahui + diundang + admin + dihapus + grup dihapus + Gagal impor tema + Impor tema + Pastikan berkas memiliki sintaksis YAML yang benar. Ekspor tema untuk mendapatkan contoh struktur berkas tema. + Ekspor tema + Sekunder + Latar + Pesan terkirim + Reset ke tema pengguna + Pasang tema bawaan + Terapkan + Sesuai + Reset ke tema aplikasi + Izin Anda + Izin kontak + bawaan (%s) + Pesan sementara + Pesan pribadi + Pesan suara + Setel preferensi grup + Preferensi Anda + Hapus untuk semua orang + diterima, dilarang + Pasang 1 hari + Kontak dapat menandai pesan untuk dihapus; Anda akan dapat melihatnya. + Melarang pengiriman pesan suara. + Hapus pesan tidak dapat dibatalkan dilarang dalam obrolan ini. + Panggilan audio/video dilarang. + Reaksi pesan dilarang. + Anggota grup dapat mengirim pesan sementara. + Pesan sementara dilarang di grup ini. + %d minggu + %d minggu + Keamanan SimpleX Chat diaudit oleh Trail of Bits. + Tautan grup + Admin dapat membuat tautan untuk gabung ke grup. + Maks 40 detik, diterima secara instan. + Hapus pesan tidak dapat dibatalkan + Penerima melihat pesan langsung saat Anda mengetik. + Isolasi transport + Pesan sambutan grup + Antarmuka bahasa Cina dan Spanyol + Video dan berkas hingga 1GB + Terima kasih kepada pengguna – berkontribusi melalui Weblate! + Reaksi pesan + Jaga koneksi Anda + Grup lebih baik + Grup samaran + Melalui protokol quantum resistant yang aman. + Gabung lebih cepat dan pesan lebih handal. + Untuk sembunyikan pesan tak diinginkan. + Gunakan aplikasi saat dalam panggilan. + Konfirmasi berkas dari server tak dikenal. + Tanda terima pengiriman dimatikan! + kustom + Verifikasi kode dengan desktop + Fitur ini belum didukung. Coba pada versi berikutnya. + Kesalahan internal + Mengunduh arsip + Unduhan gagal + Buat tautan arsip + Mulai obrolan + Pastikan Anda mengingat frasa sandi basis data untuk memindahkan. + Profil saat ini + Buat + Reset warna + kirim berkas belum didukung + data tidak valid + gagal menampilkan pesan + gagal menampilkan konten + Gagal dekripsi + Gagal negosiasi ulang enkripsi + enkripsi end-to-end dengan perfect forward secrecy, penolakan dan pemulihan pembobolan.]]> + Via peramban + Gagal mengganti profil + Buka tautan di peramban mengurangi privasi dan keamanan koneksi. Tautan SimpleX tidak tepercaya akan berwarna merah. + Gagal simpan server SMP + Pastikan alamat server SMP dalam format yang benar, pisahkan baris dan tidak terduplikasi. + Pastikan alamat server XFTP dalam format yang benar, pisahkan baris dan tidak terduplikasi. + gagal terkirim + belum dibaca + Pesan dapat disampaikan kemudian jika anggota menjadi aktif. + Selamat Datang %1$s! + %d Dipilih + Terlalu banyak video! + Pesan suara + pesan + buka + Sistem + Arsip obrolan + setel alamat kontak baru + Tema gelap + Tema + Mode warna + Balasan terkirim + Pesan diterima + Perbesar + Melarang kirim pesan sementara. + Reaksi pesan dilarang. + Panggilan suara/video dilarang. + Anda dan kontak Anda dapat menghapus pesan terkirim secara permanen. (24 jam) + Pesan sementara dilarang dalam obrolan ini. + Pesan suara dilarang dalam obrolan ini. + Anda dan kontak dapat menambahkan reaksi pesan. + Anggota grup dapat hapus pesan terkirim secara permanen. (24 jam) + Anggota grup dapat mengirim pesan suara. + Hapus pesan yang tidak dapat dibatalkan dilarang di grup ini. + Pesan pribadi antar anggota dilarang di grup ini. + Anggota grup dapat kirim tautan SimpleX. + %d jam + %d jam + %d hari + %d hari + %dmg + %dj + dibatalkan %s + Dengan pesan sambutan opsional. + Terima permintaan kontak secara otomatis + Selengkapnya + Penilaian keamanan + Konfigurasi server ditingkatkan + Tambah server dengan pindai kode QR. + Peningkatan privasi dan keamanan + Terima kasih kepada pengguna – berkontribusi melalui Weblate! + Antarmuka Prancis + Bandingkan kode keamanan dengan kontak Anda. + Simpan draf pesan terakhir, dengan lampiran. + Berdasarkan profil obrolan (bawaan) atau berdasarkan koneksi (BETA). + Untuk melindungi zona waktu, berkas gambar/suara menggunakan UTC. + Penggunaan baterai semakin sedikit + Atur pesan yang ditampilkan kepada anggota baru! + Pindahan data aplikasi + Terima berkas dengan aman + Buat obrolan Anda terlihat berbeda! + Peningkatan pengiriman pesan + UI Lituania + Arsip kontak untuk mengobrol nanti. + Gunakan aplikasi dengan satu tangan. + Terhubung dengan teman lebih cepat. + Ini melindungi alamat IP dan koneksi Anda. + Unduh versi baru dari GitHub. + Nama perangkat ini + Koneksi terputus + Desktop tidak aktif + Ingin bergabung lagi? + Mengimpor arsipan + Pindah perangkat + Gagal Ekspor basis data obrolan + Informasi server + Lihat info untuk + Galat + Diunggah + Diunduh + Pesan terkirim + Statistik server akan direset - ini tidak dapat dibatalkan! + Perbesar ukuran font. + Sumber pesan tetap pribadi. + Siapa pun dapat menjadi pemegang server. + Terdesentralisasi + Kesalahan saat menginisialisasi WebView. Pastikan Anda telah menginstal WebView dan arsitektur yang didukung adalah arm64.\nKesalahan: %s + Gunakan obrolan + Jika SimpleX tidak memiliki pengenal pengguna, bagaimana ia dapat menyampaikan pesan?]]> + Bagaimana caranya + Cara kerja SimpleX + Berkala + Panggilan suara masuk + panggilan suara terenkripsi e2e + panggilan video terenkripsi e2e + Gunakan frasa sandi acak + Panggilan suara & video + Tolak + panggilan video + kontak memiliki enkripsi e2e + Tutup + Video mati + Suara dibisukan + Panggilan tertunda + Aktifkan kunci + Tanpa Tor atau VPN, alamat IP Anda akan terlihat oleh server file. + Mode kunci + Autentikasi dibatalkan + Ubah mode kunci + Kode sandi dipasang! + Kode sandi diubah! + Kode sandi hapus otomatis diubah! + Kode sandi hapus otomatis + Aktifkan hapus otomatis + Pasang kode sandi + BANTUAN + DUKUNG SIMPLEX CHAT + PANGGILAN + Mulai ulang aplikasi untuk buat profil obrolan baru. + Hapus pesan + keluar + Anda keluar + Terang + Warna mode gelap + Reset warna + Mode gelap + Mode terang + Preferensi obrolan + Preferensi kontak + diaktifkan + diaktifkan untuk anda + \nTersedia di v5.1 + Kirim pesan suara tidak diizinkan. + Hingga 100 pesan terakhir dikirim ke anggota baru. + Pesan suara + Pesan sementara + Pesan langsung + Profil obrolan tersembunyi + Moderasi grup + Kustomisasi dan bagikan warna tema. + Perbaiki enkripsi setelah memulihkan cadangan. + Bahasa Arab, Bulgaria, Finlandia, Ibrani, Thailand, dan Ukraina - terima kasih kepada pengguna dan Weblate. + Gabung ke percakapan grup + Bilah pencarian menerima tautan undangan. + Teruskan dan simpan pesan + Panggilan gambar-dalam-gambar + Persegi, lingkaran, atau apa pun di antaranya. + Bentuk gambar profil + Putuskan desktop? + Segarkan + Acak + Buka port di firewall + Hubungkan via tautan? + Kesalahan besar + Gambar + pesan + Menunggu gambar + Diminta untuk menerima gambar + Gambar terkirim + Membuat tautan… + Kode QR tidak valid + Kesalahan saat mengganti profil + Ketuk untuk tempel tautan + Koneksi Anda dipindahkan ke %s tetapi terjadi kesalahan tak terduga saat mengarahkan Anda ke profil. + Kirim kami email + Kirim pertanyaan dan ide + Kunci SimpleX + Bantuan Markdown + Server media & berkas + Server Anda + Alamat server ditetapkan + Gunakan di koneksi baru + Simpan server? + Server untuk koneksi baru profil obrolan Anda saat ini + Gunakan host .onion + Proxy SOCKS + Bagaimana + Konfigurasi server ICE + Server ICE (satu per baris) + Bagikan tautan + Simpan pengaturan? + Buat profil + Simpan kata sandi profil + Kata sandi profil tersembunyi + Mikrofon + Privasi didefinisikan ulang + Ini dapat diubah nanti di pengaturan. + Saat aplikasi sedang berjalan + Notifikasi pribadi + Instan + Ubah kode sandi hapus otomatis + Kode sandi tidak diubah! + Hapus otomatis + Aktifkan kode sandi hapus otomatis + Ubah mode hapus otomatis + Kode sandi hapus otomatis diaktifkan! + Matikan tanda terima? + Aktifkan tanda terima? + Sedang + Buram media + Kuat + ANDA + Lunak + BASIS DATA OBROLAN + Setel frasa sandi untuk diekspor + Buka folder basis data + menghapus anda + Admin dapat memblokir anggota untuk semua. + Akan diaktifkan dalam obrolan pribadi! + Grup aman + Kode sesi + Verifikasi koneksi + %s]]> + Ini adalah alamat SimpleX Anda! + Permintaan koneksi berulang? + Gabung ke grup Anda? + Statistik + Total + ditandai dihapus + diblokir oleh admin + %d pesan diblokir + %d pesan diblokir oleh admin + diblokir + LIVE + dimoderasi + obrolan tidak valid + diteruskan + disimpan dari %s + terima berkas belum didukung + anda + format pesan tak diketahui + format pesan tidak valid + Obrolan ini dilindungi oleh enkripsi end-to-end quantum resistant. + enkripsi quantum resistant e2e dengan perfect forward secrecy, penolakan dan pemulihan pembobolan.]]> + Obrolan ini dilindungi oleh enkripsi end-to-end. + Catatan pribadi + koneksi %1$d + koneksi terjalin + menghubungkan… + Anda bagikan tautan sekali + Anda bagikan tautan sekali samaran + via tautan grup + samaran via tautan grup + via tautan alamat kontak + samaran via tautan alamat kontak + via tautan sekali + Alamat kontak SimpleX + samaran via tautan sekali + Tautan lengkap + Tautan grup SimpleX + Tautan SimpleX + Undangan sekali SimpleX + via %1$s + Gagal simpan server XFTP + Nama tampilan tidak valid! + Gagal membuat profil! + Kesalahan koneksi + Gagal mengirim pesan + Gagal membuat pesan + Waktu koneksi habis + pengiriman tidak sah + Teks ini tersedia di pengaturan + Ketuk untuk memulai obrolan baru + Anda diundang ke grup + gabung sebagai %s + menghubungkan… + Tidak dapat kirim pesan + Bagikan pesan… + Teruskan pesan… + Bagikan media… + Bagikan berkas… + Kesalahan decoding + Video terkirim + Diminta untuk menerima video + Menunggu berkas + Perubahan alamat akan dibatalkan. Alamat penerima lama akan digunakan. + Kembali + Lebih lanjut + Atau pindai kode QR + Simpan + Simpan undangan tidak terpakai? + Anda dapat melihat tautan undangan lagi dalam detail koneksi. + Alamat SimpleX + Atau perlihatkan kode ini + Pindai kode keamanan dari aplikasi kontak Anda. + Kode keamanan + Kode keamanan salah! + Cara menggunakannya + Frasa sandi & ekspor basis data + Buat profil obrolan + Markdown dalam pesan + Simpan server + Tambah server + Tambah server prasetel + Uji server gagal! + Server uji + Server uji + Masukkan server manual + Server Prasetel + Pindai kode QR server + Beberapa server gagal dalam pengujian: + Alamat server Anda + Hapus server + Server WebRTC ICE yang disimpan akan dihapus. + Server ICE Anda + Cara menggunakan server Anda + Simpan + Pengaturan jaringan lainnya + Gagal simpan server ICE + Pastikan alamat server WebRTC ICE dalam format yang benar, dipisahkan baris dan tidak terduplikasi. + Gunakan proxy SOCKS + Pengaturan proxy SOCKS + memulai… + Privasi Anda + Cadangan data aplikasi + %s dan %s + Sistem + Balasan diterima + Skala + Hanya kontak yang dapat kirim pesan suara. + Nama berkas pribadi + Pesan yang lebih baik + Menyimpan %1$s pesan + %1$d berkas gagal diunduh. + Gabung + Batal pratinjau berkas + Berkas + Pesan suara tidak diizinkan + Hapus alamat? + Keluar dari grup? + ARSIP OBROLAN + Rangkaian ini bukan tautan koneksi! + Tempel tautan yang Anda terima + Bagikan dengan kontak + Ya + Kustomisasi tema + Anda bergabung dengan grup melalui tautan ini. + Mendukung bluetooth dan peningkatan lainnya. + Pengarsipan basis data + Protokol SimpleX ditinjau oleh Trail of Bits. + - pesan suara hingga 5 menit.\n- kustom waktu pesan sementara.\n- riwayat edit. + Diaktifkan untuk + %dh + Alamat server tidak valid! + Server XFTP lainnya + Gunakan kredensial proksi yang berbeda untuk setiap profil. + Server penerusan %1$s gagal terhubung ke server tujuan %2$s. Coba lagi nanti. + Cari atau tempel tautan SimpleX + Preferensi obrolan yang dipilih melarang pesan ini. + Lampiran + Ikon konteks + Batal pratinjau gambar + panggilan ditolak + panggilan video (tidak dienkripsi e2e) + Negosiasi ulang enkripsi gagal. + gandakan pesan + hash pesan buruk + Mulai obrolan? + Hapus arsip + Jelajah dan gabung ke grup + Perutean pesan pribadi 🚀 + Lindungi alamat IP Anda dari relai pesan yang dipilih oleh kontak Anda.\nAktifkan di pengaturan *Jaringan & server*. + Tempel tautan + Bagikan tautan undangan 1-kali + Tempel + Coba lagi + Hubungkan melalui tautan + Teks yang Anda tempel bukan tautan SimpleX. + Pengaturan Anda + Alamat SimpleX Anda + Profil obrolan Anda + Server SMP dikonfigurasi + Server SMP lainnya + Gunakan server + Server XFTP dikonfigurasi + Periksa alamat server dan coba lagi. + Tampilkan persentase + Kontribusi + Instal SimpleX Chat untuk terminal + Panggilan video masuk + mengundang %1$s + pemilik + Kode sandi hapus otomatis + Temukan obrolan lebih cepat + Statistik terperinci + Berkas besar! + Pindai / Tempel tautan + Berikan izin untuk melakukan panggilan + panggilan suara (tidak dienkripsi e2e) + Tema kustom + - terhubung ke layanan direktori (BETA)!\n- tanda terima pengiriman (hingga 20 anggota).\n- lebih cepat dan stabil. + Mode samaran sederhana + Terapkan + Hubung ulang + Kesalahan pengenalan + Keluar + enkripsi disetujui + Anda dan kontak Anda dapat mengirim pesan sementara. + Hanya kontak Anda yang dapat mengirim pesan sementara. + Anda dan kontak Anda dapat mengirim pesan suara. + Kode sandi aplikasi + Atur sebagai ganti autentikasi sistem. + Akhirnya, kita mendapatkannya! 🚀 + Antarmuka Polandia + Membuat satu pesan dihapus + Bahkan saat dimatikan dalam percakapan. + Centang kedua yang terlewat! ✅ + Filter obrolan belum dibaca dan favorit. + Buat profil baru di aplikasi desktop. 💻 + - opsional memberi tahu kontak yang dihapus.\n- nama profil dengan spasi.\n- dan masih banyak lagi! + Buramkan untuk privasi lebih baik. + Perangkat ini + Berkas + Kabel ethernet + Hapus pesan tidak bisa dibatalkan dilarang. + Kirim berkas dan media dilarang. + Aplikasi mengenkripsi berkas lokal baru (kecuali video). + Aktifkan mode samaran saat menghubungkan. + Peningkatan pengiriman pesan + Penggunaan baterai yang sedikit. + UI Hongaria dan Turki + Enkripsi quantum resistant + Periksa pembaruan + Silakan coba lagi nanti. + Kesalahan perutean pribadi + Tambah alamat ke profil Anda, sehingga kontak dapat membagikannya dengan orang lain. Pembaruan profil akan dikirim ke kontak Anda. + Android Keystore digunakan untuk simpan frasa sandi dengan aman setelah Anda memulai ulang aplikasi atau ubah frasa sandi - ini mungkin dapat menerima notifikasi. + Aktifkan panggilan dari layar kunci melalui Pengaturan. + Hal ini dapat terjadi ketika:\n1. Pesan kedaluwarsa di klien pengirim setelah 2 hari atau di server setelah 30 hari.\n2. Dekripsi pesan gagal, karena Anda atau kontak Anda menggunakan cadangan basis data lama.\n3. Koneksi terganggu. + Hapus gambar + Ukuran huruf + Kirim pesan pribadi ke anggota dilarang. + Kirim tautan SimpleX dilarang + Reaksi pesan dilarang di grup ini. + Tautan SimpleX dilarang di grup ini. + Server + Tak terlindungi + Sesi aplikasi + Kredensial SOCKS baru akan digunakan untuk setiap server. + Build aplikasi: %s + Versi inti: v%s + Ketika IP disembunyikan + WARNA ANTARMUKA + Fallback perutean pesan + Mode routing pesan + Routing pribadi + JANGAN gunakan routing pribadi. + Gunakan routing pribadi dengan server yang tak dikenal ketika alamat IP tidak dilindungi. + Stable + Pembaruan tersedia: %s + Dimatikan + Lewati versi ini + Pembaruan aplikasi diunduh + Buat alamat + ID Basis Data dan Opsi Isolasi Transport. + panggilan tak terjawab + Anda dapat membuatnya terlihat oleh kontak SimpleX Anda melalui Pengaturan. + Undang + Hai!\nHubungi saya melalui SimpleX Chat: %s + Konfirmasi kata sandi + panggilan gagal + menunggu jawaban… + menerima jawaban… + menerima konfirmasi… + Pindah dari perangkat lain + Buka pengaturan + Kamera dan mikrofon + Panggilan pada layar terkunci: + Speaker + Izin dalam pengaturan + Generasi baru\ndari perpesanan pribadi + Temukan izin ini di pengaturan Android dan ubah secara manual. + Earpiece + Matikan + Server ICE Anda + Server ICE WebRTC + Jika Anda memasukkan kode sandi hapus otomatis saat membuka aplikasi: + IKON APLIKASI + Aplikasi akan meminta untuk mengonfirmasi unduhan dari server berkas yang tidak dikenal (kecuali .onion atau saat proxy SOCKS diaktifkan). + Reaksi pesan dilarang dalam obrolan ini. + Pindah ke perangkat lain + Untuk melakukan panggilan, izinkan penggunaan mikrofon. Akhiri panggilan dan coba panggil lagi. + Klik tombol info di dekat kolom alamat untuk mengizinkan penggunaan mikrofon. + Server relai melindungi alamat IP Anda, tetapi dapat mengamati durasi panggilan. + Buka Pengaturan Safari / Situs Web / Mikrofon, lalu pilih Izinkan untuk localhost. + Buka SimpleX Chat untuk terima panggilan + Buka + terenkripsi e2e + peer-to-peer + kontak tidak memiliki enkripsi e2e + via relai + Panggilan tak terjawab + Panggilan ditolak + ID pesan berikutnya salah (kurang atau sama dengan yang sebelumnya).\nHal ini dapat terjadi karena beberapa bug atau ketika koneksi terganggu. + TEMA + KIRIM TANDA TERIMA KIRIMAN KE + Hal ini dapat terjadi ketika Anda atau koneksi Anda menggunakan cadangan basis data lama. + Android Keystore digunakan untuk menyimpan frasa sandi dengan aman - memungkinkan layanan notifikasi berfungsi. + Hapus + Enkripsi + Pesan + pembuat + diundang melalui tautan grup Anda + anggota + keluar + Gelap + Gelap + Sistem + Menu & peringatan + Judul + Latar wallpaper + Aksen wallpaper + Mode sistem + Selamat siang! + Selamat pagi! + Ulangi + Isi + ya + Preferensi grup + Reaksi pesan + diaktifkan untuk kontak + Anda dan kontak dapat melakukan panggilan. + Kirim hingga 100 pesan terakhir untuk anggota baru. + Kirim pesan sementara dilarang. + Jangan perlihat pesan riwayat ke anggota baru. + Anggota grup dapat mengirim pesan pribadi. + Pesan suara dilarang di grup ini. + Anggota grup dapat memberi reaksi pesan. + %d bulan + pemilik + %d dtk + %dd + %d mnt + %d bulan + %db + %dbln + Apa yang baru + Anggota grup dapat kirim berkas dan media. + Berkas dan media dilarang di grup ini. + Riwayat pesan tidak dikirim ke anggota baru. + Sembunyikan layar aplikasi di aplikasi terbaru. + Kontak Anda dapat mengizinkan hapus semua pesan. + Pesan terkirim akan dihapus setelah waktu yang ditentukan. + Verifikasi keamanan koneksi + Nama, avatar, dan isolasi transport yang berbeda. + Draf pesan + Lindungi profil obrolan Anda dengan kata sandi! + Terima kasih kepada pengguna – berkontribusi melalui Weblate! + Cepat dan tidak perlu menunggu pengirim online! + UI Jepang dan Portugis + Catatan pribadi + Enkripsi berkas & media tersimpan + Dengan berkas dan media terenkripsi. + Hubungkan aplikasi ponsel dan desktop! 🔗 + Tanda terima kirim pesan! + Buat grup menggunakan profil acak. + Blokir anggota grup + Suara panggilan masuk + Saat menghubungkan panggilan suara dan video. + Tempel tautan untuk terhubung! + Riwayat terkini dan peningkatan bot direktori. + Aktifkan dalam obrolan pribadi (BETA)! + Pindah ke perangkat lain melalui kode QR. + Hapus hingga 20 pesan sekaligus. + Periksa pembaruan + Baca selengkapnya di repositori GitHub kami. + Unduh %s (%s) + simplexmq: v%s (%2s) + Gunakan routing pribadi dengan server yang tak dikenal. + Panggilan lebih baik + Terhubung ke diri sendiri? + Hapus atau moderasi hingga 200 pesan. + Teruskan hingga 20 pesan sekaligus. + Segera hadir! + Ganti suara dan video selama panggilan. + Pengalaman pengguna lebih baik + Total terkirim + Pesan diterima + Mulai ulang obrolan + Otentikasi proxy + Gunakan kredensial acak + menunggu konfirmasi… + Berikan izin + Gagal membuka peramban + Peramban web bawaan diperlukan untuk panggilan. Harap konfigurasikan peramban bawaan dalam sistem, dan bagikan informasi lebih lanjut dengan pengembang. + Aktifkan untuk semua + Nonaktifkan untuk semua grup + Undang anggota + Sistem + Terang + SimpleX + Berkas dan media + Tautan SimpleX + Perlihat riwayat + Panggilan suara/video + Hapus setelah + Dengan kurangi penggunaan baterai. + Toolbar obrolan mudah diakses + UI Persia + Grup terbuka + WiFi + Mulai dari %s.\nSemua data bersifat pribadi di perangkat Anda. + Sesi transport + Total diterima + Terima galat + upaya + Dikenal + Menunggu gambar + Menunggu video + PERANGKAT + OBROLAN + BERKAS + Reset semua petunjuk + Gagal menambah anggota + Gagal gabung ke grup + Pengirim batalkan kirim berkas. + Tak bisa menerima berkas + Tanpa Tor atau VPN, alamat IP Anda akan terlihat oleh relay XFTP ini:\n%1$s. + Gagal menerima berkas + Gagal membuat alamat + Anda sudah terhubung ke %1$s. + Tautan koneksi tidak valid + Harap periksa apakah tautan yang digunakan benar atau minta kontak Anda untuk kirim tautan lain. + Gagal menerima permintaan kontak + Galat + Mungkin sidik jari sertifikat di alamat server salah + Gagal mengatur alamat + Gagal hapus profil pengguna + Hapus antrian + Buat berkas + Hapus berkas + Gagal perbarui privasi pengguna + Fungsi lambat + Notifikasi instan + izinkan SimpleX berjalan di latar belakang pada dialog berikutnya. Jika tidak, notifikasi akan dimatikan.]]> + Optimalisasi baterai aktif, mematikan layanan latar belakang dan permintaan pesan baru secara berkala. Anda dapat aktifkan kembali di pengaturan. + Buka pengaturan aplikasi + Notifikasi berkala dinonaktifkan! + Matikan notifikasi + Penggunaan baterai aplikasi / Tidak dibatasi di pengaturan aplikasi.]]> + Frasa sandi diperlukan + Untuk menerima notifikasi, mohon masukkan frasa sandi basis data + Akhiri + Tidak dapat inisialisasi basis data + Basis data tidak berfungsi dengan benar. pelajari lebih lanjut + Tutup + Dimulai secara berkala + Berjalan saat aplikasi terbuka + Aplikasi hanya menerima notifikasi saat sedang berjalan, tidak ada layanan latar belakang yang dimulai + Memeriksa pesan baru setiap 10 menit hingga 1 menit + Tersembunyi + Tampilkan kontak dan pesan + Kontak tersembunyi: + Nyalakan + Mode Kunci SimpleX + Kunci SimpleX diaktifkan + Anda akan diminta untuk lakukan autentikasi saat mulai atau lanjutkan aplikasi setelah 30 detik di latar belakang. + Kode sandi aplikasi diganti dengan kode sandi hapus otomatis. + Mereka dapat ditimpa dalam pengaturan kontak dan grup. + Aktifkan (tetap ditimpa) + Matikan (tetap ditimpa) + Gagal memuat obrolan + Gagal hapus kontak + Gagal memuat server SMP + Gagal memuat obrolan + Nama tampilan ini tidak valid. Silakan pilih nama lain. + Pengirim mungkin telah hapus permintaan koneksi. + Server perlu otorisasi untuk membuat antrian, periksa kata sandi + Gagal menghapus permintaan kontak + Gagal menghapus koneksi kontak tertunda + Gagal mengubah alamat + Gagal batalkan perubahan alamat + Pesan SimpleX Chat + Penggunaan baterai aplikasi / Tidak dibatasi di pengaturan aplikasi.]]> + Aplikasi mungkin ditutup setelah 1 menit di latar belakang. + Untuk melindungi informasi Anda, aktifkan Kunci SimpleX.\nAnda akan diminta untuk menyelesaikan autentikasi sebelum fitur ini aktif. + %d menit + Gagal menampilkan notifikasi, hubungi pengembang. + Aktifkan tanda terima untuk grup? + Profil obrolan kosong dengan nama yang disediakan dibuat, dan aplikasi terbuka seperti biasa. + Jika Anda memasukkan kode sandi saat membuka aplikasi, semua data aplikasi akan dihapus secara permanen! + Pengaturan ini untuk profil Anda saat ini + Kirim tanda terima diaktifkan untuk %d kontak + Kirim tanda terima dimatikan untuk %d kontak + Gagal memuat server XFTP + Gagal memperbarui konfigurasi jaringan + Mohon perbarui aplikasi dan hubungi pengembang. + Anda sudah memiliki nama tampilan profil obrolan yang sama. Silakan pilih nama lain. + Periksa koneksi jaringan Anda dengan %1$s dan coba lagi. + Gagal memuat detail + Gagal menghapus grup + Gagal hapus catatan pribadi + Putuskan + Amankan antrian + Unduh berkas + Bandingkan berkas + Notifikasi instan! + Notifikasi instan dimatikan! + Notifikasi berkala + SimpleX tidak dapat berjalan di latar belakang. Anda hanya menerima notifikasi saat aplikasi berjalan. + Panggilan video + Menerima pesan… + Lihat pratinjau + Teks pesan + Layanan latar belakang selalu berjalan – notifikasi selalu menampilkan pesan yang tersedia. + Tampilkan hanya kontak + Sembunyikan kontak dan pesan + Kunci SimpleX + Autentikasi sistem + Autentikasi gagal + Entri kode sandi + Anda tidak dapat diverifikasi; silakan coba lagi. + Versi server penerusan tidak kompatibel dengan pengaturan jaringan: %1$s. + Versi server tujuan %1$s tidak kompatibel dengan server penerusan %2$s. + Kesalahan koneksi (AUTH) + Gagal menghubungkan ke server penerusan %1$s. Coba lagi nanti. + Alamat server penerusan tidak kompatibel dengan pengaturan jaringan: %1$s. + Unggah berkas + Eksekusi fungsi memakan waktu terlalu lama: %1$d detik: %2$s + Panggilan SimpleX Chat + Alamat server tidak kompatibel dengan pengaturan jaringan: %1$s. + Uji gagal pada langkah %s. + %d detik + Segera + Matikan tanda terima untuk grup? + Kirim tanda terima diaktifkan untuk %d grup + Kirim tanda terima dimatikan untuk %d grup + Aktifkan (grup tetap ditimpa) + Matikan (grup tetap ditimpa) + Matikan untuk semua + Aktifkan untuk semua grup + Autentikasi + Versi server tidak kompatibel dengan aplikasi Anda: %1$s. + Server tak dikenal! + Kecuali kontak Anda hapus koneksi atau tautan ini sudah digunakan, mungkin ini adalah bug - harap laporkan.\nUntuk terhubung, harap minta kontak Anda untuk buat tautan koneksi lain dan periksa apakah Anda memiliki koneksi jaringan stabil. + Gagal sinkronkan koneksi + Server perlu otorisasi untuk mengunggah, periksa kata sandi + Buat antrian + Dapat dimatikan melalui pengaturan – notifikasi akan tetap ditampilkan saat aplikasi berjalan.]]> + layanan latar belakang SimpleX – yang gunakan beberapa persen baterai per hari.]]> + Aplikasi terima pesan baru secara berkala — aplikasi ini memakai beberapa persen baterai per hari. Aplikasi ini tidak gunakan notifikasi push — data dari perangkat tidak dikirim ke server. + Layanan SimpleX Chat + Nama kontak + Masukkan Kode Sandi + Kode Sandi Saat Ini + Ubah kode sandi + Harap diingat dan simpan dengan aman - tidak ada cara untuk pulihkan kata sandi yang hilang! + Kiriman debug + Hapus + Dibuat pada: %s + diblokir \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml index c1b6a0808c..f5334d4e61 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml @@ -1607,7 +1607,7 @@ immagine del profilo rimossa Con file e multimediali criptati. La barra di ricerca accetta i link di invito. - impostata nuova immagine del profilo + ha impostato una nuova immagine del profilo impostato nuovo indirizzo di contatto profilo aggiornato Messaggio salvato diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml index a0c382f5af..cce95b5286 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml @@ -26,7 +26,7 @@ 全チャットとメッセージが削除されます(※元に戻せません※)! 送信相手からの音声メッセージを許可する。 あなたと連絡相手が音声メッセージを送信できます。 - 電池省エネに良い:バックグラウンド機能で10分毎に新着メッセージを確認します。通話と緊急メッセージを見逃す可能性があります。]]> + バッテリーに優しい。アプリは10分ごとにメッセージを確認します。ただし、電話や緊急のメッセージを見逃す可能性があります。]]> 音声オフ 添付する アプリ・ビルド番号: %s @@ -159,7 +159,7 @@ if SimpleX にユーザIDがなければ、メッセージをどうやって届けるのでしょうかと。]]> SimpleX の仕様 通話中 - 電池消費がより高い!非アクティブ時でもバックグラウンドのサービスが常に稼働します(着信次第に通知がすぐに出ます)。]]> + 電池消費がより高い!非アクティブ時でもバックグラウンドのサービスが常に稼働します(着信してすぐに通知が出ます)。]]> 発信中 通話終了 %1$s 通話が終了しました。 @@ -324,7 +324,7 @@ 接続中 発信中… 終了 - プロトコル技術とコードはオープンソースで、どなたでもご自分のサーバを運用できます。 + 誰でもサーバーをホストできます。 プライバシーを再定義 技術の説明 プライベートな接続をする @@ -1870,7 +1870,7 @@ 連絡先が削除されました。 無効 一度に最大20件のメッセージを削除できます。 - サーバに接続中 + 接続中のサーバ エラー サーバーへの再接続エラー エラー @@ -1891,4 +1891,85 @@ 保存して再接続 強め 普通 + パーセンテージを表示 + これは見た目の設定から変更できます。 + 以前接続していたサーバ + アクティブな接続 + 統計情報 + アーカイブされた連絡先 + 角丸 + 送信されたメッセージ数 + 全ての統計情報をリセットしますか? + 合計 + しっぽ + サーバ情報 + %sから計測されています。\nデバイス上の全てのデータはプライベートです。 + 受信したメッセージ数 + サーバの統計情報をリセットしようとしています - これは元に戻せません! + 全統計情報をリセットする + ヒントをリセットする + 容量を超えました - 受信者は以前に送信されたメッセージを受け取っていません。 + 試行 + 確認 + 確認エラー + 削除完了 + 作成完了 + %1$d件のその他のファイルエラー。 + メッセージの転送エラー + %1$d件のファイルエラー:\n%2$s + %1$s件のメッセージを転送しますか? + %1$d件のファイルがまだダウンロード中です。 + %1$d件のファイルがダウンロードされませんでした。 + %1$d件のファイルのダウンロードに失敗しました。 + %1$d件のファイルが削除されました。 + %1$s件のメッセージが転送されませんでした。 + ダウンロード + %1$s件のメッセージを転送中 + 会話が削除されました! + アプリのアップデートがダウンロードされました + アプリの更新をダウンロード中です。アプリを閉じないでください + %s(%s)をダウンロード + グループメンバーにメッセージを送信できません。 + 後でチャットするために連絡先をアーカイブします。 + 接続とサーバーのステータス + アドレスフィールドの近くにある情報ボタンをクリックして、マイクの使用を許可してください。 + 接続 + カスタマイズ可能なメッセージの形。 + プライベートルーティングをサポートしていなくても、メッセージを直接送信しないでください。 + 転送サーバー%1$sへの接続エラーです。後ほど再試行してください。 + WebViewの初期化エラーです。WebViewがインストールされており、サポートされているアーキテクチャがarm64であることを確認してください。\nエラー:%s + WebViewの初期化エラーです。システムを新しいバージョンに更新してください。開発者にお問い合わせください。\nエラー:%s + %1$sの宛先サーバーアドレスは、転送サーバー%2$sの設定と互換性がありません。 + %1$sの宛先サーバーバージョンは、転送サーバー%2$sと互換性がありません。 + 通知なしで削除 + チャンクが削除されました + すべてのカラーモード + グループメンバーに電話できません + 連絡先に接続中です。しばらくお待ちいただくか、後で確認してください! + ネットワークを管理 + ダウンロードしたファイル + メンバーの%d件のメッセージを削除しますか? + チャットデータベースがエクスポートされました + 通話禁止! + チャンクがダウンロードされました + チャンクがアップロードされました + ダウンロードエラー + アプリセッション + 配信のデバッグ + 改善された通話機能 + メッセージの日付 + より強力なセキュリティ ✅ + より良いユーザー体験 + 最大200件のメッセージを削除または管理します。 + プライバシー向上のためのぼかし処理。 + 友達ともっと速くつながりましょう。 + 新しいバージョンをGitHubからダウンロードしてください。 + ダウンロード完了 + 再接続 + 送信エラー + SOCKSプロキシ + パスワード + 設定 + 情報がありません、リロードしてください + SMPサーバ \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ko/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ko/strings.xml index 639a888bc9..eab9df3b92 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ko/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ko/strings.xml @@ -2,8 +2,8 @@ 연결됨 연결 중 - 그룹 링크를 통해 연결하시겠습니까\? - 초대 링크로 연결하시겠습니까\? + 그룹에 참여할까요? + 일회용 링크로 연결하시겠습니까? 연결 수립됨 연결 시간 초과 파일을 받을 수 없음 @@ -12,7 +12,7 @@ 연결 오류(인증) 대기열 만들기 데이터베이스를 초기화할 수 없음 - 백그라운드 서비스가 항상 실행 됩니다. - 메시지를 받는 즉시 알림이 표시됩니다. + 백그라운드 서비스가 항상 실행됨 – 메시지를 받는 즉시 알림이 표시됩니다. 10분마다 최대 1분간 새 메시지 확인 연결됨 숨긴 대화 상대: @@ -29,13 +29,13 @@ 뒤로 취소 라이브 메시지 취소 - 파일 선택 + 파일 확인 링크 / QR 코드로 연결 클립보드로 복사됨 비밀 그룹 생성 수락 - 모든 메시지가 삭제됩니다 - 삭제 후 되돌릴 수 없습니다! 메시지는 나에게서만 삭제됩니다. + 모든 메시지가 삭제됩니다. 이 결정은 되돌릴 수 없습니다! 메시지는 나에게서만 삭제됩니다. 지우기 지우기 채팅 지우기 @@ -56,7 +56,7 @@ 앱 버전 앱 버전 : v%s 코어 버전 : v%s - 전화 응답 + 통화 응답 굵게 전화 연결 중 색깔 @@ -68,13 +68,13 @@ 연결됨 연결 중… 내 프로필 생성 - 배터리에 좋음. 백그라운드 서비스는 10분마다 메시지를 확인합니다. 전화나 긴급 메시지를 놓칠 수 있습니다.]]> + 배터리에 좋음. 앱이 10분마다 메시지를 확인합니다. 전화나 긴급 메시지를 놓칠 수 있습니다.]]> 통화가 이미 종료되었습니다! 항상 릴레이 사용 음성 통화 음성 & 영상 통화 잠금 화면에서의 통화: - 대화 상대와 종단간 암호화되지 않음 + 대화 상대와 종단 간 암호화되지 않음 응답 소리 켜기 소리 끄기 @@ -87,9 +87,9 @@ 채팅이 작동 중 채팅 채팅 데이터베이스를 가져옴 - 주의: 비밀구절(passphrase)을 분실하면 복구하거나 비밀번호 변경을 할 수 없어요.]]> - 데이터베이스 암호구절(passphrase)을 바꾸시겠습니까\? - 새로운 암호구절(passphrase) 확인… + 주의: 암호를 분실하면 복구하거나 비밀번호 변경을 할 수 없어요.]]> + 데이터베이스 암호를 바꾸시겠습니까? + 새로운 암호 확인… 채팅 기록 보관함 내 역할이 %s 역할로 변경됨 주소 바꾸는 중… @@ -115,13 +115,13 @@ 대화 상대가 허용함 연락처 개별 설정 대화 상대가 허용한 경우에만 음성 메시지를 보낼 수 있습니다. - 대화 상대가 전송한 메시지 영구 삭제를 허용합니다. + 대화 상대가 전송한 메시지 영구 삭제를 허용합니다. (24 시간) 대화 상대가 사라지는 메시지를 전송할 수 있도록 허용합니다. 대화 상대의 음성 메시지 전송을 허용합니다. - 당신과 대화 상대 모두 메시지를 영구 삭제할 수 있습니다. + 당신과 대화 상대 모두 메시지를 영구 삭제할 수 있습니다. (24 시간) 당신과 대화 상대 모두 음성 메시지를 보낼 수 있습니다. 상대가 메시지에 삭제 표시를 할 수 있습니다. 그러나 삭제 표시된 메시지 내용은 여전히 볼 수 있습니다. - 보낸 메시지 영구 삭제를 허용합니다. + 보낸 메시지 영구 삭제를 허용합니다. (24 시간) %s 취소됨 대화 요청 자동 수락 채팅 프로필(기본값) 또는 연결(베타). @@ -142,35 +142,34 @@ QR 코드 스캔으로 서버 추가 환영 메시지 추가 관리자 - 관리자는 그룹 가입을 위한 링크를 만들 수 있습니다. + 관리자는 그룹 참여 링크를 만들 수 있습니다. 사라지는 메시지를 보낼 수 있습니다. - 모든 채팅과 메시지가 삭제됩니다 - 되돌릴 수 없습니다! + 모든 채팅과 메시지가 삭제됩니다. 이 결정은 되돌릴 수 없습니다! 음성 메시지 전송을 허용합니다. 음성 메시지를 허용하시겠습니까\? 대화상대가 허용하는 경우에만 사라지는 메시지를 허용합니다. - 그룹 구성원에게 다이렉트 메시지 보내는 것을 허용합니다. - 모든 그룹 구성원이 연결된 상태로 유지됩니다. - 대화 상대가 허용하는 경우에만 영구적인 메시지 삭제를 허용합니다. + 그룹 멤버에게 다이렉트 메시지 보내는 것을 허용합니다. + 모든 그룹 멤버가 연결된 상태로 유지됩니다. + 대화 상대가 허용하는 경우에만 영구적인 메시지 삭제를 허용합니다. (24 시간) 모든 대화 상대가 연결된 상태로 유지됩니다. 항상 켜기 - Android Keystore는 암호를 안전하게 저장하는 데 사용됩니다 - 알림 서비스가 작동할 수 있습니다. - 앱을 다시 시작하거나 암호를 변경한 후 Android Keystore를 사용하여 암호를 안전하게 저장합니다. - 알림을 받을 수 있습니다. + Android 암호 저장소는 암호를 안전하게 저장하는 데 사용됩니다 - 알림 서비스가 작동할 수 있습니다. + 앱을 다시 시작하거나 암호를 변경한 후 Android 암호 저장소를 사용하여 암호를 안전하게 저장합니다. - 알림을 받을 수 있습니다. 앱이 실행 중일 때만 알림을 받을 수 있으며, 백그라운드 서비스는 시작되지 않습니다. 앱 데이터 백업 앱 아이콘 각각의 채팅 프로필에 사용될 겁니다.]]> - 별도로 분리된 TCP 연결(및 SOCKS 자격 증명)이 각각의 대화 상대 및 그룹 구성원에게 사용될 겁니다. -\n참고: 연결이 많은 경우 배터리 및 트래픽 소비가 높을 수 있고 일부 연결이 실패할 수 있습니다. + 각각의 대화 상대 및 그룹 멤버에게 사용될 겁니다. \n참고: 연결이 많은 경우 배터리 및 트래픽 소비가 높을 수 있고 일부 연결이 실패할 수 있습니다.]]> 이미지 수신 요청됨 음성 및 영상 통화 - 음성 통화 (종단간 암호화 아님) + 음성 통화 (종단 간 암호화 아님) 인증을 사용할 수 없음 배터리 최적화가 활성화되어, 백그라운드 서비스 및 새 메시지에 대한 주기적 요청이 꺼집니다. 설정을 통해 다시 활성화할 수 있습니다. 배터리에 가장 좋음. 앱이 실행 중일 때만 알림을 받게 됩니다 (백그라운드에서 실행되지 않음).]]> 설정을 통해 비활성화할 수 있습니다. – 앱이 실행되는 동안 알림이 표시됩니다.]]> 당신과 대화 상대 모두 사라지는 메시지를 보낼 수 있습니다. - 데이터베이스 암호를 저장하고 있는 Keystore에 접근할 수 없습니다. - 배터리 더욱 사용! 백그라운드 서비스가 항상 실행됩니다. - 메시지를 수신되는 즉시 알림이 표시됩니다.]]> + 데이터베이스 암호를 저장하고 있는 암호 저장소에 접근할 수 없습니다. + 배터리를 더욱 사용함! 앱이 항상 백그라운드에서 실행됩니다. - 수신되는 즉시 알림이 표시됩니다.]]> 통화 종료됨 %1$s 전화 중… 전화 연결 중 @@ -194,9 +193,9 @@ 연결 중 (도입) 연결 오류 연결 %1$d - 링크를 통해 연결하시겠습니까\? - 대화 상대와 모든 메시지가 삭제됩니다. - 삭제 후 되돌릴 수 없습니다! - 대화 상대와 종단간 암호화됨 + 주소를 통해 연결하시겠습니까? + 대화 상대와 모든 메시지가 삭제됩니다. 이 결정은 되돌릴 수 없습니다! + 대화 상대와 종단 간 암호화됨 대화 상대와 아직 연결되지 않았습니다! %1$s에 생성 완료 비밀 그룹 생성 @@ -220,7 +219,7 @@ 삭제 대기 중인 연결을 삭제할까요\? 인증 지우기 - 데이터베이스 비밀구절(passphrase) & 내보내기 + 데이터베이스 암호 & 내보내기 서버 삭제 주소 삭제 주소를 삭제할까요\? @@ -228,12 +227,12 @@ 탈중앙화 개발자 도구 기기 - 데이터베이스 비밀구절(passphrase) + 데이터베이스 암호 모든 채팅 프로필 파일 삭제 데이터베이스 에러 - 데이터베이스 비밀구절(passphrase)이 Keystore에 저장된 것과 일치하지 않습니다. - 채팅을 열려면 데이터베이스 비밀구절(passphrase)이 필요합니다. - 보관된 채팅 삭제 + 데이터베이스 암호가 암호 저장소에 저장된 것과 일치하지 않습니다. + 채팅을 열려면 데이터베이스 암호가 필요합니다. + 보관함 삭제 보관된 채팅을 삭제할까요\? %d 개의 대화 상대가 선택되었습니다. 데이터베이스 ID @@ -242,12 +241,12 @@ 다음 기간 이후 자동 삭제 위, 다음 : 데이터베이스 삭제 - 데이터베이스는 임의의 비밀구절(passphrase)로 암호화되었습니다. 내보내기 기능 사용 전 비밀구절을 변경해 주세요. - 파일과 미디어를 삭제할까요\? - 현재 비밀구절(passphrase)… + 데이터베이스는 임의의 암호로 암호화되었습니다. 내보내기 기능 사용 전 암호를 변경해 주세요. + 파일 및 미디어를 삭제하겠습니까? + 현재 암호… 데이터베이스 암호화 완료! - 데이터베이스 암호화 비밀구절(passphrase)이 업데이트됩니다. - 데이터베이스는 임의의 비밀구절(passphrase)로 암호화되었고, 원하시면 변경할 수 있습니다. + 데이터베이스 암호화 암호가 업데이트됩니다. + 데이터베이스는 임의의 암호로 암호화되며 변경할 수 있습니다. 데이터베이스는 암호화될 것입니다. 메시지 삭제 다음 기간 이후 자동 삭제 @@ -260,14 +259,14 @@ %d일 그룹 삭제 주소 변경됨 - 데이터베이스 암호화 비밀구절(passphrase)이 업데이트되며 Keystore에 보관됩니다. - 데이터베이스는 암호화되고, 비밀구절(passphrase)은 Keystore에 보관됩니다. + 데이터베이스 암호화 암호가 업데이트되며 암호 저장소에 보관됩니다. + 데이터베이스는 암호화되고, 암호는 암호 저장소에 보관됩니다. 채팅 프로필을 삭제할까요\? 모든 파일 삭제 채팅 프로필을 삭제할까요\? 모두에게서 삭제 그룹을 삭제할까요\? - 표시 이름이 중복되어요! + 표시 이름이 중복됩니다! 연결 끊기 기기 인증이 비활성화되어 SimpleX 잠금 기능이 작동하지 않아요. SimpleX 잠금 비활성화 @@ -276,11 +275,11 @@ 새로운 채팅 시작 표시 이름 표시 이름에는 공백문자가 쓰일 수 없어요. - 표시 이름 - 종단간 암호화된 음성 전화 - 종단간 암호화된 영상 전화 + 이름을 입력: + 종단 간 암호화된 음성 전화 + 종단 간 암호화된 영상 통화 비활성화 - 종단간 암호화 + 종단 간 암호화 중복된 메시지 1일로 설정 사라지는 메시지 @@ -294,9 +293,9 @@ %d 개월 %d 주 다운그레이드하고 채팅 열기 - 1:1 메시지 + 다이렉트 메시지 사라지는 메시지 - 이 그룹에서는 멤버들의 1:1 채팅이 금지되어 있어요. + 이 그룹에서는 멤버들의 다이렉트 메시지가 금지되어 있어요. %d초 %d 초 %d시 @@ -307,7 +306,7 @@ 기기 인증을 하고 있지 않아요. 기기 인증을 켜면 설정에서 SimpleX 잠금 기능을 사용할 수 있어요. %d 시간 %d 시간 - 앱/데이터베이스의 다른 마이그레이션: %s / %s + 앱/데이터베이스의 다른 이전: %s / %s 다른 이름, 아바타 그리고 전송 격리. 다시 보지 않기 이 대화 상대로부터의 메시지를 수신할 서버와 연결되었어요. @@ -336,7 +335,7 @@ 환영 메시지 그룹 프로필 수정 그룹 나가기 - 1:1 채팅 시작하기 + 다이렉트 메시지 보내기 서버 인다이렉트 (%1$s) 허용함 @@ -375,12 +374,12 @@ 프로필 생성 오류! 그룹 링크로 익명 채팅 그룹 링크로 채팅 - 일회용 링크로 채팅 + 일회용 링크를 통해 일회용 익명 링크를 공유했어요. 일회용 링크를 공유했어요. 상대의 연락처 링크로 익명 연결 상대의 연락처 링크로 연결 - 일회용 연락처로 익명 연결 + 일회용 링크로 익명 연결 SMP 서버 주소가 올바른 형식이고 줄로 구분되어 있고 중복이 없는지 확인해 주세요. SMP 서버 저장 오류 네트워크 설정 업데이트 오류 @@ -403,13 +402,13 @@ 테스트가 %s단계에서 실패했어요. 서버는 대기열을 생성하고 비밀번호를 확인하려면 인증이 필요해요. 알림을 받으려면 데이터베이스 암호를 입력해 주세요. - 비밀번호가 필요해요. - 프로필 삭제 오류 + 암호가 필요해요. + 사용자 프로필 삭제 오류 사용자 개인정보 업데이트 오류 데이터베이스가 올바르게 작동하지 안하요. 자세히 알아보려면 탭하세요. 수정하기 - 메시지가 삭제돼요. 삭제 후 복구할 수 없어요! - 메시지가 삭제 표시될 거예요. 대화 상대는 여전히 삭제된 내용을 볼 수 있어요. + 메시지가 삭제됩니다. 이 결정은 되돌릴 수 없습니다! + 메시지가 삭제 표시됩니다. 수신자는 여전히 삭제된 내용을 볼 수 있습니다. WebRTC ICE 서버 주소가 올바른 형식이고 줄로 구분되고 중복이 없는지 확인해 주세요. ICE 서버(한 줄에 하나씩) ICE 서버 저장 오류 @@ -425,39 +424,39 @@ 데이터베이스 내보내기 자동 삭제되는 메시지를 사용할까요\? 설정 변경 오류 - 이 작업은 되돌릴 수 없어요. 선택한 시간보다 일찍 보내거나 받은 메시지는 삭제돼요. 이는 몇 분 걸릴 수 있어요. + 이 결정은 되돌릴 수 없습니다. 선택한 시간보다 일찍 보내거나 받은 메시지는 삭제됩니다. 이는 몇 분 걸릴 수 있습니다. 오류: %s - 올바른 비밀번호를 입력해 주세요. - 데이터베이스 비밀번호 변경이 완료되지 않았어요. + 올바른 암호를 입력해 주세요. + 데이터베이스 암호 변경이 완료되지 않았어요. 데이터베이스 오류 복구 그룹 링크 생성 오류 그룹 링크 업데이트 오류 역할 변경 오류 멤버 삭제 오류 데이터베이스 다운그레이드 - 마이그레이션: %s - 모든 멤버에게서 그룹이 삭제돼요. 삭제 후 복구할 수 없어요! - 나에게서만 그룹이 삭제되요. 삭제 후 복구할 수 없어요! + 이전: %s + 모든 멤버에게서 그룹이 삭제됩니다. 이 결정은 되돌릴 수 없습니다! + 나에게서만 그룹이 삭제됩니다. 이 결정은 되돌릴 수 없습니다! 파일을 찾을 수 없음 사용자 비밀번호 저장 오류 채팅 정지하기 오류 - 채팅 데이터베이스 내보내기 오류 - 채팅 데이터베이스가 암호화되지 않았어요. 비밀번호를 설정하여 보호해 주세요. - 비밀번호를 입력해 주세요… + 채팅 데이터베이스를 내보내는 동안 오류 + 채팅 데이터베이스가 암호화되지 않았어요. 암호를 설정하여 보호해 주세요. + 암호를 입력해 주세요… 검색에 비밀번호 입력 이미지 수정하기 - 이 작업은 실행 취소될 수 없어요. 프로필, 연락처, 메시지 및 파일이 영구적으로 손실돼요. + 이 결정은 되돌릴 수 없습니다. 프로필, 연락처, 메시지 및 파일이 영구적으로 손실됩니다. 채팅 데이터베이스 가져오기 오류 데이터베이스를 암호화할까요\? 데이터베이스 ID 및 전송 격리 옵션. 채팅 시작하기 오류 데이터베이스 암호화 오류 - 올바른 현재 비밀번호를 입력해 주세요. + 올바른 현재 암호를 입력해 주세요. 채팅 프로필 삭제 프로필 삭제 경고: 일부 데이터가 손실될 수 있어요! 데이터베이스 업그레이드 - 이 작업은 실행 취소될 수 없어요. 수신 및 전송된 모든 파일과 미디어가 삭제돼요. 저해상도 사진만 삭제되지 않아요. + 이 결정은 되돌릴 수 없습니다. 수신 및 전송된 모든 파일과 미디어가 삭제됩니다. 저해상도 사진은 삭제되지 않습니다. 채팅 데이터베이스 삭제 오류 그룹 링크 삭제 오류 파일 저장 오류 @@ -488,14 +487,14 @@ 그룹으로 초대 %1$s 그룹 링크 환영 메시지 - 보여지는 그룹 이름 + 그룹 이름 입력: 그룹 이름 : - 그룹은 완전히 탈중앙화되어 있으며 구성원만 그룹을 볼 수 있어요. - 프로필이 그룹 구성원에게 전송될 거예요. + 완전히 탈중앙화됨 – 멤버만 볼 수 있습니다. + 프로필이 그룹 멤버에게 전송될 거예요. 그룹 프로필은 서버가 아닌 멤버들의 기기에 저장되어요. 그룹 설정 - 그룹 구성원은 사라지는 메시지를 보낼 수 있습니다. - 그룹 멤버들끼리 1:1 채팅을 할 수 있어요. + 그룹 멤버는 사라지는 메시지를 보낼 수 있습니다. + 그룹 멤버들끼리 다이렉트 메시지를 보낼 수 있어요. 멤버 초대하기 비활성 그룹 관찰자 @@ -513,7 +512,7 @@ 마크다운 사용법 SimpleX 작동 방식 그룹 초대가 만료되었어요. - 그룹 멤버는 보낸 메시지를 영구 삭제할 수 있어요. + 그룹 멤버는 보낸 메시지를 영구 삭제할 수 있습니다. (24 시간) 그룹 멤버는 음성 메시지를 보낼 수 있어요. 숨긴 프로필 비밀번호 작동 방식 @@ -545,7 +544,7 @@ 이미지 수가 너무 많아요! 거절해도 상대에게 알림이 전송되지 않아요. 영상 통화에서 QR 코드를 보여주거나 링크를 공유해 주세요.]]> - 영상 전화 + 영상 통화 영상 끄기 스피커 켜기 영상 켜기 @@ -556,7 +555,7 @@ 대화 상대가 업로드를 완료하면 이미지가 수신될 거예요. 프로필 이미지 하나의 프로필로 여러 사람과 연락할 필요 없이 무수히 많은 익명 프로필로 연락할 수 있어요. - 스팸 및 남용에 면역 + 스팸 방지 무시하기 SimpleX Chat 초대 링크를 받으면 브라우저에서 참여할 수 있어요 : 링크 미리보기 이미지 @@ -586,16 +585,16 @@ 그룹 호환되지 않는 데이터베이스 버전 그룹에 참여 중 - 익명 모드는 기본 프로필 이름과 사진과 같은 개인 정보를 보호해줘요. 새 대화 상대마다 새로운 랜덤 프로필이 만들어져요. + 익명 모드는 대화 상대마다 새로운 무작위 프로필을 사용하여 개인 정보를 보호합니다. %s 은(는) 인증되었어요. 기울게 익명 프로필 사용 중 초대받은 그룹에 참여하면, 그 그룹에서도 동일한 익명 프로필이 사용되어요. - 내 랜덤 프로필 + 내 무작위 프로필 음성 전화 옴 %s은(는) 인증되지 않았어요. 터미널용 SimpleX Chat를 설치하세요 - 영상 전화 옴 - 잘못된 마이그레이션 확인 + 영상 통화 옴 + 잘못된 이전 확인 익명 모드로 참여 잘못된 QR 코드 잘못된 보안 코드! @@ -617,7 +616,7 @@ 연락처 이름 및 메시지 숨기기 켜기 대화 상대가 나와의 연결을 삭제했을 가능성이 커요. - 메시지 전달 오류 + 메시지 전송 오류 조정 모든 멤버에게서 메시지가 삭제될 거예요. 이 메시지는 모든 멤버에게 조정됨으로 표시될 거예요. @@ -634,11 +633,11 @@ 메시지 이 설정은 현재 내 프로필의 메시지에 적용되어요. 멤버 - 역할이 "%s"(으)로 변경되고, 회원은 새로운 초대를 받게 될 거예요. + 역할이 %s(으)로 변경되고, 멤버는 새로운 초대를 받게 될 거예요. 이 채팅에서는 메시지 영구 삭제가 허용되지 않았어요. 나가기 큰 파일! - 네트워크 설정 + 고급 설정 연결하려면 Onion 호스트가 필요해요. 핑 횟수 핑 간격 @@ -667,14 +666,14 @@ Onion 호스트가 사용되지 않을 거예요. 전송 격리 차세대 사생활 보호 메시징 - 새 비밀번호… + 새 암호… TCP 연결 유지 활성화 %s의 새로운 기능 마크다운 도움말 SimpleX에는 사용자 식별자가 없는데도 어떻게 메시지를 전달할 수 있어요\?]]> 그룹에서 나갈까요\? - 데이터베이스 버전이 앱보다 최신이지만, 다음에 대한 다운 마이그레이션 없음: %s - 멤버가 그룹에서 제거되어요. 이 작업은 되돌릴 수 없어요! + 앱 버전보다 최신 버전의 데이터베이스를 사용하고 있지만 데이터베이스를 다운그레이드할 수 없습니다: %s + 멤버가 그룹에서 제거됩니다. 이 결정은 되돌릴 수 없습니다! 역할이 "%s"(으)로 변경되어요. 그룹의 모든 멤버에게 알림이 전송됩니다. 기본값으로 재설정 메시지 내용 @@ -690,18 +689,18 @@ SimpleX Chat 메시지 그룹 관리자에게 문의해 주세요. 메시지를 보낼 수 없습니다! - + OK 거절 비밀번호 표시 2계층 종단 간 암호화 로 전송된 사용자 프로필, 연락처, 그룹 및 메시지를 저장되어요.]]> 자세한 내용은 GitHub에서 확인해 주세요. - 개인 정보 및 보안 + 개인 정보 보호 및 보안 알림은 앱이 중지되기 전까지만 전달될 거예요! 당신만 사라지는 메시지를 보낼 수 있습니다. 사라지는 메시지 전송은 허용되지 않습니다. 음성 메시지 허용되지 않음. 사라지는 메시지 전송은 허용되지 않습니다. - 이전 데이터베이스 기록 + 이전 데이터베이스 보관함 %1$s 초대됨 나감 강퇴됨 @@ -709,7 +708,7 @@ 그룹 소유자만 그룹 설정을 변경할 수 있어요. 다음을 통해 수신 대화 상대만 사라지는 메시지를 보낼 수 있습니다. - 멤버들 간의 1:1 채팅이 허용되지 않음. + 멤버들 간의 다이렉트 메시지가 허용되지 않음. 나만 음성 메시지를 보낼 수 있어요. 대화 상대만 음성 메시지를 보낼 수 있어요. 메시지 영구 삭제 허용되지 않음. @@ -720,9 +719,9 @@ 붙여넣기 프로필은 대화 상대들하고만 공유됩니다. 프라이버시의 재정의 - 오픈 소스 프로토콜과 코드 - 누구나 자신만의 서버를 구축할 수 있어요. + 누구나 서버를 호스팅할 수 있습니다. 앱이 실행 중일 때 - GitHub 에서 확인해 주세요.]]> + GitHub 에서 확인해 주세요.]]> 릴레이 서버는 IP 주소를 숨겨주지만, 통화 시간을 관찰 할 수 있어요. 그룹 링크로 초대 설정을 통해 나중에 변경할 수 있어요. @@ -732,7 +731,7 @@ 열기 앱 잠금 %1$s님, 환영합니다! - (그룹 구성원에게만 저장됨) + (그룹 멤버에게만 저장됨) 앱 평가하기 주기적 즉시 @@ -752,12 +751,12 @@ 저장하고 그룹 멤버들에게 알리기 저장하고 대화 상대에게 알리기 지우기 - 아카이브 저장하기 - 암호 저장소에 비밀번호 저장하기 + 보관함 저장하기 + 암호 저장소에 암호 저장하기 데이터베이스 백업 복원하기 - 데이터베이스 백업을 복원한 후 이전 비밀번호를 입력해 주세요. 이 작업은 되돌릴 수 없어요. + 데이터베이스 백업을 복원한 후 이전 비밀번호를 입력해 주십시오. 이 결정은 되돌릴 수 없습니다. 데이터베이스 백업을 복원할까요\? - 키스토어에서 암호를 찾을 수 없어요. 직접 입력해 주세요. 백업 도구를 사용하여 복원했을 때 이 문제가 발생할 수 있는데, 그런 경우가 아니라면 개발자에게 알려주세요. + 암호 저장소에서 암호를 찾을 수 없어요. 직접 입력해 주세요. 백업 도구를 사용하여 복원했을 때 이 문제가 발생할 수 있는데, 그런 경우가 아니라면 개발자에게 알려주세요. 저장하고 그룹 프로필 업데이트하기 환영 메시지를 저장할까요\? 그룹 프로필 저장하기 @@ -766,13 +765,13 @@ 코드 스캔하기 대화 상대의 앱에서 보안 코드를 스캔해 주세요. 저장된 WebRTC ICE 서버가 제거될 거예요. - 비밀번호 저장하고 채팅 열기 + 암호를 저장하고 채팅 열기 역할 설정을 저장할까요\? 프로필 비밀번호 저장하기 가져온 채팅 데이터베이스를 사용하려면 앱을 다시 실행해 주세요. 새 프로필을 만드려면 앱을 다시 실행해 주세요. - 암호 저장소에서 비밀번호를 삭제할까요\? + 암호 저장소에서 암호를 삭제할까요? 채팅 기능 실행하기 복원하기 대화 상대가 파일 전송을 취소했어요. @@ -805,7 +804,7 @@ 파일 공유… 이미지 공유… 메시지 공유… - 라이브 메시지 보내기 - 입력 과정을 실시간으로 상대에게 보여줘요. + 라이브 메시지 보내기 - 입력 과정을 실시간으로 상대에게 보여줍니다. 보내기 초대 링크 공유 보안 코드 @@ -854,16 +853,16 @@ 메시지 및 파일 익명 모드 실험적 - 내보낼 비밀번호 설정 + 내보낼 암호 설정 SMP 서버 미리 설정된 서버 주소 내 서버 내 서버 주소 %1$s을(를) 강퇴했어요. 채팅 데이터베이스를 내보내기, 가져오기 또는 삭제 하려면 채팅 기능을 중지해 주세요. 채팅 기능이 중지된 동안에는 메시지를 주고받을 수 없어요. - 비밀번호를 모르면 변경하거나 찾을 수 없으므로 비밀번호를 안전하게 보관해 주세요. + 암호를 모르면 변경하거나 찾을 수 없으므로 암호를 안전하게 보관해 주세요. 제출하기 - 비밀번호를 모르면 채팅에 액세스할 수 없으니 비밀번호를 안전하게 보관해 주세요. + 암호를 모르면 채팅에 액세스할 수 없으니 암호를 안전하게 보관해 주세요. 채팅 기능을 중지할까요\? 데이터베이스 작업을 할 수 있도록 채팅 기능을 중지하기 수신 주소 바꾸기 @@ -884,7 +883,7 @@ 사용자화 전송 - 자폭 패스코드 변경 + 자체 소멸 패스코드 변경 모든 앱 데이터가 삭제되었습니다. 인증 패스코드 변경 @@ -897,12 +896,12 @@ 다른 사용자와 연결할 수 있도록 주소를 만듭니다. 커스텀 테마 자동 수락 - 자폭 모드 변경 + 자체 소멸 모드 변경 (현재) 다크 테마 메시지 반응을 허용합니다. 당신과 대화 상대 모두 메시지 반응을 추가할 수 있습니다. - 대화 상대가 당신에게 전화할 수 있도록 허용합니다. + 대화 상대가 당신에게 통화할 수 있도록 허용합니다. 현재 패스코드 SimpleX 주소에 대하여 인증 취소됨 @@ -917,12 +916,12 @@ 대화 상대가 허용하는 경우에만 메시지 반응을 허용합니다. 모든 대화가 연결된 상태로 유지됩니다. 프로필 업데이트가 대화 상대에게 전송됩니다. 전송된 메시지는 설정된 시간이 지나면 삭제됩니다. - 앱 패스코드가 자체소멸 패스코드로 대체되었습니다. + 앱 패스코드가 자체 소멸 패스코드로 대체되었습니다. 음성/영상 통화가 허가되지 않았습니다. 잘못된 메시지 해쉬 인증 실패 카메라 - 대화 상대가 메시지 응답을 추가할 수 있도록 허용합니다. + 대화 상대가 메시지 반응을 추가할 수 있도록 허용합니다. 잘못된 메시지 아이디 당신과 대화 상대 모두 전화를 걸 수 있습니다. 앱 패스코드 @@ -947,7 +946,7 @@ 연락처 추가 관리자 관리자는 모든 멤버를 위해 특정 멤버를 차단할 수 있습니다. - 메시지 전달 확인서! + 메시지 전송 확인서! 우리가 놓친 두 번째 체크! ✅ 주소 변경 중지 - 최대 5분의 음성 메시지. @@ -963,4 +962,470 @@ 더 보기 보안 평가 SimpleX Chat 보안은 Trail of Bits에 의해 감사되었습니다. + 이미 연결 중입니다! + %1$d개 기타 파일 오류. + %1$d개 파일이 다운로드되지 않았습니다. + %1$d개의 파일을 다운로드하지 못했습니다. + 암호화 동의 중… + 몇 가지 더 + 허용 + %1$s개의 메시지가 전송되지 않았습니다. + %1$d개의 파일이 아직 다운로드 중입니다. + %1$d개 파일이 삭제되었습니다. + 확인됨 + 승인 오류 + SimpleX 링크 전송을 허용합니다. + 앱 데이터 이전 + 모든 연락처, 대화 및 파일은 안전하게 암호화되어 구성된 XFTP 릴레이에 일괄 업로드됩니다. + 활성 연결 + 모든 프로필 + 적용 + 모든 메시지가 삭제됩니다. 이 결정은 취소할 수 없습니다! + 파일 및 미디어 전송을 허용합니다. + 새로운 무작위 프로필이 공유됩니다. + 모든 색상 모드 + 항상 + 외 %d개 이벤트 + 앱이 새 로컬 파일 (비디오 제외)을 암호화합니다. + 이미 그룹에 있습니다! + 항상 프라이빗 라우팅 사용. + %s에 대한 암호화에 동의 중… + %s의 새 메시지는 모두 숨겨집니다! + 통화를 허용할까요? + 다운그레이드 허용 + 채팅 + 데스크톱과의 연결이 잘못된 상태입니다. + 보관 및 업로드 + 연결 중 + %1$d항목의 파일 오류:\n%2$s + 생성 + 올바른 이름을 %s 로 지정하시겠습니까? + 카메라 + 카메라와 마이크 + 연락처 + 연락처 %1$s가 %2$s (으)로 변경됨 + 직접 연결하시겠습니까? + 적용 대상 + 무작위 프로필을 사용하여 그룹을 만듭니다. + 잘못된 데스크톱 주소 + 연결이 종료됨 + 연결됨 + 연결 + 음성 통화 + 곧 출시 예정입니다! + 채팅 데이터베이스 + 채팅 테마 + 친구들과 더 빠르게 연결하세요. + 연결이 중지됨 + 데스크톱에 연결됨 + 데스크톱에 연결 중 + 자신과 연결하겠습니까? + 채팅이 이전되었습니다! + - 디렉터리 서비스(베타)에 연결하세요!\n- 전송 알림(최대 20명).\n- 더 빠르고 안정적입니다. + 이전 취소 + 이전하려는 데이터베이스의 암호를 기억하고 있는지 확인합니다. + 익명 모드로 연결 + 베타 + 앱 패스코드 + 계속 + 채팅이 중지되었습니다. 다른 기기에서 이 데이터베이스를 이미 사용하고 있다면, 채팅을 시작하기 전에 다시 전송해야 합니다. + 직접 연결됨 + 일괄 다운로드됨 + %1$s과(와) 연결하시겠습니까? + 앱 테마 + 앱 업데이트가 다운로드되었음 + 아랍어, 불가리아어, 핀란드어, 히브리어, 태국어 및 우크라이나어 - 사용자와 Weblate 덕분입니다. + 향상된 그룹 기능 + 데이터베이스를 저장 중 + 저자 + 용량 초과 - 수신자가 이전에 보낸 메시지를 받지 못했습니다. + 셀룰러 + 채팅 색상 + 업데이트 확인 + 인터넷 연결을 확인하고 다시 시도하십시오 + 다른 기기에서 이전을 선택하고 QR 코드를 스캔합니다.]]> + 완료 + 알 수 없는 서버의 파일을 확인합니다. + 연결된 서버 + 연결 및 서버 상태. + %s 과의 연결이 잘못된 상태입니다.]]> + 네트워크 관리 + 수신 주소를 변경하겠습니까? + 데스크톱에 연결 + 메시지를 보낼 수 없음 + 연락처 삭제를 확인하시겠습니까? + 연락처가 삭제됩니다. 이 결정은 되돌릴 수 없습니다! + 대화가 삭제되었습니다! + 연락처가 삭제되었어요! + 파일 선택 + 카메라를 사용할 수 없음 + 그룹 생성: 새로운 그룹을 생성합니다.]]> + 연락처 추가 : 새 초대 링크를 만들거나 받은 링크를 통해 연결합니다.]]> + 개인 메모를 지우시겠습니까? + XFTP 서버 구성 + 나중에 채팅할 수 있도록 연락처를 보관합니다. + 연결된 데스크톱 + 일괄 업로드됨 + 보관된 연락처 + SMP 서버 구성 + 채팅 프로필 생성 + 그룹 멤버에 전화를 걸 수 없음 + 자동 연결 + 연결이 중지됨 + 시도 + 앱 세션 + 업데이트 확인 + 주소 필드 근처에 있는 정보 버튼을 클릭하여 마이크를 사용할 수 있습니다. + 모서리 + 채팅 데이터베이스를 내보냈습니다 + 연락처에 전화를 걸 수 없음 + 연락처에 연결하니, 잠시 기다리거나 나중에 확인하십시오! + 연락처가 삭제됩니다. + 색상 모드 + 복사 오류 + 향상된 통화 기능 + 향상된 메시지 날짜 기능 + 향상된 보안 ✅ + 링크로 연결하시겠습니까? + 네트워크 설정 확인 + 일괄 삭제됨 + 업로드 확인 + 모바일에 연결 + 연결된 모바일 + 연결 + 그룹 멤버에게 메시지를 보낼 수 없음 + 전송 알림을 활성화하시겠습니까? + 암호화 OK + 데스크톱 + 기능 처리 시간이: %1$d 초 이상: %2$s + 주소를 만들지 않음 + 기기 + 파일 및 미디어 금지됨! + 링크 생성 중… + 전달 서버 %1$s에 연결하는 동안 오류가 발생했습니다. 나중에 다시 시도하십시오. + %d 초 + 모든 그룹에 사용 안 함 + 사용 안 함 (그룹 변경 사항 유지) + 데스크톱 앱에서 새 프로필 생성합니다. 💻 + 오류 + 주소 변경을 중단하는 중 오류 + 메시지를 전송하는 동안 오류 + 멤버의 %d 메시지를 삭제하시겠습니까? + 파일을 찾을 수 없음 - 파일이 삭제되었거나 취소되었을 가능성이 큽니다. + 통화 + 다운로드 + 프록시에서 자격 증명을 사용하지 마십시오. + 프록시를 저장하는 중 오류 + 이어폰 + 블루투스 + 암호화 재협상 실패 + 자체 소멸 활성화 + 활성화 (변경 사항 유지) + 전송 알림을 비활성화하시겠습니까? + 그룹 전송 알림을 비활성화하시겠습니까? + 모두 사용 안 함 + 모두 활성화 + 데이터베이스가 암호화되고 암호가 설정에 저장됩니다. + %s 차단됨 + %s 의 암호화 재협상 필요 + %s 의 암호화 재협상 허용 + 멤버 블록 오류 + 그룹 검색 및 참여하기 + 더 빠른 참여와 더 안정적인 메시지. + 전송 알림! + 활성화 + 데스크톱 연결을 끊으시겠습니까? + 데스크톱에 잘못된 초대 코드가 잘못되었습니다 + 데스크톱 연결이 끊어졌습니다 + 다운로드 실패 + 채팅 데이터베이스를 내보내는 동안 오류 + 파일 + 오류 + 다운로드됨 + 만료 + 삭제됨 + 삭제 오류 + 새 멤버에게 내역을 보내지 마십시오. + 삭제 및 연락처에 알림 + 차단됨 + 관리자에 의해 차단됨 + %d 개의 메시지가 차단됨 + %d 개의 메시지가 관리자에 의해 차단됨 + %1$s의 대상 서버 주소가 전달 서버 %2$s의 설정과 호환되지 않습니다. + 대상 서버 버전 %1$s이 전달 서버 %2$s와 호환되지 않습니다. + 확장 + 카메라 권한 활성화 + 잠금 활성화 + 로컬 파일 암호화 + 암호화 재협상 필요 + 멤버 연락처를 만드는 동안 오류 + 초대장을 보내는 중 오류 + 비활성화됨 + 모두 차단 + 이 멤버를 차단하시겠습니까? + 저장된 파일 & 미디어 암호화 + 한 번에 최대 20개의 메시지를 삭제할 수 있습니다. + 더 나은 개인정보 보호를 위한 흐리기 + 오류 + %s 에서 연결이 끊어졌습니다]]> + 데스크톱 기기 + 데스크톱 버전이 지원되지 않습니다. 두 기기가 동일한 버전에 있는지 확인하십시오. + 암호 입력 + 보관함을 다운로드하는 동안 오류 + 보관함을 업로드하는 중 오류 + 설정을 저장하는 중 오류 + 내보낸 파일이 없음 + 이 기기에서 데이터베이스 삭제 + 경고: 보관된 데이터가 삭제됩니다.]]> + 상세 + 서버를 다시 연결하는 중 오류 + 다운로드 오류 + 사용 안 함 + 전송 알림을 활성화하는 동안 오류! + 데스크톱이 비활성 상태입니다 + 보관함 다운로드 중 + 보관 링크를 생성 중 + 파일 오류 + 프로필 생성 + 파일 및 미디어 + 로컬 네트워크를 통해 탐색 가능 + 암호화 재협상 오류 + %d 메시지를 삭제하시겠습니까? + 다운로드 + GitHub에서 새 버전을 다운로드합니다. + 데스크톱 주소 + 패스코드 + 모든 그룹에 활성화 + 활성화 (그룹 변경 사항 유지) + 파일 + 삭제된 연락처 + %s 의암호화에 동의함 + %s 의 암호화 OK + 상대가 온라인 상태가 될 때까지 기다릴 필요가 없습니다! + 그룹 멤버 차단 + 현재 프로필 + SMP 서버를 로드하는 중 오류 + 개인 메모를 삭제하는 동안 오류 + 데이터베이스를 삭제하는 동안 오류 + 다음과 같은 이유로 끊어졌습니다: %s + 통화 종료 + 알림을 표시하는 동안 오류가 발생하였으니, 개발자에게 문의하십시오. + %d 분 + %d 개의 메시지가 삭제됨 + XFTP 서버를 저장하는 중 오류 + 알림 비활성화 + 패스코드 입력 + 주소를 설정하는 중 오류 + 좋아함 + 연결을 동기화하는 중 오류 + 콘텐츠를 표시하는 중 오류 + 메시지를 표시하는 중 오류 + 미디어 흐리기 + 참고: 두 기기에서 동일한 데이터베이스를 사용하면 연결된 사람들의 메시지 복호화가 깨질 수 있으며, 이는 보안 보호 조치입니다.]]> + 다크 + 데이터베이스 이전이 진행 중입니다.\n이 작업은 몇 분 정도 걸릴 수 있습니다. + 대상 서버 오류: %1$s + 전송 + 상세 통계 + 개발자 옵션 + 사용자 또는 대상의 서버가 프라이빗 라우팅을 지원하지 않는 경우에도 메시지를 직접 보내지 마십시오. + 알림 없이 삭제 + 프로필을 전환하는 중 오류 + 삭제 완료 + 에 사라짐 + 다크 모드 + 로컬 네트워크를 통해 탐색 + 차단됨 + 다이렉트 채팅에서 활성화함 (베타)! + 자체 소멸 + 환영 메시지 입력…(선택사항) + WebView를 초기화하는 중 오류가 발생했습니다. WebView가 설치되어 있고 지원되는 아키텍처가 arm64인지 확인합니다.\n오류: %s + XFTP 서버를 로드하는 중 오류가 발생했습니다. + 세부 정보를 로드하는 중 오류 + 통계를 재설정하는 중 오류 + 대화에서 비활성화 된 경우에도 마찬가지입니다. + 테마 내보내기 + 삭제 완료: %s + 보낸 날짜: %s + 관리자에 의해 차단됨 + 비활성화됨 + 치명적 오류 + 오류: %1$s + WebView를 초기화하는 중 오류가 발생했습니다. 시스템을 새 버전으로 업데이트하십시오. 개발자에게 문의하세요.\n오류: %s + 오류 + 생성 완료 + 암호화에 동의함 + 암호화 재협상 허용 + %s :에 사라짐 + 멤버 차단 + 멤버를 차단하시겠습니까? + 통화 금지! + 전송 알림이 더 이상 유효하지 않습니다! + 연결 끊기 + 데스크톱 앱 버전 %s은(는) 이 앱과 호환되지 않습니다. + 모바일 연결 끊기 + 데스크톱이 사용 중입니다 + 서버를 다시 연결하는 중 오류 + 메시지를 만드는 동안 오류 + 참고: 메시지 및 파일 릴레이는 SOCKS 프록시를 통해 연결됩니다. 통화 및 전송 링크 미리 보기는 직접 연결을 사용합니다.]]> + 비활성화됨 + 프라이빗 라우팅 사용안함 + 앱 업데이트를 다운로드하는 중입니다. 앱을 닫지 마세요 + %s (%s) 를 다운로드 + 비활성화 + 브라우저를 여는 중 오류 + 차단 + 검은색 + 사용 안 함 (변경 사항 유지) + 자체 소멸 패스코드 활성화 + 데이터베이스 암호화 암호가 업데이트되고 설정에 저장됩니다. + 환영 메시지 입력… + 그룹 생성 + 다크 모드 색상 + 파일 및 미디어가 허용되지 않음 + 향상된 사용자 경험 + 사용자 지정 가능한 메시지 모양. + 최대 200개의 메시지를 삭제하거나 관리할 수 있습니다. + 링크 세부 정보를 다운로드하는 중 + 암호 해독 오류 + 다운로드한 파일 + 중복 + 암호를 확인하는 중 오류: + 그룹 전송 알림을 활성화하시겠습니까? + %d 그룹 이벤트 + 보낸 날짜 + 전송 디버그 + 파일 및 미디어는 이 그룹에서 금지됩니다. + 이 장치의 이름을 입력하십시오… + 데스크톱을 찾음 + 전달 서버: %1$s\n대상 서버 오류: %2$s + 전달 서버: %1$s\n오류: %2$s + %1$s 메시지를 전송하시겠습니까? + 파일 없이 메시지를 전달하시겠습니까? + %1$s 메시지 전송 중 + 메시지 전송… + Android 설정에서 이 권한을 찾아 수동으로 허용하십시오. + 글꼴 크기 + 안녕하세요! + 안녕하세요! + 파일 서버 오류: %1$s + 파일이 서버에서 삭제됩니다. + 전달 서버 %1$s가 대상 서버 %2$s에 연결하지 못했습니다. 나중에 시도하십시오. + 전달 서버 버전이 네트워크 설정과 호환되지 않습니다: %1$s. + 전달 서버 주소가 네트워크 설정과 호환되지 않습니다: %1$s. + 전송됨 + 에서 전송됨 + 파일 상태: %s + 맞춤 + 드디어, 우리는 그것들을 얻었냈습니다! 🚀 + 파일이 삭제되었거나 링크가 유효하지 않음 + 이전 완료 + 전송 + 전송됨 + 메시지 전송… + 연락처에서 지원하지 않는 수정 + 채우기 + 수정 + 연결 수정 + 연결을 수정하시겠습니까? + 그룹 멤버에서 지원하지 않는 수정 + 메시지 전송 및 저장 + 배터리 사용량을 더욱 줄임 + 한 번에 최대 20개의 메시지를 전달할 수 있습니다. + 다른 기기로 이전을 완료합니다. + 파일 상태 + 프랑스어 인터페이스 + 메시지 전송 경고 + 종단 간 암호화로 보호되며, 완벽한 전방 비밀성, 부인 방지 및 침입 복구 기능이 포함되어 있습니다.]]> + 양자 저항 종단 간 암호화로 보호되며, 완벽한 전방 비밀성, 메시지 부인 방지 및 침입 복구 기능이 포함되어 있습니다.]]> + 보관함을 가져오는 중 + 메시지를 선택한 후 메시지가 삭제되었습니다. + 메시지 + 권한 부여 + 설정에서 부여 + 전화 권한 부여 + 헤드폰 + 메시지 초안 + 앱을 열 때 자체 소멸 패스코드를 입력하는 경우: + 메시지 초안 + 향상된 메시지 전송 + 메시지 출처는 비공개로 유지됩니다. + 호환되지 않는 버전 + 가져오기 실패 + 수신된 메시지 + 보낸 메시지 + 메시지 상태 + 익명 그룹 + 내역 + 정보 + 답장 대상 + 숨기기 + 테마 가져오기 + 메시지 반응 + 이 채팅에서는 메시지 반응이 금지됩니다. + 그룹 관리 + 헝가리어 및 튀르키예어 UI + 향상된 메시지 전송 + 시간 + 글꼴 크기 키우기 + 최근 앱 목록에서 앱 화면을 숨깁니다. + (신규)]]> + 호스트 + 즉시 + 잘못된 패스코드 + 앱을 열 때 이 패스코드를 입력하면 모든 앱 데이터가 되돌릴 수 없게 제거됩니다! + 메시지 수신 + 그룹 멤버는 파일 및 미디어를 보낼 수 있습니다. + 그룹이 이미 존재합니다! + 벨소리 + 메시지 전송됨 + 멤버가 활동 상태가 되면 나중에 메시지가 전달될 수 있습니다. + 메시지가 삭제됩니다. 이 결정은 되돌릴 수 없습니다! + 이미지 + 직접 만날 수 없는 경우 영상 통화에서 QR 코드를 보여주거나 링크를 공유하세요. + 테마 가져오기 오류 + 향상된 익명성 및 보안 + 비활성 + 즉시 알림! + 메시지 반응 + 성공적으로 설치됨 + 업데이트 설치 + 인터페이스 색상 + 메시지 라우팅 대체 + 그룹 멤버가 메시지 반응을 추가할 수 있습니다. + 메시지 모양 + 메시지 대기열 정보 + 메뉴 & 알림 + 그룹 환영 메시지 + 숨겨진 채팅 프로필 + 계속하려면 채팅을 중지시켜야 합니다. + 메시지 상태: %s + %s에서 보낸 메시지가 표시됩니다! + 내역이 새 멤버에게 전송되지 않습니다. + 이 그룹에서는 메시지 반응이 금지됩니다. + 그룹 멤버가 SimpleX 링크를 보낼 수 있습니다. + 향상된 서버 구성 + 메시지 + 메시지 서버 + 메시지 라우팅 모드 + 안녕하세요!\nSimpleX Chat 초대장이 도착했습니다: %s + 내부 오류 + 잘못된 이름입니다! + 유효하지 않은 링크 + 유지 + 유효하지 않은 링크 + 이 문제는 이전 데이터베이스의 백업을 사용하는 경우에 발생할 수 있습니다. + 이탈리아어 인터페이스 + 그룹에 참여하시겠습니까? + k + 그룹 대화에 참여 + (이 기기 v%s)]]> + 잘못된 표시 이름입니다! + 잘못된 파일 경로 + 잘못된 QR 코드 + IP 주소와 연결을 보호합니다. + 초대 + 친구 초대 + 메시지 영구 삭제 + 초대 \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/lt/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/lt/strings.xml index 8d8d73b206..bf50e67bb8 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/lt/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/lt/strings.xml @@ -1780,4 +1780,8 @@ Taip Kopijavimo klaida Pritaikyti prie + %1$d failo klaida (-os):\n%2$s. + %1$d failas (-ai, -ų) vis dar atsisiunčiamas (-i, -a). + Nepavyko atsisiųsti %1$d failo (-ų). + %d pasirinkta \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/pt-rBR/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/pt-rBR/strings.xml index b890213da2..d683d194ba 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/pt-rBR/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/pt-rBR/strings.xml @@ -2104,4 +2104,5 @@ Erro ao salvar proxy Senha Nome de usuário + Sessão do aplicativo \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml index fd9492c8d7..604fd1fa1a 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml @@ -2115,4 +2115,14 @@ Натисніть кнопку інформації поруч із полем адреси, щоб дозволити використання мікрофона. Відкрийте Налаштування Safari / Сайти / Мікрофон, а потім виберіть \"Дозволити для localhost\". Щоб здійснювати дзвінки, дозволіть використовувати ваш мікрофон. Завершіть дзвінок і спробуйте зателефонувати знову. + Кращі дзвінки + Краща безпека ✅ + Налаштовувана форма повідомлень. + Протоколи SimpleX перевірені компанією Trail of Bits. + Переключити аудіо та відео під час дзвінка. + Кращі дати повідомлень. + Кращий користувацький досвід + Видалити або модерувати до 200 повідомлень. + Переслати до 20 повідомлень одночасно. + Переключити профіль чату для одноразових запрошень. \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml index d464ce6bba..eb19247a12 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml @@ -904,4 +904,457 @@ Chuyển tiếp tối đa 20 tin nhắn cùng một lúc. Cách sử dụng máy chủ của bạn Giao diện Hungary và Thổ Nhĩ Kỳ + Miễn nhiễm với tin nhắn rác + Nhập cơ sở dữ liệu trò chuyện? + Nếu bạn nhập mã tự hủy của mình khi mở ứng dụng: + Máy chủ ICE (một dòng mỗi máy) + Nếu bạn nhập mã truy cập này khi mở ứng dụng, tất cả dữ liệu ứng dụng sẽ bị xóa vĩnh viễn! + Hình ảnh sẽ được nhận khi liên hệ của bạn trực tuyến, xin vui lòng chờ hoặc kiểm tra lại sau! + Ngay lập tức + Hình ảnh sẽ được nhận khi liên hệ của bạn hoàn thành việc tải lên. + Hình ảnh + Nếu bạn không thể gặp mặt trực tiếp, cho liên hệ của bạn xem mã QR trong một cuộc gọi video, hoặc chia sẻ liên kết. + Hình ảnh đã được gửi + Hình ảnh + Hình ảnh đã được lưu vào Thư viện + Nếu bạn nhận được liên kết mời SimpleX Chat, bạn có thể mở nó trong trình duyệt của mình: + quét mã QR trong cuộc gọi video, hoặc liên hệ của bạn có thể chia sẻ một liên kết mời.]]> + Nếu bạn xác nhận, các máy chủ truyền tin nhắn sẽ có thể biết địa chỉ IP, và nhà cung cấp của bạn - máy chủ nào mà bạn đang kết nối. + Nếu bạn chọn từ chối người gửi sẽ KHÔNG được thông báo. + cho liên hệ của bạn xem mã QR trong cuộc gọi video, hoặc chia sẻ liên kết.]]> + Nhập + Bỏ qua + không hoạt động + Nhập dữ liệu không thành công + Đã cải thiện việc chuyển gửi tin nhắn + Lỗi nhập chủ đề + Chế độ ẩn danh + Ẩn danh + Đang nhập dữ liệu từ kho lưu trữ + Nhập chủ đề + Đã cải thiện cấu hình máy chủ + Nhóm ẩn danh + Chế độ ẩn danh bảo vệ sự riêng tư của bạn bằng cách sử dụng một hồ sơ ngẫu nhiên mới với mỗi liên hệ. + Đã cải thiện việc chuyển gửi tin nhắn + Nhập cơ sở dữ liệu + Âm thanh trong cuộc gọi + Nâng cao bảo mật và sự riêng tư + ẩn danh qua liên kết dùng một lần + Thông tin + gián tiếp (%1$s) + Để tiếp tục, hãy ngắt kết nối tới các máy chủ dùng để truyền dẫn tin nhắn. + Cài đặt SimpleX Chat cho cửa sổ câu lệnh + ẩn danh qua liên kết địa chỉ liên lạc + Cuộc gọi thoại đến + Quyền hạn ban đầu + ẩn danh qua liên kết nhóm + Mã bảo mật không đúng! + Trả lời đến + Phiên bản cơ sở dữ liệu không tương thích + Ngay lập tức + Mã truy cập không đúng + Tăng cỡ chữ. + (mới)]]> + Đã cài đặt thành công + Cài đặt cập nhật + Cuộc gọi video đến + Phiên bản không tương thích + MÀU SẮC GIAO DIỆN + đã được mời + Liên kết không hợp lệ + tác vụ trò chuyện không hợp lệ + dữ liệu không hợp lệ + định dạng tin nhắn không hợp lệ + Mời + Lời mời đã hết hạn! + Liên kết không hợp lệ! + Liên kết không hợp lệ + Tên hiển thị không hợp lệ! + Liên kết kết nối không hợp lệ + Thông báo tức thời! + Mã QR không hợp lệ + Thông báo tức thời + Thông báo tức thời đã bị tắt! + Đường dẫn tệp không hợp lệ + Lỗi nội bộ + Mã QR không hợp lệ + Địa chỉ máy chủ không hợp lệ! + Xác nhận di dời không hợp lệ + Tên không hợp lệ! + Mời + lời mời tham gia nhóm %1$s + Mời thành viên + Tên cục bộ + Cảnh báo chuyển gửi tin nhắn + Đảm bảo địa chỉ máy chủ SMP ở đúng định dạng, dòng được phân tách và không bị trùng lặp. + Đảm bảo địa chỉ máy chủ XFTP ở đúng định dạng, dòng được phân tách và không bị trùng lặp. + Liên kết với điện thoại + Thành viên không hoạt động + Tin nhắn đã được chuyển tiếp + Tin nhắn có thể được gửi sau nếu thành viên hoạt động + Đảm bảo cấu hình proxy là chính xác. + Đang tham gia nhóm + Sáng + Sáng + Các thiết bị di động đã được liên kết + Tham gia nhóm của bạn? + UI Nhật Bản và Bồ Đào Nha + được đánh dấu là đã xóa + TRỰC TIẾP + Tham gia + Tham gia ẩn danh + Bản nháp tin nhắn + Rời nhóm? + Mời thành viên + Mời vào nhóm + Rời nhóm + THÀNH VIÊN + Thông tin hàng đợi tin nhắn + Chỉ dữ liệu hồ sơ cục bộ + Giữ lại các kết nối của bạn + Làm cho một tin nhắn biến mất + Tin nhắn động! + Giữ lại lời mời chưa sử dụng? + Dự phòng định tuyến tin nhắn + Mời bạn bè + Đánh dấu đã xác thực + Điều này có thể xảy ra khi:\n1. Tin nhắn hết hạn sau 2 ngày trên máy gửi hoặc sau 30 ngày trên máy chủ.\n2. Quá trình giải mã tin nhắn thất bại do bạn hoặc liên hệ của bạn sử dụng bản sao lưu cơ sở dữ liệu cũ.\n3. Kết nối bị xâm phạm. + Tham gia nhóm? + Liên kết ứng dụng trên điện thoại và máy tính! 🔗 + thành viên %1$s đã đổi thành %2$s + Tạo kết nối riêng tư + Tạo hồ sơ riêng tư! + Đảm bảo địa chỉ máy chủ WebRTC ICE ở đúng định dạng, dòng được phân tách và không bị trùng lặp. + nếu SimpleX không có thông tin định danh người dùng, thì làm thế nào mà nó có thể chuyển tin nhắn đi được?]]> + Nó có thể xảy ra khi bạn hoặc liên hệ của bạn sử dụng bản sao lưu cơ sở dữ liệu cũ. + Chế độ khóa + Giao diện tiếng Ý + thiết bị này v%s)]]> + Biểu đạt cảm xúc tin nhắn bị cấm trong nhóm này. + đã được mời để kết nối + Tìm hiểu thêm + Giữ + Rời + Đăng nhập bằng thông tin xác thực của bạn + Lỗi chuyển gửi tin nhắn + tham gia với tư cách %s + Trợ giúp markdown + Sử dụng markdown trong tin nhắn + Tham gia nhóm? + k + Đang tải tệp + Các máy tính đã được liên kết + Đang tải các cuộc trò chuyện… + tin nhắn + Đánh dấu chưa đọc + Tối đa 40 giây, được nhận ngay lập tức. + Chỉ báo đã nhận tin nhắn! + Tệp lớn! + Đánh dấu đã đọc + Trung bình + Tin nhắn động + Giữ lại cuộc trò chuyện + Hình ảnh xem trước của liên kết + Thành viên sẽ bị xóa khỏi nhóm - việc này không thể được hoàn tác! + Chế độ sáng + UI tiếng Litva + TIN NHẮN VÀ TỆP + Việc xóa tin nhắn mà không thể phục hồi bị cấm trong nhóm này. + Tham gia vào các cuộc trò chuyện nhóm + Chế độ định tuyến tin nhắn + Hãy trò chuyện trên SimpleX Chat + in nghiêng + Nó có thể được thay đổi sau trong phần cài đặt. + Việc xóa tin nhắn mà không thể phục hồi bị cấm trong cuộc trò chuyện này. + Biểu đạt cảm xúc tin nhắn bị cấm trong cuộc trò chuyện này. + Bản nháp tin nhắn + Nó bảo vệ địa chỉ IP và các kết nối của bạn. + Khóa sau + Tin nhắn + Lỗi keychain + đã rời + đã được mời thông qua liên kết nhóm của bạn + thành viên + đã rời + Nó cho phép việc có các kết nối ẩn danh mà không có bất kỳ dữ liệu chung nào giữa chúng trong một hồ sơ trò chuyện + Đảm bảo tệp có cú pháp YAML chính xác. Xuất chủ đề để có một ví dụ về cấu trúc tệp chủ đề. + Menu và cảnh báo + Cảm xúc tin nhắn + Cảm xúc tin nhắn + Làm cho các cuộc trò chuyện của bạn trở nên khác biệt! + Tiếp nhận tin nhắn + Cài đặt máy tính đã được liên kết + đã được mời %1$s + Xóa tin nhắn mà không thể phục hồi + Tin nhắn + Máy chủ tin nhắn + Máy chủ tệp và phương tiện + Nội dung tin nhắn + Tin nhắn đã bị xóa sau khi bạn chọn chúng. + Tin nhắn từ %s sẽ được hiển thị! + Trạng thái tin nhắn + Tin nhắn đã được nhận + Tin nhắn đã được gửi + Trạng thái tin nhắn: %s + Tin nhắn quá lớn + mã hóa đầu cuốivới bí mật chuyển tiếp hoàn hảo, sự cự tuyệt và khôi phục xâm nhập.]]> + Tin nhắn sẽ bị xóa - việc này không thể được hoàn tác! + Di chuyển từ một thiết bị khác + Tin nhắn sẽ bị xóa - việc này không thể được hoàn tác! + Tin nhắn sẽ được đánh dấu để xóa. Người nhận sẽ có thể xem lại những tin nhắn này. + mã hóa đầu cuối kháng lượng tử với bí mật chuyển tiếp hoàn hảo, sự cự tuyệt và khôi phục xâm nhập.]]> + Mic + Hình dạng tin nhắn + Nguồn tin nhắn vẫn còn riêng tư. + Di chuyển thiết bị + Tin nhắn sẽ được đánh dấu để xóa. Người nhận sẽ có thể xem lại những tin nhắn này. + Di chuyển tới đây + tháng + Nhiều hồ sơ trò chuyện + Thiết bị di động mới + Không bao giờ + Trải nghiệm trò chuyện mới 🎉 + Ứng dụng máy tính mới! + Mạng & máy chủ + cuộc gọi nhỡ + Mã truy cập mới + Tên hiển thị mới: + Kết nối mạng + Trạng thái mạng + Chủ đề trò chuyện mới + đã được kiểm duyệt bởi %s + đã được kiểm duyệt + Đã được kiểm duyệt vào + Đã được kiểm duyệt vào: %s + %s đã bị ngắt kết nối]]> + Mở trong ứng dụng di động, sau đó nhấn Kết nối trong ứng dung.]]> + Tin nhắn mới + Đã tắt thông báo khi không hoạt động! + Mới trong %s + - chuyển gửi tin nhắn ổn định hơn.\n- các nhóm đã được cải thiện hơn một chút.\n- và hơn thế nữa! + Kết nối mạng ổn định hơn. + Quản lý mạng + %s bị thiếu]]> + %s đang bận]]> + Di chuyển sang một thiết bị khác + Đang di chuyển + Hơn nữa + tin nhắn mới + Khả năng cao liên hệ này đã xóa kết nối với bạn. + Sự cố mạng - tin nhắn đã hết hạn sau nhiều lần cố gắng gửi đi. + Di chuyển: %s + Tắt thông báo + Yêu cầu liên lạc mới + Kiểm duyệt + không bao giờ + Tắt thông báo + Cuộc trò chuyện mới + phút + Kho lưu trữ cơ sở dữ liệu mới + Quyền hạn thành viên mới + Nhiều cải tiến hơn nữa sắp ra mắt! + %s có một phiên bản không được hỗ trợ. Xin vui lòng đảm bảo rằng bạn dùng cùng một phiên bản trên cả hai thiết bị.]]> + Các tùy chọn phương tiện mới + Cuộc gọi nhỡ + Nhiều cải tiến hơn nữa sắp ra mắt! + Di chuyển sang một thiết bị khác qua mã QR. + Quá trình di chuyển hoàn tất + %s đang không hoạt động]]> + %s đã bị ngắt kết nối]]> + Không có thông tin chuyển gửi + Chưa có kết nối trực tiếp, tin nhắn được chuyển tiếp bởi quản trị viên. + Không có liên hệ để thêm + Không có thiết bị di động nào được kết nối + không + không có thông tin + Mật khẩu mới… + Thông tin xác thực SOCKS mới sẽ được sử dụng mỗi khi bạn khởi động ứng dụng. + Không + Không có liên hệ nào được chọn + Không có cuộc gọi nền + Không có mã truy cập ứng dụng + Không có cuộc trò chuyện nào được lọc + Không có lịch sử + Không có liên hệ nào được lọc + không + Không + Thông tin xác thực SOCKS mới sẽ được sử dụng cho mỗi máy chủ. + không có mã hóa đầu cuối + Không có thông tin, hãy thử tải lại + bật + Không có gì để chuyển tiếp! + Thông báo sẽ dừng hoạt động cho đến khi bạn khởi động lại ứng dụng + Không có thông tin định danh người dùng. + không có nội dung + tắt + tắt` + Chỉ bạn mới có thể thực hiện cuộc gọi. + Chỉ liên hệ của bạn mới có thể thả cảm xúc tin nhắn. + Chỉ có thể gửi 10 video cùng một lúc + Liên kết lời mời dùng một lần + (chỉ được lưu trữ bởi thành viên nhóm) + Xem trước thông báo + Dịch vụ thông báo + Tắt + Không có tệp nào được gửi hay được nhận + quan sát viên + Không có kết nối mạng + Chỉ chủ nhóm mới có thể bật tính năng cho phép gửi tệp và phương tiện. + Chỉ có thể gửi 10 hình ảnh cùng một lúc + Chỉ một thiết bị mới có thể hoạt động cùng một lúc + Không tương thích! + Thông báo + OK + Chỉ xóa cuộc trò chuyện + Chỉ bạn mới có thể gửi tin nhắn thoại. + bảo mật đầu cuối 2 lớp.]]> + Chỉ chủ nhóm mới có thể bật tính năng tin nhắn thoại. + Chỉ bạn mới có thể gửi tin nhắn tự xóa. + Liên kết lời mời dùng một lần + Chỉ có bạn mới có thể thả cảm xúc tin nhắn. + Thông báo sẽ chỉ được gửi cho đến khi ứng dụng dừng! + Chỉ bạn mới có thể xóa tin nhắn mà không thể phục hồi (liên hệ của bạn có thể đánh dấu chúng để xóa). (24 giờ) + Bản lưu trữ cơ sở dữ liệu cũ + được đề nghị %s + được đề nghị %s: %2s + Tắt + Chỉ chủ nhóm mới có thể điều chỉnh các tùy chọn nhóm. + Giờ thì quản trị viên có thể:\n- xóa tin nhắn của thành viên\n- vô hiệu hóa thành viên (quyền hạn quan sát viên) + Không có cuộc trò chuyện nào được chọn + Không có gì được chọn + Mở + Mở bảng điều khiển trò chuyện + Mở hồ sơ trò chuyện + Dịch vụ onion sẽ được yêu cầu để kết nối.\nXin lưu ý: bạn sẽ không thể kết nối tới các máy chủ mà không có địa chỉ .onion. + Mở + Mở thư mục cơ sở dữ liệu + Chỉ liên hệ của bạn mới có thể gửi tin nhắn tự xóa. + Khác + Hoặc quét mã QR + Dịch vụ onion sẽ được sử dụng khi có sẵn. + Chỉ liên hệ của bạn mới có thể gửi tin nhắn thoại. + chủ sở hữu + Mở cài đặt máy chủ + Mở liên kết trong trình duyệt có thể làm giảm sự riêng tư và bảo mật của kết nối. Liên kết SimpleX không đáng tin cậy sẽ được đánh dấu màu đỏ. + Chỉ liên hệ của bạn mới có thể xóa tin nhắn mà không thể phục hồi (bạn có thể đánh dấu chúng để xóa). (24 giờ) + Mở cài đặt ứng dụng + Mục mã truy cập + Mở màn hình di chuyển + Dịch vụ onion sẽ không được sử dụng. + Đang mở cơ sở dữ liệu… + Hoặc hiển thị mã này + Chỉ liên hệ của bạn mới có thể thực hiện cuộc gọi. + mở + - tùy chọn thông báo khi xóa liên hệ.\n- tên hồ sơ với dấu cách.\n- và hơn thế nữa! + Mở cài đặt + Hoặc chia sẻ đường dẫn tệp này một cách an toàn. + Mở SimpleX Chat để chấp nhận cuộc gọi + Mở Cài đặt Safari / Trang Web / Mic, rồi chọn Cho phép với localhost. + Mã truy cập + Mở cuộc hội thoại + Mở vị trí tệp + Sử dụng từ máy tính trong ứng dụng di động và quét mã QR.]]> + Hoặc dán đường dẫn lưu trữ + các chủ sở hữu + Mở cổng trong tường lửa + Mã truy cập đã được đổi! + Mở nhóm + khác + các lỗi khác + Các máy chủ SMP khác + Các máy chủ XFTP khác + Dán đường dẫn mà bạn nhận được để kết nối với liên hệ hệ của bạn… + Dán đường dẫn + Mật khẩu + Định kỳ + Đang chờ xử lý + Đang chờ xử lý + Không tìm thấy mật khẩu trong Keystore, vui lòng nhập thủ công. Điều này có thể xảy ra nếu bạn khôi phục dữ liệu ứng dụng bằng một công cụ sao lưu. Nếu không phải như vậy, xin vui lòng liên hệ với nhà phát triển. + Thành viên cũ %1$s + Dán đường dẫn để kết nối! + Thông báo định kỳ + Dán đường dẫn mà bạn nhận được + Dán + ngang hàng + Cần có mật khẩu + Dán địa chỉ máy tính + Dán đường dẫn sao lưu + Mật khẩu để hiển thị + Cuộc gọi chờ + Mã truy cập không đổi! + Mã truy cập đã được đặt! + bộ đếm PING + UI tiếng Ba Tư + Xin vui lòng xác nhận rằng cài đặt mạng cho thiết bị này là chính xác. + Xin vui lòng kiểm tra rằng đường dẫn SimpleX là chính xác. + Xin vui lòng yêu cầu liên hệ của bạn mở tính năng thực hiện cuộc gọi. + Xin vui lòng kiểm tra kết nối mạng của bạn với %1$s và thử lại. + Xin vui lòng nhập đúng mật khẩu hiện tại. + Mở từ danh sách cuộc trò chuyện. + Cuộc gọi hình trong hình + Xin vui lòng báo cáo với các nhà phát triển:\n%s + Thông báo định kỳ đã bị tắt! + Xin vui lòng kiểm tra rằng thiết bị di động và máy tính kết nối tới cùng một mạng cục bộ, và tường lửa của máy tính cho phép kết nối.\nHãy chia sẻ bất kỳ vấn đề nào khác với nhà phát triển. + Xin vui lòng kiểm tra rằng bạn đã dùng đúng đường dẫn hoặc yêu cầu liên hệ của bạn gửi cho bạn một đường dẫn khác. + Xin vui lòng ghi nhớ hoặc lưu trữ nó một cách an toàn - không có cách nào để khôi phục một mật khẩu đã bị mất! + Quyền truy cập bị tự chối! + Xin vui lòng yêu cầu liên hệ của bạn mở tính năng gửi tin nhắn thoại. + khoảng PING + Xin vui lòng nhập mật khẩu trước đó sau khi khôi phục bản sao lưu cơ sở dữ liệu. Việc này không thể được hoàn tác. + Xin vui lòng liên lạc với quản trị viên nhóm. + Xin vui lòng báo cáo với các nhà phát triển. + Xin vui lòng báo cáo tới các nhà phát triển:\n%s\n\nGợi ý rằng bạn nên khởi động lại ứng dụng. + Có lẽ vân tay chứng chỉ trong địa chỉ máy chủ là không chính xác + Đang chuẩn bị tải lên + Cổng + cổng %d + Xin vui lòng lưu trữ mật khẩu một cách an toàn, bạn sẽ KHÔNG thể trò chuyện nếu bạn làm mất nó. + Xin vui lòng chờ trong khi tệp đang được tải từ thiết bị được liên kết + Địa chỉ máy chủ cài sẵn + Lưu lại bản nháp tin nhắn cuối cùng, với các tệp đính kèm. + Đang chuẩn bị tải xuống + Xin vui lòng cập nhật ứng dụng và liên lạc với các nhà phát triển. + Máy chủ cài sẵn + Xin vui lòng thử lại sau. + Xem trước + Xin vui lòng lưu trữ mật khẩu một cách an toàn, bạn sẽ KHÔNG thể thay đổi nếu bạn làm mất nó. + Giao diện tiếng Ba Lan + Xin vui lòng khởi động lại ứng dụng. + Các máy chủ đã kết nối trước đó + Định hình lại sự riêng tư + Quyền riêng tư & bảo mật + Bản cập nhật hồ sơ sẽ được gửi đến các liên hệ của bạn. + Cấm thả cảm xúc tin nhắn. + Cấm các cuộc gọi thoại/video. + Thông báo riêng tư + Các ảnh đại diện + Mật khẩu hồ sơ + Ghi chú riêng tư + Cấm xóa tin nhắn mà không thể phục hồi. + ĐỊNH TUYẾN TIN NHẮN RIÊNG TƯ + Tên hồ sơ: + ảnh đại diện + Hồ sơ và các kết nối máy chủ + chỗ để ảnh đại diện + Tên tệp riêng tư + Định tuyến tin nhắn riêng tư 🚀 + Lỗi định tuyến riêng tư + Ghi chú riêng tư + Định tuyến riêng tư + Chủ đề hồ sơ + Cấm thả cảm xúc tin nhắn. + Thời gian chờ giao thức trên mỗi KB + Bảo vệ các hồ sơ trò chuyện của bạn bằng mật khẩu! + Bảo vệ địa chỉ IP của bạn khỏi các máy chủ tiếp tin được chọn bởi liên hệ của bạn.\nBật trong cài đặt *Mạng & các máy chủ* + Bảo vệ địa chỉ IP + Cấm gửi tin nhắn trực tiếp tới các thành viên. + Cấm gửi tin nhắn thoại. + Thời gian chờ giao thức + Cấm gửi tin nhắn tự xóa. + Bảo vệ màn hình ứng dụng + Cấm gửi tệp và phương tiện truyền thông. + Cấm gửi tin nhắn thoại. + Cấm gửi tin nhắn tự xóa. + Cấm gửi đường dẫn SimpleX + Được proxy \ No newline at end of file From 614846465fb069cb039d1416c6ed7c549359c3ce Mon Sep 17 00:00:00 2001 From: Evgeny Date: Wed, 27 Nov 2024 23:51:51 +0000 Subject: [PATCH 079/167] website: translations (#5266) * ep/blog-v61 * Translated using Weblate (Hungarian) Currently translated at 100.0% (257 of 257 strings) Translation: SimpleX Chat/SimpleX Chat website Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/hu/ --------- Co-authored-by: summoner001 --- website/langs/hu.json | 72 +++++++++++++++++++++---------------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/website/langs/hu.json b/website/langs/hu.json index f4a40a02bb..7ff66a5ab9 100644 --- a/website/langs/hu.json +++ b/website/langs/hu.json @@ -1,5 +1,5 @@ { - "home": "Főoldal", + "home": "Kezdőoldal", "developers": "Fejlesztők", "reference": "Referencia", "blog": "Blog", @@ -11,17 +11,17 @@ "simplex-explained-tab-1-text": "1. Felhasználói élmény", "simplex-explained-tab-2-text": "2. Hogyan működik", "simplex-explained-tab-3-text": "3. Mit látnak a kiszolgálók", - "simplex-explained-tab-1-p-1": "Létrehozhat kapcsolatokat és csoportokat, valamint kétirányú beszélgetéseket folytathat, mint bármely más üzenetküldőben.", - "simplex-explained-tab-1-p-2": "Hogyan működhet egyirányú üzenet várakoztatással és felhasználói profil azonosítók nélkül?", - "simplex-explained-tab-2-p-1": "Minden kapcsolathoz két különböző üzenetküldési várakoztatást használ a különböző kiszolgálókon keresztül történő üzenetküldéshez és -fogadáshoz.", + "simplex-explained-tab-1-p-1": "Létrehozhat kapcsolatokat és csoportokat, valamint kétirányú beszélgetéseket folytathat, ugyanúgy mint bármely más üzenetküldőben.", + "simplex-explained-tab-1-p-2": "Hogyan működhet egyirányú üzenet sorbaállítással és felhasználói profil-azonosítók nélkül?", + "simplex-explained-tab-2-p-1": "Minden kapcsolathoz két különböző üzenetküldési sorbaállítást használ a különböző kiszolgálókon keresztül történő üzenetküldéshez és -fogadáshoz.", "simplex-explained-tab-2-p-2": "A kiszolgálók csak egyirányú üzeneteket továbbítanak, anélkül, hogy teljes képet kapnának a felhasználók beszélgetéseiről vagy kapcsolatairól.", - "simplex-explained-tab-3-p-1": "A kiszolgálók minden egyes üzenet várakoztatáshoz külön névtelen hitelesítő adatokkal rendelkeznek, és nem tudják, hogy melyik felhasználóhoz tartoznak.", - "simplex-explained-tab-3-p-2": "A felhasználók tovább fokozhatják a metaadatok adatvédelmét, ha a Tor segítségével férnek hozzá a kiszolgálókhoz, megakadályozva az IP-cím szerinti korrelációt.", + "simplex-explained-tab-3-p-1": "A kiszolgálók minden egyes üzenetsorbaállításhoz külön névtelen hitelesítő-adatokkal rendelkeznek, és nem tudják, hogy melyik felhasználóhoz tartoznak.", + "simplex-explained-tab-3-p-2": "A felhasználók tovább fokozhatják a metaadatok adatvédelmét, ha a Tor segítségével férnek hozzá a kiszolgálókhoz, így megakadályozva az IP-cím szerinti korrelációt.", "smp-protocol": "SMP-protokoll", - "chat-protocol": "Csevegés protokoll", + "chat-protocol": "Csevegésprotokoll", "donate": "Támogatás", "copyright-label": "© 2020-2024 SimpleX | Nyílt forráskódú projekt", - "simplex-chat-protocol": "SimpleX Chat protokoll", + "simplex-chat-protocol": "SimpleX Chat-protokoll", "terminal-cli": "Terminál CLI", "terms-and-privacy-policy": "Adatvédelmi irányelvek", "hero-header": "Újradefiniált adatvédelem", @@ -31,7 +31,7 @@ "hero-overlay-2-textlink": "Hogyan működik a SimpleX?", "hero-overlay-3-textlink": "A biztonság értékelése", "hero-2-header": "Privát kapcsolat létrehozása", - "hero-2-header-desc": "A videó bemutatja, hogyan kapcsolódhat az ismerőséhez egy egyszer használatos QR-kód segítségével, személyesen vagy videokapcsolaton keresztül. Ugyanakkor egy meghívó megosztásával is kapcsolódhat.", + "hero-2-header-desc": "A videó bemutatja, hogyan kapcsolódhat az ismerőséhez egy egyszer használható QR-kód segítségével, személyesen vagy videokapcsolaton keresztül. Ugyanakkor egy meghívó-hivatkozás megosztásával is kapcsolódhat.", "hero-overlay-1-title": "Hogyan működik a SimpleX?", "hero-overlay-2-title": "Miért ártanak a felhasználói azonosítók az adatvédelemnek?", "hero-overlay-3-title": "A biztonság értékelése", @@ -41,33 +41,33 @@ "feature-4-title": "E2E-titkosított hangüzenetek", "feature-5-title": "Eltűnő üzenetek", "feature-6-title": "E2E-titkosított
hang- és videohívások", - "feature-7-title": "Hordozható titkosított alkalmazás-adattárolás — profil áthelyezése egy másik eszközre", - "feature-8-title": "Az inkognitó mód —
egyedülálló a SimpleX Chatben", + "feature-7-title": "Hordozható titkosított alkalmazás-adattárolás — profil átköltöztetése egy másik eszközre", + "feature-8-title": "Az inkognitómód —
egyedülálló a SimpleX Chatben", "simplex-network-overlay-1-title": "Összehasonlítás más P2P üzenetküldő protokollokkal", "simplex-private-1-title": "2 rétegű végpontok közötti titkosítás", - "simplex-private-2-title": "További rétege a
kiszolgáló titkosítás", + "simplex-private-2-title": "További rétege a
kiszolgáló-titkosítás", "simplex-private-4-title": "Nem kötelező
hozzáférés Tor-on keresztül", "simplex-private-5-title": "Több rétegű
tartalom kitöltés", "simplex-private-6-title": "Sávon kívüli
kulcscsere", "simplex-private-7-title": "Üzenetintegritás
hitelesítés", "simplex-private-8-title": "Üzenetek keverése
a korreláció csökkentése érdekében", - "simplex-private-9-title": "Egyirányú
üzenet várakoztatás", - "simplex-private-10-title": "Ideiglenes névtelen páronkénti azonosítók", - "simplex-private-card-1-point-1": "Dupla-ratchet protokoll —
OTR üzenetküldés, sérülés utáni titkosság-védelemmel és -helyreállítással.", - "simplex-private-card-1-point-2": "NaCL cryptobox minden egyes üzenet várakoztatáshoz, hogy megakadályozza a forgalom korrelációját az üzenet várakoztatások között, ha a TLS veszélybe kerül.", + "simplex-private-9-title": "Egyirányú
üzenetsorbaállítás", + "simplex-private-10-title": "Ideiglenes, névtelen, páronkénti azonosítók", + "simplex-private-card-1-point-1": "Double-Ratchet-protokoll —
OTR-üzenetküldés, sérülés utáni titkosság-védelemmel és -helyreállítással.", + "simplex-private-card-1-point-2": "NaCL cryptobox minden egyes üzenet sorbaállításához, hogy megakadályozza a forgalom korrelációját az üzenet-sorbaállítások között, ha a TLS veszélybe kerül.", "simplex-private-card-2-point-1": "Kiegészítő kiszolgáló titkosítási réteg a címzettnek történő kézbesítéshez, hogy megakadályozza a fogadott és az elküldött kiszolgálóforgalom közötti korrelációt, ha a TLS veszélybe kerül.", - "simplex-private-card-3-point-1": "Az ügyfél-kiszolgáló kapcsolatokhoz csak az erős algoritmusokkal rendelkező TLS 1.2/1.3 protokollt használ.", + "simplex-private-card-3-point-1": "A kliens és a kiszolgálók közötti kapcsolatokhoz csak az erős algoritmusokkal rendelkező TLS 1.2/1.3 protokollt használja.", "simplex-private-card-3-point-2": "A kiszolgáló ujjlenyomata és a csatornakötés megakadályozza a MITM- és a visszajátszási támadásokat.", "simplex-private-card-3-point-3": "Az újrakapcsolódás le van tiltva a munkamenet elleni támadások megelőzése érdekében.", - "simplex-private-card-4-point-1": "Az IP-címe védelme érdekében a kiszolgálókat a Tor-on vagy más átviteli fedett hálózaton keresztül érheti el.", - "simplex-private-card-6-point-1": "Számos kommunikációs platform sebezhető a kiszolgálók vagy a hálózati szolgáltatók MITM-támadásaival szemben.", + "simplex-private-card-4-point-1": "Az IP-címe védelme érdekében a kiszolgálókat a TORon vagy más átvitel-átfedő-hálózaton keresztül is elérheti.", + "simplex-private-card-6-point-1": "Számos kommunikációs platform sebezhető a kiszolgálók vagy a hálózat-szolgáltatók MITM-támadásaival szemben.", "simplex-private-card-6-point-2": "Ennek megakadályozása érdekében a SimpleX-alkalmazások egyszeri kulcsokat adnak át sávon kívül, amikor egy címet hivatkozásként vagy QR-kódként oszt meg.", - "simplex-private-card-7-point-1": "Az integritás garantálása érdekében az üzenetek sorszámozással vannak ellátva, és tartalmazzák az előző üzenet hash-ét.", + "simplex-private-card-7-point-1": "Az integritás garantálása érdekében az üzenetek sorszámozással vannak ellátva, és tartalmazzák az előző üzenet hasítóértékét.", "simplex-private-card-7-point-2": "Ha bármilyen üzenetet hozzáadnak, eltávolítanak vagy módosítanak, a címzett értesítést kap róla.", "simplex-private-card-8-point-1": "A SimpleX-kiszolgálók alacsony késleltetésű keverési csomópontokként működnek — a bejövő és kimenő üzenetek sorrendje eltérő.", - "simplex-private-card-9-point-1": "Minden üzenetet egyetlen irányba várakoztat, a különböző küldési és vételi címekkel.", + "simplex-private-card-9-point-1": "Minden üzenetsorbaállítás egy irányba továbbítja az üzeneteket, a különböző küldési és vételi címekkel.", "simplex-private-card-9-point-2": "A hagyományos üzenetküldőkhöz képest csökkenti a támadási vektorokat és a rendelkezésre álló metaadatokat.", - "simplex-private-card-10-point-1": "A SimpleX ideiglenes névtelen páros címeket és hitelesítő adatokat használ minden egyes felhasználói kapcsolat vagy csoporttag számára.", + "simplex-private-card-10-point-1": "A SimpleX ideiglenes, névtelen, páros címeket és hitelesítő adatokat használ minden egyes felhasználói kapcsolathoz vagy csoporttaghoz.", "simplex-private-card-10-point-2": "Lehetővé teszi az üzenetek felhasználói profilazonosítók nélküli kézbesítését, ami az alternatíváknál jobb metaadat-védelmet biztosít.", "privacy-matters-1-overlay-1-title": "Az adatvédelemmel pénzt spórol meg", "privacy-matters-1-overlay-1-linkText": "Az adatvédelemmel pénzt spórol meg", @@ -79,28 +79,28 @@ "privacy-matters-3-overlay-1-linkText": "Az adatvédelem szabaddá tesz", "simplex-unique-1-title": "Teljes magánéletet élvezhet", "simplex-unique-1-overlay-1-title": "Személyazonosságának, profiljának, kapcsolatainak és metaadatainak teljes körű védelme", - "simplex-unique-2-title": "Véd
a spamektől és a visszaélésektől", - "simplex-unique-2-overlay-1-title": "A legjobb védelem a spam és a visszaélések ellen", - "simplex-unique-3-title": "Az ön adatai fölött csak ön rendelkezik", - "simplex-unique-3-overlay-1-title": "Az ön adatai fölött csak ön rendelkezik", + "simplex-unique-2-title": "Véd
a kéretlen üzenetektől és a visszaélésektől", + "simplex-unique-2-overlay-1-title": "A legjobb védelem a kéretlen üzenetek és a visszaélések ellen", + "simplex-unique-3-title": "Ön kezeli az adatait", + "simplex-unique-3-overlay-1-title": "Az adatok biztonsága és kezelése az Ön kezében van", "simplex-unique-4-title": "Öné a SimpleX-hálózat", "simplex-unique-4-overlay-1-title": "Teljesen decentralizált — a SimpleX-hálózat a felhasználóké", "hero-overlay-card-1-p-1": "Sok felhasználó kérdezte: ha a SimpleXnek nincsenek felhasználói azonosítói, honnan tudja, hogy hová kell eljuttatni az üzeneteket?", - "hero-overlay-card-1-p-2": "Az üzenetek kézbesítéséhez az összes többi platform által használt felhasználói azonosítók helyett a SimpleX az üzenetek várakoztatásához ideiglenes, névtelen, páros azonosítókat használ, külön-külön minden egyes kapcsolathoz — nincsenek hosszú távú azonosítók.", - "hero-overlay-card-1-p-4": "Ez a kialakítás megakadályozza a felhasználók metaadatainak kiszivárgását az alkalmazás szintjén. Az adatvédelem további javítása és az IP-cím védelme érdekében az üzenetküldő kiszolgálókhoz Tor hálózaton keresztül is kapcsolódhat.", + "hero-overlay-card-1-p-2": "Az üzenetek kézbesítéséhez az összes többi platform által használt felhasználói azonosítók helyett a SimpleX az üzenetek sorbaállításához ideiglenes, névtelen, páros azonosítókat használ, külön-külön minden egyes kapcsolathoz — nincsenek hosszú távú azonosítók.", + "hero-overlay-card-1-p-4": "Ez a kialakítás megakadályozza a felhasználók metaadatainak kiszivárgását az alkalmazás szintjén. Az adatvédelem további javítása és az IP-cím védelme érdekében az üzenetküldő kiszolgálókhoz TOR hálózaton keresztül is kapcsolódhat.", "hero-overlay-card-1-p-5": "Csak a kliensek tárolják a felhasználói profilokat, kapcsolatokat és csoportokat; az üzenetek küldése 2 rétegű végpontok közötti titkosítással történik.", "hero-overlay-card-1-p-6": "További leírást a SimpleX ismertetőben olvashat.", - "hero-overlay-card-2-p-1": "Ha a felhasználók állandó azonosítóval rendelkeznek, még akkor is, ha ez csak egy véletlenszerű szám, például egy munkamenet-azonosító, fennáll annak a veszélye, hogy a szolgáltató vagy egy támadó megfigyelheti, hogyan kapcsolódnak a felhasználók, és hány üzenetet küldenek.", + "hero-overlay-card-2-p-1": "Ha a felhasználók állandó azonosítóval rendelkeznek, még akkor is, ha ez csak egy véletlenszerű szám, például egy munkamenet-azonosító, fennáll annak a veszélye, hogy a szolgáltató vagy egy támadó megfigyelheti, azt hogy hogyan kapcsolódnak a felhasználók egymáshoz, és hány üzenetet küldenek egymásnak.", "hero-overlay-card-2-p-2": "Ezt az információt aztán összefüggésbe hozhatják a meglévő nyilvános közösségi hálózatokkal, és meghatározhatnak néhány valódi személyazonosságot.", - "hero-overlay-card-2-p-3": "Még a Tor v3 szolgáltatásokat használó, legprivátabb alkalmazások esetében is, ha két különböző kapcsolattartóval beszél ugyanazon a profilon keresztül, bizonyítani tudják, hogy ugyanahhoz a személyhez kapcsolódnak.", - "hero-overlay-card-2-p-4": "A SimpleX úgy védekezik ezen támadások ellen, hogy nem tartalmaz felhasználói azonosítókat. Ha pedig használja az inkognitó módot, akkor minden egyes létrejött kapcsolatban más-más felhasználó név jelenik meg, így elkerülhető a közöttük lévő összefüggések bizonyítása.", - "hero-overlay-card-3-p-1": "Trail of Bits egy vezető biztonsági és technológiai tanácsadó cég, amelynek ügyfelei közé tartoznak a nagy technológiai cégek, kormányzati ügynökségek és jelentős blokklánc projektek.", + "hero-overlay-card-2-p-3": "Még a TOR v3 szolgáltatásokat használó, legprivátabb alkalmazások esetében is, ha két különböző kapcsolattartóval beszél ugyanazon a profilon keresztül, bizonyítani tudják, hogy ugyanahhoz a személyhez kapcsolódnak.", + "hero-overlay-card-2-p-4": "A SimpleX úgy védekezik ezen támadások ellen, hogy nem tartalmaz felhasználói azonosítókat. Ha pedig használja az inkognitómódot, akkor minden egyes létrejött kapcsolatban más-más felhasználó név jelenik meg, így elkerülhető a közöttük lévő összefüggések teljes bizonyítása.", + "hero-overlay-card-3-p-1": "Trail of Bits egy vezető biztonsági és technológiai tanácsadó cég, amelynek az ügyfelei közé tartoznak nagy technológiai cégek, kormányzati ügynökségek és jelentős blokklánc projektek.", "hero-overlay-card-3-p-2": "A Trail of Bits 2022 novemberében áttekintette a SimpleX-platform kriptográfiai és hálózati komponenseit. További információk.", "simplex-network-overlay-card-1-li-1": "A P2P-hálózatok az üzenetek továbbítására a DHT valamelyik változatát használják. A DHT kialakításakor egyensúlyt kell teremteni a kézbesítési garancia és a késleltetés között. A SimpleX jobb kézbesítési garanciával és alacsonyabb késleltetéssel rendelkezik, mint a P2P, mivel az üzenet redundánsan, a címzett által kiválasztott kiszolgálók segítségével több kiszolgálón keresztül párhuzamosan továbbítható. A P2P-hálózatokban az üzenet O(log N) csomóponton halad át szekvenciálisan, az algoritmus által kiválasztott csomópontok segítségével.", - "simplex-network-overlay-card-1-li-2": "A SimpleX kialakítása a legtöbb P2P-hálózattól eltérően nem rendelkezik semmiféle globális felhasználói azonosítóval, még ideiglenesen sem, és csak ideiglenes páros azonosítókat használ, ami jobb névtelenséget és metaadatvédelmet biztosít.", + "simplex-network-overlay-card-1-li-2": "A SimpleX kialakítása a legtöbb P2P-hálózattól eltérően nem rendelkezik semmiféle globális felhasználói azonosítóval, még ideiglenessel sem, és csak az üzenetekhez használ ideiglenes, páros azonosítókat, ami jobb névtelenséget és metaadatvédelmet biztosít.", "simplex-network-overlay-card-1-li-3": "A P2P nem oldja meg a MITM-támadás problémát, és a legtöbb létező implementáció nem használ sávon kívüli üzeneteket a kezdeti kulcscseréhez. A SimpleX a kezdeti kulcscseréhez sávon kívüli üzeneteket, vagy bizonyos esetekben már meglévő biztonságos és megbízható kapcsolatokat használ.", - "simplex-network-overlay-card-1-li-5": "Minden ismert P2P-hálózat sebezhető Sybil támadással, mert minden egyes csomópont felderíthető, és a hálózat egészként működik. A támadások enyhítésére szolgáló ismert intézkedés lehet egy központi kiszolgáló (pl.: tracker), vagy egy drága tanúsítvány. A SimpleX-hálózat nem ismeri fel a kiszolgálókat, töredezett és több elszigetelt alhálózatként működik, ami lehetetlenné teszi az egész hálózatra kiterjedő támadásokat.", "simplex-network-overlay-card-1-li-6": "A P2P-hálózatok sebezhetőek lehetnek a DRDoS-támadással szemben, amikor a kliensek képesek a forgalmat újraközvetíteni és felerősíteni, ami az egész hálózatra kiterjedő szolgáltatásmegtagadást eredményez. A SimpleX-kliensek csak az ismert kapcsolatból származó forgalmat továbbítják, és a támadó nem használhatja őket arra, hogy az egész hálózatban felerősítse a forgalmat.", + "simplex-network-overlay-card-1-li-5": "Minden ismert P2P-hálózat sebezhető Sybil támadással, mert minden egyes csomópont felderíthető, és a hálózat egészként működik. A támadások enyhítésére szolgáló ismert intézkedés lehet egy központi kiszolgáló (pl.: tracker), vagy egy drága tanúsítvány. A SimpleX-hálózat nem ismeri fel a kiszolgálókat, töredezett és több elszigetelt alhálózatként működik, ami lehetetlenné teszi az egész hálózatra kiterjedő támadásokat.", "privacy-matters-overlay-card-1-p-1": "Sok nagyvállalat arra használja fel az önnel kapcsolatban álló személyek adatait, hogy megbecsülje az ön jövedelmét, hogy olyan termékeket adjon el önnek, amelyekre valójában nincs is szüksége, és hogy meghatározza az árakat.", "privacy-matters-overlay-card-1-p-2": "Az online kiskereskedők tudják, hogy az alacsonyabb jövedelműek nagyobb valószínűséggel vásárolnak azonnal, ezért magasabb árakat számíthatnak fel, vagy eltörölhetik a kedvezményeket.", "privacy-matters-overlay-card-1-p-3": "Egyes pénzügyi és biztosítótársaságok szociális grafikonokat használnak a kamatlábak és a díjak meghatározásához. Ez gyakran arra készteti az alacsonyabb jövedelmű embereket, hogy többet fizessenek — ez az úgynevezett „szegénységi prémium”.", @@ -223,8 +223,8 @@ "please-use-link-in-mobile-app": "Használja a mobilalkalmazásban található hivatkozást", "contact-hero-header": "Kapott egy címet a SimpleX Chat-en való kapcsolódáshoz", "invitation-hero-header": "Kapott egy egyszer használatos hivatkozást a SimpleX Chat-en való kapcsolódáshoz", - "simplex-network-overlay-card-1-li-4": "A P2P-megvalósításokat egyes internetszolgáltatók blokkolhatják (mint például a BitTorrent). A SimpleX átvitel-független - a szabványos webes protokollokon, pl. WebSockets-en keresztül is működik.", - "simplex-private-card-4-point-2": "A SimpleX Toron keresztüli használatához telepítse az Orbot alkalmazást és engedélyezze a SOCKS5 proxyt (vagy a VPN-t az iOS-ban).", + "simplex-network-overlay-card-1-li-4": "A P2P-megvalósításokat egyes internetszolgáltatók blokkolhatják (mint például a BitTorrent). A SimpleX átvitel-független - a szabványos webes protokollokon, pl. WebSocketsen keresztül is működik.", + "simplex-private-card-4-point-2": "A SimpleX TORon keresztüli használatához telepítse az Orbot alkalmazást és engedélyezze a SOCKS5 proxyt (vagy a VPN-t az iOS-ban).", "simplex-private-card-5-point-1": "A SimpleX minden titkosítási réteghez tartalomkitöltést használ, hogy meghiúsítsa az üzenetméret ellen irányuló támadásokat.", "simplex-private-card-5-point-2": "A kiszolgálók és a hálózatot megfigyelők számára a különböző méretű üzenetek egyformának tűnnek.", "privacy-matters-1-title": "Hirdetés és árdiszkrimináció", From c3991aad87526620d2b0c3fc33da906715a5f9f0 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Thu, 28 Nov 2024 00:12:53 +0000 Subject: [PATCH 080/167] website: translations corrections * Translated using Weblate (French) Currently translated at 98.8% (254 of 257 strings) Translation: SimpleX Chat/SimpleX Chat website Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/fr/ * Translated using Weblate (German) Currently translated at 98.8% (254 of 257 strings) Translation: SimpleX Chat/SimpleX Chat website Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/de/ * Translated using Weblate (Dutch) Currently translated at 98.8% (254 of 257 strings) Translation: SimpleX Chat/SimpleX Chat website Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/nl/ * Translated using Weblate (Czech) Currently translated at 98.8% (254 of 257 strings) Translation: SimpleX Chat/SimpleX Chat website Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/cs/ * Translated using Weblate (Arabic) Currently translated at 98.8% (254 of 257 strings) Translation: SimpleX Chat/SimpleX Chat website Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/ar/ * Translated using Weblate (Italian) Currently translated at 98.8% (254 of 257 strings) Translation: SimpleX Chat/SimpleX Chat website Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/it/ * Translated using Weblate (Spanish) Currently translated at 98.8% (254 of 257 strings) Translation: SimpleX Chat/SimpleX Chat website Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/es/ * Translated using Weblate (Ukrainian) Currently translated at 98.8% (254 of 257 strings) Translation: SimpleX Chat/SimpleX Chat website Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/uk/ * Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 98.8% (254 of 257 strings) Translation: SimpleX Chat/SimpleX Chat website Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/zh_Hans/ * Translated using Weblate (Portuguese (Brazil)) Currently translated at 98.0% (252 of 257 strings) Translation: SimpleX Chat/SimpleX Chat website Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/pt_BR/ * Translated using Weblate (Polish) Currently translated at 98.8% (254 of 257 strings) Translation: SimpleX Chat/SimpleX Chat website Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/pl/ * Translated using Weblate (Japanese) Currently translated at 98.8% (254 of 257 strings) Translation: SimpleX Chat/SimpleX Chat website Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/ja/ * Translated using Weblate (Russian) Currently translated at 99.2% (255 of 257 strings) Translation: SimpleX Chat/SimpleX Chat website Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/ru/ * Translated using Weblate (Hebrew) Currently translated at 98.8% (254 of 257 strings) Translation: SimpleX Chat/SimpleX Chat website Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/he/ * Translated using Weblate (Finnish) Currently translated at 96.4% (248 of 257 strings) Translation: SimpleX Chat/SimpleX Chat website Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/fi/ * Translated using Weblate (Hungarian) Currently translated at 98.8% (254 of 257 strings) Translation: SimpleX Chat/SimpleX Chat website Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/hu/ --------- Co-authored-by: Anonymous --- website/langs/ar.json | 2 +- website/langs/cs.json | 2 +- website/langs/de.json | 2 +- website/langs/es.json | 2 +- website/langs/fi.json | 2 +- website/langs/fr.json | 2 +- website/langs/he.json | 2 +- website/langs/hu.json | 2 +- website/langs/it.json | 2 +- website/langs/ja.json | 2 +- website/langs/nl.json | 2 +- website/langs/pl.json | 2 +- website/langs/pt_BR.json | 2 +- website/langs/ru.json | 2 +- website/langs/uk.json | 2 +- website/langs/zh_Hans.json | 2 +- 16 files changed, 16 insertions(+), 16 deletions(-) diff --git a/website/langs/ar.json b/website/langs/ar.json index 5c3bb0dd48..927dcb0c49 100644 --- a/website/langs/ar.json +++ b/website/langs/ar.json @@ -255,4 +255,4 @@ "docs-dropdown-10": "الشفافية", "docs-dropdown-11": "الأسئلة الأكثر شيوعًا", "docs-dropdown-12": "الأمان" -} \ No newline at end of file +} diff --git a/website/langs/cs.json b/website/langs/cs.json index d10b14a85c..0e19fdbfe4 100644 --- a/website/langs/cs.json +++ b/website/langs/cs.json @@ -255,4 +255,4 @@ "docs-dropdown-10": "Transparentnost", "docs-dropdown-11": "FAQ (často kladené dotazy)", "docs-dropdown-12": "Bezpečnost" -} \ No newline at end of file +} diff --git a/website/langs/de.json b/website/langs/de.json index c57d059fb3..1a5c42d980 100644 --- a/website/langs/de.json +++ b/website/langs/de.json @@ -255,4 +255,4 @@ "docs-dropdown-10": "Transparent", "docs-dropdown-11": "FAQ", "docs-dropdown-12": "Sicherheit" -} \ No newline at end of file +} diff --git a/website/langs/es.json b/website/langs/es.json index 8f4ff0912e..b88a592ac4 100644 --- a/website/langs/es.json +++ b/website/langs/es.json @@ -255,4 +255,4 @@ "docs-dropdown-10": "Transparencia", "docs-dropdown-11": "FAQ", "docs-dropdown-12": "Seguridad" -} \ No newline at end of file +} diff --git a/website/langs/fi.json b/website/langs/fi.json index 13ef20bfa5..68c0d4f1b4 100644 --- a/website/langs/fi.json +++ b/website/langs/fi.json @@ -252,4 +252,4 @@ "hero-overlay-card-3-p-2": "Trail of Bits tarkasteli SimpleX-alustan salaus- ja verkkokomponentteja marraskuussa 2022. Lue lisää ilmoituksesta.", "please-enable-javascript": "Ota JavaScript käyttöön nähdäksesi QR-koodin.", "please-use-link-in-mobile-app": "Käytä mobiilisovelluksessa olevaa linkkiä" -} \ No newline at end of file +} diff --git a/website/langs/fr.json b/website/langs/fr.json index f907757388..61be2c8621 100644 --- a/website/langs/fr.json +++ b/website/langs/fr.json @@ -256,4 +256,4 @@ "docs-dropdown-10": "Transparence", "docs-dropdown-12": "Sécurité", "docs-dropdown-11": "FAQ" -} \ No newline at end of file +} diff --git a/website/langs/he.json b/website/langs/he.json index 9cf7ce194d..4fd966f05d 100644 --- a/website/langs/he.json +++ b/website/langs/he.json @@ -255,4 +255,4 @@ "docs-dropdown-10": "שקיפות", "docs-dropdown-11": "שאלות ותשובות", "docs-dropdown-12": "אבטחה" -} \ No newline at end of file +} diff --git a/website/langs/hu.json b/website/langs/hu.json index 7ff66a5ab9..f7cf1c558b 100644 --- a/website/langs/hu.json +++ b/website/langs/hu.json @@ -255,4 +255,4 @@ "simplex-chat-via-f-droid": "SimpleX Chat az F-Droidon keresztül", "simplex-chat-repo": "SimpleX Chat tároló", "stable-and-beta-versions-built-by-developers": "A fejlesztők által készített stabil és béta verziók" -} \ No newline at end of file +} diff --git a/website/langs/it.json b/website/langs/it.json index b593c395d8..431354c068 100644 --- a/website/langs/it.json +++ b/website/langs/it.json @@ -255,4 +255,4 @@ "docs-dropdown-10": "Trasparenza", "docs-dropdown-12": "Sicurezza", "docs-dropdown-11": "Domande frequenti" -} \ No newline at end of file +} diff --git a/website/langs/ja.json b/website/langs/ja.json index 4adf8705da..6f994b59da 100644 --- a/website/langs/ja.json +++ b/website/langs/ja.json @@ -255,4 +255,4 @@ "docs-dropdown-10": "透明性", "docs-dropdown-11": "よくある質問", "docs-dropdown-12": "セキュリティ" -} \ No newline at end of file +} diff --git a/website/langs/nl.json b/website/langs/nl.json index 02be8cbe41..18edf45369 100644 --- a/website/langs/nl.json +++ b/website/langs/nl.json @@ -255,4 +255,4 @@ "docs-dropdown-10": "Transparantie", "docs-dropdown-11": "FAQ", "docs-dropdown-12": "Beveiliging" -} \ No newline at end of file +} diff --git a/website/langs/pl.json b/website/langs/pl.json index c25ab4cd05..d0674e3d8a 100644 --- a/website/langs/pl.json +++ b/website/langs/pl.json @@ -255,4 +255,4 @@ "docs-dropdown-10": "Przezroczystość", "docs-dropdown-12": "Bezpieczeństwo", "docs-dropdown-11": "Często zadawane pytania" -} \ No newline at end of file +} diff --git a/website/langs/pt_BR.json b/website/langs/pt_BR.json index 12fd10d10c..73095c6db2 100644 --- a/website/langs/pt_BR.json +++ b/website/langs/pt_BR.json @@ -255,4 +255,4 @@ "docs-dropdown-11": "FAQ", "docs-dropdown-10": "Transparência", "docs-dropdown-12": "Segurança" -} \ No newline at end of file +} diff --git a/website/langs/ru.json b/website/langs/ru.json index 3c71bc24eb..5d59ec2c76 100644 --- a/website/langs/ru.json +++ b/website/langs/ru.json @@ -256,4 +256,4 @@ "docs-dropdown-10": "Прозрачность", "docs-dropdown-12": "Безопасность", "docs-dropdown-11": "Часто задаваемые вопросы" -} \ No newline at end of file +} diff --git a/website/langs/uk.json b/website/langs/uk.json index de18cbda85..5fa2f02b99 100644 --- a/website/langs/uk.json +++ b/website/langs/uk.json @@ -255,4 +255,4 @@ "docs-dropdown-11": "ПОШИРЕНІ ЗАПИТАННЯ", "docs-dropdown-10": "Прозорість", "docs-dropdown-12": "Безпека" -} \ No newline at end of file +} diff --git a/website/langs/zh_Hans.json b/website/langs/zh_Hans.json index 7e44bf75e2..87a8cc3c78 100644 --- a/website/langs/zh_Hans.json +++ b/website/langs/zh_Hans.json @@ -255,4 +255,4 @@ "docs-dropdown-10": "透明度", "docs-dropdown-11": "常问问题", "docs-dropdown-12": "安全性" -} \ No newline at end of file +} From 13efdf259501b9290c918bdc56c17b200cdcccd1 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Thu, 28 Nov 2024 11:24:29 +0400 Subject: [PATCH 081/167] core: apiGetReactionMembers api implementation (#5263) --- src/Simplex/Chat.hs | 17 ++++++++++------- src/Simplex/Chat/Store/Messages.hs | 16 ++++++++++++++++ src/Simplex/Chat/View.hs | 9 ++++++--- tests/ChatTests/Groups.hs | 3 +++ 4 files changed, 35 insertions(+), 10 deletions(-) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 707163fde7..66c11dac15 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -316,7 +316,7 @@ newChatController randomPresetServers <- chooseRandomServers presetServers' let rndSrvs = L.toList randomPresetServers operatorWithId (i, op) = (\o -> o {operatorId = DBEntityId i}) <$> pOperator op - opDomains = operatorDomains $ mapMaybe operatorWithId $ zip [1..] rndSrvs + opDomains = operatorDomains $ mapMaybe operatorWithId $ zip [1 ..] rndSrvs agentSMP <- randomServerCfgs "agent SMP servers" SPSMP opDomains rndSrvs agentXFTP <- randomServerCfgs "agent XFTP servers" SPXFTP opDomains rndSrvs let randomAgentServers = RandomAgentServers {smpServers = agentSMP, xftpServers = agentXFTP} @@ -1078,8 +1078,11 @@ processChatCommand' vr = \case throwChatError (CECommandError $ "reaction already " <> if add then "added" else "removed") when (add && length rs >= maxMsgReactions) $ throwChatError (CECommandError "too many reactions") - APIGetReactionMembers _userId _groupId _itemId _reaction -> withUser $ \user -> do - pure $ chatCmdError (Just user) "not supported" + APIGetReactionMembers userId groupId itemId reaction -> withUserId userId $ \user -> do + memberReactions <- withStore $ \db -> do + CChatItem _ ChatItem {meta = CIMeta {itemSharedMsgId = Just itemSharedMId}} <- getGroupChatItem db user groupId itemId + liftIO $ getReactionMembers db groupId itemSharedMId reaction + pure $ CRReactionMembers user memberReactions APIPlanForwardChatItems (ChatRef fromCType fromChatId) itemIds -> withUser $ \user -> case fromCType of CTDirect -> planForward user . snd =<< getCommandDirectChatItems user fromChatId itemIds CTGroup -> planForward user . snd =<< getCommandGroupChatItems user fromChatId itemIds @@ -1633,7 +1636,7 @@ processChatCommand' vr = \case liftIO $ fmap (opsConds,) . mapM (getServers db as ops' opDomains) =<< getUsers db lift $ withAgent' $ \a -> forM_ srvs $ \(auId, (smp', xftp')) -> do setProtocolServers a auId smp' - setProtocolServers a auId xftp' + setProtocolServers a auId xftp' pure $ CRServerOperatorConditions opsConds where getServers :: DB.Connection -> RandomAgentServers -> [Maybe ServerOperator] -> [(Text, ServerOperator)] -> User -> IO (UserId, (NonEmpty (ServerCfg 'PSMP), NonEmpty (ServerCfg 'PXFTP))) @@ -1942,7 +1945,7 @@ processChatCommand' vr = \case canKeepLink (CRInvitationUri crData _) newUser = do let ConnReqUriData {crSmpQueues = q :| _} = crData SMPQueueUri {queueAddress = SMPQueueAddress {smpServer}} = q - newUserServers <- + newUserServers <- map protoServer' . L.filter (\ServerCfg {enabled} -> enabled) <$> getKnownAgentServers SPSMP newUser pure $ smpServer `elem` newUserServers @@ -3430,7 +3433,7 @@ processChatCommand' vr = \case msgInfo <- withFastStore' (`getLastRcvMsgInfo` connId) CRQueueInfo user msgInfo <$> withAgent (`getConnectionQueueInfo` acId) -protocolServers :: UserProtocol p => SProtocolType p -> ([Maybe ServerOperator], [UserServer 'PSMP], [UserServer 'PXFTP]) -> ([Maybe ServerOperator], [UserServer 'PSMP], [UserServer 'PXFTP]) +protocolServers :: UserProtocol p => SProtocolType p -> ([Maybe ServerOperator], [UserServer 'PSMP], [UserServer 'PXFTP]) -> ([Maybe ServerOperator], [UserServer 'PSMP], [UserServer 'PXFTP]) protocolServers p (operators, smpServers, xftpServers) = case p of SPSMP -> (operators, smpServers, []) SPXFTP -> (operators, [], xftpServers) @@ -8269,7 +8272,7 @@ chatCommandP = "/_delete item " *> (APIDeleteChatItem <$> chatRefP <*> _strP <* A.space <*> ciDeleteMode), "/_delete member item #" *> (APIDeleteMemberChatItem <$> A.decimal <*> _strP), "/_reaction " *> (APIChatItemReaction <$> chatRefP <* A.space <*> A.decimal <* A.space <*> onOffP <* A.space <*> jsonP), - "/_reaction members " *> (APIGetReactionMembers <$> A.decimal <* A.space <*> A.decimal <* A.space <*> A.decimal <* A.space <*> jsonP), + "/_reaction members " *> (APIGetReactionMembers <$> A.decimal <* " #" <*> A.decimal <* A.space <*> A.decimal <* A.space <*> jsonP), "/_forward plan " *> (APIPlanForwardChatItems <$> chatRefP <*> _strP), "/_forward " *> (APIForwardChatItems <$> chatRefP <* A.space <*> chatRefP <*> _strP <*> sendMessageTTLP), "/_read user " *> (APIUserRead <$> A.decimal), diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs index a79eb98f14..74951cf3d1 100644 --- a/src/Simplex/Chat/Store/Messages.hs +++ b/src/Simplex/Chat/Store/Messages.hs @@ -76,6 +76,7 @@ module Simplex.Chat.Store.Messages getGroupCIReactions, getGroupReactions, setGroupReaction, + getReactionMembers, getChatItemIdsByAgentMsgId, getDirectChatItem, getDirectCIWithReactions, @@ -2852,6 +2853,21 @@ setGroupReaction db GroupInfo {groupId} m itemMemberId itemSharedMId sent reacti |] (groupId, groupMemberId' m, itemSharedMId, itemMemberId, sent, reaction) +getReactionMembers :: DB.Connection -> GroupId -> SharedMsgId -> MsgReaction -> IO [MemberReaction] +getReactionMembers db groupId itemSharedMId reaction = + map toMemberReaction + <$> DB.query + db + [sql| + SELECT group_member_id, reaction_ts + FROM chat_item_reactions + WHERE group_id = ? AND shared_msg_id = ? AND reaction = ? + |] + (groupId, itemSharedMId, reaction) + where + toMemberReaction :: (GroupMemberId, UTCTime) -> MemberReaction + toMemberReaction (groupMemberId, reactionTs) = MemberReaction {groupMemberId, reactionTs} + getTimedItems :: DB.Connection -> User -> UTCTime -> IO [((ChatRef, ChatItemId), UTCTime)] getTimedItems db User {userId} startTimedThreadCutoff = mapMaybe toCIRefDeleteAt diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index e0c836d8d7..093d750a42 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -154,7 +154,7 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe ttyUser u $ unmuted u chat deletedItem $ viewItemDelete chat deletedItem toItem byUser timed ts tz testView deletions' -> ttyUser u [sShow (length deletions') <> " messages deleted"] CRChatItemReaction u added (ACIReaction _ _ chat reaction) -> ttyUser u $ unmutedReaction u chat reaction $ viewItemReaction showReactions chat reaction added ts tz - CRReactionMembers u memberReactions -> [] + CRReactionMembers u memberReactions -> ttyUser u $ viewReactionMembers memberReactions CRChatItemDeletedNotFound u Contact {localDisplayName = c} _ -> ttyUser u [ttyFrom $ c <> "> [deleted - original message not found]"] CRBroadcastSent u mc s f t -> ttyUser u $ viewSentBroadcast mc s f ts tz t CRMsgIntegrityError u mErr -> ttyUser u $ viewMsgIntegrityError mErr @@ -848,6 +848,9 @@ viewItemReactions ChatItem {reactions} = [" " <> viewReactions reactions | viewReaction CIReactionCount {reaction = MREmoji (MREmojiChar emoji), userReacted, totalReacted} = plain [emoji, ' '] <> (if userReacted then styled Italic else plain) (show totalReacted) +viewReactionMembers :: [MemberReaction] -> [StyledString] +viewReactionMembers memberReactions = [sShow (length memberReactions) <> " member(s) reacted"] + directQuote :: forall d'. MsgDirectionI d' => CIDirection 'CTDirect d' -> CIQuote 'CTDirect -> [StyledString] directQuote _ CIQuote {content = qmc, chatDir = quoteDir} = quoteText qmc $ if toMsgDirection (msgDirection @d') == quoteMsgDirection quoteDir then ">>" else ">" @@ -1227,7 +1230,7 @@ viewUserServers UserOperatorServers {operator, smpServers, xftpServers} = viewServers p srvs | maybe True (\ServerOperator {enabled} -> enabled) operator = [" " <> protocolName p <> " servers" <> maybe "" ((" " <>) . viewRoles) operator] - <> map (plain . (" " <> ) . viewServer) srvs + <> map (plain . (" " <>) . viewServer) srvs | otherwise = [] where viewServer UserServer {server, preset, tested, enabled} = safeDecodeUtf8 (strEncode server) <> serverInfo @@ -1280,7 +1283,7 @@ viewOperator op@ServerOperator {tradeName, legalName, serverDomains, conditionsA viewOpIdTag op <> tradeName <> maybe "" parens legalName - <> (", domains: " <> T.intercalate ", " serverDomains) + <> (", domains: " <> T.intercalate ", " serverDomains) <> (", servers: " <> viewOpEnabled op) <> (", conditions: " <> viewOpConditions conditionsAcceptance) diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index bdd3b53829..a1d9951088 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -3738,6 +3738,9 @@ testSetGroupMessageReactions = cath ##> "/tail #team 1" cath <# "#team alice> hi" cath <## " 👍 2 🚀 1" + itemId' <- lastItemId alice + alice ##> ("/_reaction members 1 #1 " <> itemId' <> " {\"type\": \"emoji\", \"emoji\": \"👍\"}") + alice <## "2 member(s) reacted" bob ##> "-1 #team hi" bob <## "removed 👍" alice <# "#team bob> > alice hi" From 68be4b4ba510038af5bc5875398758de325947da Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Thu, 28 Nov 2024 13:49:20 +0400 Subject: [PATCH 082/167] core: return member records from apiGetReactionMembers (#5270) --- src/Simplex/Chat.hs | 2 +- src/Simplex/Chat/Messages.hs | 2 +- src/Simplex/Chat/Store/Messages.hs | 15 +++++++++------ 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 66c11dac15..2555582fe9 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -1081,7 +1081,7 @@ processChatCommand' vr = \case APIGetReactionMembers userId groupId itemId reaction -> withUserId userId $ \user -> do memberReactions <- withStore $ \db -> do CChatItem _ ChatItem {meta = CIMeta {itemSharedMsgId = Just itemSharedMId}} <- getGroupChatItem db user groupId itemId - liftIO $ getReactionMembers db groupId itemSharedMId reaction + liftIO $ getReactionMembers db vr user groupId itemSharedMId reaction pure $ CRReactionMembers user memberReactions APIPlanForwardChatItems (ChatRef fromCType fromChatId) itemIds -> withUser $ \user -> case fromCType of CTDirect -> planForward user . snd =<< getCommandDirectChatItems user fromChatId itemIds diff --git a/src/Simplex/Chat/Messages.hs b/src/Simplex/Chat/Messages.hs index 274308176b..a477deeb2c 100644 --- a/src/Simplex/Chat/Messages.hs +++ b/src/Simplex/Chat/Messages.hs @@ -475,7 +475,7 @@ deriving instance Show ACIReaction data JSONCIReaction c d = JSONCIReaction {chatInfo :: ChatInfo c, chatReaction :: CIReaction c d} data MemberReaction = MemberReaction - { groupMemberId :: GroupMemberId, + { groupMember :: GroupMember, reactionTs :: UTCTime } deriving (Show) diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs index 74951cf3d1..f94cbbd81d 100644 --- a/src/Simplex/Chat/Store/Messages.hs +++ b/src/Simplex/Chat/Store/Messages.hs @@ -2853,10 +2853,10 @@ setGroupReaction db GroupInfo {groupId} m itemMemberId itemSharedMId sent reacti |] (groupId, groupMemberId' m, itemSharedMId, itemMemberId, sent, reaction) -getReactionMembers :: DB.Connection -> GroupId -> SharedMsgId -> MsgReaction -> IO [MemberReaction] -getReactionMembers db groupId itemSharedMId reaction = - map toMemberReaction - <$> DB.query +getReactionMembers :: DB.Connection -> VersionRangeChat -> User -> GroupId -> SharedMsgId -> MsgReaction -> IO [MemberReaction] +getReactionMembers db vr user groupId itemSharedMId reaction = do + reactions <- + DB.query db [sql| SELECT group_member_id, reaction_ts @@ -2864,9 +2864,12 @@ getReactionMembers db groupId itemSharedMId reaction = WHERE group_id = ? AND shared_msg_id = ? AND reaction = ? |] (groupId, itemSharedMId, reaction) + rights <$> mapM (runExceptT . toMemberReaction) reactions where - toMemberReaction :: (GroupMemberId, UTCTime) -> MemberReaction - toMemberReaction (groupMemberId, reactionTs) = MemberReaction {groupMemberId, reactionTs} + toMemberReaction :: (GroupMemberId, UTCTime) -> ExceptT StoreError IO MemberReaction + toMemberReaction (groupMemberId, reactionTs) = do + groupMember <- getGroupMemberById db vr user groupMemberId + pure MemberReaction {groupMember, reactionTs} getTimedItems :: DB.Connection -> User -> UTCTime -> IO [((ChatRef, ChatItemId), UTCTime)] getTimedItems db User {userId} startTimedThreadCutoff = From 9c6e0a7051c4dc90503722ee14622edd7db17236 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Thu, 28 Nov 2024 17:37:52 +0400 Subject: [PATCH 083/167] desktop: fix avatar crop (#5271) * desktop: fix avatar crop wip * fix --- .../chat/simplex/common/platform/Images.desktop.kt | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Images.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Images.desktop.kt index 0f53adaf0b..53f3301507 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Images.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Images.desktop.kt @@ -1,6 +1,8 @@ package chat.simplex.common.platform import androidx.compose.ui.graphics.* +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize import boofcv.io.image.ConvertBufferedImage import boofcv.struct.image.GrayU8 import chat.simplex.res.MR @@ -67,8 +69,16 @@ actual fun cropToSquare(image: ImageBitmap): ImageBitmap { } else { yOffset = (image.height - side) / 2 } - // LALAL MAKE REAL CROP - return image + val croppedImage = ImageBitmap(side, side) + val canvas = Canvas(croppedImage) + canvas.drawImageRect( + image, + srcOffset = IntOffset(xOffset, yOffset), + srcSize = IntSize(side, side), + dstSize = IntSize(side, side), + paint = Paint() + ) + return croppedImage } actual fun compressImageStr(bitmap: ImageBitmap): String { From 9915e5572e274afea0701a9cb352085391202909 Mon Sep 17 00:00:00 2001 From: Diogo Date: Fri, 29 Nov 2024 11:19:11 +0000 Subject: [PATCH 084/167] desktop, android: show group reactions (#5277) * desktop, android: show group reactions * handle long names in single line * swap ordering for composable item action * Revert "swap ordering for composable item action" This reverts commit 385825e7f26dd5562ee76f3e379994bc692d4359. --------- Co-authored-by: Evgeny Poberezkin --- .../chat/simplex/common/model/ChatModel.kt | 6 ++ .../chat/simplex/common/model/SimpleXAPI.kt | 14 ++++ .../simplex/common/views/chat/ChatView.kt | 2 +- .../common/views/chat/item/ChatItemView.kt | 81 ++++++++++++++++++- 4 files changed, 98 insertions(+), 5 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index 56e1376ea2..857d21b966 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -1962,6 +1962,12 @@ class ACIReaction( val chatReaction: CIReaction ) +@Serializable +data class MemberReaction( + val groupMember: GroupMember, + val reactionTs: Instant +) + @Serializable class CIReaction( val chatDir: CIDirection, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index a18dd0ac14..def6d81897 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -951,6 +951,14 @@ object ChatController { return null } + suspend fun apiGetReactionMembers(rh: Long?, groupId: Long, itemId: Long, reaction: MsgReaction): List? { + val userId = currentUserId("apiGetReactionMembers") + val r = sendCmd(rh, CC.ApiGetReactionMembers(userId, groupId, itemId, reaction)) + if (r is CR.ReactionMembers) return r.memberReactions + Log.e(TAG, "apiGetReactionMembers bad response: ${r.responseType} ${r.details}") + return null + } + suspend fun apiDeleteChatItems(rh: Long?, type: ChatType, id: Long, itemIds: List, mode: CIDeleteMode): List? { val r = sendCmd(rh, CC.ApiDeleteChatItem(type, id, itemIds, mode)) if (r is CR.ChatItemsDeleted) return r.chatItemDeletions @@ -3092,6 +3100,7 @@ sealed class CC { class ApiDeleteChatItem(val type: ChatType, val id: Long, val itemIds: List, val mode: CIDeleteMode): CC() class ApiDeleteMemberChatItem(val groupId: Long, val itemIds: List): CC() class ApiChatItemReaction(val type: ChatType, val id: Long, val itemId: Long, val add: Boolean, val reaction: MsgReaction): CC() + class ApiGetReactionMembers(val userId: Long, val groupId: Long, val itemId: Long, val reaction: MsgReaction): CC() class ApiPlanForwardChatItems(val fromChatType: ChatType, val fromChatId: Long, val chatItemIds: List): CC() class ApiForwardChatItems(val toChatType: ChatType, val toChatId: Long, val fromChatType: ChatType, val fromChatId: Long, val itemIds: List, val ttl: Int?): CC() class ApiNewGroup(val userId: Long, val incognito: Boolean, val groupProfile: GroupProfile): CC() @@ -3253,6 +3262,7 @@ sealed class CC { is ApiDeleteChatItem -> "/_delete item ${chatRef(type, id)} ${itemIds.joinToString(",")} ${mode.deleteMode}" is ApiDeleteMemberChatItem -> "/_delete member item #$groupId ${itemIds.joinToString(",")}" is ApiChatItemReaction -> "/_reaction ${chatRef(type, id)} $itemId ${onOff(add)} ${json.encodeToString(reaction)}" + is ApiGetReactionMembers -> "/_reaction members $userId #$groupId $itemId ${json.encodeToString(reaction)}" is ApiForwardChatItems -> { val ttlStr = if (ttl != null) "$ttl" else "default" "/_forward ${chatRef(toChatType, toChatId)} ${chatRef(fromChatType, fromChatId)} ${itemIds.joinToString(",")} ttl=${ttlStr}" @@ -3409,6 +3419,7 @@ sealed class CC { is ApiDeleteChatItem -> "apiDeleteChatItem" is ApiDeleteMemberChatItem -> "apiDeleteMemberChatItem" is ApiChatItemReaction -> "apiChatItemReaction" + is ApiGetReactionMembers -> "apiGetReactionMembers" is ApiForwardChatItems -> "apiForwardChatItems" is ApiPlanForwardChatItems -> "apiPlanForwardChatItems" is ApiNewGroup -> "apiNewGroup" @@ -5429,6 +5440,7 @@ sealed class CR { @Serializable @SerialName("chatItemUpdated") class ChatItemUpdated(val user: UserRef, val chatItem: AChatItem): CR() @Serializable @SerialName("chatItemNotChanged") class ChatItemNotChanged(val user: UserRef, val chatItem: AChatItem): CR() @Serializable @SerialName("chatItemReaction") class ChatItemReaction(val user: UserRef, val added: Boolean, val reaction: ACIReaction): CR() + @Serializable @SerialName("reactionMembers") class ReactionMembers(val user: UserRef, val memberReactions: List): CR() @Serializable @SerialName("chatItemsDeleted") class ChatItemsDeleted(val user: UserRef, val chatItemDeletions: List, val byUser: Boolean): CR() @Serializable @SerialName("forwardPlan") class ForwardPlan(val user: UserRef, val itemsCount: Int, val chatItemIds: List, val forwardConfirmation: ForwardConfirmation? = null): CR() // group events @@ -5610,6 +5622,7 @@ sealed class CR { is ChatItemUpdated -> "chatItemUpdated" is ChatItemNotChanged -> "chatItemNotChanged" is ChatItemReaction -> "chatItemReaction" + is ReactionMembers -> "reactionMembers" is ChatItemsDeleted -> "chatItemsDeleted" is ForwardPlan -> "forwardPlan" is GroupCreated -> "groupCreated" @@ -5783,6 +5796,7 @@ sealed class CR { is ChatItemUpdated -> withUser(user, json.encodeToString(chatItem)) is ChatItemNotChanged -> withUser(user, json.encodeToString(chatItem)) is ChatItemReaction -> withUser(user, "added: $added\n${json.encodeToString(reaction)}") + is ReactionMembers -> withUser(user, "memberReactions: ${json.encodeToString(memberReactions)}") is ChatItemsDeleted -> withUser(user, "${chatItemDeletions.map { (deletedChatItem, toChatItem) -> "deletedChatItem: ${json.encodeToString(deletedChatItem)}\ntoChatItem: ${json.encodeToString(toChatItem)}" }} \nbyUser: $byUser") is ForwardPlan -> withUser(user, "itemsCount: $itemsCount\nchatItemIds: ${json.encodeToString(chatItemIds)}\nforwardConfirmation: ${json.encodeToString(forwardConfirmation)}") is GroupCreated -> withUser(user, json.encodeToString(groupInfo)) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index 2a66daf2db..5db413a17a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -1062,7 +1062,7 @@ fun BoxScope.ChatItemsList( highlightedItems.value = setOf() } } - ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, highlighted = highlighted, range = range, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, scrollToQuotedItemFromItem = scrollToQuotedItemFromItem, setReaction = setReaction, showItemDetails = showItemDetails, reveal = reveal, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp) + ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, highlighted = highlighted, range = range, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, scrollToQuotedItemFromItem = scrollToQuotedItemFromItem, setReaction = setReaction, showItemDetails = showItemDetails, reveal = reveal, showMemberInfo = showMemberInfo, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt index bd79b78c45..82744fdc39 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.text.* import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.* import chat.simplex.common.model.* import chat.simplex.common.model.ChatModel.controller @@ -28,6 +29,7 @@ import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chat.* import chat.simplex.common.views.helpers.* import chat.simplex.res.MR +import kotlinx.coroutines.launch import kotlinx.datetime.Clock import kotlin.math.* @@ -84,6 +86,7 @@ fun ChatItemView( setReaction: (ChatInfo, ChatItem, Boolean, MsgReaction) -> Unit, showItemDetails: (ChatInfo, ChatItem) -> Unit, reveal: (Boolean) -> Unit, + showMemberInfo: (GroupInfo, GroupMember) -> Unit, developerTools: Boolean, showViaProxy: Boolean, showTimestamp: Boolean, @@ -116,14 +119,54 @@ fun ChatItemView( fun ChatItemReactions() { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.chatItemOffset(cItem, itemSeparation.largeGap, inverted = true, revealed = true)) { cItem.reactions.forEach { r -> + val showReactionMenu = remember { mutableStateOf(false) } + val reactionMembers = remember { mutableStateOf(emptyList()) } + var modifier = Modifier.padding(horizontal = 5.dp, vertical = 2.dp).clip(RoundedCornerShape(8.dp)) - if (cInfo.featureEnabled(ChatFeature.Reactions) && (cItem.allowAddReaction || r.userReacted)) { - modifier = modifier.clickable { - setReaction(cInfo, cItem, !r.userReacted, r.reaction) - } + if (cInfo.featureEnabled(ChatFeature.Reactions)) { + modifier = modifier + .combinedClickable( + onClick = { + if (cItem.allowAddReaction || r.userReacted) { + setReaction(cInfo, cItem, !r.userReacted, r.reaction) + } + }, + onLongClick = { + if (cInfo is ChatInfo.Group) { + withBGApi { + try { + val members = controller.apiGetReactionMembers(rhId, cInfo.groupInfo.groupId, cItem.id, r.reaction) + if (members != null) { + showReactionMenu.value = true + reactionMembers.value = members + } + } catch (e: Exception) { + Log.d(TAG, "hatItemView ChatItemReactions onLongClick: unexpected exception: ${e.stackTraceToString()}") + } + } + } + } + ) } Row(modifier.padding(2.dp), verticalAlignment = Alignment.CenterVertically) { ReactionIcon(r.reaction.text, fontSize = 12.sp) + DefaultDropdownMenu(showMenu = showReactionMenu) { + reactionMembers.value.forEach { m -> + ItemAction( + text = m.groupMember.displayName, + composable = { ProfileImage(44.dp, m.groupMember.image) }, + onClick = { + if (cInfo is ChatInfo.Group && cInfo.groupInfo.membership.groupMemberId != m.groupMember.groupMemberId) { + showMemberInfo(cInfo.groupInfo, m.groupMember) + showReactionMenu.value = false + } else { + showReactionMenu.value = false + } + }, + lineLimit = 1 + ) + } + } if (r.totalReacted > 1) { Spacer(Modifier.width(4.dp)) Text( @@ -781,6 +824,34 @@ fun ItemAction(text: String, icon: Painter, color: Color = Color.Unspecified, on } } +@Composable +fun ItemAction( + text: String, + composable: @Composable () -> Unit, + color: Color = Color.Unspecified, + onClick: () -> Unit, + lineLimit: Int = Int.MAX_VALUE +) { + val finalColor = if (color == Color.Unspecified) { + MenuTextColor + } else color + DropdownMenuItem(onClick, contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f)) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text, + modifier = Modifier + .fillMaxWidth() + .weight(1F) + .padding(end = 15.dp), + color = finalColor, + maxLines = lineLimit, + overflow = TextOverflow.Ellipsis + ) + composable() + } + } +} + @Composable fun ItemAction(text: String, icon: ImageVector, onClick: () -> Unit, color: Color = Color.Unspecified) { val finalColor = if (color == Color.Unspecified) { @@ -1101,6 +1172,7 @@ fun PreviewChatItemView( setReaction = { _, _, _, _ -> }, showItemDetails = { _, _ -> }, reveal = {}, + showMemberInfo = { _, _ ->}, developerTools = false, showViaProxy = false, showTimestamp = true, @@ -1145,6 +1217,7 @@ fun PreviewChatItemViewDeletedContent() { setReaction = { _, _, _, _ -> }, showItemDetails = { _, _ -> }, reveal = {}, + showMemberInfo = { _, _ ->}, developerTools = false, showViaProxy = false, preview = true, From b0f3f0a523b2d8b42afcc444f2f95ac1735ea148 Mon Sep 17 00:00:00 2001 From: Diogo Date: Fri, 29 Nov 2024 16:04:29 +0000 Subject: [PATCH 085/167] ios: fix alignment on operators review later button and notice (#5280) --- apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift b/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift index 910f2a4127..fb3db2b585 100644 --- a/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift +++ b/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift @@ -140,6 +140,7 @@ struct ChooseServerOperators: View { .font(.footnote) .padding(.horizontal, 32) } + .frame(maxWidth: .infinity) .disabled(!canReviewLater) .padding(.bottom) } From 03bc4e5d01ad92ca8eeb82351d15a444401fcd66 Mon Sep 17 00:00:00 2001 From: Diogo Date: Sat, 30 Nov 2024 12:23:51 +0000 Subject: [PATCH 086/167] ios: display reactions in groups by member (#5265) * ios: display reactions in groups by member * fetch data * load on open * wip * fix text * less api calls * matching image sizes * updates * progress dump * mostly works * add member to list needed * open member faster --------- Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Co-authored-by: Evgeny Poberezkin --- apps/ios/Shared/Model/SimpleXAPI.swift | 7 ++ apps/ios/Shared/Views/Chat/ChatView.swift | 129 ++++++++++++++++++++-- apps/ios/SimpleXChat/APITypes.swift | 6 + apps/ios/SimpleXChat/ChatTypes.swift | 5 + apps/ios/SimpleXChat/ImageUtils.swift | 2 +- 5 files changed, 138 insertions(+), 11 deletions(-) diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index c03483311d..d99e97f2e1 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -446,6 +446,13 @@ func apiChatItemReaction(type: ChatType, id: Int64, itemId: Int64, add: Bool, re throw r } +func apiGetReactionMembers(groupId: Int64, itemId: Int64, reaction: MsgReaction) async throws -> [MemberReaction] { + let userId = try currentUserId("apiGetReactionMemebers") + let r = await chatSendCmd(.apiGetReactionMembers(userId: userId, groupId: groupId, itemId: itemId, reaction: reaction )) + if case let .reactionMembers(_, memberReactions) = r { return memberReactions } + throw r +} + func apiDeleteChatItems(type: ChatType, id: Int64, itemIds: [Int64], mode: CIDeleteMode) async throws -> [ChatItemDeletion] { let r = await chatSendCmd(.apiDeleteChatItem(type: type, id: id, itemIds: itemIds, mode: mode), bgDelay: msgDelay) if case let .chatItemsDeleted(_, items, _) = r { return items } diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index 6b287d52a1..cfbbfe6080 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -901,7 +901,7 @@ struct ChatView: View { @State private var showChatItemInfoSheet: Bool = false @State private var chatItemInfo: ChatItemInfo? @State private var msgWidth: CGFloat = 0 - + @Binding var selectedChatItems: Set? @Binding var forwardedChatItems: [ChatItem] @@ -1117,14 +1117,12 @@ struct ChatView: View { HStack(alignment: .top, spacing: 10) { MemberProfileImage(member, size: memberImageSize, backgroundColor: theme.colors.background) .onTapGesture { - if let member = m.getGroupMember(member.groupMemberId) { - selectedMember = member + if let mem = m.getGroupMember(member.groupMemberId) { + selectedMember = mem } else { - Task { - await m.loadGroupMembers(groupInfo) { - selectedMember = m.getGroupMember(member.groupMemberId) - } - } + let mem = GMember.init(member) + m.groupMembers.append(mem) + selectedMember = mem } } chatItemWithMenu(ci, range, maxWidth, itemSeparation) @@ -1244,11 +1242,20 @@ struct ChatView: View { } .padding(.horizontal, 6) .padding(.vertical, 4) - - if chat.chatInfo.featureEnabled(.reactions) && (ci.allowAddReaction || r.userReacted) { + .if(chat.chatInfo.featureEnabled(.reactions) && (ci.allowAddReaction || r.userReacted)) { v in v.onTapGesture { setReaction(ci, add: !r.userReacted, reaction: r.reaction) } + } + if case let .group(groupInfo) = chat.chatInfo { + v.contextMenu { + ReactionContextMenu( + groupInfo: groupInfo, + itemId: ci.id, + reactionCount: r, + selectedMember: $selectedMember + ) + } } else { v } @@ -1838,6 +1845,108 @@ private func buildTheme() -> AppTheme { } } +struct ReactionContextMenu: View { + @EnvironmentObject var m: ChatModel + let groupInfo: GroupInfo + var itemId: Int64 + var reactionCount: CIReactionCount + @Binding var selectedMember: GMember? + @State private var memberReactions: [MemberReaction] = [] + @AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var radius = defaultProfileImageCorner + + var body: some View { + groupMemberReactionList() + .task { + logger.debug("ReactionContextMenu task \(radius)") + await loadChatItemReaction() + } + } + + @ViewBuilder private func groupMemberReactionList() -> some View { + if memberReactions.isEmpty { + ForEach(Array(repeating: 0, count: reactionCount.totalReacted), id: \.self) { _ in + Text(verbatim: " ") + } + } else { + ForEach(memberReactions, id: \.groupMember.groupMemberId) { mr in + let mem = mr.groupMember + let userMember = mem.groupMemberId == groupInfo.membership.groupMemberId + Button { + if let member = m.getGroupMember(mem.groupMemberId) { + selectedMember = member + } else { + let member = GMember.init(mem) + m.groupMembers.append(member) + selectedMember = member + } + } label: { + HStack { + Text(mem.displayName) + if let img = cropImage(mem.image) { + Image(uiImage: img) + } else { + Image(systemName: "person.crop.circle") + } + } + } + .disabled(userMember) + } + } + } + + private func cropImage(_ img: String?) -> UIImage? { + return if let originalImage = imageFromBase64(img) { + maskToCustomShape(originalImage, size: 30, radius: radius) + } else { + nil + } + } + + private func loadChatItemReaction() async { + do { + let memberReactions = try await apiGetReactionMembers( + groupId: groupInfo.groupId, + itemId: itemId, + reaction: reactionCount.reaction + ) + await MainActor.run { + self.memberReactions = memberReactions + } + } catch let error { + logger.error("apiGetReactionMembers error: \(responseError(error))") + } + } +} + +func maskToCustomShape(_ image: UIImage, size: CGFloat, radius: CGFloat) -> UIImage { + let path = Path { path in + if radius >= 50 { + path.addEllipse(in: CGRect(x: 0, y: 0, width: size, height: size)) + } else if radius <= 0 { + path.addRect(CGRect(x: 0, y: 0, width: size, height: size)) + } else { + let cornerRadius = size * CGFloat(radius) / 100 + path.addRoundedRect( + in: CGRect(x: 0, y: 0, width: size, height: size), + cornerSize: CGSize(width: cornerRadius, height: cornerRadius), + style: .continuous + ) + } + } + + return UIGraphicsImageRenderer(size: CGSize(width: size, height: size)).image { context in + context.cgContext.addPath(path.cgPath) + context.cgContext.clip() + let scale = size / max(image.size.width, image.size.height) + let imageSize = CGSize(width: image.size.width * scale, height: image.size.height * scale) + let imageOrigin = CGPoint( + x: (size - imageSize.width) / 2, + y: (size - imageSize.height) / 2 + ) + image.draw(in: CGRect(origin: imageOrigin, size: imageSize)) + } +} + struct ToggleNtfsButton: View { @ObservedObject var chat: Chat diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 1df6d07813..83c74178ba 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -49,6 +49,7 @@ public enum ChatCommand { case apiDeleteChatItem(type: ChatType, id: Int64, itemIds: [Int64], mode: CIDeleteMode) case apiDeleteMemberChatItem(groupId: Int64, itemIds: [Int64]) case apiChatItemReaction(type: ChatType, id: Int64, itemId: Int64, add: Bool, reaction: MsgReaction) + case apiGetReactionMembers(userId: Int64, groupId: Int64, itemId: Int64, reaction: MsgReaction) case apiPlanForwardChatItems(toChatType: ChatType, toChatId: Int64, itemIds: [Int64]) case apiForwardChatItems(toChatType: ChatType, toChatId: Int64, fromChatType: ChatType, fromChatId: Int64, itemIds: [Int64], ttl: Int?) case apiGetNtfToken @@ -212,6 +213,7 @@ public enum ChatCommand { case let .apiDeleteChatItem(type, id, itemIds, mode): return "/_delete item \(ref(type, id)) \(itemIds.map({ "\($0)" }).joined(separator: ",")) \(mode.rawValue)" case let .apiDeleteMemberChatItem(groupId, itemIds): return "/_delete member item #\(groupId) \(itemIds.map({ "\($0)" }).joined(separator: ","))" case let .apiChatItemReaction(type, id, itemId, add, reaction): return "/_reaction \(ref(type, id)) \(itemId) \(onOff(add)) \(encodeJSON(reaction))" + case let .apiGetReactionMembers(userId, groupId, itemId, reaction): return "/_reaction members \(userId) #\(groupId) \(itemId) \(encodeJSON(reaction))" case let .apiPlanForwardChatItems(type, id, itemIds): return "/_forward plan \(ref(type, id)) \(itemIds.map({ "\($0)" }).joined(separator: ","))" case let .apiForwardChatItems(toChatType, toChatId, fromChatType, fromChatId, itemIds, ttl): let ttlStr = ttl != nil ? "\(ttl!)" : "default" @@ -375,6 +377,7 @@ public enum ChatCommand { case .apiConnectContactViaAddress: return "apiConnectContactViaAddress" case .apiDeleteMemberChatItem: return "apiDeleteMemberChatItem" case .apiChatItemReaction: return "apiChatItemReaction" + case .apiGetReactionMembers: return "apiGetReactionMembers" case .apiPlanForwardChatItems: return "apiPlanForwardChatItems" case .apiForwardChatItems: return "apiForwardChatItems" case .apiGetNtfToken: return "apiGetNtfToken" @@ -629,6 +632,7 @@ public enum ChatResponse: Decodable, Error { case chatItemUpdated(user: UserRef, chatItem: AChatItem) case chatItemNotChanged(user: UserRef, chatItem: AChatItem) case chatItemReaction(user: UserRef, added: Bool, reaction: ACIReaction) + case reactionMembers(user: UserRef, memberReactions: [MemberReaction]) case chatItemsDeleted(user: UserRef, chatItemDeletions: [ChatItemDeletion], byUser: Bool) case contactsList(user: UserRef, contacts: [Contact]) // group events @@ -805,6 +809,7 @@ public enum ChatResponse: Decodable, Error { case .chatItemUpdated: return "chatItemUpdated" case .chatItemNotChanged: return "chatItemNotChanged" case .chatItemReaction: return "chatItemReaction" + case .reactionMembers: return "reactionMembers" case .chatItemsDeleted: return "chatItemsDeleted" case .contactsList: return "contactsList" case .groupCreated: return "groupCreated" @@ -983,6 +988,7 @@ public enum ChatResponse: Decodable, Error { case let .chatItemUpdated(u, chatItem): return withUser(u, String(describing: chatItem)) case let .chatItemNotChanged(u, chatItem): return withUser(u, String(describing: chatItem)) case let .chatItemReaction(u, added, reaction): return withUser(u, "added: \(added)\n\(String(describing: reaction))") + case let .reactionMembers(u, reaction): return withUser(u, "memberReactions: \(String(describing: reaction))") case let .chatItemsDeleted(u, items, byUser): let itemsString = items.map { item in "deletedChatItem:\n\(String(describing: item.deletedChatItem))\ntoChatItem:\n\(String(describing: item.toChatItem))" }.joined(separator: "\n") diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 1bd5673f01..de671ee203 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -2311,6 +2311,11 @@ public struct ACIReaction: Decodable, Hashable { public var chatReaction: CIReaction } +public struct MemberReaction: Decodable, Hashable { + public var groupMember: GroupMember + public var reactionTs: Date +} + public struct CIReaction: Decodable, Hashable { public var chatDir: CIDirection public var chatItem: ChatItem diff --git a/apps/ios/SimpleXChat/ImageUtils.swift b/apps/ios/SimpleXChat/ImageUtils.swift index 9702408c27..89cc45c4f5 100644 --- a/apps/ios/SimpleXChat/ImageUtils.swift +++ b/apps/ios/SimpleXChat/ImageUtils.swift @@ -138,7 +138,7 @@ private func reduceSize(_ image: UIImage, ratio: CGFloat, hasAlpha: Bool) -> UII return resizeImage(image, newBounds: bounds, drawIn: bounds, hasAlpha: hasAlpha) } -private func resizeImage(_ image: UIImage, newBounds: CGRect, drawIn: CGRect, hasAlpha: Bool) -> UIImage { +public func resizeImage(_ image: UIImage, newBounds: CGRect, drawIn: CGRect, hasAlpha: Bool) -> UIImage { let format = UIGraphicsImageRendererFormat() format.scale = 1.0 format.opaque = !hasAlpha From 961bdbfc59ed9034fbbba8989bcfe7d17e87689d Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Sat, 30 Nov 2024 23:29:27 +0700 Subject: [PATCH 087/167] ios: start/stop chat toggle refactoring (#5275) * ios: start/stop chat toggle refactoring * changes * changes * return back * reduce diff * better * update button * ios: do not start chat after export, always show run toggle (#5284) --------- Co-authored-by: Evgeny Poberezkin --- apps/ios/Shared/AppDelegate.swift | 1 + apps/ios/Shared/Model/SimpleXAPI.swift | 9 + .../Views/Database/ChatArchiveView.swift | 68 ---- .../Database/DatabaseEncryptionView.swift | 87 +++-- .../Shared/Views/Database/DatabaseView.swift | 356 +++++++++++------- .../Database/MigrateToAppGroupView.swift | 8 +- .../Views/Migration/MigrateFromDevice.swift | 5 +- .../Views/Migration/MigrateToDevice.swift | 37 ++ .../Views/UserSettings/SettingsView.swift | 3 + apps/ios/SimpleX.xcodeproj/project.pbxproj | 4 - 10 files changed, 340 insertions(+), 238 deletions(-) delete mode 100644 apps/ios/Shared/Views/Database/ChatArchiveView.swift diff --git a/apps/ios/Shared/AppDelegate.swift b/apps/ios/Shared/AppDelegate.swift index 5845793aa7..ad8c661e1c 100644 --- a/apps/ios/Shared/AppDelegate.swift +++ b/apps/ios/Shared/AppDelegate.swift @@ -17,6 +17,7 @@ class AppDelegate: NSObject, UIApplicationDelegate { application.registerForRemoteNotifications() removePasscodesIfReinstalled() prepareForLaunch() + deleteOldChatArchive() return true } diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index d99e97f2e1..459ece32da 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -1600,6 +1600,15 @@ func initializeChat(start: Bool, confirmStart: Bool = false, dbKey: String? = ni m.chatInitialized = true m.currentUser = try apiGetActiveUser() m.conditions = try getServerOperators() + if shouldImportAppSettingsDefault.get() { + do { + let appSettings = try apiGetAppSettings(settings: AppSettings.current.prepareForExport()) + appSettings.importIntoApp() + shouldImportAppSettingsDefault.set(false) + } catch { + logger.error("Error while importing app settings: \(error)") + } + } if m.currentUser == nil { onboardingStageDefault.set(.step1_SimpleXInfo) privacyDeliveryReceiptsSet.set(true) diff --git a/apps/ios/Shared/Views/Database/ChatArchiveView.swift b/apps/ios/Shared/Views/Database/ChatArchiveView.swift deleted file mode 100644 index 3ab4ac9a31..0000000000 --- a/apps/ios/Shared/Views/Database/ChatArchiveView.swift +++ /dev/null @@ -1,68 +0,0 @@ -// -// ChatArchiveView.swift -// SimpleXChat -// -// Created by Evgeny on 23/06/2022. -// Copyright © 2022 SimpleX Chat. All rights reserved. -// - -import SwiftUI -import SimpleXChat - -struct ChatArchiveView: View { - @EnvironmentObject var theme: AppTheme - var archiveName: String - @AppStorage(DEFAULT_CHAT_ARCHIVE_NAME) private var chatArchiveName: String? - @AppStorage(DEFAULT_CHAT_ARCHIVE_TIME) private var chatArchiveTime: Double = 0 - @State private var showDeleteAlert = false - - var body: some View { - let fileUrl = getDocumentsDirectory().appendingPathComponent(archiveName) - let fileTs = chatArchiveTimeDefault.get() - List { - Section { - settingsRow("square.and.arrow.up", color: theme.colors.secondary) { - Button { - showShareSheet(items: [fileUrl]) - } label: { - Text("Save archive") - } - } - settingsRow("trash", color: theme.colors.secondary) { - Button { - showDeleteAlert = true - } label: { - Text("Delete archive").foregroundColor(.red) - } - } - } header: { - Text("Chat archive") - .foregroundColor(theme.colors.secondary) - } footer: { - Text("Created on \(fileTs)") - .foregroundColor(theme.colors.secondary) - } - } - .alert(isPresented: $showDeleteAlert) { - Alert( - title: Text("Delete chat archive?"), - primaryButton: .destructive(Text("Delete")) { - do { - try FileManager.default.removeItem(atPath: fileUrl.path) - chatArchiveName = nil - chatArchiveTime = 0 - } catch let error { - logger.error("removeItem error \(String(describing: error))") - } - }, - secondaryButton: .cancel() - ) - } - } -} - -struct ChatArchiveView_Previews: PreviewProvider { - static var previews: some View { - ChatArchiveView(archiveName: "") - } -} diff --git a/apps/ios/Shared/Views/Database/DatabaseEncryptionView.swift b/apps/ios/Shared/Views/Database/DatabaseEncryptionView.swift index be167b92b9..3cd37e4930 100644 --- a/apps/ios/Shared/Views/Database/DatabaseEncryptionView.swift +++ b/apps/ios/Shared/Views/Database/DatabaseEncryptionView.swift @@ -48,6 +48,8 @@ struct DatabaseEncryptionView: View { @State private var confirmNewKey = "" @State private var currentKeyShown = false + let stopChatRunBlockStartChat: (Binding, @escaping () async throws -> Bool) -> Void + var body: some View { ZStack { List { @@ -134,46 +136,61 @@ struct DatabaseEncryptionView: View { .onAppear { if initialRandomDBPassphrase { currentKey = kcDatabasePassword.get() ?? "" } } - .disabled(m.chatRunning != false) + .disabled(progressIndicator) .alert(item: $alert) { item in databaseEncryptionAlert(item) } } - private func encryptDatabase() { - progressIndicator = true - Task { - do { - encryptionStartedDefault.set(true) - encryptionStartedAtDefault.set(Date.now) - if !m.chatDbChanged { - try apiSaveAppSettings(settings: AppSettings.current.prepareForExport()) - } - try await apiStorageEncryption(currentKey: currentKey, newKey: newKey) - encryptionStartedDefault.set(false) - initialRandomDBPassphraseGroupDefault.set(false) - if migration { - storeDBPassphraseGroupDefault.set(useKeychain) - } - if useKeychain { - if kcDatabasePassword.set(newKey) { - await resetFormAfterEncryption(true) - await operationEnded(.databaseEncrypted) - } else { - await resetFormAfterEncryption() - await operationEnded(.error(title: "Keychain error", error: "Error saving passphrase to keychain")) - } - } else { - if migration { - removePassphraseFromKeyChain() - } - await resetFormAfterEncryption() + private func encryptDatabaseAsync() async -> Bool { + await MainActor.run { + progressIndicator = true + } + do { + encryptionStartedDefault.set(true) + encryptionStartedAtDefault.set(Date.now) + if !m.chatDbChanged { + try apiSaveAppSettings(settings: AppSettings.current.prepareForExport()) + } + try await apiStorageEncryption(currentKey: currentKey, newKey: newKey) + encryptionStartedDefault.set(false) + initialRandomDBPassphraseGroupDefault.set(false) + if migration { + storeDBPassphraseGroupDefault.set(useKeychain) + } + if useKeychain { + if kcDatabasePassword.set(newKey) { + await resetFormAfterEncryption(true) await operationEnded(.databaseEncrypted) - } - } catch let error { - if case .chatCmdError(_, .errorDatabase(.errorExport(.errorNotADatabase))) = error as? ChatResponse { - await operationEnded(.currentPassphraseError) } else { - await operationEnded(.error(title: "Error encrypting database", error: "\(responseError(error))")) + await resetFormAfterEncryption() + await operationEnded(.error(title: "Keychain error", error: "Error saving passphrase to keychain")) } + } else { + if migration { + removePassphraseFromKeyChain() + } + await resetFormAfterEncryption() + await operationEnded(.databaseEncrypted) + } + return true + } catch let error { + if case .chatCmdError(_, .errorDatabase(.errorExport(.errorNotADatabase))) = error as? ChatResponse { + await operationEnded(.currentPassphraseError) + } else { + await operationEnded(.error(title: "Error encrypting database", error: "\(responseError(error))")) + } + return false + } + } + + private func encryptDatabase() { + // it will try to stop and start the chat in case of: non-migration && successful encryption. In migration the chat will remain stopped + if migration { + Task { + await encryptDatabaseAsync() + } + } else { + stopChatRunBlockStartChat($progressIndicator) { + return await encryptDatabaseAsync() } } } @@ -371,6 +388,6 @@ func validKey(_ s: String) -> Bool { struct DatabaseEncryptionView_Previews: PreviewProvider { static var previews: some View { - DatabaseEncryptionView(useKeychain: Binding.constant(true), migration: false) + DatabaseEncryptionView(useKeychain: Binding.constant(true), migration: false, stopChatRunBlockStartChat: { _, _ in true }) } } diff --git a/apps/ios/Shared/Views/Database/DatabaseView.swift b/apps/ios/Shared/Views/Database/DatabaseView.swift index 804f2307ef..4a367f7722 100644 --- a/apps/ios/Shared/Views/Database/DatabaseView.swift +++ b/apps/ios/Shared/Views/Database/DatabaseView.swift @@ -46,6 +46,7 @@ struct DatabaseView: View { @EnvironmentObject var theme: AppTheme let dismissSettingsSheet: DismissAction @State private var runChat = false + @State private var stoppingChat = false @State private var alert: DatabaseAlert? = nil @State private var showFileImporter = false @State private var importedArchivePath: URL? @@ -57,6 +58,8 @@ struct DatabaseView: View { @State private var useKeychain = storeDBPassphraseGroupDefault.get() @State private var appFilesCountAndSize: (Int, Int)? + @State private var showDatabaseEncryptionView = false + @State var chatItemTTL: ChatItemTTL @State private var currentChatItemTTL: ChatItemTTL = .none @@ -69,7 +72,20 @@ struct DatabaseView: View { } } + @ViewBuilder private func chatDatabaseView() -> some View { + NavigationLink(isActive: $showDatabaseEncryptionView) { + DatabaseEncryptionView(useKeychain: $useKeychain, migration: false, stopChatRunBlockStartChat: { progressIndicator, block in + stopChatRunBlockStartChat(false, progressIndicator, block) + }) + .navigationTitle("Database passphrase") + .modifier(ThemedBackground(grouped: true)) + } label: { + EmptyView() + } + .frame(width: 1, height: 1) + .hidden() + List { let stopped = m.chatRunning == false Section { @@ -101,9 +117,10 @@ struct DatabaseView: View { isOn: $runChat ) .onChange(of: runChat) { _ in - if (runChat) { - startChat() - } else { + if runChat { + DatabaseView.startChat($runChat, $progressIndicator) + } else if !stoppingChat { + stoppingChat = false alert = .stopChat } } @@ -123,7 +140,9 @@ struct DatabaseView: View { let color: Color = unencrypted ? .orange : theme.colors.secondary settingsRow(unencrypted ? "lock.open" : useKeychain ? "key" : "lock", color: color) { NavigationLink { - DatabaseEncryptionView(useKeychain: $useKeychain, migration: false) + DatabaseEncryptionView(useKeychain: $useKeychain, migration: false, stopChatRunBlockStartChat: { progressIndicator, block in + stopChatRunBlockStartChat(false, progressIndicator, block) + }) .navigationTitle("Database passphrase") .modifier(ThemedBackground(grouped: true)) } label: { @@ -133,9 +152,14 @@ struct DatabaseView: View { settingsRow("square.and.arrow.up", color: theme.colors.secondary) { Button("Export database") { if initialRandomDBPassphraseGroupDefault.get() && !unencrypted { - alert = .exportProhibited + showDatabaseEncryptionView = true + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + alert = .exportProhibited + } } else { - exportArchive() + stopChatRunBlockStartChat(stopped, $progressIndicator) { + await exportArchive() + } } } } @@ -144,20 +168,6 @@ struct DatabaseView: View { showFileImporter = true } } - if let archiveName = chatArchiveName { - let title: LocalizedStringKey = chatArchiveTimeDefault.get() < chatLastStartGroupDefault.get() - ? "Old database archive" - : "New database archive" - settingsRow("archivebox", color: theme.colors.secondary) { - NavigationLink { - ChatArchiveView(archiveName: archiveName) - .navigationTitle(title) - .modifier(ThemedBackground(grouped: true)) - } label: { - Text(title) - } - } - } settingsRow("trash.slash", color: theme.colors.secondary) { Button("Delete database", role: .destructive) { alert = .deleteChat @@ -167,14 +177,10 @@ struct DatabaseView: View { Text("Chat database") .foregroundColor(theme.colors.secondary) } footer: { - Text( - stopped - ? "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." - : "Stop chat to enable database actions" - ) + Text("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.") .foregroundColor(theme.colors.secondary) } - .disabled(!stopped) + .disabled(progressIndicator) if case .group = dbContainer, legacyDatabase { Section(header: Text("Old database").foregroundColor(theme.colors.secondary)) { @@ -190,7 +196,7 @@ struct DatabaseView: View { Button(m.users.count > 1 ? "Delete files for all chat profiles" : "Delete all files", role: .destructive) { alert = .deleteFilesAndMedia } - .disabled(!stopped || appFilesCountAndSize?.0 == 0) + .disabled(progressIndicator || appFilesCountAndSize?.0 == 0) } header: { Text("Files & media") .foregroundColor(theme.colors.secondary) @@ -255,7 +261,10 @@ struct DatabaseView: View { title: Text("Import chat database?"), message: Text("Your current chat database will be DELETED and REPLACED with the imported one.") + Text("This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost."), primaryButton: .destructive(Text("Import")) { - importArchive(fileURL) + stopChatRunBlockStartChat(m.chatRunning == false, $progressIndicator) { + _ = await DatabaseView.importArchive(fileURL, $progressIndicator, $alert) + return true + } }, secondaryButton: .cancel() ) @@ -263,19 +272,15 @@ struct DatabaseView: View { return Alert(title: Text("Error: no database file")) } case .archiveImported: - return Alert( - title: Text("Chat database imported"), - message: Text("Restart the app to use imported chat database") - ) + let (title, message) = archiveImportedAlertText() + return Alert(title: Text(title), message: Text(message)) case let .archiveImportedWithErrors(errs): - return Alert( - title: Text("Chat database imported"), - message: Text("Restart the app to use imported chat database") + Text(verbatim: "\n") + Text("Some non-fatal errors occurred during import:") + archiveErrorsText(errs) - ) + let (title, message) = archiveImportedWithErrorsAlertText(errs: errs) + return Alert(title: Text(title), message: Text(message)) case let .archiveExportedWithErrors(archivePath, errs): return Alert( title: Text("Chat database exported"), - message: Text("You may save the exported archive.") + Text(verbatim: "\n") + Text("Some file(s) were not exported:") + archiveErrorsText(errs), + message: Text("You may save the exported archive.") + Text(verbatim: "\n") + Text("Some file(s) were not exported:") + Text(archiveErrorsText(errs)), dismissButton: .default(Text("Continue")) { showShareSheet(items: [archivePath]) } @@ -285,15 +290,17 @@ struct DatabaseView: View { title: Text("Delete chat profile?"), message: Text("This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost."), primaryButton: .destructive(Text("Delete")) { - deleteChat() + let wasStopped = m.chatRunning == false + stopChatRunBlockStartChat(wasStopped, $progressIndicator) { + _ = await deleteChat() + return true + } }, secondaryButton: .cancel() ) case .chatDeleted: - return Alert( - title: Text("Chat database deleted"), - message: Text("Restart the app to create a new chat profile") - ) + let (title, message) = chatDeletedAlertText() + return Alert(title: Text(title), message: Text(message)) case .deleteLegacyDatabase: return Alert( title: Text("Delete old database?"), @@ -308,7 +315,10 @@ struct DatabaseView: View { title: Text("Delete files and media?"), message: Text("This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain."), primaryButton: .destructive(Text("Delete")) { - deleteFiles() + stopChatRunBlockStartChat(m.chatRunning == false, $progressIndicator) { + deleteFiles() + return true + } }, secondaryButton: .cancel() ) @@ -328,95 +338,180 @@ struct DatabaseView: View { } } - private func authStopChat() { + private func authStopChat(_ onStop: (() -> Void)? = nil) { if UserDefaults.standard.bool(forKey: DEFAULT_PERFORM_LA) { authenticate(reason: NSLocalizedString("Stop SimpleX", comment: "authentication reason")) { laResult in switch laResult { - case .success: stopChat() - case .unavailable: stopChat() + case .success: stopChat(onStop) + case .unavailable: stopChat(onStop) case .failed: withAnimation { runChat = true } } } } else { - stopChat() + stopChat(onStop) } } - private func stopChat() { + private func stopChat(_ onStop: (() -> Void)? = nil) { Task { do { try await stopChatAsync() + onStop?() } catch let error { await MainActor.run { runChat = true - alert = .error(title: "Error stopping chat", error: responseError(error)) + showAlert("Error stopping chat", message: responseError(error)) } } } } - private func exportArchive() { - progressIndicator = true - Task { - do { - let (archivePath, archiveErrors) = try await exportChatArchive() - if archiveErrors.isEmpty { - showShareSheet(items: [archivePath]) - await MainActor.run { progressIndicator = false } - } else { - await MainActor.run { - alert = .archiveExportedWithErrors(archivePath: archivePath, archiveErrors: archiveErrors) - progressIndicator = false + func stopChatRunBlockStartChat( + _ stopped: Bool, + _ progressIndicator: Binding, + _ block: @escaping () async throws -> Bool + ) { + // if the chat was running, the sequence is: stop chat, run block, start chat. + // Otherwise, just run block and do nothing - the toggle will be visible anyway and the user can start the chat or not + if stopped { + Task { + do { + _ = try await block() + } catch { + logger.error("Error while executing block: \(error)") + } + } + } else { + authStopChat { + stoppingChat = true + runChat = false + Task { + // if it throws, let's start chat again anyway + var canStart = false + do { + canStart = try await block() + } catch { + logger.error("Error executing block: \(error)") + canStart = true + } + if canStart { + await MainActor.run { + DatabaseView.startChat($runChat, $progressIndicator) + } } } + } + } + } + + static func startChat(_ runChat: Binding, _ progressIndicator: Binding) { + progressIndicator.wrappedValue = true + let m = ChatModel.shared + if m.chatDbChanged { + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + resetChatCtrl() + do { + let hadDatabase = hasDatabase() + try initializeChat(start: true) + m.chatDbChanged = false + AppChatState.shared.set(.active) + if m.chatDbStatus != .ok || !hadDatabase { + // Hide current view and show `DatabaseErrorView` + dismissAllSheets(animated: true) + } + } catch let error { + fatalError("Error starting chat \(responseError(error))") + } + progressIndicator.wrappedValue = false + } + } else { + do { + _ = try apiStartChat() + runChat.wrappedValue = true + m.chatRunning = true + ChatReceiver.shared.start() + chatLastStartGroupDefault.set(Date.now) + AppChatState.shared.set(.active) } catch let error { + runChat.wrappedValue = false + showAlert(NSLocalizedString("Error starting chat", comment: ""), message: responseError(error)) + } + progressIndicator.wrappedValue = false + } + } + + private func exportArchive() async -> Bool { + await MainActor.run { + progressIndicator = true + } + do { + let (archivePath, archiveErrors) = try await exportChatArchive() + if archiveErrors.isEmpty { + showShareSheet(items: [archivePath]) + await MainActor.run { progressIndicator = false } + } else { await MainActor.run { - alert = .error(title: "Error exporting chat database", error: responseError(error)) + alert = .archiveExportedWithErrors(archivePath: archivePath, archiveErrors: archiveErrors) progressIndicator = false } } + } catch let error { + await MainActor.run { + alert = .error(title: "Error exporting chat database", error: responseError(error)) + progressIndicator = false + } } + return false } - private func importArchive(_ archivePath: URL) { + static func importArchive( + _ archivePath: URL, + _ progressIndicator: Binding, + _ alert: Binding + ) async -> Bool { if archivePath.startAccessingSecurityScopedResource() { - progressIndicator = true - Task { - do { - try await apiDeleteStorage() - try? FileManager.default.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true) - do { - let config = ArchiveConfig(archivePath: archivePath.path) - let archiveErrors = try await apiImportArchive(config: config) - _ = kcDatabasePassword.remove() - if archiveErrors.isEmpty { - await operationEnded(.archiveImported) - } else { - await operationEnded(.archiveImportedWithErrors(archiveErrors: archiveErrors)) - } - } catch let error { - await operationEnded(.error(title: "Error importing chat database", error: responseError(error))) - } - } catch let error { - await operationEnded(.error(title: "Error deleting chat database", error: responseError(error))) - } - archivePath.stopAccessingSecurityScopedResource() + await MainActor.run { + progressIndicator.wrappedValue = true } + do { + try await apiDeleteStorage() + try? FileManager.default.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true) + do { + let config = ArchiveConfig(archivePath: archivePath.path) + let archiveErrors = try await apiImportArchive(config: config) + shouldImportAppSettingsDefault.set(true) + _ = kcDatabasePassword.remove() + if archiveErrors.isEmpty { + await operationEnded(.archiveImported, progressIndicator, alert) + } else { + await operationEnded(.archiveImportedWithErrors(archiveErrors: archiveErrors), progressIndicator, alert) + } + return true + } catch let error { + await operationEnded(.error(title: "Error importing chat database", error: responseError(error)), progressIndicator, alert) + } + } catch let error { + await operationEnded(.error(title: "Error deleting chat database", error: responseError(error)), progressIndicator, alert) + } + archivePath.stopAccessingSecurityScopedResource() } else { - alert = .error(title: "Error accessing database file") + showAlert("Error accessing database file") } + return false } - private func deleteChat() { - progressIndicator = true - Task { - do { - try await deleteChatAsync() - await operationEnded(.chatDeleted) - appFilesCountAndSize = directoryFileCountAndSize(getAppFilesDirectory()) - } catch let error { - await operationEnded(.error(title: "Error deleting database", error: responseError(error))) - } + private func deleteChat() async -> Bool { + await MainActor.run { + progressIndicator = true + } + do { + try await deleteChatAsync() + appFilesCountAndSize = directoryFileCountAndSize(getAppFilesDirectory()) + await DatabaseView.operationEnded(.chatDeleted, $progressIndicator, $alert) + return true + } catch let error { + await DatabaseView.operationEnded(.error(title: "Error deleting database", error: responseError(error)), $progressIndicator, $alert) + return false } } @@ -428,39 +523,28 @@ struct DatabaseView: View { } } - private func operationEnded(_ dbAlert: DatabaseAlert) async { + private static func operationEnded(_ dbAlert: DatabaseAlert, _ progressIndicator: Binding, _ alert: Binding) async { await MainActor.run { + let m = ChatModel.shared m.chatDbChanged = true m.chatInitialized = false - progressIndicator = false - alert = dbAlert + progressIndicator.wrappedValue = false } - } - - private func startChat() { - if m.chatDbChanged { - dismissSettingsSheet() - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - resetChatCtrl() - do { - try initializeChat(start: true) - m.chatDbChanged = false - AppChatState.shared.set(.active) - } catch let error { - fatalError("Error starting chat \(responseError(error))") - } - } - } else { - do { - _ = try apiStartChat() - runChat = true - m.chatRunning = true - ChatReceiver.shared.start() - chatLastStartGroupDefault.set(Date.now) - AppChatState.shared.set(.active) - } catch let error { - runChat = false - alert = .error(title: "Error starting chat", error: responseError(error)) + await withCheckedContinuation { cont in + let okAlertActionWaiting = UIAlertAction(title: NSLocalizedString("Ok", comment: "alert button"), style: .default, handler: { _ in cont.resume() }) + // show these alerts globally so they are visible when all sheets will be hidden + if case .archiveImported = dbAlert { + let (title, message) = archiveImportedAlertText() + showAlert(title, message: message, actions: { [okAlertActionWaiting] }) + } else if case .archiveImportedWithErrors(let errs) = dbAlert { + let (title, message) = archiveImportedWithErrorsAlertText(errs: errs) + showAlert(title, message: message, actions: { [okAlertActionWaiting] }) + } else if case .chatDeleted = dbAlert { + let (title, message) = chatDeletedAlertText() + showAlert(title, message: message, actions: { [okAlertActionWaiting] }) + } else { + alert.wrappedValue = dbAlert + cont.resume() } } } @@ -503,8 +587,28 @@ struct DatabaseView: View { } } -func archiveErrorsText(_ errs: [ArchiveError]) -> Text { - return Text("\n" + errs.map(showArchiveError).joined(separator: "\n")) +private func archiveImportedAlertText() -> (String, String) { + ( + NSLocalizedString("Chat database imported", comment: ""), + NSLocalizedString("Restart the app to use imported chat database", comment: "") + ) +} +private func archiveImportedWithErrorsAlertText(errs: [ArchiveError]) -> (String, String) { + ( + NSLocalizedString("Chat database imported", comment: ""), + NSLocalizedString("Restart the app to use imported chat database", comment: "") + "\n" + NSLocalizedString("Some non-fatal errors occurred during import:", comment: "") + archiveErrorsText(errs) + ) +} + +private func chatDeletedAlertText() -> (String, String) { + ( + NSLocalizedString("Chat database deleted", comment: ""), + NSLocalizedString("Restart the app to create a new chat profile", comment: "") + ) +} + +func archiveErrorsText(_ errs: [ArchiveError]) -> String { + return "\n" + errs.map(showArchiveError).joined(separator: "\n") func showArchiveError(_ err: ArchiveError) -> String { switch err { diff --git a/apps/ios/Shared/Views/Database/MigrateToAppGroupView.swift b/apps/ios/Shared/Views/Database/MigrateToAppGroupView.swift index e79f24c6d9..79c0a42ae0 100644 --- a/apps/ios/Shared/Views/Database/MigrateToAppGroupView.swift +++ b/apps/ios/Shared/Views/Database/MigrateToAppGroupView.swift @@ -117,7 +117,7 @@ struct MigrateToAppGroupView: View { setV3DBMigration(.migration_error) migrationError = "Error starting chat: \(responseError(error))" } - deleteOldArchive() + deleteOldChatArchive() } label: { Text("Start chat") .font(.title) @@ -235,14 +235,16 @@ func exportChatArchive(_ storagePath: URL? = nil) async throws -> (URL, [Archive try? FileManager.default.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true) let errs = try await apiExportArchive(config: config) if storagePath == nil { - deleteOldArchive() + deleteOldChatArchive() UserDefaults.standard.set(archiveName, forKey: DEFAULT_CHAT_ARCHIVE_NAME) chatArchiveTimeDefault.set(archiveTime) } return (archivePath, errs) } -func deleteOldArchive() { +/// Deprecated. Remove in the end of 2025. All unused archives should be deleted for the most users til then. +/// Remove DEFAULT_CHAT_ARCHIVE_NAME and DEFAULT_CHAT_ARCHIVE_TIME as well +func deleteOldChatArchive() { let d = UserDefaults.standard if let archiveName = d.string(forKey: DEFAULT_CHAT_ARCHIVE_NAME) { do { diff --git a/apps/ios/Shared/Views/Migration/MigrateFromDevice.swift b/apps/ios/Shared/Views/Migration/MigrateFromDevice.swift index 829cea0165..eb8df5fb04 100644 --- a/apps/ios/Shared/Views/Migration/MigrateFromDevice.swift +++ b/apps/ios/Shared/Views/Migration/MigrateFromDevice.swift @@ -177,7 +177,7 @@ struct MigrateFromDevice: View { case let .archiveExportedWithErrors(archivePath, errs): return Alert( title: Text("Chat database exported"), - message: Text("You may migrate the exported database.") + Text(verbatim: "\n") + Text("Some file(s) were not exported:") + archiveErrorsText(errs), + message: Text("You may migrate the exported database.") + Text(verbatim: "\n") + Text("Some file(s) were not exported:") + Text(archiveErrorsText(errs)), dismissButton: .default(Text("Continue")) { Task { await uploadArchive(path: archivePath) } } @@ -222,7 +222,8 @@ struct MigrateFromDevice: View { } private func passphraseNotSetView() -> some View { - DatabaseEncryptionView(useKeychain: $useKeychain, migration: true) + DatabaseEncryptionView(useKeychain: $useKeychain, migration: true, stopChatRunBlockStartChat: { _, _ in + }) .onChange(of: initialRandomDBPassphrase) { initial in if !initial { migrationState = .uploadConfirmation diff --git a/apps/ios/Shared/Views/Migration/MigrateToDevice.swift b/apps/ios/Shared/Views/Migration/MigrateToDevice.swift index fe0eec609b..763cd473fe 100644 --- a/apps/ios/Shared/Views/Migration/MigrateToDevice.swift +++ b/apps/ios/Shared/Views/Migration/MigrateToDevice.swift @@ -103,6 +103,9 @@ struct MigrateToDevice: View { @State private var showQRCodeScanner: Bool = true @State private var pasteboardHasStrings = UIPasteboard.general.hasStrings + @State private var importingArchiveFromFileProgressIndicator = false + @State private var showFileImporter = false + var body: some View { VStack { switch migrationState { @@ -200,6 +203,12 @@ struct MigrateToDevice: View { Section(header: Text("Or paste archive link").foregroundColor(theme.colors.secondary)) { pasteLinkView() } + Section(header: Text("Or import archive file").foregroundColor(theme.colors.secondary)) { + archiveImportFromFileView() + } + } + if importingArchiveFromFileProgressIndicator { + progressView() } } } @@ -220,6 +229,34 @@ struct MigrateToDevice: View { .frame(maxWidth: .infinity, alignment: .center) } + private func archiveImportFromFileView() -> some View { + Button { + showFileImporter = true + } label: { + Label("Import database", systemImage: "square.and.arrow.down") + } + .disabled(importingArchiveFromFileProgressIndicator) + .fileImporter( + isPresented: $showFileImporter, + allowedContentTypes: [.zip], + allowsMultipleSelection: false + ) { result in + if case let .success(files) = result, let fileURL = files.first { + Task { + let success = await DatabaseView.importArchive(fileURL, $importingArchiveFromFileProgressIndicator, Binding.constant(nil)) + if success { + DatabaseView.startChat( + Binding.constant(false), + $importingArchiveFromFileProgressIndicator + ) + hideView() + } + } + } + } + } + + private func linkDownloadingView(_ link: String) -> some View { ZStack { List { diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift index 95bf327f1b..8a4ccce91b 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift @@ -39,6 +39,7 @@ let DEFAULT_EXPERIMENTAL_CALLS = "experimentalCalls" let DEFAULT_CHAT_ARCHIVE_NAME = "chatArchiveName" let DEFAULT_CHAT_ARCHIVE_TIME = "chatArchiveTime" let DEFAULT_CHAT_V3_DB_MIGRATION = "chatV3DBMigration" +let DEFAULT_SHOULD_IMPORT_APP_SETTINGS = "shouldImportAppSettings" let DEFAULT_DEVELOPER_TOOLS = "developerTools" let DEFAULT_ENCRYPTION_STARTED = "encryptionStarted" let DEFAULT_ENCRYPTION_STARTED_AT = "encryptionStartedAt" @@ -192,6 +193,8 @@ let customDisappearingMessageTimeDefault = IntDefault(defaults: UserDefaults.sta let showDeleteConversationNoticeDefault = BoolDefault(defaults: UserDefaults.standard, forKey: DEFAULT_SHOW_DELETE_CONVERSATION_NOTICE) let showDeleteContactNoticeDefault = BoolDefault(defaults: UserDefaults.standard, forKey: DEFAULT_SHOW_DELETE_CONTACT_NOTICE) +/// after importing new database, this flag will be set and unset only after importing app settings in `initializeChat` */ +let shouldImportAppSettingsDefault = BoolDefault(defaults: UserDefaults.standard, forKey: DEFAULT_SHOULD_IMPORT_APP_SETTINGS) let currentThemeDefault = StringDefault(defaults: UserDefaults.standard, forKey: DEFAULT_CURRENT_THEME, withDefault: DefaultTheme.SYSTEM_THEME_NAME) let systemDarkThemeDefault = StringDefault(defaults: UserDefaults.standard, forKey: DEFAULT_SYSTEM_DARK_THEME, withDefault: DefaultTheme.DARK.themeName) let currentThemeIdsDefault = CodableDefault<[String: String]>(defaults: UserDefaults.standard, forKey: DEFAULT_CURRENT_THEME_IDS, withDefault: [:] ) diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 0ffe9d1f40..8d2d05489d 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -139,7 +139,6 @@ 5CF937202B24DE8C00E1D781 /* SharedFileSubscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF9371F2B24DE8C00E1D781 /* SharedFileSubscriber.swift */; }; 5CF937232B2503D000E1D781 /* NSESubscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF937212B25034A00E1D781 /* NSESubscriber.swift */; }; 5CFA59C42860BC6200863A68 /* MigrateToAppGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */; }; - 5CFA59D12864782E00863A68 /* ChatArchiveView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFA59CF286477B400863A68 /* ChatArchiveView.swift */; }; 5CFE0921282EEAF60002594B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */; }; 5CFE0922282EEAF60002594B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */; }; 640417CD2B29B8C200CCB412 /* NewChatMenuButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 640417CB2B29B8C200CCB412 /* NewChatMenuButton.swift */; }; @@ -489,7 +488,6 @@ 5CF9371F2B24DE8C00E1D781 /* SharedFileSubscriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedFileSubscriber.swift; sourceTree = ""; }; 5CF937212B25034A00E1D781 /* NSESubscriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSESubscriber.swift; sourceTree = ""; }; 5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateToAppGroupView.swift; sourceTree = ""; }; - 5CFA59CF286477B400863A68 /* ChatArchiveView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatArchiveView.swift; sourceTree = ""; }; 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ZoomableScrollView.swift; path = Shared/Views/ZoomableScrollView.swift; sourceTree = SOURCE_ROOT; }; 640417CB2B29B8C200CCB412 /* NewChatMenuButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewChatMenuButton.swift; sourceTree = ""; }; 640417CC2B29B8C200CCB412 /* NewChatView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewChatView.swift; sourceTree = ""; }; @@ -1058,7 +1056,6 @@ isa = PBXGroup; children = ( 5C4B3B09285FB130003915F2 /* DatabaseView.swift */, - 5CFA59CF286477B400863A68 /* ChatArchiveView.swift */, 5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */, 5C9CC7A828C532AB00BEF955 /* DatabaseErrorView.swift */, 5C9CC7AC28C55D7800BEF955 /* DatabaseEncryptionView.swift */, @@ -1511,7 +1508,6 @@ 8C9BC2652C240D5200875A27 /* ThemeModeEditor.swift in Sources */, 5CB346E92869E8BA001FD2EF /* PushEnvironment.swift in Sources */, 5C55A91F283AD0E400C4E99E /* CallManager.swift in Sources */, - 5CFA59D12864782E00863A68 /* ChatArchiveView.swift in Sources */, 649BCDA22805D6EF00C3A862 /* CIImageView.swift in Sources */, 5CADE79C292131E900072E13 /* ContactPreferencesView.swift in Sources */, CEA6E91C2CBD21B0002B5DB4 /* UserDefault.swift in Sources */, From 4738286f4e2fb8726da2ad2fe80f155c5a6316c5 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Sat, 30 Nov 2024 23:33:38 +0700 Subject: [PATCH 088/167] android, desktop: start/stop chat toggle refactoring (#5261) * android, desktop: start/stop chat toggle refactoring * changes * translations * better * better * do not start chat after export, always show run toggle (#5283) * update heading --------- Co-authored-by: Evgeny Poberezkin --- .../main/java/chat/simplex/app/SimplexApp.kt | 2 + apps/multiplatform/common/build.gradle.kts | 4 +- .../chat/simplex/common/model/SimpleXAPI.kt | 6 +- .../chat/simplex/common/platform/Core.kt | 9 + .../chat/simplex/common/platform/Files.kt | 2 +- .../common/views/database/ChatArchiveView.kt | 108 ------- .../views/database/DatabaseEncryptionView.kt | 8 +- .../common/views/database/DatabaseView.kt | 297 +++++++++--------- .../views/migration/MigrateFromDevice.kt | 2 +- .../common/views/migration/MigrateToDevice.kt | 60 +++- .../common/views/usersettings/SettingsView.kt | 2 +- .../commonMain/resources/MR/ar/strings.xml | 6 - .../commonMain/resources/MR/base/strings.xml | 10 +- .../commonMain/resources/MR/bg/strings.xml | 6 - .../commonMain/resources/MR/cs/strings.xml | 6 - .../commonMain/resources/MR/de/strings.xml | 6 - .../commonMain/resources/MR/el/strings.xml | 3 - .../commonMain/resources/MR/es/strings.xml | 6 - .../commonMain/resources/MR/fa/strings.xml | 6 - .../commonMain/resources/MR/fi/strings.xml | 6 - .../commonMain/resources/MR/fr/strings.xml | 6 - .../commonMain/resources/MR/hi/strings.xml | 4 - .../commonMain/resources/MR/hu/strings.xml | 6 - .../commonMain/resources/MR/in/strings.xml | 4 - .../commonMain/resources/MR/it/strings.xml | 6 - .../commonMain/resources/MR/iw/strings.xml | 6 - .../commonMain/resources/MR/ja/strings.xml | 6 - .../commonMain/resources/MR/ko/strings.xml | 6 - .../commonMain/resources/MR/lt/strings.xml | 6 - .../commonMain/resources/MR/nl/strings.xml | 6 - .../commonMain/resources/MR/pl/strings.xml | 6 - .../resources/MR/pt-rBR/strings.xml | 6 - .../commonMain/resources/MR/pt/strings.xml | 6 - .../commonMain/resources/MR/ro/strings.xml | 6 - .../commonMain/resources/MR/ru/strings.xml | 6 - .../commonMain/resources/MR/th/strings.xml | 6 - .../commonMain/resources/MR/tr/strings.xml | 6 - .../commonMain/resources/MR/uk/strings.xml | 6 - .../commonMain/resources/MR/vi/strings.xml | 5 - .../resources/MR/zh-rCN/strings.xml | 6 - .../resources/MR/zh-rTW/strings.xml | 6 - .../common/platform/AppCommon.desktop.kt | 2 + .../views/database/DatabaseView.desktop.kt | 4 +- 43 files changed, 236 insertions(+), 446 deletions(-) delete mode 100644 apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/ChatArchiveView.kt diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt index 13f9b888b9..f46ed4775f 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt @@ -28,6 +28,7 @@ import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.call.* +import chat.simplex.common.views.database.deleteOldChatArchive import chat.simplex.common.views.helpers.* import chat.simplex.common.views.onboarding.OnboardingStage import com.jakewharton.processphoenix.ProcessPhoenix @@ -72,6 +73,7 @@ class SimplexApp: Application(), LifecycleEventObserver { runMigrations() tmpDir.deleteRecursively() tmpDir.mkdir() + deleteOldChatArchive() // Present screen for continue migration if it wasn't finished yet if (chatModel.migrationState.value != null) { diff --git a/apps/multiplatform/common/build.gradle.kts b/apps/multiplatform/common/build.gradle.kts index b9b307c8f4..ad67b7cf1e 100644 --- a/apps/multiplatform/common/build.gradle.kts +++ b/apps/multiplatform/common/build.gradle.kts @@ -266,7 +266,9 @@ afterEvaluate { if (isBase) { baseFormatting[lineId] = fixedLine.formatting(file.absolutePath) } else if (baseFormatting[lineId] != fixedLine.formatting(file.absolutePath)) { - errors.add("Incorrect formatting in string: $fixedLine \nin ${file.absolutePath}") + errors.add("Incorrect formatting in string: $fixedLine \nin ${file.absolutePath}.\n" + + "If you want to remove non-base translation, search this Regex and replace with empty value in IDE:\n" + + "[ ]*<.*\"${line.substringAfter("\"").substringBefore("\"")}\"[^/]*\\n*.*string>\\n") } finalLines.add(fixedLine) } else if (multiline.isEmpty() && startStringRegex.containsMatchIn(line)) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index def6d81897..7ada27d000 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -211,6 +211,9 @@ class AppPreferences { // Note that this situation can only happen if passphrase for the first database is incorrect because, otherwise, backend will re-create second database automatically val newDatabaseInitialized = mkBoolPreference(SHARED_PREFS_NEW_DATABASE_INITIALIZED, false) + /** after importing new database, this flag will be set and unset only after importing app settings in [initChatController] */ + val shouldImportAppSettings = mkBoolPreference(SHARED_PREFS_SHOULD_IMPORT_APP_SETTINGS, false) + val currentTheme = mkStrPreference(SHARED_PREFS_CURRENT_THEME, DefaultTheme.SYSTEM_THEME_NAME) val systemDarkTheme = mkStrPreference(SHARED_PREFS_SYSTEM_DARK_THEME, DefaultTheme.SIMPLEX.themeName) val currentThemeIds = mkMapPreference(SHARED_PREFS_CURRENT_THEME_IDs, mapOf(), encode = { @@ -425,6 +428,7 @@ class AppPreferences { private const val SHARED_PREFS_INITIALIZATION_VECTOR_SELF_DESTRUCT_PASSPHRASE = "InitializationVectorSelfDestructPassphrase" private const val SHARED_PREFS_ENCRYPTION_STARTED_AT = "EncryptionStartedAt" private const val SHARED_PREFS_NEW_DATABASE_INITIALIZED = "NewDatabaseInitialized" + private const val SHARED_PREFS_SHOULD_IMPORT_APP_SETTINGS = "ShouldImportAppSettings" private const val SHARED_PREFS_CONFIRM_DB_UPGRADES = "ConfirmDBUpgrades" private const val SHARED_PREFS_ONE_HAND_UI = "OneHandUI" private const val SHARED_PREFS_SELF_DESTRUCT = "LocalAuthenticationSelfDestruct" @@ -6909,7 +6913,7 @@ data class AppSettings( uiDarkColorScheme?.let { def.systemDarkTheme.set(it) } uiCurrentThemeIds?.let { def.currentThemeIds.set(it) } uiThemes?.let { def.themeOverrides.set(it.skipDuplicates()) } - oneHandUI?.let { def.oneHandUI.set(if (appPlatform.isAndroid) it else false) } + oneHandUI?.let { def.oneHandUI.set(it) } } companion object { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt index 08ca72c6bd..5262714099 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt @@ -119,6 +119,15 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat val user = chatController.apiGetActiveUser(null) chatModel.currentUser.value = user chatModel.conditions.value = chatController.getServerOperators(null) ?: ServerOperatorConditionsDetail.empty + if (appPrefs.shouldImportAppSettings.get()) { + try { + val appSettings = controller.apiGetAppSettings(AppSettings.current.prepareForExport()) + appSettings.importIntoApp() + appPrefs.shouldImportAppSettings.set(false) + } catch (e: Exception) { + Log.e(TAG, "Error while importing app settings: " + e.stackTraceToString()) + } + } if (user == null) { chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true) chatModel.currentUser.value = null diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt index 9110987190..e9fc8c97f9 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt @@ -26,7 +26,7 @@ expect val agentDatabaseFileName: String /** * This is used only for temporary storing db archive for export. -* Providing [tmpDir] instead crashes the app. Check db export before moving from this path to something else +* Providing [tmpDir] instead crashes the app on Android (only). Check db export before moving from this path to something else * */ expect val databaseExportDir: File diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/ChatArchiveView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/ChatArchiveView.kt deleted file mode 100644 index 96acea5446..0000000000 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/ChatArchiveView.kt +++ /dev/null @@ -1,108 +0,0 @@ -package chat.simplex.common.views.database - -import SectionBottomSpacer -import SectionTextFooter -import SectionView -import androidx.compose.desktop.ui.tooling.preview.Preview -import androidx.compose.foundation.layout.* -import androidx.compose.material.* -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import dev.icerock.moko.resources.compose.painterResource -import dev.icerock.moko.resources.compose.stringResource -import chat.simplex.common.model.ChatModel -import chat.simplex.common.platform.* -import chat.simplex.common.ui.theme.SimpleXTheme -import chat.simplex.common.views.helpers.* -import chat.simplex.common.views.usersettings.* -import chat.simplex.res.MR -import kotlinx.datetime.* -import java.io.File -import java.net.URI -import java.text.SimpleDateFormat -import java.util.* - -@Composable -fun ChatArchiveView(m: ChatModel, title: String, archiveName: String, archiveTime: Instant) { - val archivePath = filesDir.absolutePath + File.separator + archiveName - val saveArchiveLauncher = rememberFileChooserLauncher(false) { to: URI? -> - if (to != null) { - copyFileToFile(File(archivePath), to) {} - } - } - ChatArchiveLayout( - title, - archiveTime, - saveArchive = { withLongRunningApi { saveArchiveLauncher.launch(archivePath.substringAfterLast(File.separator)) }}, - deleteArchiveAlert = { deleteArchiveAlert(m, archivePath) } - ) -} - -@Composable -fun ChatArchiveLayout( - title: String, - archiveTime: Instant, - saveArchive: () -> Unit, - deleteArchiveAlert: () -> Unit -) { - ColumnWithScrollBar { - AppBarTitle(title) - SectionView(stringResource(MR.strings.chat_archive_section)) { - SettingsActionItem( - painterResource(MR.images.ic_ios_share), - stringResource(MR.strings.save_archive), - saveArchive, - textColor = MaterialTheme.colors.primary, - iconColor = MaterialTheme.colors.primary, - ) - SettingsActionItem( - painterResource(MR.images.ic_delete), - stringResource(MR.strings.delete_archive), - deleteArchiveAlert, - textColor = Color.Red, - iconColor = Color.Red, - ) - } - val archiveTs = SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z", Locale.US).format(Date.from(archiveTime.toJavaInstant())) - SectionTextFooter( - String.format(generalGetString(MR.strings.archive_created_on_ts), archiveTs) - ) - SectionBottomSpacer() - } -} - -private fun deleteArchiveAlert(m: ChatModel, archivePath: String) { - AlertManager.shared.showAlertDialog( - title = generalGetString(MR.strings.delete_chat_archive_question), - confirmText = generalGetString(MR.strings.delete_verb), - onConfirm = { - val fileDeleted = File(archivePath).delete() - if (fileDeleted) { - m.controller.appPrefs.chatArchiveName.set(null) - m.controller.appPrefs.chatArchiveTime.set(null) - ModalManager.start.closeModal() - } else { - Log.e(TAG, "deleteArchiveAlert delete() error") - } - }, - destructive = true, - ) -} - -@Preview/*( - uiMode = Configuration.UI_MODE_NIGHT_YES, - showBackground = true, - name = "Dark Mode" -)*/ -@Composable -fun PreviewChatArchiveLayout() { - SimpleXTheme { - ChatArchiveLayout( - "New database archive", - archiveTime = Clock.System.now(), - saveArchive = {}, - deleteArchiveAlert = {} - ) - } -} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.kt index 654d250274..c2e1d67d50 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.kt @@ -48,6 +48,7 @@ fun DatabaseEncryptionView(m: ChatModel, migration: Boolean) { val currentKey = remember { mutableStateOf(if (initialRandomDBPassphrase.value) DatabaseUtils.ksDatabasePassword.get() ?: "" else "") } val newKey = rememberSaveable { mutableStateOf("") } val confirmNewKey = rememberSaveable { mutableStateOf("") } + val chatLastStart = remember { mutableStateOf(appPrefs.chatLastStart.get()) } Box( Modifier.fillMaxSize(), @@ -63,8 +64,9 @@ fun DatabaseEncryptionView(m: ChatModel, migration: Boolean) { progressIndicator, migration, onConfirmEncrypt = { - withLongRunningApi { - encryptDatabase( + // it will try to stop and start the chat in case of: non-migration && successful encryption. In migration the chat will remain stopped + stopChatRunBlockStartChat(migration, chatLastStart, progressIndicator, ) { + val success = encryptDatabase( currentKey = currentKey, newKey = newKey, confirmNewKey = confirmNewKey, @@ -74,6 +76,7 @@ fun DatabaseEncryptionView(m: ChatModel, migration: Boolean) { progressIndicator = progressIndicator, migration = migration ) + success && !migration } } ) @@ -306,7 +309,6 @@ private fun operationEnded(m: ChatModel, progressIndicator: MutableState, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt index 3dd7e673c4..8185122089 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt @@ -26,7 +26,6 @@ import chat.simplex.common.views.helpers.* import chat.simplex.common.views.usersettings.* import chat.simplex.common.platform.* import chat.simplex.res.MR -import kotlinx.coroutines.sync.withLock import kotlinx.datetime.* import java.io.* import java.net.URI @@ -36,18 +35,14 @@ import java.util.* import kotlin.collections.ArrayList @Composable -fun DatabaseView( - m: ChatModel, - showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit) -) { - val currentRemoteHost by remember { chatModel.currentRemoteHost } +fun DatabaseView() { + val m = chatModel val progressIndicator = remember { mutableStateOf(false) } val prefs = m.controller.appPrefs val useKeychain = remember { mutableStateOf(prefs.storeDBPassphrase.get()) } - val chatArchiveName = remember { mutableStateOf(prefs.chatArchiveName.get()) } - val chatArchiveTime = remember { mutableStateOf(prefs.chatArchiveTime.get()) } val chatLastStart = remember { mutableStateOf(prefs.chatLastStart.get()) } val chatArchiveFile = remember { mutableStateOf(null) } + val stopped = remember { m.chatRunning }.value == false val saveArchiveLauncher = rememberFileChooserLauncher(false) { to: URI? -> val file = chatArchiveFile.value if (file != null && to != null) { @@ -59,8 +54,11 @@ fun DatabaseView( val appFilesCountAndSize = remember { mutableStateOf(directoryFileCountAndSize(appFilesDir.absolutePath)) } val importArchiveLauncher = rememberFileChooserLauncher(true) { to: URI? -> if (to != null) { - importArchiveAlert(m, to, appFilesCountAndSize, progressIndicator) { - startChat(m, chatLastStart, m.chatDbChanged) + importArchiveAlert { + stopChatRunBlockStartChat(stopped, chatLastStart, progressIndicator) { + importArchive(to, appFilesCountAndSize, progressIndicator) + true + } } } } @@ -71,27 +69,40 @@ fun DatabaseView( val user = m.currentUser.value val rhId = user?.remoteHostId DatabaseLayout( - currentRemoteHost = currentRemoteHost, progressIndicator.value, - remember { m.chatRunning }.value != false, - m.chatDbChanged.value, + stopped, useKeychain.value, m.chatDbEncrypted.value, m.controller.appPrefs.storeDBPassphrase.state.value, m.controller.appPrefs.initialRandomDBPassphrase, importArchiveLauncher, - chatArchiveName, - chatArchiveTime, - chatLastStart, appFilesCountAndSize, chatItemTTL, user, m.users, startChat = { startChat(m, chatLastStart, m.chatDbChanged, progressIndicator) }, stopChatAlert = { stopChatAlert(m, progressIndicator) }, - exportArchive = { exportArchive(m, progressIndicator, chatArchiveName, chatArchiveTime, chatArchiveFile, saveArchiveLauncher) }, - deleteChatAlert = { deleteChatAlert(m, progressIndicator) }, - deleteAppFilesAndMedia = { deleteFilesAndMediaAlert(appFilesCountAndSize) }, + exportArchive = { + stopChatRunBlockStartChat(stopped, chatLastStart, progressIndicator) { + exportArchive(m, progressIndicator, chatArchiveFile, saveArchiveLauncher) + } + }, + deleteChatAlert = { + deleteChatAlert { + stopChatRunBlockStartChat(stopped, chatLastStart, progressIndicator) { + deleteChat(m, progressIndicator) + true + } + } + }, + deleteAppFilesAndMedia = { + deleteFilesAndMediaAlert { + stopChatRunBlockStartChat(stopped, chatLastStart, progressIndicator) { + deleteFiles(appFilesCountAndSize) + true + } + } + }, onChatItemTTLSelected = { val oldValue = chatItemTTL.value chatItemTTL.value = it @@ -101,7 +112,6 @@ fun DatabaseView( setCiTTL(m, rhId, chatItemTTL, progressIndicator, appFilesCountAndSize) } }, - showSettingsModal, disconnectAllHosts = { val connected = chatModel.remoteHosts.filter { it.sessionState is RemoteHostSessionState.Connected } connected.forEachIndexed { index, h -> @@ -128,18 +138,13 @@ fun DatabaseView( @Composable fun DatabaseLayout( - currentRemoteHost: RemoteHostInfo?, progressIndicator: Boolean, - runChat: Boolean, - chatDbChanged: Boolean, + stopped: Boolean, useKeyChain: Boolean, chatDbEncrypted: Boolean?, passphraseSaved: Boolean, initialRandomDBPassphrase: SharedPreference, importArchiveLauncher: FileChooserLauncher, - chatArchiveName: MutableState, - chatArchiveTime: MutableState, - chatLastStart: MutableState, appFilesCountAndSize: MutableState>, chatItemTTL: MutableState, currentUser: User?, @@ -150,11 +155,9 @@ fun DatabaseLayout( deleteChatAlert: () -> Unit, deleteAppFilesAndMedia: () -> Unit, onChatItemTTLSelected: (ChatItemTTL) -> Unit, - showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), disconnectAllHosts: () -> Unit, ) { - val stopped = !runChat - val operationsDisabled = (!stopped || progressIndicator) && !chatModel.desktopNoUserNoRemote + val operationsDisabled = progressIndicator && !chatModel.desktopNoUserNoRemote ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.your_chat_database)) @@ -178,13 +181,16 @@ fun DatabaseLayout( } val toggleEnabled = remember { chatModel.remoteHosts }.none { it.sessionState is RemoteHostSessionState.Connected } if (chatModel.localUserCreated.value == true) { + // still show the toggle in case database was stopped when the user opened this screen because it can be in the following situations: + // - database was stopped after migration and the app relaunched + // - something wrong happened with database operations and the database couldn't be launched when it should SectionView(stringResource(MR.strings.run_chat_section)) { if (!toggleEnabled) { SectionItemView(disconnectAllHosts) { Text(generalGetString(MR.strings.disconnect_remote_hosts), Modifier.fillMaxWidth(), color = WarningOrange) } } - RunChatSetting(runChat, stopped, toggleEnabled && !progressIndicator, startChat, stopChatAlert) + RunChatSetting(stopped, toggleEnabled && !progressIndicator, startChat, stopChatAlert) } SectionTextFooter( if (stopped) { @@ -207,7 +213,7 @@ fun DatabaseLayout( if (unencrypted) painterResource(MR.images.ic_lock_open_right) else if (useKeyChain) painterResource(MR.images.ic_vpn_key_filled) else painterResource(MR.images.ic_lock), stringResource(MR.strings.database_passphrase), - click = showSettingsModal() { DatabaseEncryptionView(it, false) }, + click = { ModalManager.start.showModal { DatabaseEncryptionView(chatModel, false) } }, iconColor = if (unencrypted || (appPlatform.isDesktop && passphraseSaved)) WarningOrange else MaterialTheme.colors.secondary, disabled = operationsDisabled ) @@ -225,6 +231,9 @@ fun DatabaseLayout( click = { if (initialRandomDBPassphrase.get()) { exportProhibitedAlert() + ModalManager.start.showModal { + DatabaseEncryptionView(chatModel, false) + } } else { exportArchive() } @@ -241,18 +250,6 @@ fun DatabaseLayout( iconColor = Color.Red, disabled = operationsDisabled ) - val chatArchiveNameVal = chatArchiveName.value - val chatArchiveTimeVal = chatArchiveTime.value - val chatLastStartVal = chatLastStart.value - if (chatArchiveNameVal != null && chatArchiveTimeVal != null && chatLastStartVal != null) { - val title = chatArchiveTitle(chatArchiveTimeVal, chatLastStartVal) - SettingsActionItem( - painterResource(MR.images.ic_inventory_2), - title, - click = showSettingsModal { ChatArchiveView(it, title, chatArchiveNameVal, chatArchiveTimeVal) }, - disabled = operationsDisabled - ) - } SettingsActionItem( painterResource(MR.images.ic_delete_forever), stringResource(MR.strings.delete_database), @@ -333,7 +330,6 @@ private fun TtlOptions(current: State, enabled: State, onS @Composable fun RunChatSetting( - runChat: Boolean, stopped: Boolean, enabled: Boolean, startChat: () -> Unit, @@ -346,7 +342,7 @@ fun RunChatSetting( iconColor = if (stopped) Color.Red else MaterialTheme.colors.primary, ) { DefaultSwitch( - checked = runChat, + checked = !stopped, onCheckedChange = { runChatSwitch -> if (runChatSwitch) { startChat() @@ -359,12 +355,12 @@ fun RunChatSetting( } } -@Composable -fun chatArchiveTitle(chatArchiveTime: Instant, chatLastStart: Instant): String { - return stringResource(if (chatArchiveTime < chatLastStart) MR.strings.old_database_archive else MR.strings.new_database_archive) -} - -fun startChat(m: ChatModel, chatLastStart: MutableState, chatDbChanged: MutableState, progressIndicator: MutableState? = null) { +fun startChat( + m: ChatModel, + chatLastStart: MutableState, + chatDbChanged: MutableState, + progressIndicator: MutableState? = null +) { withLongRunningApi { try { progressIndicator?.value = true @@ -469,6 +465,40 @@ suspend fun stopChatAsync(m: ChatModel) { controller.appPrefs.chatStopped.set(true) } +fun stopChatRunBlockStartChat( + stopped: Boolean, + chatLastStart: MutableState, + progressIndicator: MutableState, + block: suspend () -> Boolean +) { + // if the chat was running, the sequence is: stop chat, run block, start chat. + // Otherwise, just run block and do nothing - the toggle will be visible anyway and the user can start the chat or not + if (stopped) { + withLongRunningApi { + try { + block() + } catch (e: Throwable) { + Log.e(TAG, e.stackTraceToString()) + } + } + } else { + authStopChat(chatModel, progressIndicator) { + withLongRunningApi { + // if it throws, let's start chat again anyway + val canStart = try { + block() + } catch (e: Throwable) { + Log.e(TAG, e.stackTraceToString()) + true + } + if (canStart) { + startChat(chatModel, chatLastStart, chatModel.chatDbChanged, progressIndicator) + } + } + } + } +} + suspend fun deleteChatAsync(m: ChatModel) { m.controller.apiDeleteStorage() DatabaseUtils.ksDatabasePassword.remove() @@ -511,47 +541,42 @@ fun deleteChatDatabaseFilesAndState() { ntfManager.cancelAllNotifications() } -private fun exportArchive( +private suspend fun exportArchive( m: ChatModel, progressIndicator: MutableState, - chatArchiveName: MutableState, - chatArchiveTime: MutableState, chatArchiveFile: MutableState, saveArchiveLauncher: FileChooserLauncher -) { +): Boolean { progressIndicator.value = true - withLongRunningApi { - try { - val (archiveFile, archiveErrors) = exportChatArchive(m, null, chatArchiveName, chatArchiveTime, chatArchiveFile) - chatArchiveFile.value = archiveFile - if (archiveErrors.isEmpty()) { - saveArchiveLauncher.launch(archiveFile.substringAfterLast(File.separator)) - } else { - showArchiveExportedWithErrorsAlert(generalGetString(MR.strings.chat_database_exported_save), archiveErrors) { - withLongRunningApi { - saveArchiveLauncher.launch(archiveFile.substringAfterLast(File.separator)) - } + try { + val (archiveFile, archiveErrors) = exportChatArchive(m, null, chatArchiveFile) + chatArchiveFile.value = archiveFile + if (archiveErrors.isEmpty()) { + saveArchiveLauncher.launch(archiveFile.substringAfterLast(File.separator)) + } else { + showArchiveExportedWithErrorsAlert(generalGetString(MR.strings.chat_database_exported_save), archiveErrors) { + withLongRunningApi { + saveArchiveLauncher.launch(archiveFile.substringAfterLast(File.separator)) } } - progressIndicator.value = false - } catch (e: Error) { - AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_exporting_chat_database), e.toString()) - progressIndicator.value = false } + progressIndicator.value = false + } catch (e: Throwable) { + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_exporting_chat_database), e.toString()) + progressIndicator.value = false } + return false } suspend fun exportChatArchive( m: ChatModel, storagePath: File?, - chatArchiveName: MutableState, - chatArchiveTime: MutableState, chatArchiveFile: MutableState ): Pair> { val archiveTime = Clock.System.now() val ts = SimpleDateFormat("yyyy-MM-dd'T'HHmmss", Locale.US).format(Date.from(archiveTime.toJavaInstant())) val archiveName = "simplex-chat.$ts.zip" - val archivePath = "${(storagePath ?: filesDir).absolutePath}${File.separator}$archiveName" + val archivePath = "${(storagePath ?: databaseExportDir).absolutePath}${File.separator}$archiveName" val config = ArchiveConfig(archivePath, parentTempDirectory = databaseExportDir.toString()) // Settings should be saved before changing a passphrase, otherwise the database needs to be migrated first if (!m.chatDbChanged.value) { @@ -560,42 +585,37 @@ suspend fun exportChatArchive( wallpapersDir.mkdirs() val archiveErrors = m.controller.apiExportArchive(config) if (storagePath == null) { - deleteOldArchive(m) + deleteOldChatArchive() m.controller.appPrefs.chatArchiveName.set(archiveName) m.controller.appPrefs.chatArchiveTime.set(archiveTime) } - chatArchiveName.value = archiveName - chatArchiveTime.value = archiveTime chatArchiveFile.value = archivePath return archivePath to archiveErrors } -private fun deleteOldArchive(m: ChatModel) { - val chatArchiveName = m.controller.appPrefs.chatArchiveName.get() +// Deprecated. Remove in the end of 2025. All unused archives should be deleted for the most users til then. +/** Remove [AppPreferences.chatArchiveName] and [AppPreferences.chatArchiveTime] as well */ +fun deleteOldChatArchive() { + val chatArchiveName = chatModel.controller.appPrefs.chatArchiveName.get() if (chatArchiveName != null) { - val file = File("${filesDir.absolutePath}${File.separator}$chatArchiveName") - val fileDeleted = file.delete() - if (fileDeleted) { - m.controller.appPrefs.chatArchiveName.set(null) - m.controller.appPrefs.chatArchiveTime.set(null) + val file1 = File("${filesDir.absolutePath}${File.separator}$chatArchiveName") + val file2 = File("${databaseExportDir.absolutePath}${File.separator}$chatArchiveName") + val fileDeleted = file1.delete() || file2.delete() + if (fileDeleted || (!file1.exists() && !file2.exists())) { + chatModel.controller.appPrefs.chatArchiveName.set(null) + chatModel.controller.appPrefs.chatArchiveTime.set(null) } else { Log.e(TAG, "deleteOldArchive file.delete() error") } } } -private fun importArchiveAlert( - m: ChatModel, - importedArchiveURI: URI, - appFilesCountAndSize: MutableState>, - progressIndicator: MutableState, - startChat: () -> Unit, -) { +private fun importArchiveAlert(onConfirm: () -> Unit, ) { AlertManager.shared.showAlertDialog( title = generalGetString(MR.strings.import_database_question), text = generalGetString(MR.strings.your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one), confirmText = generalGetString(MR.strings.import_database_confirmation), - onConfirm = { importArchive(m, importedArchiveURI, appFilesCountAndSize, progressIndicator, startChat) }, + onConfirm = onConfirm, destructive = true, ) } @@ -622,52 +642,51 @@ private fun archiveErrorsText(errs: List): String = "\n" + errs.ma } }.joinToString(separator = "\n") -private fun importArchive( - m: ChatModel, +suspend fun importArchive( importedArchiveURI: URI, appFilesCountAndSize: MutableState>, progressIndicator: MutableState, - startChat: () -> Unit, -) { +): Boolean { + val m = chatModel progressIndicator.value = true val archivePath = saveArchiveFromURI(importedArchiveURI) if (archivePath != null) { - withLongRunningApi { + try { + m.controller.apiDeleteStorage() + wallpapersDir.mkdirs() try { - m.controller.apiDeleteStorage() - wallpapersDir.mkdirs() - try { - val config = ArchiveConfig(archivePath, parentTempDirectory = databaseExportDir.toString()) - val archiveErrors = m.controller.apiImportArchive(config) - DatabaseUtils.ksDatabasePassword.remove() - appFilesCountAndSize.value = directoryFileCountAndSize(appFilesDir.absolutePath) - if (archiveErrors.isEmpty()) { - operationEnded(m, progressIndicator) { - AlertManager.shared.showAlertMsg(generalGetString(MR.strings.chat_database_imported), text = generalGetString(MR.strings.restart_the_app_to_use_imported_chat_database)) - } - if (chatModel.localUserCreated.value == false) { - chatModel.chatRunning.value = false - startChat() - } - } else { - operationEnded(m, progressIndicator) { - showArchiveImportedWithErrorsAlert(archiveErrors) - } - } - } catch (e: Error) { + val config = ArchiveConfig(archivePath, parentTempDirectory = databaseExportDir.toString()) + val archiveErrors = m.controller.apiImportArchive(config) + appPrefs.shouldImportAppSettings.set(true) + DatabaseUtils.ksDatabasePassword.remove() + appFilesCountAndSize.value = directoryFileCountAndSize(appFilesDir.absolutePath) + if (archiveErrors.isEmpty()) { operationEnded(m, progressIndicator) { - AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_importing_database), e.toString()) + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.chat_database_imported), text = generalGetString(MR.strings.restart_the_app_to_use_imported_chat_database)) + } + if (chatModel.localUserCreated.value == false) { + chatModel.chatRunning.value = false + } + } else { + operationEnded(m, progressIndicator) { + showArchiveImportedWithErrorsAlert(archiveErrors) } } + return true } catch (e: Error) { operationEnded(m, progressIndicator) { - AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_deleting_database), e.toString()) + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_importing_database), e.toString()) } - } finally { - File(archivePath).delete() } + } catch (e: Error) { + operationEnded(m, progressIndicator) { + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_deleting_database), e.toString()) + } + } finally { + File(archivePath).delete() } } + return false } private fun saveArchiveFromURI(importedArchiveURI: URI): String? { @@ -689,28 +708,26 @@ private fun saveArchiveFromURI(importedArchiveURI: URI): String? { } } -private fun deleteChatAlert(m: ChatModel, progressIndicator: MutableState) { +private fun deleteChatAlert(onConfirm: () -> Unit) { AlertManager.shared.showAlertDialog( title = generalGetString(MR.strings.delete_chat_profile_question), text = generalGetString(MR.strings.delete_chat_profile_action_cannot_be_undone_warning), confirmText = generalGetString(MR.strings.delete_verb), - onConfirm = { deleteChat(m, progressIndicator) }, + onConfirm = onConfirm, destructive = true, ) } -private fun deleteChat(m: ChatModel, progressIndicator: MutableState) { +private suspend fun deleteChat(m: ChatModel, progressIndicator: MutableState) { progressIndicator.value = true - withBGApi { - try { - deleteChatAsync(m) - operationEnded(m, progressIndicator) { - AlertManager.shared.showAlertMsg(generalGetString(MR.strings.chat_database_deleted), generalGetString(MR.strings.restart_the_app_to_create_a_new_chat_profile)) - } - } catch (e: Error) { - operationEnded(m, progressIndicator) { - AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_deleting_database), e.toString()) - } + try { + deleteChatAsync(m) + operationEnded(m, progressIndicator) { + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.chat_database_deleted), generalGetString(MR.strings.restart_the_app_to_create_a_new_chat_profile)) + } + } catch (e: Error) { + operationEnded(m, progressIndicator) { + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_deleting_database), e.toString()) } } } @@ -759,12 +776,12 @@ private fun afterSetCiTTL( } } -private fun deleteFilesAndMediaAlert(appFilesCountAndSize: MutableState>) { +private fun deleteFilesAndMediaAlert(onConfirm: () -> Unit) { AlertManager.shared.showAlertDialog( title = generalGetString(MR.strings.delete_files_and_media_question), text = generalGetString(MR.strings.delete_files_and_media_desc), confirmText = generalGetString(MR.strings.delete_verb), - onConfirm = { deleteFiles(appFilesCountAndSize) }, + onConfirm = onConfirm, destructive = true ) } @@ -789,18 +806,13 @@ private fun operationEnded(m: ChatModel, progressIndicator: MutableState.exportArchive() { withLongRunningApi { try { getMigrationTempFilesDirectory().mkdir() - val (archivePath, archiveErrors) = exportChatArchive(chatModel, getMigrationTempFilesDirectory(), mutableStateOf(""), mutableStateOf(Instant.DISTANT_PAST), mutableStateOf("")) + val (archivePath, archiveErrors) = exportChatArchive(chatModel, getMigrationTempFilesDirectory(), mutableStateOf("")) if (archiveErrors.isEmpty()) { uploadArchive(archivePath) } else { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateToDevice.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateToDevice.kt index 28ec77de70..f4f537aab7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateToDevice.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateToDevice.kt @@ -35,6 +35,7 @@ import kotlinx.datetime.Clock import kotlinx.datetime.toJavaInstant import kotlinx.serialization.* import java.io.File +import java.net.URI import java.text.SimpleDateFormat import java.util.* import kotlin.math.max @@ -180,7 +181,7 @@ private fun ModalData.SectionByState( ) { when (val s = migrationState.value) { null -> {} - is MigrationToState.PasteOrScanLink -> migrationState.PasteOrScanLinkView() + is MigrationToState.PasteOrScanLink -> migrationState.PasteOrScanLinkView(close) is MigrationToState.Onion -> OnionView(s.link, s.legacySocksProxy, s.networkProxy, s.hostMode, s.requiredHostMode, migrationState) is MigrationToState.DatabaseInit -> migrationState.DatabaseInitView(s.link, tempDatabaseFile, s.netCfg, s.networkProxy) is MigrationToState.LinkDownloading -> migrationState.LinkDownloadingView(s.link, s.ctrl, s.user, s.archivePath, tempDatabaseFile, chatReceiver, s.netCfg, s.networkProxy) @@ -195,18 +196,30 @@ private fun ModalData.SectionByState( } @Composable -private fun MutableState.PasteOrScanLinkView() { - if (appPlatform.isAndroid) { - SectionView(stringResource(MR.strings.scan_QR_code).replace('\n', ' ').uppercase()) { - QRCodeScanner(showQRCodeScanner = remember { mutableStateOf(true) }) { text -> - withBGApi { checkUserLink(text) } +private fun MutableState.PasteOrScanLinkView(close: () -> Unit) { + Box { + val progressIndicator = remember { mutableStateOf(false) } + Column { + if (appPlatform.isAndroid) { + SectionView(stringResource(MR.strings.scan_QR_code).replace('\n', ' ').uppercase()) { + QRCodeScanner(showQRCodeScanner = remember { mutableStateOf(true) }) { text -> + withBGApi { checkUserLink(text) } + } + } + SectionSpacer() + } + + SectionView(stringResource(if (appPlatform.isAndroid) MR.strings.or_paste_archive_link else MR.strings.paste_archive_link).uppercase()) { + PasteLinkView() + } + SectionSpacer() + + SectionView(stringResource(MR.strings.chat_archive).uppercase()) { + ArchiveImportView(progressIndicator, close) } } - SectionSpacer() - } - - SectionView(stringResource(if (appPlatform.isAndroid) MR.strings.or_paste_archive_link else MR.strings.paste_archive_link).uppercase()) { - PasteLinkView() + if (progressIndicator.value) + ProgressView() } } @@ -221,6 +234,31 @@ private fun MutableState.PasteLinkView() { } } +@Composable +private fun ArchiveImportView(progressIndicator: MutableState, close: () -> Unit) { + val importArchiveLauncher = rememberFileChooserLauncher(true) { to: URI? -> + if (to != null) { + withLongRunningApi { + val success = importArchive(to, mutableStateOf(0 to 0), progressIndicator) + if (success) { + startChat( + chatModel, + mutableStateOf(Clock.System.now()), + chatModel.chatDbChanged, + progressIndicator + ) + hideView(close) + } + } + } + } + SectionItemView({ + withLongRunningApi { importArchiveLauncher.launch("application/zip") } + }) { + Text(stringResource(MR.strings.import_database)) + } +} + @Composable private fun ModalData.OnionView(link: String, legacyLinkSocksProxy: String?, linkNetworkProxy: NetworkProxy?, hostMode: HostMode, requiredHostMode: Boolean, state: MutableState) { val onionHosts = remember { stateGetOrPut("onionHosts") { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt index f3d22e0cdf..ac6431f6fc 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt @@ -109,7 +109,7 @@ fun SettingsLayout( SectionDividerSpaced() SectionView(stringResource(MR.strings.settings_section_title_chat_database)) { - DatabaseItem(encrypted, passphraseSaved, showSettingsModal { DatabaseView(it, showSettingsModal) }, stopped) + DatabaseItem(encrypted, passphraseSaved, showSettingsModal { DatabaseView() }, stopped) SettingsActionItem(painterResource(MR.images.ic_ios_share), stringResource(MR.strings.migrate_from_device_to_another_device), { withAuth(generalGetString(MR.strings.auth_open_migration_to_another_device), generalGetString(MR.strings.auth_log_in_using_credential)) { ModalManager.fullscreen.showCustomModal { close -> MigrateFromDeviceView(close) } } }, disabled = stopped) } diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml index d9d86634b1..b2cb0acc4b 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml @@ -242,7 +242,6 @@ تغيير وضع التدمير الذاتي تغيير رمز المرور التدمير الذاتي تأكيد ترقيات قاعدة البيانات - أرشيف الدردشة الاتصال (دعوة مقدمة) مسح خطأ في إنشاء رابط المجموعة @@ -252,8 +251,6 @@ جار الاتصال… أرسلت طلب الاتصال! حُذفت قاعدة بيانات الدردشة - أرشيف الدردشة - نشأ في %1$s جارِ تغيير العنوان… جار الاتصال (قُبِل) فُحصت جهة الاتصال @@ -349,7 +346,6 @@ تختلف عبارة مرور قاعدة البيانات عن تلك المحفوظة في Keystore. خطأ في قاعدة البيانات ترقية قاعدة البيانات - حذف أرشيف الدردشة؟ حُددت %d جهة اتصال حذف المجموعة حذف المجموعة؟ @@ -372,7 +368,6 @@ أيام حذف العنوان سيتم تحديث عبارة مرور تعمية قاعدة البيانات. - حذف الأرشيف حذف الرابط؟ الرجوع إلى إصدار سابق من قاعدة البيانات قاعدة البيانات مُعمّاة باستخدام عبارة مرور عشوائية. يُرجى تغييره قبل التصدير. @@ -844,7 +839,6 @@ دليل المستخدم.]]> افتح ملفات تعريف الدردشة اسحب الوصول - حفظ الأرشيف كشف سيتم إيقاف استلام الملف. رفض diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 5f095fbf7c..6d0200256c 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -1263,6 +1263,7 @@ Your chat database RUN CHAT + Remote mobiles Chat is running Chat is stopped CHAT DATABASE @@ -1404,14 +1405,6 @@ Start chat? Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat. - - Chat archive - CHAT ARCHIVE - Save archive - Delete archive - Created on %1$s - Delete chat archive? - invitation to group %1$s Join group? @@ -2292,6 +2285,7 @@ Or paste archive link Paste archive link Invalid link + Or import archive file Migrating Preparing download Downloading link details diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/bg/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/bg/strings.xml index 54eb0e9034..042089e226 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/bg/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/bg/strings.xml @@ -228,7 +228,6 @@ Базата данни на чат е импортирана Потвърди новата парола… Потвърди актуализаациите на базата данни - Архив на чата свързан промяна на адреса… промяна на адреса за %s… @@ -317,7 +316,6 @@ Промени режима на самоунищожение Промени кода за достъп за самоунищожение ЧАТОВЕ - АРХИВ НА ЧАТА промяна на адреса… В момента максималният поддържан размер на файла е %1$s. ID в базата данни @@ -341,9 +339,6 @@ Понижаване на версията на базата данни Актуализация на базата данни версията на базата данни е по-нова от приложението, но няма миграция надолу за: %s - Създаден на %1$s - Изтрий архив - Изтриване на архива на чата\? групата изтрита Контактът е проверен създател @@ -996,7 +991,6 @@ Режим на заключване Моля, докладвайте го на разработчиците. Защити екрана на приложението - Запази архив ЧЛЕН Премахване PING бройка diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml index 59c531f3d3..64a87736c4 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml @@ -303,8 +303,6 @@ Obnovte zálohu databáze Po obnovení zálohy databáze zadejte předchozí frázi. Tuto akci nelze vrátit zpět. Chat je zastaven - Chat se archivuje - Smazat archiv chatu? Připojit se ke skupině\? Připojte se na Opustit @@ -352,7 +350,6 @@ Databáze chatu importována Nová přístupová fráze… Uložte přístupovou frázi a otevřete chat - ARCHIV CHATU Nebyl vybrán žádný kontakt Snažíte se pozvat kontakt, se kterým jste sdíleli inkognito profil, do skupiny, ve které používáte svůj hlavní profil Skupina @@ -776,10 +773,7 @@ Chyba při obnovování databáze Přístupová fráze nebyla v klíčence nalezena, zadejte jej prosím ručně. K této situaci mohlo dojít, pokud jste obnovili data aplikace pomocí zálohovacího nástroje. Pokud tomu tak není, obraťte se na vývojáře. Chat můžete spustit v Nastavení / Databáze nebo restartováním aplikace. - Uložit archiv - Smazat archiv pozvánka do skupiny %1$s - Vytvořeno dne %1$s Jste zváni do skupiny. Připojte se k členům skupiny. Připojit se inkognito Připojit ke skupině diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml index 912f3fb84e..75f6ac2c29 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml @@ -672,12 +672,6 @@ Der Chat wurde beendet Sie können den Chat über die App-Einstellungen/Datenbank oder durch Neustart der App starten. - Datenbank-Archiv - CHAT-ARCHIV - Archiv speichern - Archiv löschen - Erstellt am %1$s - Chat-Archiv löschen\? Einladung zur Gruppe %1$s Der Gruppe beitreten? diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/el/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/el/strings.xml index 4b1d1b8838..179c7fec52 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/el/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/el/strings.xml @@ -100,7 +100,6 @@ Δεν είναι δυνατή η προετοιμασία της βάσης δεδομένων Ένα νέο τυχαίο προφίλ θα μοιραστεί. Δεν είναι δυνατή η πρόσκληση επαφών! - Αρχείο συνομιλίας Αλλαγή διεύθυνσης λήψης Πιστοποίηση μη διαθέσιμη Αλλαγή @@ -112,7 +111,6 @@ %1$d αποτυχία κρυπτογράφησης μηνύματος αλλαγή διεύθυνσης για %s… Αλλαγή ρόλου ομάδας; - ΑΡΧΕΙΟ ΣΥΝΟΜΙΛΙΑΣ Δεν είναι δυνατή η πρόσκληση επαφής! Αυτόματη αποδοχή αιτήματος επαφής Κλήση… @@ -189,7 +187,6 @@ συνδέεται… Δημιουργία σύνδεσμο ομάδας Σύνδεση σε επιφάνεια εργασίας - Δημιουργήθηκε στις %1$s Συνδεδεμένο στο κινητό Σύνδεση μέσω σύνδεσμο Επαφές diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml index 5ce86cd374..1cde9ed7c7 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml @@ -191,7 +191,6 @@ conectando… Descentralizada La base de datos será cifrada. - ¿Eliminar archivo del chat\? Crear enlace de grupo Eliminar enlace ¿Eliminar perfil? @@ -215,8 +214,6 @@ %dd %d días ¿Eliminar archivos y multimedia\? - Creado: %1$s - Eliminar archivo conectado directa El contacto permite @@ -289,8 +286,6 @@ Llamada en curso ¿Cambiar contraseña de la base de datos\? No se puede acceder a Keystore para guardar la base de datos de contraseñas - Archivo del chat - ARCHIVOS DE CHAT Cancelar Cancelar mensaje en directo Confirmar @@ -662,7 +657,6 @@ Llamada pendiente Privacidad y Seguridad Guarda la contraseña de forma segura, NO podrás acceder al chat si la pierdes. - Guardar archivo Introduce la contraseña anterior después de restaurar la copia de seguridad de la base de datos. Esta acción no se puede deshacer. te ha expulsado Recibiendo vía diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml index 2fe4ec452e..dc4552a33e 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml @@ -956,9 +956,6 @@ حذف خودکار پیام فعال شود؟ برگرداندن ارتقا و گشودن گپ - آرشیو گپ - ذخیره آرشیو - حذف آرشیو دعوت به گروه %1$s به گروه می‌پیوندید؟ ترک @@ -1092,9 +1089,6 @@ نسخه پایگاه داده از برنامه جدیدتر است، اما بدون جابه‌جایی تنزلی برای: %s جابه‌جایی متفاوت در برنامه/پایگاه داده: %s / %s جابه‌جایی‌ها: %s - آرشیو گپ - ایجاد شده در %1$s - آرشیو گپ حذف شود؟ شما به گروه دعوت شده‌اید. برای متصل شدن به اعضای گروه، به گروه بپیوندید. پیوستن به صورت ناشناس این گروه دیگر وجود ندارد. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/fi/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/fi/strings.xml index c3ea7d89f7..682854f9dd 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/fi/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/fi/strings.xml @@ -108,7 +108,6 @@ Tietoja SimpleX:stä Hajautettu Ääni pois päältä - ARKISTO Vaihda rooli Poista kaikilta Luodaan tyhjä chat-profiili annetulla nimellä, ja sovellus avautuu normaalisti. @@ -168,9 +167,6 @@ Tumma Tunnistautuminen epäonnistui Lisää esiasetettuja palvelimia - Arkisto - Poista keskusteluarkisto\? - Luotu %1$s poistettu ryhmä yhdistää yhdistäminen (hyväksytty) @@ -326,7 +322,6 @@ Tietokannan alentaminen Tietokannan päivitys Tietokanta salataan. - Poista arkisto yhdistäminen (esittelykutsu) Poistettu klo Muuta @@ -969,7 +964,6 @@ Turvallinen jono Tunnuslausetta ei löydy Keystoresta, kirjoita se manuaalisesti. Tämä on saattanut tapahtua, jos olet palauttanut sovelluksen tiedot varmuuskopiointityökalulla. Jos näin ei ole, ota yhteyttä kehittäjiin. Anna edellinen salasana tietokannan varmuuskopion palauttamisen jälkeen. Tätä toimintoa ei voi kumota. - Tallenna arkisto Tuonnin aikana tapahtui joitakin ei-vakavia virheitä – saatat nähdä Chat-konsolissa lisätietoja. Tietue päivitetty klo Moderoitu klo diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml index 8c88c1ea67..3bef77138e 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml @@ -475,7 +475,6 @@ La phrase secrète n\'a pas été trouvée dans le Keystore, veuillez la saisir manuellement. Cela a pu se produire si vous avez restauré les données de l\'app à l\'aide d\'un outil de sauvegarde. Si ce n\'est pas le cas, veuillez contacter les développeurs. Veuillez entrer le mot de passe précédent après avoir restauré la sauvegarde de la base de données. Cette action ne peut pas être annulée. Erreur de restauration de la base de données - Créé le %1$s appel vidéo (chiffrement de bout en bout) appel audio (sans chiffrement) appel audio (chiffrement de bout en bout) @@ -653,11 +652,6 @@ Restaurer Le chat est arrêté Vous pouvez lancer le chat via les Paramètres / la Base de données de l\'app ou en la redémarrant. - Archives du chat - ARCHIVE DU CHAT - Enregistrer l\'archive - Supprimer l\'archive - Supprimer l\'archive du chat \? Invitation au groupe %1$s Rejoindre le groupe \? Vous êtes invité·e dans un groupe. Rejoignez le pour vous connecter avec ses membres. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/hi/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/hi/strings.xml index 9c283a98e9..9e0d476dc1 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/hi/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/hi/strings.xml @@ -117,7 +117,6 @@ नेटवर्क की स्थिति नया संपर्क अनुरोध सभी फाइलों को मिटा दें - संग्रह हटा देना नया डेटाबेस संग्रह नए सदस्य की भूमिका अधिसूचना सेवा @@ -129,7 +128,6 @@ अधिसूचना पूर्वावलोकन सूचनाएं सभी के लिए हटाएं - लिखचीत संग्रह हटा दे\? चैट प्रोफ़ाइल हटाएं\? चैट प्रोफ़ाइल हटाएं\? के लिए चैट प्रोफ़ाइल हटाएं @@ -218,9 +216,7 @@ कॉल समाप्त कॉल चल रहा है छवियों को स्वत: स्वीकार करें - चैट संग्रह चैट रोक दी गई है - चैट संग्रह आप इस समूह से संदेश प्राप्त करना बंद कर देंगे। चैट इतिहास संरक्षित किया जाएगा। %s की भूमिका को %s में बदला पूर्ण diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml index 6fe624d621..f0355d51ac 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml @@ -262,7 +262,6 @@ Törlés az összes tagnál Hivatkozás létrehozása Csevegési beállítások - Csevegési archívum Profil törlése Jelenlegi jelkód kapcsolódás @@ -277,7 +276,6 @@ Hamarosan! cím megváltoztatása nála: %s … Csevegési adatbázis importálva - CSEVEGÉSI ARCHÍVUM Üzenetek törlése Kiürítés Bezárás gomb @@ -329,7 +327,6 @@ Fájlok törlése az összes csevegési profilból Sorbaállítás törlése Ismerős törlése - Létrehozva ekkor: %1$s cím megváltoztatása… Társítva a hordozható eszközhöz Jelenlegi jelmondat… @@ -413,7 +410,6 @@ Kézbesítés jelentések letiltása a csoportok számára? nap %d nap - Csevegési archívum törlése? Duplikált megjelenített név! Letiltás (felülírások megtartásával) Adatbázis fejlesztése @@ -451,7 +447,6 @@ Eszközök Látható a helyi hálózaton Ne engedélyezze - Archívum törlése Az eltűnő üzenetek küldése le van tiltva ebben a csevegésben. alapértelmezett (%s) duplikált üzenet @@ -998,7 +993,6 @@ %s: %s A SimpleX nem tud a háttérben futni. Csak akkor fog értesítéseket kapni, amikor az alkalmazás meg van nyitva. Túl sok kép! - Archívum mentése %s, %s és %d tag Csevegési szolgáltatás megállítása SimpleX-hivatkozások diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml index 29c6430839..dec20e2a36 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml @@ -450,7 +450,6 @@ Kode sandi aplikasi Kode sandi %s detik - Simpan arsip Ketuk untuk gabung diblokir %s Buka @@ -548,7 +547,6 @@ pesan buka Sistem - Arsip obrolan setel alamat kontak baru Tema gelap Tema @@ -862,7 +860,6 @@ Pesan suara tidak diizinkan Hapus alamat? Keluar dari grup? - ARSIP OBROLAN Rangkaian ini bukan tautan koneksi! Tempel tautan yang Anda terima Bagikan dengan kontak @@ -890,7 +887,6 @@ gandakan pesan hash pesan buruk Mulai obrolan? - Hapus arsip Jelajah dan gabung ke grup Perutean pesan pribadi 🚀 Lindungi alamat IP Anda dari relai pesan yang dipilih oleh kontak Anda.\nAktifkan di pengaturan *Jaringan & server*. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml index f5334d4e61..e699714a6a 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml @@ -277,7 +277,6 @@ Impossibile accedere al Keystore per salvare la password del database Impossibile invitare i contatti! Cambia ruolo - ARCHIVIO CHAT cambio indirizzo… Chat fermata in connessione (presentato) @@ -398,13 +397,9 @@ Funzionalità sperimentali Esporta database AIUTO - Archivio chat Chat fermata - Creato il %1$s Errore del database La password del database è diversa da quella salvata nel Keystore. - Elimina archivio - Eliminare l\'archivio della chat\? Database crittografato Inserisci la password giusta. Inserisci la password… @@ -745,7 +740,6 @@ Ripristina backup del database Ripristinare il backup del database\? Errore di ripristino del database - Salva archivio Salva la password e apri la chat Il tentativo di cambiare la password del database non è stato completato. Errore del database sconosciuto: %s diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/iw/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/iw/strings.xml index d207057d0c..64f86a6ecb 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/iw/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/iw/strings.xml @@ -165,8 +165,6 @@ מסד הנתונים של הצ׳אט נמחק ‬מסד הנתונים של הצ׳אט יובא אשר שדרוגי מסד נתונים - ארכיון צ׳אט - ארכיון צ׳אט צ׳אט מופסק לא ניתן להזמין את אנשי הקשר! שונה תפקידך ל%s @@ -222,7 +220,6 @@ צור צור פרופיל יצירת הפרופיל שלך - נוצר ב־%1$s צור קישור קבוצה צור קישור יוצר הקבוצה @@ -253,7 +250,6 @@ צרו כתובת כדי לאפשר לאנשים להתחבר אליכם. מבוזר מסד הנתונים מוצפן באמצעות סיסמה אקראית. אנא שנו אותה לפני הייצוא. - למחוק ארכיון צ׳אט\? מחק פרופיל צ׳אט ברירת מחדל (%s) %d יום @@ -307,7 +303,6 @@ סיסמה וייצוא של מסד הנתונים מחק אחרי מחק את כל הקבצים - מחק ארכיון מחק עבורי מחק קישור למחוק פרופיל צ׳אט\? @@ -893,7 +888,6 @@ שחזור גיבוי מסד נתונים לשחזר גיבוי מסד נתונים\? שמור סיסמה ופתח את הצ׳אט - שמור ארכיון בחירת אנשי קשר קוד גישה להשמדה עצמית שניות diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml index cce95b5286..632e62ef09 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml @@ -77,8 +77,6 @@ 電池消費が最少:アプリがアクティブ時のみに通知が出ます(バックグラウンドサービス無し)。]]> 設定メニューにてオフにできます。 アプリがアクティブ時に通知が出ます。]]> あなたと連絡相手が送信済みメッセージを永久削除できます。(24時間) - チャットのアーカイブ - チャットのアーカイブを削除しますか? シークレットモードで参加 接続待ち (招待) 接続待ち (承諾済み) @@ -369,8 +367,6 @@ データベース暗号化のパスフレーズが更新されます。 チャットを開くにはデータベースパスフレーズが必要です。 ファイル: %s - 作成日時 %1$s - アーカイブを削除 参加 グループに参加しますか? グループに参加 @@ -551,7 +547,6 @@ 端末 送受信済みのファイルがありません メッセージを削除 - チャットのアーカイブ 接続中 あなたを除名しました。 グループのリンク @@ -921,7 +916,6 @@ データベースのエクスポート、読み込み、削除するにはチャット機能を停止する必要があります。チャット機能を停止すると送受信ができなくなります。 あなたのプロフィール、連絡先、メッセージ、ファイルが完全削除されます (※元に戻せません※)。 データベースパスフレーズを更新 - アーカイブを保存 あなたが自分の役割を次に変えました:%s アドレスを変えました %sのアドレスを変えました diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ko/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ko/strings.xml index eab9df3b92..6af84d88c1 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ko/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ko/strings.xml @@ -90,7 +90,6 @@ 주의: 암호를 분실하면 복구하거나 비밀번호 변경을 할 수 없어요.]]> 데이터베이스 암호를 바꾸시겠습니까? 새로운 암호 확인… - 채팅 기록 보관함 내 역할이 %s 역할로 변경됨 주소 바꾸는 중… 주소 바꾸는 중… @@ -179,7 +178,6 @@ 채팅 데이터베이스 대화 상대를 초대할 수 없습니다! 변경 - 채팅 기록 보관함 역할 변경 채팅 데이터베이스가 삭제됨 채팅이 멈춤 @@ -197,7 +195,6 @@ 대화 상대와 모든 메시지가 삭제됩니다. 이 결정은 되돌릴 수 없습니다! 대화 상대와 종단 간 암호화됨 대화 상대와 아직 연결되지 않았습니다! - %1$s에 생성 완료 비밀 그룹 생성 익명 수락 1개월 @@ -232,8 +229,6 @@ 데이터베이스 에러 데이터베이스 암호가 암호 저장소에 저장된 것과 일치하지 않습니다. 채팅을 열려면 데이터베이스 암호가 필요합니다. - 보관함 삭제 - 보관된 채팅을 삭제할까요\? %d 개의 대화 상대가 선택되었습니다. 데이터베이스 ID 다음 채팅 프로필 삭제 @@ -751,7 +746,6 @@ 저장하고 그룹 멤버들에게 알리기 저장하고 대화 상대에게 알리기 지우기 - 보관함 저장하기 암호 저장소에 암호 저장하기 데이터베이스 백업 복원하기 데이터베이스 백업을 복원한 후 이전 비밀번호를 입력해 주십시오. 이 결정은 되돌릴 수 없습니다. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/lt/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/lt/strings.xml index bf50e67bb8..c3505460ae 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/lt/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/lt/strings.xml @@ -125,8 +125,6 @@ Failai ir medija Ištrinti visus failus Ištrinti failus ir mediją\? - Ištrinti archyvą - Ištrinti pokalbio archyvą\? grupės profilis atnaujintas Grupė Ištrinti pokalbio profilį\? @@ -393,7 +391,6 @@ Žymėti kaip patvirtintą SimpleX užraktas Įrašyti WebRTC ICE serveriai bus pašalinti. - Įrašyti archyvą Siųsti tiesioginę žinutę Šalinti narį Šviesus @@ -834,7 +831,6 @@ Prašome įvesti praeitą slaptažodį po duomenų bazės atsarginės kopijos atstatymo. Šis veiksmas negali būti atšauktas. duomenų bazė naujesnė nei programėlė, bet nėra perkėlimo į senesnę versiją: %s skirtinga migracija programėlėje/duomenų bazėje: %s / %s - Pokalbio archyvas Grupė neaktyvi Gauta Užblokuota administratoriaus @@ -903,7 +899,6 @@ Atsitiktinė slaptafrazė yra saugoma nustatymuose kaip paprastas tekstas. \nJūs galite tai pakeisti vėliau. Pokalbiai veikia - POKALBIO ARCHYVAS Pokalbiai sustabdyti. Jei jau naudojote šią duomenų bazę kitame įrenginyje, turėtumėte perkelti ją atgal prieš pradedant pokalbius. užblokavo %s Keisti gavimo adresą @@ -1061,7 +1056,6 @@ Pokalbiai sustabdyti jungiamasi (priimtas) Kontaktas patikrintas - Sukurta %1$s Duomenų bazės slaptafrazė yra kitokia nei išsaugota raktų saugykloje. Duomenų bazė bus užšifruota ir slaptafrazė bus saugoma nustatymuose. Grupės pakvietimas nebegalioja, siuntėjas jį pašalino. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml index 22112a8376..649f586620 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml @@ -24,7 +24,6 @@ Chat is actief Wissen CHAT DATABASE - CHAT ARCHIEF Chat console Chat database geïmporteerd Chat database verwijderd @@ -117,7 +116,6 @@ Chat is gestopt Controleert nieuwe berichten elke 10 minuten gedurende maximaal 1 minuut je rol gewijzigd in %s - Gesprek archief Wachtwoord database wijzigen\? Chat is gestopt Chat voorkeuren @@ -215,7 +213,6 @@ Huidige wachtwoord… Database versleuteld! is toegetreden - Gemaakt op %1$s compleet Wissen verbonden @@ -230,8 +227,6 @@ Contact personen kunnen berichten markeren voor verwijdering; u kunt ze wel bekijken. Donker standaard (%s) - Chat archief verwijderen\? - Archief verwijderen Verwijder contact\? Chatprofiel verwijderen? Verwijderen voor iedereen @@ -621,7 +616,6 @@ Video aan Deze actie kan niet ongedaan worden gemaakt. Uw profiel, contacten, berichten en bestanden gaan onomkeerbaar verloren. Deze instelling is van toepassing op berichten in uw huidige chatprofiel - Bewaar archief bijgewerkt groep profiel verwijderd Uw chatprofiel wordt verzonden naar de groepsleden diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml index 8056422e5f..e793ff2a72 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml @@ -601,14 +601,11 @@ Nieprawidłowe hasło bazy danych Nieprawidłowe hasło! Musisz wprowadzić hasło przy każdym uruchomieniu aplikacji - nie jest one przechowywane na urządzeniu. - Archiwum czatu - ARCHIWUM CZATU Czat jest zatrzymany Potwierdź aktualizacje bazy danych Obniż wersję bazy danych Aktualizacja bazy danych wersja bazy danych jest nowsza od aplikacji, ale nie ma migracji w dół dla: %s - Usuń archiwum różne migracje w aplikacji/bazy danych: %s / %s Obniż wersję i otwórz czat Grupa nieaktywna @@ -630,7 +627,6 @@ Przywróć kopię zapasową bazy danych Przywrócić kopię zapasową bazy danych\? Błąd przywracania bazy danych - Zapisz archiwum Próba zmiany hasła bazy danych nie została zakończona. Ta grupa już nie istnieje. Zaktualizuj i otwórz czat @@ -925,13 +921,11 @@ Kontakt i wszystkie wiadomości zostaną usunięte - nie można tego cofnąć! Błąd połączenia (UWIERZYTELNIANIE) Połącz się przez link / kod QR - Utworzony na %1$s Utwórz tajną grupę Utwórz tajną grupę Baza danych jest zaszyfrowana przy użyciu losowego hasła. Proszę zmienić je przed eksportem. %d dni Usuń - Usunąć archiwum czatu\? Usuń wiadomości po Znikające wiadomości są zabronione w tej grupie. Błąd usuwania prośby o kontakt diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/pt-rBR/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/pt-rBR/strings.xml index d683d194ba..140b208db9 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/pt-rBR/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/pt-rBR/strings.xml @@ -57,7 +57,6 @@ Android Keystore é usada para armazenar a senha com segurança - permite que o serviço de notificação funcione. A Android Keystore será usada para armazenar a senha com segurança depois que você reiniciar o aplicativo ou alterar a senha - isso permitirá o recebimento de notificações. Não é possível acessar a Keystore para salvar a senha do banco de dados - ARQUIVO DE BATE-PAPO O bate-papo está parado Limpar Preferências de bate-papo @@ -81,7 +80,6 @@ O bate-papo está em execução O bate-papo está parado Alterar senha do banco de dados\? - Arquivo de chat endereço alterado para você Você e seu contato podem enviar mensagens temporárias. Backup de dados do aplicativo @@ -180,8 +178,6 @@ Confirmar nova senha… Senha atual… Senha do banco de dados é necessária para abrir o chat. - Excluir arquivo - Excluir arquivo de chat\? cargo alterado de %s para %s conectado Excluir link @@ -218,7 +214,6 @@ Erro de conexão (AUTH) conexão estabelecida conexão %1$d - Criado em %1$s Atualmente, o tamanho máximo de arquivo suportado é %1$s. Excluir Ssnha de criptografia do banco de dados será atualizada e armazenada na Keystore. @@ -685,7 +680,6 @@ Esta ação não pode ser desfeita - seu perfil, contatos, mensagens e arquivos serão irreversivelmente perdidos. Remover Senha do banco de dados incorreta - Salvar arquivo Senha não encontrada na Keystore, por favor digite-a manualmente. Isso pode ter ocorrido se você recuperou os dados do app usando uma ferramenta de backup. Se esse não é o caso, por favor, contate os desenvolvedores. Você se juntou a este grupo. Conectando-se a um membro convidado do grupo. Sair do grupo\? diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/pt/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/pt/strings.xml index 548a495222..ee5b82d490 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/pt/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/pt/strings.xml @@ -33,7 +33,6 @@ ÍCONE DA APLICAÇÃO 1 mês Mensagens - ARQUIVO DE CONVERSA Adicionar mensagem de boas-vindas Adicional perfil Apenas dados de perfil local @@ -135,11 +134,8 @@ Eliminar após Eliminar Eliminar - Arquivo de conversa Eliminar Eliminar todos os ficheiros - Eliminar arquivo - Eliminar arquivo de conversa\? Eliminar base de dados BASE DE DADOS DE CONVERSA Base de dados de conversa eliminada @@ -355,7 +351,6 @@ Contribuir Copiar Versão principal: v%s - Criado a %1$s Criar ligação de grupo Ligações de grupo conectando chamada… @@ -383,7 +378,6 @@ Salvar e atualizar o perfil do grupo Salvar Salvar senha e abrir conversa - Salvar arquivo Junte-se em modo anónimo Apagar ligação Endereço diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml index 498d3282de..7f8e0e2743 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml @@ -229,7 +229,6 @@ Salvezi servere? Apel respins Mesaj salvat - Salvează arhiva Repornește aplicația pentru a crea un nou profil Salvează fraza de acces în Keystore Salvează profilul grupului @@ -441,7 +440,6 @@ Baza ta de date a conversațiilor Baza ta de date a conversațiilor nu este criptată - setează frază de acces pentru a o proteja. Fraza de acces de criptare a bazei de date va fi actualizată și stocată în setări. - Creat pe %1$s ai schimbat rolul %s la %s Nu se pot invita contactele! Te-ai alăturat grupului @@ -595,7 +593,6 @@ Consolă conversație Confirmați parola colorat - Arhivă conversație Toate contactele, conversațiile și fișierele dumneavoastră vor fi encriptate într-un mod sigur și încărcate pe bucăți pe releurile XFTP configurate. Rugat să primească videoclipul Comparați fișierul @@ -616,7 +613,6 @@ Vă rugăm să rețineți: folosind aceeași bază de date pe două dispozitive, va intrerupe decripția mesajelor de la conexiunile dumneavoastră, ca protecție de securitate.]]> Confirmați încărcarea Confirmați că țineți minte parola de la baza de date pentru a o migra. - ARHIVĂ CONVERSAȚIE Conexiune Ștergi profilul de conversație? Șterge pentru mine @@ -626,7 +622,6 @@ Șters la Versiunea aplicației desktop %s nu este compatibilă cu această aplicație. Ștergi mesajul membrului? - Șterge arhiva Șterge grup Șterge și notifică contactele Decentralizat @@ -652,7 +647,6 @@ Șterge Șterge Ștergi fișiere și media? - Ștergi arhiva conversației? %d zile %d zi Șterge adresa diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml index aa9856eb9f..10d1de26ae 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml @@ -676,12 +676,6 @@ Чат остановлен Вы можете запустить чат через Настройки приложения или перезапустив приложение. - Архив чата - АРХИВ ЧАТА - Сохранить архив - Удалить архив - Дата создания %1$s - Удалить архив чата? приглашение в группу %1$s Вступить в группу? diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/th/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/th/strings.xml index 03bf9f0f27..c1f88cf3b1 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/th/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/th/strings.xml @@ -160,7 +160,6 @@ หมดเวลาการเชื่อมต่อ เชื่อมต่อผ่านลิงค์กลุ่ม\? ผู้ติดต่อและข้อความทั้งหมดจะถูกลบ - ไม่สามารถยกเลิกได้! - สร้างเมื่อ %1$s ขนาดไฟล์สูงสุดที่รองรับในปัจจุบันคือ %1$s ธีมที่กำหนดเอง ID ฐานข้อมูลและตัวเลือกการแยกการส่งผ่าน @@ -239,8 +238,6 @@ ความผิดพลาดในฐานข้อมูล ยืนยันการอัพเกรดฐานข้อมูล ดาวน์เกรดฐานข้อมูล - ที่เก็บแชทถาวร - ที่เก็บแชทถาวร การแชทหยุดทํางานแล้ว เชื่อมต่อสำเร็จ กำลังเปลี่ยนที่อยู่… @@ -432,8 +429,6 @@ เวอร์ชันฐานข้อมูลใหม่กว่าแอป แต่ไม่มีการย้ายข้อมูลลงสำหรับ: %s การย้ายข้อมูลที่แตกต่างกันในแอป/ฐานข้อมูล: %s / %s ปรับลดรุ่นและเปิดแชท - ลบที่เก็บถาวร - ลบที่เก็บแชทถาวร\? กลุ่มที่ไม่ได้ใช้งาน ไม่พบกลุ่ม! คำเชิญเข้าร่วมกลุ่มหมดอายุแล้ว @@ -837,7 +832,6 @@ คืนค่า คืนค่าฐานข้อมูลสำรองไหม\? กู้คืนข้อผิดพลาดของฐานข้อมูล - บันทึกไฟล์เก็บถาวร ลบแล้ว %1$s ลบคุณออกแล้ว ถูกลบแล้ว diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml index 30afe21e50..9dcc45e6c4 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml @@ -106,7 +106,6 @@ Veri tabanı yedeğini geri yükle\? Veri tabanını geri yüklerken hata Veritabanı sürüm düşürme - Arşivi kaydet %s (mevcut) Kaydet ve grup profilini güncelle Karşılama mesajı kaydedilsin mi? @@ -429,9 +428,6 @@ Veri tabanı, rastgele bir parola ile şifrelendi. Dışa aktarmadan önce lütfen değiştir. Dosyaları ve medyayı sil\? Veri tabanı şifrelenecektir. - %1$s tarihinde oluşturuldu - Belgeliği sil - Konuşma belgeliğini sil\? %s üyesinin yetkisi %s olarak değiştirildi silinmiş grup kendi yetkini, %s olarak değiştirdin @@ -740,8 +736,6 @@ kişi uçtan uca şifrelemeye sahip değildir Sohbet durduruldu Aklınızda bulunsun: kaybederseniz, parolayı kurtaramaz veya değiştiremezsiniz.]]> - Sohbet arşivi - SOHBET ARŞİVİ %1$s grubuna davet Gruba katıl\? %1$s davet edildi diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml index 604fd1fa1a..732f85e473 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml @@ -294,8 +294,6 @@ Зашифрувати базу даних\? Неправильна ключова фраза! Введіть правильну ключову фразу. - Архів чату - АРХІВ ЧАТУ підключив(лась) змінив(ла) вашу роль на %s ви змінили свою роль на %s @@ -375,7 +373,6 @@ Виклики на екрані блокування: від абонента до абонента Завершити дзвінок - Створено %1$s Розгорнути вибір ролі так Налаштування контакту @@ -528,7 +525,6 @@ Відео увімкнено Це може трапитися, якщо ви або ваше з\'єднання використовували застарілу резервну копію бази даних. Відновити резервну копію бази даних - Зберегти архів запрошення до групи %1$s Вас запрошено в групу. Приєднуйтесь, щоб спілкуватися з учасниками групи. Реакції на повідомлення @@ -1070,8 +1066,6 @@ Для відкриття чату потрібна ключова фраза бази даних. Приєднатися до групи\? Оновлення бази даних - Видалити архів - Видалити архів чату\? Вийти з групи? Групу не знайдено! Ця група більше не існує. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml index eb19247a12..d12db70de6 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml @@ -226,8 +226,6 @@ Thay đổi quyền hạn đang thay đổi địa chỉ… đang thay đổi địa chỉ… - KHO LƯU TRỮ SIMPLEX CHAT - Kho lưu trữ SimpleX Chat Bảng điều khiển trò chuyện Ứng dụng SimpleX Chat đang hoạt động Cơ sở dữ liệu SimpleX Chat đã bị xóa @@ -361,7 +359,6 @@ Tạo liên kết Được tạo ra tại Tạo địa chỉ - Được tạo ra vào %1$s Được tạo ra tại: %s Tạo hồ sơ trò chuyện Tạo nhóm @@ -437,9 +434,7 @@ Xóa cơ sở dữ liệu Xóa tất cả các tệp Xóa tệp và đa phương tiện? - Xóa kho lữu trữ mặc định (%s) - Xóa kho lữu trữ SimpleX Chat? đã xóa nhóm Xóa tệp Xóa liên hệ? diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml index eefd310fd0..ed60383eb1 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml @@ -47,7 +47,6 @@ 删除 删除地址? 在此后删除 - 删除档案 已删除 删除文件和媒体文件? 为所有人删除 @@ -81,7 +80,6 @@ 允许向成员发送私信。 允许发送限时消息。 删除地址 - 删除聊天档案? 删除聊天资料? 删除联系人 删除联系人? @@ -161,8 +159,6 @@ 聊天数据库已删除 聊天数据库已导入 钥匙串错误 - 聊天档案 - 聊天档案 聊天控制台 聊天数据库 聊天已停止 @@ -719,7 +715,6 @@ 回复 重置为默认 运行聊天程序 - 保存存档 扫码 从您联系人的应用程序中扫描安全码。 安全码 @@ -873,7 +868,6 @@ 接收消息,您的联系人 - 您用来向他们发送消息的服务器。]]> 您将在组主设备上线时连接到该群组,请稍等或稍后再检查! 当您启动应用或在应用程序驻留后台超过30 秒后,您将需要进行身份验证。 - 创建于 %1$s 连接到 SimpleX Chat 开发者提出任何问题并接收更新 。]]> 您已接受连接 您的 SMP 服务器 diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml index 17cc45334e..58372bfa7a 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml @@ -632,7 +632,6 @@ %s 秒(s) 加密數據庫時出錯 在金鑰庫儲存密碼 - 建立於 %1$s 還原數據庫的備份 群組為不活躍狀態 邀請連結過時! @@ -700,8 +699,6 @@ 還原 還原數據庫的備份? 還原數據庫時出錯 - 儲存存檔 - 刪除存檔 加入 確定要加入群組? 加入匿名聊天模式 @@ -777,7 +774,6 @@ 匯出數據庫時出錯 匯入數據庫時出錯 受加密的數據庫密碼會再次更新。 - 刪除封存對話? 加密數據庫? 邀請至群組 %1$s 邀請成員 @@ -799,13 +795,11 @@ 對話沒有經過端對端加密 數據庫已加密! 已加密數據庫 - 封存對話 群組資料已經更新 成員 你:%1$s 刪除群組 即時訊息 - 封存對話 移除成員時出錯 修改身份時出錯 群組 diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/AppCommon.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/AppCommon.desktop.kt index 38d87fc497..d425d81875 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/AppCommon.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/AppCommon.desktop.kt @@ -3,6 +3,7 @@ package chat.simplex.common.platform import chat.simplex.common.model.* import chat.simplex.common.simplexWindowState import chat.simplex.common.views.call.RcvCallInvitation +import chat.simplex.common.views.database.deleteOldChatArchive import chat.simplex.common.views.helpers.* import java.util.* import chat.simplex.res.MR @@ -30,6 +31,7 @@ fun initApp() { override fun showMessage(title: String, text: String) = chat.simplex.common.model.NtfManager.showMessage(title, text) } applyAppLocale() + deleteOldChatArchive() if (DatabaseUtils.ksSelfDestructPassword.get() == null) { initChatControllerOnStart() } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/database/DatabaseView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/database/DatabaseView.desktop.kt index 889ac98e2d..9ed7170a31 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/database/DatabaseView.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/database/DatabaseView.desktop.kt @@ -9,14 +9,14 @@ import kotlinx.datetime.Instant actual fun restartChatOrApp() { if (chatModel.chatRunning.value == false) { chatModel.chatDbChanged.value = true - startChat(chatModel, mutableStateOf(Instant.DISTANT_PAST), chatModel.chatDbChanged) + startChat(chatModel, mutableStateOf(Instant.DISTANT_PAST), chatModel.chatDbChanged, mutableStateOf(false)) } else { authStopChat(chatModel) { withBGApi { // adding delay in order to prevent locked database by previous initialization delay(1000) chatModel.chatDbChanged.value = true - startChat(chatModel, mutableStateOf(Instant.DISTANT_PAST), chatModel.chatDbChanged) + startChat(chatModel, mutableStateOf(Instant.DISTANT_PAST), chatModel.chatDbChanged, mutableStateOf(false)) } } } From e9853fe3fcc2f97f63ee22b98451efcf6d6e58a6 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sat, 30 Nov 2024 18:06:36 +0000 Subject: [PATCH 089/167] ios: update alert message for SimpleX address card --- .../Onboarding/AddressCreationCard.swift | 2 +- .../bg.xcloc/Localized Contents/bg.xliff | 57 +++--------------- .../cs.xcloc/Localized Contents/cs.xliff | 57 +++--------------- .../de.xcloc/Localized Contents/de.xliff | 57 +++--------------- .../en.xcloc/Localized Contents/en.xliff | 60 ++++--------------- .../es.xcloc/Localized Contents/es.xliff | 57 +++--------------- .../fi.xcloc/Localized Contents/fi.xliff | 57 +++--------------- .../fr.xcloc/Localized Contents/fr.xliff | 57 +++--------------- .../hu.xcloc/Localized Contents/hu.xliff | 57 +++--------------- .../it.xcloc/Localized Contents/it.xliff | 57 +++--------------- .../ja.xcloc/Localized Contents/ja.xliff | 57 +++--------------- .../nl.xcloc/Localized Contents/nl.xliff | 57 +++--------------- .../pl.xcloc/Localized Contents/pl.xliff | 57 +++--------------- .../ru.xcloc/Localized Contents/ru.xliff | 57 +++--------------- .../th.xcloc/Localized Contents/th.xliff | 57 +++--------------- .../tr.xcloc/Localized Contents/tr.xliff | 57 +++--------------- .../uk.xcloc/Localized Contents/uk.xliff | 57 +++--------------- .../Localized Contents/zh-Hans.xliff | 57 +++--------------- apps/ios/bg.lproj/Localizable.strings | 27 --------- apps/ios/cs.lproj/Localizable.strings | 27 --------- apps/ios/de.lproj/Localizable.strings | 27 --------- apps/ios/es.lproj/Localizable.strings | 27 --------- apps/ios/fi.lproj/Localizable.strings | 27 --------- apps/ios/fr.lproj/Localizable.strings | 27 --------- apps/ios/hu.lproj/Localizable.strings | 27 --------- apps/ios/it.lproj/Localizable.strings | 27 --------- apps/ios/ja.lproj/Localizable.strings | 27 --------- apps/ios/nl.lproj/Localizable.strings | 27 --------- apps/ios/pl.lproj/Localizable.strings | 27 --------- apps/ios/ru.lproj/Localizable.strings | 27 --------- apps/ios/th.lproj/Localizable.strings | 27 --------- apps/ios/tr.lproj/Localizable.strings | 27 --------- apps/ios/uk.lproj/Localizable.strings | 27 --------- apps/ios/zh-Hans.lproj/Localizable.strings | 27 --------- 34 files changed, 139 insertions(+), 1267 deletions(-) diff --git a/apps/ios/Shared/Views/Onboarding/AddressCreationCard.swift b/apps/ios/Shared/Views/Onboarding/AddressCreationCard.swift index c757dcfeeb..2069ca9487 100644 --- a/apps/ios/Shared/Views/Onboarding/AddressCreationCard.swift +++ b/apps/ios/Shared/Views/Onboarding/AddressCreationCard.swift @@ -62,7 +62,7 @@ struct AddressCreationCard: View { .alert(isPresented: $showAddressCreationAlert) { Alert( title: Text("SimpleX address"), - message: Text("You can create it in user picker."), + message: Text("Tap Create SimpleX address in the menu to create it later."), dismissButton: .default(Text("Ok")) { withAnimation { addressCreationCardShown = true diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff index 597041e163..2964742c85 100644 --- a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff @@ -1318,11 +1318,6 @@ Change user profiles authentication reason - - Chat archive - Архив на чата - No comment provided by engineer. - Chat colors No comment provided by engineer. @@ -1895,11 +1890,6 @@ This is your own one-time link! Създаден на: %@ copied message info - - Created on %@ - Създаден на %@ - No comment provided by engineer. - Creating archive link Създаване на архивен линк @@ -2108,16 +2098,6 @@ This is your own one-time link! Изтрий и уведоми контакт No comment provided by engineer. - - Delete archive - Изтрий архив - No comment provided by engineer. - - - Delete chat archive? - Изтриване на архива на чата? - No comment provided by engineer. - Delete chat profile Изтрий чат профила @@ -2794,11 +2774,6 @@ This is your own one-time link! Грешка при приемане на заявка за контакт No comment provided by engineer. - - Error accessing database file - Грешка при достъпа до файла с базата данни - No comment provided by engineer. - Error adding member(s) Грешка при добавяне на член(ове) @@ -4533,11 +4508,6 @@ This is your link for group %@! Нов контакт: notification - - New database archive - Нов архив на база данни - No comment provided by engineer. - New desktop app! Ново настолно приложение! @@ -4746,11 +4716,6 @@ This is your link for group %@! Стара база данни No comment provided by engineer. - - Old database archive - Стар архив на база данни - No comment provided by engineer. - One-time invitation link Линк за еднократна покана @@ -4900,6 +4865,10 @@ Requires compatible VPN. Operator server alert title + + Or import archive file + No comment provided by engineer. + Or paste archive link Или постави архивен линк @@ -5741,11 +5710,6 @@ Enable in *Network & servers* settings. Запази и актуализирай профила на групата No comment provided by engineer. - - Save archive - Запази архив - No comment provided by engineer. - Save group profile Запази профила на групата @@ -6519,11 +6483,6 @@ Enable in *Network & servers* settings. Спри чата No comment provided by engineer. - - Stop chat to enable database actions - Спрете чата, за да активирате действията с базата данни - No comment provided by engineer. - Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped. Спрете чата, за да експортирате, импортирате или изтриете чат базата данни. Няма да можете да получавате и изпращате съобщения, докато чатът е спрян. @@ -6641,6 +6600,10 @@ Enable in *Network & servers* settings. Направи снимка No comment provided by engineer. + + Tap Create SimpleX address in the menu to create it later. + No comment provided by engineer. + Tap button Докосни бутона @@ -7695,10 +7658,6 @@ Repeat join request? You can configure servers via settings. No comment provided by engineer. - - You can create it in user picker. - No comment provided by engineer. - You can create it later Можете да го създадете по-късно diff --git a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff index e2e77572c5..bf9436afe3 100644 --- a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff @@ -1277,11 +1277,6 @@ Change user profiles authentication reason - - Chat archive - Chat se archivuje - No comment provided by engineer. - Chat colors No comment provided by engineer. @@ -1828,11 +1823,6 @@ This is your own one-time link! Created at: %@ copied message info - - Created on %@ - Vytvořeno na %@ - No comment provided by engineer. - Creating archive link No comment provided by engineer. @@ -2037,16 +2027,6 @@ This is your own one-time link! Delete and notify contact No comment provided by engineer. - - Delete archive - Smazat archiv - No comment provided by engineer. - - - Delete chat archive? - Smazat archiv chatu? - No comment provided by engineer. - Delete chat profile Smazat chat profil @@ -2702,11 +2682,6 @@ This is your own one-time link! Chyba při přijímání žádosti o kontakt No comment provided by engineer. - - Error accessing database file - Chyba přístupu k souboru databáze - No comment provided by engineer. - Error adding member(s) Chyba přidávání člena(ů) @@ -4377,11 +4352,6 @@ This is your link for group %@! Nový kontakt: notification - - New database archive - Archiv nové databáze - No comment provided by engineer. - New desktop app! Nová desktopová aplikace! @@ -4587,11 +4557,6 @@ This is your link for group %@! Stará databáze No comment provided by engineer. - - Old database archive - Archiv staré databáze - No comment provided by engineer. - One-time invitation link Jednorázový zvací odkaz @@ -4738,6 +4703,10 @@ Vyžaduje povolení sítě VPN. Operator server alert title + + Or import archive file + No comment provided by engineer. + Or paste archive link No comment provided by engineer. @@ -5551,11 +5520,6 @@ Enable in *Network & servers* settings. Uložit a aktualizovat profil skupiny No comment provided by engineer. - - Save archive - Uložit archiv - No comment provided by engineer. - Save group profile Uložení profilu skupiny @@ -6312,11 +6276,6 @@ Enable in *Network & servers* settings. Stop chat No comment provided by engineer. - - Stop chat to enable database actions - Zastavte chat pro povolení akcí databáze - No comment provided by engineer. - Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped. Zastavení chatu pro export, import nebo smazání databáze chatu. Během zastavení chatu nebudete moci přijímat a odesílat zprávy. @@ -6433,6 +6392,10 @@ Enable in *Network & servers* settings. Vyfotit No comment provided by engineer. + + Tap Create SimpleX address in the menu to create it later. + No comment provided by engineer. + Tap button Klepněte na tlačítko @@ -7435,10 +7398,6 @@ Repeat join request? You can configure servers via settings. No comment provided by engineer. - - You can create it in user picker. - No comment provided by engineer. - You can create it later Můžete vytvořit později diff --git a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff index 4d1508dce3..6a92589851 100644 --- a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff +++ b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff @@ -1359,11 +1359,6 @@ Change user profiles authentication reason - - Chat archive - Datenbank Archiv - No comment provided by engineer. - Chat colors Chat-Farben @@ -1965,11 +1960,6 @@ Das ist Ihr eigener Einmal-Link! Erstellt um: %@ copied message info - - Created on %@ - Erstellt am %@ - No comment provided by engineer. - Creating archive link Archiv-Link erzeugen @@ -2184,16 +2174,6 @@ Das ist Ihr eigener Einmal-Link! Kontakt löschen und benachrichtigen No comment provided by engineer. - - Delete archive - Archiv löschen - No comment provided by engineer. - - - Delete chat archive? - Chat Archiv löschen? - No comment provided by engineer. - Delete chat profile Chat-Profil löschen @@ -2891,11 +2871,6 @@ Das ist Ihr eigener Einmal-Link! Fehler beim Annehmen der Kontaktanfrage No comment provided by engineer. - - Error accessing database file - Fehler beim Zugriff auf die Datenbankdatei - No comment provided by engineer. - Error adding member(s) Fehler beim Hinzufügen von Mitgliedern @@ -4690,11 +4665,6 @@ Das ist Ihr Link für die Gruppe %@! Neuer Kontakt: notification - - New database archive - Neues Datenbankarchiv - No comment provided by engineer. - New desktop app! Neue Desktop-App! @@ -4910,11 +4880,6 @@ Das ist Ihr Link für die Gruppe %@! Alte Datenbank No comment provided by engineer. - - Old database archive - Altes Datenbankarchiv - No comment provided by engineer. - One-time invitation link Einmal-Einladungslink @@ -5065,6 +5030,10 @@ Dies erfordert die Aktivierung eines VPNs. Operator server alert title + + Or import archive file + No comment provided by engineer. + Or paste archive link Oder fügen Sie den Archiv-Link ein @@ -5948,11 +5917,6 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Gruppen-Profil sichern und aktualisieren No comment provided by engineer. - - Save archive - Archiv speichern - No comment provided by engineer. - Save group profile Gruppenprofil speichern @@ -6770,11 +6734,6 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Chat beenden No comment provided by engineer. - - Stop chat to enable database actions - Chat beenden, um Datenbankaktionen zu erlauben - No comment provided by engineer. - Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped. Beenden Sie den Chat, um die Chat-Datenbank zu exportieren, zu importieren oder zu löschen. Solange der Chat angehalten ist, können Sie keine Nachrichten empfangen oder senden. @@ -6900,6 +6859,10 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Machen Sie ein Foto No comment provided by engineer. + + Tap Create SimpleX address in the menu to create it later. + No comment provided by engineer. + Tap button Schaltfläche antippen @@ -7989,10 +7952,6 @@ Verbindungsanfrage wiederholen? You can configure servers via settings. No comment provided by engineer. - - You can create it in user picker. - No comment provided by engineer. - You can create it later Sie können dies später erstellen diff --git a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff index 9973cfeeba..09a63ab3c4 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff +++ b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff @@ -1372,11 +1372,6 @@ Change user profiles authentication reason - - Chat archive - Chat archive - No comment provided by engineer. - Chat colors Chat colors @@ -1991,11 +1986,6 @@ This is your own one-time link! Created at: %@ copied message info - - Created on %@ - Created on %@ - No comment provided by engineer. - Creating archive link Creating archive link @@ -2211,16 +2201,6 @@ This is your own one-time link! Delete and notify contact No comment provided by engineer. - - Delete archive - Delete archive - No comment provided by engineer. - - - Delete chat archive? - Delete chat archive? - No comment provided by engineer. - Delete chat profile Delete chat profile @@ -2922,11 +2902,6 @@ This is your own one-time link! Error accepting contact request No comment provided by engineer. - - Error accessing database file - Error accessing database file - No comment provided by engineer. - Error adding member(s) Error adding member(s) @@ -4735,11 +4710,6 @@ This is your link for group %@! New contact: notification - - New database archive - New database archive - No comment provided by engineer. - New desktop app! New desktop app! @@ -4964,11 +4934,6 @@ This is your link for group %@! Old database No comment provided by engineer. - - Old database archive - Old database archive - No comment provided by engineer. - One-time invitation link One-time invitation link @@ -5123,6 +5088,11 @@ Requires compatible VPN. Operator server alert title + + Or import archive file + Or import archive file + No comment provided by engineer. + Or paste archive link Or paste archive link @@ -6013,11 +5983,6 @@ Enable in *Network & servers* settings. Save and update group profile No comment provided by engineer. - - Save archive - Save archive - No comment provided by engineer. - Save group profile Save group profile @@ -6846,11 +6811,6 @@ Enable in *Network & servers* settings. Stop chat No comment provided by engineer. - - Stop chat to enable database actions - Stop chat to enable database actions - No comment provided by engineer. - Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped. Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped. @@ -6976,6 +6936,11 @@ Enable in *Network & servers* settings. Take picture No comment provided by engineer. + + Tap Create SimpleX address in the menu to create it later. + Tap Create SimpleX address in the menu to create it later. + No comment provided by engineer. + Tap button Tap button @@ -8084,11 +8049,6 @@ Repeat join request? You can configure servers via settings. No comment provided by engineer. - - You can create it in user picker. - You can create it in user picker. - No comment provided by engineer. - You can create it later You can create it later diff --git a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff index 01e534f424..8d109187c2 100644 --- a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff +++ b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff @@ -1359,11 +1359,6 @@ Change user profiles authentication reason - - Chat archive - Archivo del chat - No comment provided by engineer. - Chat colors Colores del chat @@ -1965,11 +1960,6 @@ This is your own one-time link! Creado: %@ copied message info - - Created on %@ - Creado en %@ - No comment provided by engineer. - Creating archive link Creando enlace al archivo @@ -2184,16 +2174,6 @@ This is your own one-time link! Eliminar y notificar contacto No comment provided by engineer. - - Delete archive - Eliminar archivo - No comment provided by engineer. - - - Delete chat archive? - ¿Eliminar archivo del chat? - No comment provided by engineer. - Delete chat profile Eliminar perfil @@ -2891,11 +2871,6 @@ This is your own one-time link! Error al aceptar solicitud del contacto No comment provided by engineer. - - Error accessing database file - Error al acceder al archivo de la base de datos - No comment provided by engineer. - Error adding member(s) Error al añadir miembro(s) @@ -4690,11 +4665,6 @@ This is your link for group %@! Contacto nuevo: notification - - New database archive - Nuevo archivo de bases de datos - No comment provided by engineer. - New desktop app! Nueva aplicación para PC! @@ -4909,11 +4879,6 @@ This is your link for group %@! Base de datos antigua No comment provided by engineer. - - Old database archive - Archivo de bases de datos antiguas - No comment provided by engineer. - One-time invitation link Enlace de invitación de un solo uso @@ -5064,6 +5029,10 @@ Requiere activación de la VPN. Operator server alert title + + Or import archive file + No comment provided by engineer. + Or paste archive link O pegar enlace del archivo @@ -5947,11 +5916,6 @@ Actívalo en ajustes de *Servidores y Redes*. Guardar y actualizar perfil del grupo No comment provided by engineer. - - Save archive - Guardar archivo - No comment provided by engineer. - Save group profile Guardar perfil de grupo @@ -6769,11 +6733,6 @@ Actívalo en ajustes de *Servidores y Redes*. Parar SimpleX No comment provided by engineer. - - Stop chat to enable database actions - Para habilitar las acciones sobre la base de datos, debes parar SimpleX - No comment provided by engineer. - Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped. Para poder exportar, importar o eliminar la base de datos primero debes parar SimpleX. Mientras tanto no podrás recibir ni enviar mensajes. @@ -6899,6 +6858,10 @@ Actívalo en ajustes de *Servidores y Redes*. Tomar foto No comment provided by engineer. + + Tap Create SimpleX address in the menu to create it later. + No comment provided by engineer. + Tap button Pulsa el botón @@ -7988,10 +7951,6 @@ Repeat join request? You can configure servers via settings. No comment provided by engineer. - - You can create it in user picker. - No comment provided by engineer. - You can create it later Puedes crearla más tarde diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff index fa5b2967e3..325732cb8d 100644 --- a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff @@ -1270,11 +1270,6 @@ Change user profiles authentication reason - - Chat archive - Chat-arkisto - No comment provided by engineer. - Chat colors No comment provided by engineer. @@ -1821,11 +1816,6 @@ This is your own one-time link! Created at: %@ copied message info - - Created on %@ - Luotu %@ - No comment provided by engineer. - Creating archive link No comment provided by engineer. @@ -2030,16 +2020,6 @@ This is your own one-time link! Delete and notify contact No comment provided by engineer. - - Delete archive - Poista arkisto - No comment provided by engineer. - - - Delete chat archive? - Poista keskusteluarkisto? - No comment provided by engineer. - Delete chat profile Poista keskusteluprofiili @@ -2694,11 +2674,6 @@ This is your own one-time link! Virhe kontaktipyynnön hyväksymisessä No comment provided by engineer. - - Error accessing database file - Virhe tietokantatiedoston käyttämisessä - No comment provided by engineer. - Error adding member(s) Virhe lisättäessä jäseniä @@ -4367,11 +4342,6 @@ This is your link for group %@! Uusi kontakti: notification - - New database archive - Uusi tietokanta-arkisto - No comment provided by engineer. - New desktop app! No comment provided by engineer. @@ -4576,11 +4546,6 @@ This is your link for group %@! Vanha tietokanta No comment provided by engineer. - - Old database archive - Vanha tietokanta-arkisto - No comment provided by engineer. - One-time invitation link Kertakutsulinkki @@ -4726,6 +4691,10 @@ Edellyttää VPN:n sallimista. Operator server alert title + + Or import archive file + No comment provided by engineer. + Or paste archive link No comment provided by engineer. @@ -5539,11 +5508,6 @@ Enable in *Network & servers* settings. Tallenna ja päivitä ryhmäprofiili No comment provided by engineer. - - Save archive - Tallenna arkisto - No comment provided by engineer. - Save group profile Tallenna ryhmäprofiili @@ -6298,11 +6262,6 @@ Enable in *Network & servers* settings. Stop chat No comment provided by engineer. - - Stop chat to enable database actions - Pysäytä keskustelu tietokantatoimien mahdollistamiseksi - No comment provided by engineer. - Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped. Pysäytä keskustelut viedäksesi, tuodaksesi tai poistaaksesi keskustelujen tietokannan. Et voi vastaanottaa ja lähettää viestejä, kun keskustelut on pysäytetty. @@ -6419,6 +6378,10 @@ Enable in *Network & servers* settings. Ota kuva No comment provided by engineer. + + Tap Create SimpleX address in the menu to create it later. + No comment provided by engineer. + Tap button Napauta painiketta @@ -7420,10 +7383,6 @@ Repeat join request? You can configure servers via settings. No comment provided by engineer. - - You can create it in user picker. - No comment provided by engineer. - You can create it later Voit luoda sen myöhemmin diff --git a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff index ecea1c6eb7..56c57a2237 100644 --- a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff @@ -1359,11 +1359,6 @@ Change user profiles authentication reason - - Chat archive - Archives du chat - No comment provided by engineer. - Chat colors Couleurs de chat @@ -1965,11 +1960,6 @@ Il s'agit de votre propre lien unique ! Créé à : %@ copied message info - - Created on %@ - Créé le %@ - No comment provided by engineer. - Creating archive link Création d'un lien d'archive @@ -2184,16 +2174,6 @@ Il s'agit de votre propre lien unique ! Supprimer et en informer le contact No comment provided by engineer. - - Delete archive - Supprimer l'archive - No comment provided by engineer. - - - Delete chat archive? - Supprimer l'archive du chat ? - No comment provided by engineer. - Delete chat profile Supprimer le profil de chat @@ -2891,11 +2871,6 @@ Il s'agit de votre propre lien unique ! Erreur de validation de la demande de contact No comment provided by engineer. - - Error accessing database file - Erreur d'accès au fichier de la base de données - No comment provided by engineer. - Error adding member(s) Erreur lors de l'ajout de membre·s @@ -4690,11 +4665,6 @@ Voici votre lien pour le groupe %@ ! Nouveau contact : notification - - New database archive - Nouvelle archive de base de données - No comment provided by engineer. - New desktop app! Nouvelle application de bureau ! @@ -4910,11 +4880,6 @@ Voici votre lien pour le groupe %@ ! Ancienne base de données No comment provided by engineer. - - Old database archive - Archives de l'ancienne base de données - No comment provided by engineer. - One-time invitation link Lien d'invitation unique @@ -5065,6 +5030,10 @@ Nécessite l'activation d'un VPN. Operator server alert title + + Or import archive file + No comment provided by engineer. + Or paste archive link Ou coller le lien de l'archive @@ -5948,11 +5917,6 @@ Activez-le dans les paramètres *Réseau et serveurs*. Enregistrer et mettre à jour le profil du groupe No comment provided by engineer. - - Save archive - Enregistrer l'archive - No comment provided by engineer. - Save group profile Enregistrer le profil du groupe @@ -6770,11 +6734,6 @@ Activez-le dans les paramètres *Réseau et serveurs*. Arrêter le chat No comment provided by engineer. - - Stop chat to enable database actions - Arrêter le chat pour permettre des actions sur la base de données - No comment provided by engineer. - Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped. Arrêtez le chat pour exporter, importer ou supprimer la base de données du chat. Vous ne pourrez pas recevoir et envoyer de messages pendant que le chat est arrêté. @@ -6900,6 +6859,10 @@ Activez-le dans les paramètres *Réseau et serveurs*. Prendre une photo No comment provided by engineer. + + Tap Create SimpleX address in the menu to create it later. + No comment provided by engineer. + Tap button Appuyez sur le bouton @@ -7989,10 +7952,6 @@ Répéter la demande d'adhésion ? You can configure servers via settings. No comment provided by engineer. - - You can create it in user picker. - No comment provided by engineer. - You can create it later Vous pouvez la créer plus tard diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff index bca18b73e6..b8b760b5a0 100644 --- a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff @@ -1359,11 +1359,6 @@ Change user profiles authentication reason - - Chat archive - Csevegési archívum - No comment provided by engineer. - Chat colors Csevegés színei @@ -1965,11 +1960,6 @@ Ez az Ön egyszer használható hivatkozása! Létrehozva ekkor: %@ copied message info - - Created on %@ - Létrehozva %@ - No comment provided by engineer. - Creating archive link Archívum hivatkozás létrehozása @@ -2184,16 +2174,6 @@ Ez az Ön egyszer használható hivatkozása! Törlés, és az ismerős értesítése No comment provided by engineer. - - Delete archive - Archívum törlése - No comment provided by engineer. - - - Delete chat archive? - Csevegési archívum törlése? - No comment provided by engineer. - Delete chat profile Csevegési profil törlése @@ -2891,11 +2871,6 @@ Ez az Ön egyszer használható hivatkozása! Hiba történt a kapcsolatkérés elfogadásakor No comment provided by engineer. - - Error accessing database file - Hiba az adatbázisfájl elérésekor - No comment provided by engineer. - Error adding member(s) Hiba a tag(ok) hozzáadásakor @@ -4690,11 +4665,6 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! Új kapcsolat: notification - - New database archive - Új adatbázis-archívum - No comment provided by engineer. - New desktop app! Új számítógép-alkalmazás! @@ -4910,11 +4880,6 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! Régi adatbázis No comment provided by engineer. - - Old database archive - Régi adatbázis-archívum - No comment provided by engineer. - One-time invitation link Egyszer használható meghívó-hivatkozás @@ -5065,6 +5030,10 @@ VPN engedélyezése szükséges. Operator server alert title + + Or import archive file + No comment provided by engineer. + Or paste archive link Vagy az archívum hivatkozásának beillesztése @@ -5948,11 +5917,6 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Mentés és a csoportprofil frissítése No comment provided by engineer. - - Save archive - Archívum mentése - No comment provided by engineer. - Save group profile Csoportprofil mentése @@ -6770,11 +6734,6 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Csevegési szolgáltatás megállítása No comment provided by engineer. - - Stop chat to enable database actions - Csevegés megállítása az adatbázis-műveletek engedélyezéséhez - No comment provided by engineer. - Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped. A csevegés megállítása a csevegési adatbázis exportálásához, importálásához vagy törléséhez. A csevegés megállításakor nem tud üzeneteket fogadni és küldeni. @@ -6900,6 +6859,10 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Kép készítése No comment provided by engineer. + + Tap Create SimpleX address in the menu to create it later. + No comment provided by engineer. + Tap button Koppintson a @@ -7989,10 +7952,6 @@ Csatlakozáskérés megismétlése? You can configure servers via settings. No comment provided by engineer. - - You can create it in user picker. - No comment provided by engineer. - You can create it later Létrehozás később diff --git a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff index acb46596ce..55eee758d5 100644 --- a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff +++ b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff @@ -1359,11 +1359,6 @@ Change user profiles authentication reason - - Chat archive - Archivio chat - No comment provided by engineer. - Chat colors Colori della chat @@ -1965,11 +1960,6 @@ Questo è il tuo link una tantum! Creato il: %@ copied message info - - Created on %@ - Creato il %@ - No comment provided by engineer. - Creating archive link Creazione link dell'archivio @@ -2184,16 +2174,6 @@ Questo è il tuo link una tantum! Elimina e avvisa il contatto No comment provided by engineer. - - Delete archive - Elimina archivio - No comment provided by engineer. - - - Delete chat archive? - Eliminare l'archivio della chat? - No comment provided by engineer. - Delete chat profile Elimina il profilo di chat @@ -2891,11 +2871,6 @@ Questo è il tuo link una tantum! Errore nell'accettazione della richiesta di contatto No comment provided by engineer. - - Error accessing database file - Errore nell'accesso al file del database - No comment provided by engineer. - Error adding member(s) Errore di aggiunta membro/i @@ -4690,11 +4665,6 @@ Questo è il tuo link per il gruppo %@! Nuovo contatto: notification - - New database archive - Nuovo archivio database - No comment provided by engineer. - New desktop app! Nuova app desktop! @@ -4910,11 +4880,6 @@ Questo è il tuo link per il gruppo %@! Database vecchio No comment provided by engineer. - - Old database archive - Vecchio archivio del database - No comment provided by engineer. - One-time invitation link Link di invito una tantum @@ -5065,6 +5030,10 @@ Richiede l'attivazione della VPN. Operator server alert title + + Or import archive file + No comment provided by engineer. + Or paste archive link O incolla il link dell'archivio @@ -5948,11 +5917,6 @@ Attivalo nelle impostazioni *Rete e server*. Salva e aggiorna il profilo del gruppo No comment provided by engineer. - - Save archive - Salva archivio - No comment provided by engineer. - Save group profile Salva il profilo del gruppo @@ -6770,11 +6734,6 @@ Attivalo nelle impostazioni *Rete e server*. Ferma la chat No comment provided by engineer. - - Stop chat to enable database actions - Ferma la chat per attivare le azioni del database - No comment provided by engineer. - Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped. Ferma la chat per esportare, importare o eliminare il database della chat. Non potrai ricevere e inviare messaggi mentre la chat è ferma. @@ -6900,6 +6859,10 @@ Attivalo nelle impostazioni *Rete e server*. Scatta foto No comment provided by engineer. + + Tap Create SimpleX address in the menu to create it later. + No comment provided by engineer. + Tap button Tocca il pulsante @@ -7989,10 +7952,6 @@ Ripetere la richiesta di ingresso? You can configure servers via settings. No comment provided by engineer. - - You can create it in user picker. - No comment provided by engineer. - You can create it later Puoi crearlo più tardi diff --git a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff index 12a34e3569..6b833800c0 100644 --- a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff +++ b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff @@ -1294,11 +1294,6 @@ Change user profiles authentication reason - - Chat archive - チャットのアーカイブ - No comment provided by engineer. - Chat colors No comment provided by engineer. @@ -1845,11 +1840,6 @@ This is your own one-time link! Created at: %@ copied message info - - Created on %@ - %@ によって作成されました - No comment provided by engineer. - Creating archive link No comment provided by engineer. @@ -2054,16 +2044,6 @@ This is your own one-time link! Delete and notify contact No comment provided by engineer. - - Delete archive - アーカイブを削除 - No comment provided by engineer. - - - Delete chat archive? - チャットのアーカイブを削除しますか? - No comment provided by engineer. - Delete chat profile チャットのプロフィールを削除する @@ -2719,11 +2699,6 @@ This is your own one-time link! 連絡先リクエストの承諾にエラー発生 No comment provided by engineer. - - Error accessing database file - データベースファイルへのアクセスエラー - No comment provided by engineer. - Error adding member(s) メンバー追加にエラー発生 @@ -4393,11 +4368,6 @@ This is your link for group %@! 新しい連絡先: notification - - New database archive - 新しいデータベースのアーカイブ - No comment provided by engineer. - New desktop app! 新しいデスクトップアプリ! @@ -4603,11 +4573,6 @@ This is your link for group %@! 古いデータベース No comment provided by engineer. - - Old database archive - 過去のデータベースアーカイブ - No comment provided by engineer. - One-time invitation link 使い捨ての招待リンク @@ -4754,6 +4719,10 @@ VPN を有効にする必要があります。 Operator server alert title + + Or import archive file + No comment provided by engineer. + Or paste archive link No comment provided by engineer. @@ -5566,11 +5535,6 @@ Enable in *Network & servers* settings. グループプロファイルの保存と更新 No comment provided by engineer. - - Save archive - アーカイブを保存 - No comment provided by engineer. - Save group profile グループプロフィールの保存 @@ -6319,11 +6283,6 @@ Enable in *Network & servers* settings. Stop chat No comment provided by engineer. - - Stop chat to enable database actions - チャットを停止してデータベースアクションを有効にします - No comment provided by engineer. - Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped. データベースのエクスポート、読み込み、削除するにはチャットを閉じてからです。チャットを閉じると送受信ができなくなります。 @@ -6440,6 +6399,10 @@ Enable in *Network & servers* settings. 写真を撮影 No comment provided by engineer. + + Tap Create SimpleX address in the menu to create it later. + No comment provided by engineer. + Tap button ボタンをタップ @@ -7440,10 +7403,6 @@ Repeat join request? You can configure servers via settings. No comment provided by engineer. - - You can create it in user picker. - No comment provided by engineer. - You can create it later 後からでも作成できます diff --git a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff index b7d4260354..603db9d75a 100644 --- a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff @@ -1359,11 +1359,6 @@ Change user profiles authentication reason - - Chat archive - Gesprek archief - No comment provided by engineer. - Chat colors Chat kleuren @@ -1965,11 +1960,6 @@ Dit is uw eigen eenmalige link! Aangemaakt op: %@ copied message info - - Created on %@ - Gemaakt op %@ - No comment provided by engineer. - Creating archive link Archief link maken @@ -2184,16 +2174,6 @@ Dit is uw eigen eenmalige link! Verwijderen en contact op de hoogte stellen No comment provided by engineer. - - Delete archive - Archief verwijderen - No comment provided by engineer. - - - Delete chat archive? - Chat archief verwijderen? - No comment provided by engineer. - Delete chat profile Chatprofiel verwijderen @@ -2891,11 +2871,6 @@ Dit is uw eigen eenmalige link! Fout bij het accepteren van een contactverzoek No comment provided by engineer. - - Error accessing database file - Fout bij toegang tot database bestand - No comment provided by engineer. - Error adding member(s) Fout bij het toevoegen van leden @@ -4690,11 +4665,6 @@ Dit is jouw link voor groep %@! Nieuw contact: notification - - New database archive - Nieuw database archief - No comment provided by engineer. - New desktop app! Nieuwe desktop app! @@ -4910,11 +4880,6 @@ Dit is jouw link voor groep %@! Oude database No comment provided by engineer. - - Old database archive - Oud database archief - No comment provided by engineer. - One-time invitation link Eenmalige uitnodiging link @@ -5065,6 +5030,10 @@ Vereist het inschakelen van VPN. Operator server alert title + + Or import archive file + No comment provided by engineer. + Or paste archive link Of plak de archief link @@ -5948,11 +5917,6 @@ Schakel dit in in *Netwerk en servers*-instellingen. Groep profiel opslaan en bijwerken No comment provided by engineer. - - Save archive - Bewaar archief - No comment provided by engineer. - Save group profile Groep profiel opslaan @@ -6770,11 +6734,6 @@ Schakel dit in in *Netwerk en servers*-instellingen. Stop chat No comment provided by engineer. - - Stop chat to enable database actions - Stop de chat om database acties mogelijk te maken - No comment provided by engineer. - Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped. Stop de chat om de chat database te exporteren, importeren of verwijderen. U kunt geen berichten ontvangen en verzenden terwijl de chat is gestopt. @@ -6900,6 +6859,10 @@ Schakel dit in in *Netwerk en servers*-instellingen. Foto nemen No comment provided by engineer. + + Tap Create SimpleX address in the menu to create it later. + No comment provided by engineer. + Tap button Tik op de knop @@ -7989,10 +7952,6 @@ Deelnameverzoek herhalen? You can configure servers via settings. No comment provided by engineer. - - You can create it in user picker. - No comment provided by engineer. - You can create it later U kan het later maken diff --git a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff index 38e6c8991d..8a772bf470 100644 --- a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff @@ -1354,11 +1354,6 @@ Change user profiles authentication reason - - Chat archive - Archiwum czatu - No comment provided by engineer. - Chat colors Kolory czatu @@ -1960,11 +1955,6 @@ To jest twój jednorazowy link! Utworzony o: %@ copied message info - - Created on %@ - Utworzony w dniu %@ - No comment provided by engineer. - Creating archive link Tworzenie linku archiwum @@ -2178,16 +2168,6 @@ To jest twój jednorazowy link! Usuń i powiadom kontakt No comment provided by engineer. - - Delete archive - Usuń archiwum - No comment provided by engineer. - - - Delete chat archive? - Usunąć archiwum czatu? - No comment provided by engineer. - Delete chat profile Usuń profil czatu @@ -2884,11 +2864,6 @@ To jest twój jednorazowy link! Błąd przyjmowania prośby o kontakt No comment provided by engineer. - - Error accessing database file - Błąd dostępu do pliku bazy danych - No comment provided by engineer. - Error adding member(s) Błąd dodawania członka(ów) @@ -4680,11 +4655,6 @@ To jest twój link do grupy %@! Nowy kontakt: notification - - New database archive - Nowe archiwum bazy danych - No comment provided by engineer. - New desktop app! Nowa aplikacja desktopowa! @@ -4900,11 +4870,6 @@ To jest twój link do grupy %@! Stara baza danych No comment provided by engineer. - - Old database archive - Stare archiwum bazy danych - No comment provided by engineer. - One-time invitation link Jednorazowy link zaproszenia @@ -5055,6 +5020,10 @@ Wymaga włączenia VPN. Operator server alert title + + Or import archive file + No comment provided by engineer. + Or paste archive link Lub wklej link archiwum @@ -5938,11 +5907,6 @@ Włącz w ustawianiach *Sieć i serwery* . Zapisz i zaktualizuj profil grupowy No comment provided by engineer. - - Save archive - Zapisz archiwum - No comment provided by engineer. - Save group profile Zapisz profil grupy @@ -6759,11 +6723,6 @@ Włącz w ustawianiach *Sieć i serwery* . Zatrzymaj czat No comment provided by engineer. - - Stop chat to enable database actions - Zatrzymaj czat, aby umożliwić działania na bazie danych - No comment provided by engineer. - Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped. Zatrzymaj czat, aby wyeksportować, zaimportować lub usunąć bazę danych czatu. Podczas zatrzymania chatu nie będzie można odbierać ani wysyłać wiadomości. @@ -6887,6 +6846,10 @@ Włącz w ustawianiach *Sieć i serwery* . Zrób zdjęcie No comment provided by engineer. + + Tap Create SimpleX address in the menu to create it later. + No comment provided by engineer. + Tap button Naciśnij przycisk @@ -7976,10 +7939,6 @@ Powtórzyć prośbę dołączenia? You can configure servers via settings. No comment provided by engineer. - - You can create it in user picker. - No comment provided by engineer. - You can create it later Możesz go utworzyć później diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff index a36257c392..9b6cbf519e 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff @@ -1360,11 +1360,6 @@ Change user profiles authentication reason - - Chat archive - Архив чата - No comment provided by engineer. - Chat colors Цвета чата @@ -1966,11 +1961,6 @@ This is your own one-time link! Создано: %@ copied message info - - Created on %@ - Дата создания %@ - No comment provided by engineer. - Creating archive link Создание ссылки на архив @@ -2185,16 +2175,6 @@ This is your own one-time link! Удалить и уведомить контакт No comment provided by engineer. - - Delete archive - Удалить архив - No comment provided by engineer. - - - Delete chat archive? - Удалить архив чата? - No comment provided by engineer. - Delete chat profile Удалить профиль чата @@ -2892,11 +2872,6 @@ This is your own one-time link! Ошибка при принятии запроса на соединение No comment provided by engineer. - - Error accessing database file - Ошибка при доступе к данным чата - No comment provided by engineer. - Error adding member(s) Ошибка при добавлении членов группы @@ -4690,11 +4665,6 @@ This is your link for group %@! Новый контакт: notification - - New database archive - Новый архив чата - No comment provided by engineer. - New desktop app! Приложение для компьютера! @@ -4910,11 +4880,6 @@ This is your link for group %@! Предыдущая версия данных чата No comment provided by engineer. - - Old database archive - Старый архив чата - No comment provided by engineer. - One-time invitation link Одноразовая ссылка @@ -5065,6 +5030,10 @@ Requires compatible VPN. Operator server alert title + + Or import archive file + No comment provided by engineer. + Or paste archive link Или вставьте ссылку архива @@ -5948,11 +5917,6 @@ Enable in *Network & servers* settings. Сохранить сообщение и обновить группу No comment provided by engineer. - - Save archive - Сохранить архив - No comment provided by engineer. - Save group profile Сохранить профиль группы @@ -6770,11 +6734,6 @@ Enable in *Network & servers* settings. Остановить чат No comment provided by engineer. - - Stop chat to enable database actions - Остановите чат, чтобы разблокировать операции с архивом чата - No comment provided by engineer. - Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped. Остановите чат, чтобы экспортировать или импортировать архив чата или удалить данные чата. Вы не сможете получать и отправлять сообщения, пока чат остановлен. @@ -6900,6 +6859,10 @@ Enable in *Network & servers* settings. Сделать фото No comment provided by engineer. + + Tap Create SimpleX address in the menu to create it later. + No comment provided by engineer. + Tap button Нажмите кнопку @@ -7989,10 +7952,6 @@ Repeat join request? You can configure servers via settings. No comment provided by engineer. - - You can create it in user picker. - No comment provided by engineer. - You can create it later Вы можете создать его позже diff --git a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff index 6fc740ccea..8066daf54d 100644 --- a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff +++ b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff @@ -1262,11 +1262,6 @@ Change user profiles authentication reason - - Chat archive - ที่เก็บแชทถาวร - No comment provided by engineer. - Chat colors No comment provided by engineer. @@ -1810,11 +1805,6 @@ This is your own one-time link! Created at: %@ copied message info - - Created on %@ - สร้างเมื่อ %@ - No comment provided by engineer. - Creating archive link No comment provided by engineer. @@ -2019,16 +2009,6 @@ This is your own one-time link! Delete and notify contact No comment provided by engineer. - - Delete archive - ลบที่เก็บถาวร - No comment provided by engineer. - - - Delete chat archive? - ลบที่เก็บแชทถาวร? - No comment provided by engineer. - Delete chat profile ลบโปรไฟล์แชท @@ -2680,11 +2660,6 @@ This is your own one-time link! เกิดข้อผิดพลาดในการรับคำขอติดต่อ No comment provided by engineer. - - Error accessing database file - เกิดข้อผิดพลาดในการเข้าถึงไฟล์ฐานข้อมูล - No comment provided by engineer. - Error adding member(s) เกิดข้อผิดพลาดในการเพิ่มสมาชิก @@ -4349,11 +4324,6 @@ This is your link for group %@! คำขอติดต่อใหม่: notification - - New database archive - ฐานข้อมูลใหม่สำหรับการเก็บถาวร - No comment provided by engineer. - New desktop app! No comment provided by engineer. @@ -4557,11 +4527,6 @@ This is your link for group %@! ฐานข้อมูลเก่า No comment provided by engineer. - - Old database archive - คลังฐานข้อมูลเก่า - No comment provided by engineer. - One-time invitation link ลิงก์คำเชิญแบบใช้ครั้งเดียว @@ -4705,6 +4670,10 @@ Requires compatible VPN. Operator server alert title + + Or import archive file + No comment provided by engineer. + Or paste archive link No comment provided by engineer. @@ -5516,11 +5485,6 @@ Enable in *Network & servers* settings. บันทึกและอัปเดตโปรไฟล์กลุ่ม No comment provided by engineer. - - Save archive - บันทึกไฟล์เก็บถาวร - No comment provided by engineer. - Save group profile บันทึกโปรไฟล์กลุ่ม @@ -6271,11 +6235,6 @@ Enable in *Network & servers* settings. Stop chat No comment provided by engineer. - - Stop chat to enable database actions - หยุดการแชทเพื่อเปิดใช้งานการดำเนินการกับฐานข้อมูล - No comment provided by engineer. - Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped. หยุดแชทเพื่อส่งออก นำเข้า หรือลบฐานข้อมูลแชท คุณจะไม่สามารถรับและส่งข้อความได้ในขณะที่การแชทหยุดลง @@ -6392,6 +6351,10 @@ Enable in *Network & servers* settings. ถ่ายภาพ No comment provided by engineer. + + Tap Create SimpleX address in the menu to create it later. + No comment provided by engineer. + Tap button แตะปุ่ม @@ -7390,10 +7353,6 @@ Repeat join request? You can configure servers via settings. No comment provided by engineer. - - You can create it in user picker. - No comment provided by engineer. - You can create it later คุณสามารถสร้างได้ในภายหลัง diff --git a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff index 9c9fe3e253..f578a6225d 100644 --- a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff @@ -1359,11 +1359,6 @@ Change user profiles authentication reason - - Chat archive - Sohbet arşivi - No comment provided by engineer. - Chat colors Sohbet renkleri @@ -1965,11 +1960,6 @@ Bu senin kendi tek kullanımlık bağlantın! Şurada oluşturuldu: %@ copied message info - - Created on %@ - %@ de oluşturuldu - No comment provided by engineer. - Creating archive link Arşiv bağlantısı oluşturuluyor @@ -2184,16 +2174,6 @@ Bu senin kendi tek kullanımlık bağlantın! Sil ve kişiye bildir No comment provided by engineer. - - Delete archive - Arşivi sil - No comment provided by engineer. - - - Delete chat archive? - Sohbet arşivi silinsin mi? - No comment provided by engineer. - Delete chat profile Sohbet profilini sil @@ -2891,11 +2871,6 @@ Bu senin kendi tek kullanımlık bağlantın! Bağlantı isteği kabul edilirken hata oluştu No comment provided by engineer. - - Error accessing database file - Veritabanı dosyasına erişilirken hata oluştu - No comment provided by engineer. - Error adding member(s) Üye(ler) eklenirken hata oluştu @@ -4690,11 +4665,6 @@ Bu senin grup için bağlantın %@! Yeni kişi: notification - - New database archive - Yeni veritabanı arşivi - No comment provided by engineer. - New desktop app! Yeni bilgisayar uygulaması! @@ -4910,11 +4880,6 @@ Bu senin grup için bağlantın %@! Eski veritabanı No comment provided by engineer. - - Old database archive - Eski veritabanı arşivi - No comment provided by engineer. - One-time invitation link Tek zamanlı bağlantı daveti @@ -5065,6 +5030,10 @@ VPN'nin etkinleştirilmesi gerekir. Operator server alert title + + Or import archive file + No comment provided by engineer. + Or paste archive link Veya arşiv bağlantısını yapıştırın @@ -5948,11 +5917,6 @@ Enable in *Network & servers* settings. Kaydet ve grup profilini güncelle No comment provided by engineer. - - Save archive - Arşivi kaydet - No comment provided by engineer. - Save group profile Grup profilini kaydet @@ -6770,11 +6734,6 @@ Enable in *Network & servers* settings. Sohbeti kes No comment provided by engineer. - - Stop chat to enable database actions - Veritabanı eylemlerini etkinleştirmek için sohbeti durdur - No comment provided by engineer. - Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped. Sohbet veritabanını dışa aktarmak, içe aktarmak veya silmek için sohbeti durdurun. Sohbet durdurulduğunda mesaj alamaz ve gönderemezsiniz. @@ -6900,6 +6859,10 @@ Enable in *Network & servers* settings. Fotoğraf çek No comment provided by engineer. + + Tap Create SimpleX address in the menu to create it later. + No comment provided by engineer. + Tap button Tuşa bas @@ -7989,10 +7952,6 @@ Katılma isteği tekrarlansın mı? You can configure servers via settings. No comment provided by engineer. - - You can create it in user picker. - No comment provided by engineer. - You can create it later Daha sonra oluşturabilirsiniz diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff index c4beadaf66..136f45830b 100644 --- a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff @@ -1349,11 +1349,6 @@ Change user profiles authentication reason - - Chat archive - Архів чату - No comment provided by engineer. - Chat colors Кольори чату @@ -1953,11 +1948,6 @@ This is your own one-time link! Створено за адресою: %@ copied message info - - Created on %@ - Створено %@ - No comment provided by engineer. - Creating archive link Створення архівного посилання @@ -2171,16 +2161,6 @@ This is your own one-time link! Видалити та повідомити контакт No comment provided by engineer. - - Delete archive - Видалити архів - No comment provided by engineer. - - - Delete chat archive? - Видалити архів чату? - No comment provided by engineer. - Delete chat profile Видалити профіль чату @@ -2875,11 +2855,6 @@ This is your own one-time link! Помилка при прийнятті запиту на контакт No comment provided by engineer. - - Error accessing database file - Помилка доступу до файлу бази даних - No comment provided by engineer. - Error adding member(s) Помилка додавання користувача(ів) @@ -4656,11 +4631,6 @@ This is your link for group %@! Новий контакт: notification - - New database archive - Новий архів бази даних - No comment provided by engineer. - New desktop app! Новий десктопний додаток! @@ -4873,11 +4843,6 @@ This is your link for group %@! Стара база даних No comment provided by engineer. - - Old database archive - Старий архів бази даних - No comment provided by engineer. - One-time invitation link Посилання на одноразове запрошення @@ -5028,6 +4993,10 @@ Requires compatible VPN. Operator server alert title + + Or import archive file + No comment provided by engineer. + Or paste archive link Або вставте посилання на архів @@ -5904,11 +5873,6 @@ Enable in *Network & servers* settings. Збереження та оновлення профілю групи No comment provided by engineer. - - Save archive - Зберегти архів - No comment provided by engineer. - Save group profile Зберегти профіль групи @@ -6718,11 +6682,6 @@ Enable in *Network & servers* settings. Припинити чат No comment provided by engineer. - - Stop chat to enable database actions - Зупиніть чат, щоб увімкнути дії з базою даних - No comment provided by engineer. - Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped. Зупиніть чат, щоб експортувати, імпортувати або видалити базу даних чату. Ви не зможете отримувати та надсилати повідомлення, поки чат зупинено. @@ -6845,6 +6804,10 @@ Enable in *Network & servers* settings. Сфотографуйте No comment provided by engineer. + + Tap Create SimpleX address in the menu to create it later. + No comment provided by engineer. + Tap button Натисніть кнопку @@ -7929,10 +7892,6 @@ Repeat join request? You can configure servers via settings. No comment provided by engineer. - - You can create it in user picker. - No comment provided by engineer. - You can create it later Ви можете створити його пізніше diff --git a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff index 1daae62a6d..1663530290 100644 --- a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff +++ b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff @@ -1346,11 +1346,6 @@ Change user profiles authentication reason - - Chat archive - 聊天档案 - No comment provided by engineer. - Chat colors 聊天颜色 @@ -1950,11 +1945,6 @@ This is your own one-time link! 创建于:%@ copied message info - - Created on %@ - 创建于 %@ - No comment provided by engineer. - Creating archive link 正在创建存档链接 @@ -2168,16 +2158,6 @@ This is your own one-time link! 删除并通知联系人 No comment provided by engineer. - - Delete archive - 删除档案 - No comment provided by engineer. - - - Delete chat archive? - 删除聊天档案? - No comment provided by engineer. - Delete chat profile 删除聊天资料 @@ -2872,11 +2852,6 @@ This is your own one-time link! 接受联系人请求错误 No comment provided by engineer. - - Error accessing database file - 访问数据库文件错误 - No comment provided by engineer. - Error adding member(s) 添加成员错误 @@ -4653,11 +4628,6 @@ This is your link for group %@! 新联系人: notification - - New database archive - 新数据库存档 - No comment provided by engineer. - New desktop app! 全新桌面应用! @@ -4870,11 +4840,6 @@ This is your link for group %@! 旧的数据库 No comment provided by engineer. - - Old database archive - 旧数据库存档 - No comment provided by engineer. - One-time invitation link 一次性邀请链接 @@ -5025,6 +4990,10 @@ Requires compatible VPN. Operator server alert title + + Or import archive file + No comment provided by engineer. + Or paste archive link 或粘贴存档链接 @@ -5901,11 +5870,6 @@ Enable in *Network & servers* settings. 保存和更新组配置文件 No comment provided by engineer. - - Save archive - 保存存档 - No comment provided by engineer. - Save group profile 保存群组资料 @@ -6715,11 +6679,6 @@ Enable in *Network & servers* settings. 停止聊天程序 No comment provided by engineer. - - Stop chat to enable database actions - 停止聊天以启用数据库操作 - No comment provided by engineer. - Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped. 停止聊天以便导出、导入或删除聊天数据库。在聊天停止期间,您将无法收发消息。 @@ -6842,6 +6801,10 @@ Enable in *Network & servers* settings. 拍照 No comment provided by engineer. + + Tap Create SimpleX address in the menu to create it later. + No comment provided by engineer. + Tap button 点击按钮 @@ -7926,10 +7889,6 @@ Repeat join request? You can configure servers via settings. No comment provided by engineer. - - You can create it in user picker. - No comment provided by engineer. - You can create it later 您可以以后创建它 diff --git a/apps/ios/bg.lproj/Localizable.strings b/apps/ios/bg.lproj/Localizable.strings index 5734fc58cc..aa955d7a7a 100644 --- a/apps/ios/bg.lproj/Localizable.strings +++ b/apps/ios/bg.lproj/Localizable.strings @@ -741,9 +741,6 @@ /* chat item text */ "changing address…" = "промяна на адреса…"; -/* No comment provided by engineer. */ -"Chat archive" = "Архив на чата"; - /* No comment provided by engineer. */ "Chat console" = "Конзола"; @@ -1035,9 +1032,6 @@ /* copied message info */ "Created at: %@" = "Създаден на: %@"; -/* No comment provided by engineer. */ -"Created on %@" = "Създаден на %@"; - /* No comment provided by engineer. */ "Creating archive link" = "Създаване на архивен линк"; @@ -1163,12 +1157,6 @@ /* No comment provided by engineer. */ "Delete and notify contact" = "Изтрий и уведоми контакт"; -/* No comment provided by engineer. */ -"Delete archive" = "Изтрий архив"; - -/* No comment provided by engineer. */ -"Delete chat archive?" = "Изтриване на архива на чата?"; - /* No comment provided by engineer. */ "Delete chat profile" = "Изтрий чат профила"; @@ -1581,9 +1569,6 @@ /* No comment provided by engineer. */ "Error accepting contact request" = "Грешка при приемане на заявка за контакт"; -/* No comment provided by engineer. */ -"Error accessing database file" = "Грешка при достъпа до файла с базата данни"; - /* No comment provided by engineer. */ "Error adding member(s)" = "Грешка при добавяне на член(ове)"; @@ -2544,9 +2529,6 @@ /* notification */ "New contact:" = "Нов контакт:"; -/* No comment provided by engineer. */ -"New database archive" = "Нов архив на база данни"; - /* No comment provided by engineer. */ "New desktop app!" = "Ново настолно приложение!"; @@ -2660,9 +2642,6 @@ /* No comment provided by engineer. */ "Old database" = "Стара база данни"; -/* No comment provided by engineer. */ -"Old database archive" = "Стар архив на база данни"; - /* group pref value */ "on" = "включено"; @@ -3169,9 +3148,6 @@ /* No comment provided by engineer. */ "Save and update group profile" = "Запази и актуализирай профила на групата"; -/* No comment provided by engineer. */ -"Save archive" = "Запази архив"; - /* No comment provided by engineer. */ "Save group profile" = "Запази профила на групата"; @@ -3536,9 +3512,6 @@ /* No comment provided by engineer. */ "Stop chat" = "Спри чата"; -/* No comment provided by engineer. */ -"Stop chat to enable database actions" = "Спрете чата, за да активирате действията с базата данни"; - /* No comment provided by engineer. */ "Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." = "Спрете чата, за да експортирате, импортирате или изтриете чат базата данни. Няма да можете да получавате и изпращате съобщения, докато чатът е спрян."; diff --git a/apps/ios/cs.lproj/Localizable.strings b/apps/ios/cs.lproj/Localizable.strings index 462988855b..9e96aafcfd 100644 --- a/apps/ios/cs.lproj/Localizable.strings +++ b/apps/ios/cs.lproj/Localizable.strings @@ -603,9 +603,6 @@ /* chat item text */ "changing address…" = "změna adresy…"; -/* No comment provided by engineer. */ -"Chat archive" = "Chat se archivuje"; - /* No comment provided by engineer. */ "Chat console" = "Konzola pro chat"; @@ -822,9 +819,6 @@ /* No comment provided by engineer. */ "Create your profile" = "Vytvořte si profil"; -/* No comment provided by engineer. */ -"Created on %@" = "Vytvořeno na %@"; - /* No comment provided by engineer. */ "creator" = "tvůrce"; @@ -938,12 +932,6 @@ /* No comment provided by engineer. */ "Delete all files" = "Odstranit všechny soubory"; -/* No comment provided by engineer. */ -"Delete archive" = "Smazat archiv"; - -/* No comment provided by engineer. */ -"Delete chat archive?" = "Smazat archiv chatu?"; - /* No comment provided by engineer. */ "Delete chat profile" = "Smazat chat profil"; @@ -1289,9 +1277,6 @@ /* No comment provided by engineer. */ "Error accepting contact request" = "Chyba při přijímání žádosti o kontakt"; -/* No comment provided by engineer. */ -"Error accessing database file" = "Chyba přístupu k souboru databáze"; - /* No comment provided by engineer. */ "Error adding member(s)" = "Chyba přidávání člena(ů)"; @@ -2054,9 +2039,6 @@ /* notification */ "New contact:" = "Nový kontakt:"; -/* No comment provided by engineer. */ -"New database archive" = "Archiv nové databáze"; - /* No comment provided by engineer. */ "New desktop app!" = "Nová desktopová aplikace!"; @@ -2161,9 +2143,6 @@ /* No comment provided by engineer. */ "Old database" = "Stará databáze"; -/* No comment provided by engineer. */ -"Old database archive" = "Archiv staré databáze"; - /* group pref value */ "on" = "zapnuto"; @@ -2568,9 +2547,6 @@ /* No comment provided by engineer. */ "Save and update group profile" = "Uložit a aktualizovat profil skupiny"; -/* No comment provided by engineer. */ -"Save archive" = "Uložit archiv"; - /* No comment provided by engineer. */ "Save group profile" = "Uložení profilu skupiny"; @@ -2869,9 +2845,6 @@ /* No comment provided by engineer. */ "Stop" = "Zastavit"; -/* No comment provided by engineer. */ -"Stop chat to enable database actions" = "Zastavte chat pro povolení akcí databáze"; - /* No comment provided by engineer. */ "Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." = "Zastavení chatu pro export, import nebo smazání databáze chatu. Během zastavení chatu nebudete moci přijímat a odesílat zprávy."; diff --git a/apps/ios/de.lproj/Localizable.strings b/apps/ios/de.lproj/Localizable.strings index 312cab136a..f680727010 100644 --- a/apps/ios/de.lproj/Localizable.strings +++ b/apps/ios/de.lproj/Localizable.strings @@ -870,9 +870,6 @@ /* chat item text */ "changing address…" = "Wechsel der Empfängeradresse wurde gestartet…"; -/* No comment provided by engineer. */ -"Chat archive" = "Datenbank Archiv"; - /* No comment provided by engineer. */ "Chat colors" = "Chat-Farben"; @@ -1251,9 +1248,6 @@ /* copied message info */ "Created at: %@" = "Erstellt um: %@"; -/* No comment provided by engineer. */ -"Created on %@" = "Erstellt am %@"; - /* No comment provided by engineer. */ "Creating archive link" = "Archiv-Link erzeugen"; @@ -1400,12 +1394,6 @@ /* No comment provided by engineer. */ "Delete and notify contact" = "Kontakt löschen und benachrichtigen"; -/* No comment provided by engineer. */ -"Delete archive" = "Archiv löschen"; - -/* No comment provided by engineer. */ -"Delete chat archive?" = "Chat Archiv löschen?"; - /* No comment provided by engineer. */ "Delete chat profile" = "Chat-Profil löschen"; @@ -1884,9 +1872,6 @@ /* No comment provided by engineer. */ "Error accepting contact request" = "Fehler beim Annehmen der Kontaktanfrage"; -/* No comment provided by engineer. */ -"Error accessing database file" = "Fehler beim Zugriff auf die Datenbankdatei"; - /* No comment provided by engineer. */ "Error adding member(s)" = "Fehler beim Hinzufügen von Mitgliedern"; @@ -3024,9 +3009,6 @@ /* notification */ "New contact:" = "Neuer Kontakt:"; -/* No comment provided by engineer. */ -"New database archive" = "Neues Datenbankarchiv"; - /* No comment provided by engineer. */ "New desktop app!" = "Neue Desktop-App!"; @@ -3167,9 +3149,6 @@ /* No comment provided by engineer. */ "Old database" = "Alte Datenbank"; -/* No comment provided by engineer. */ -"Old database archive" = "Altes Datenbankarchiv"; - /* group pref value */ "on" = "Ein"; @@ -3796,9 +3775,6 @@ /* No comment provided by engineer. */ "Save and update group profile" = "Gruppen-Profil sichern und aktualisieren"; -/* No comment provided by engineer. */ -"Save archive" = "Archiv speichern"; - /* No comment provided by engineer. */ "Save group profile" = "Gruppenprofil speichern"; @@ -4307,9 +4283,6 @@ /* No comment provided by engineer. */ "Stop chat" = "Chat beenden"; -/* No comment provided by engineer. */ -"Stop chat to enable database actions" = "Chat beenden, um Datenbankaktionen zu erlauben"; - /* No comment provided by engineer. */ "Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." = "Beenden Sie den Chat, um die Chat-Datenbank zu exportieren, zu importieren oder zu löschen. Solange der Chat angehalten ist, können Sie keine Nachrichten empfangen oder senden."; diff --git a/apps/ios/es.lproj/Localizable.strings b/apps/ios/es.lproj/Localizable.strings index 9f775acb54..103065a05a 100644 --- a/apps/ios/es.lproj/Localizable.strings +++ b/apps/ios/es.lproj/Localizable.strings @@ -870,9 +870,6 @@ /* chat item text */ "changing address…" = "cambiando de servidor…"; -/* No comment provided by engineer. */ -"Chat archive" = "Archivo del chat"; - /* No comment provided by engineer. */ "Chat colors" = "Colores del chat"; @@ -1251,9 +1248,6 @@ /* copied message info */ "Created at: %@" = "Creado: %@"; -/* No comment provided by engineer. */ -"Created on %@" = "Creado en %@"; - /* No comment provided by engineer. */ "Creating archive link" = "Creando enlace al archivo"; @@ -1400,12 +1394,6 @@ /* No comment provided by engineer. */ "Delete and notify contact" = "Eliminar y notificar contacto"; -/* No comment provided by engineer. */ -"Delete archive" = "Eliminar archivo"; - -/* No comment provided by engineer. */ -"Delete chat archive?" = "¿Eliminar archivo del chat?"; - /* No comment provided by engineer. */ "Delete chat profile" = "Eliminar perfil"; @@ -1884,9 +1872,6 @@ /* No comment provided by engineer. */ "Error accepting contact request" = "Error al aceptar solicitud del contacto"; -/* No comment provided by engineer. */ -"Error accessing database file" = "Error al acceder al archivo de la base de datos"; - /* No comment provided by engineer. */ "Error adding member(s)" = "Error al añadir miembro(s)"; @@ -3024,9 +3009,6 @@ /* notification */ "New contact:" = "Contacto nuevo:"; -/* No comment provided by engineer. */ -"New database archive" = "Nuevo archivo de bases de datos"; - /* No comment provided by engineer. */ "New desktop app!" = "Nueva aplicación para PC!"; @@ -3164,9 +3146,6 @@ /* No comment provided by engineer. */ "Old database" = "Base de datos antigua"; -/* No comment provided by engineer. */ -"Old database archive" = "Archivo de bases de datos antiguas"; - /* group pref value */ "on" = "Activado"; @@ -3793,9 +3772,6 @@ /* No comment provided by engineer. */ "Save and update group profile" = "Guardar y actualizar perfil del grupo"; -/* No comment provided by engineer. */ -"Save archive" = "Guardar archivo"; - /* No comment provided by engineer. */ "Save group profile" = "Guardar perfil de grupo"; @@ -4304,9 +4280,6 @@ /* No comment provided by engineer. */ "Stop chat" = "Parar SimpleX"; -/* No comment provided by engineer. */ -"Stop chat to enable database actions" = "Para habilitar las acciones sobre la base de datos, debes parar SimpleX"; - /* No comment provided by engineer. */ "Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." = "Para poder exportar, importar o eliminar la base de datos primero debes parar SimpleX. Mientras tanto no podrás recibir ni enviar mensajes."; diff --git a/apps/ios/fi.lproj/Localizable.strings b/apps/ios/fi.lproj/Localizable.strings index f927dbdabb..6f28ddd3b0 100644 --- a/apps/ios/fi.lproj/Localizable.strings +++ b/apps/ios/fi.lproj/Localizable.strings @@ -588,9 +588,6 @@ /* chat item text */ "changing address…" = "muuttamassa osoitetta…"; -/* No comment provided by engineer. */ -"Chat archive" = "Chat-arkisto"; - /* No comment provided by engineer. */ "Chat console" = "Chat-konsoli"; @@ -804,9 +801,6 @@ /* No comment provided by engineer. */ "Create your profile" = "Luo profiilisi"; -/* No comment provided by engineer. */ -"Created on %@" = "Luotu %@"; - /* No comment provided by engineer. */ "creator" = "luoja"; @@ -920,12 +914,6 @@ /* No comment provided by engineer. */ "Delete all files" = "Poista kaikki tiedostot"; -/* No comment provided by engineer. */ -"Delete archive" = "Poista arkisto"; - -/* No comment provided by engineer. */ -"Delete chat archive?" = "Poista keskusteluarkisto?"; - /* No comment provided by engineer. */ "Delete chat profile" = "Poista keskusteluprofiili"; @@ -1268,9 +1256,6 @@ /* No comment provided by engineer. */ "Error accepting contact request" = "Virhe kontaktipyynnön hyväksymisessä"; -/* No comment provided by engineer. */ -"Error accessing database file" = "Virhe tietokantatiedoston käyttämisessä"; - /* No comment provided by engineer. */ "Error adding member(s)" = "Virhe lisättäessä jäseniä"; @@ -2030,9 +2015,6 @@ /* notification */ "New contact:" = "Uusi kontakti:"; -/* No comment provided by engineer. */ -"New database archive" = "Uusi tietokanta-arkisto"; - /* No comment provided by engineer. */ "New display name" = "Uusi näyttönimi"; @@ -2134,9 +2116,6 @@ /* No comment provided by engineer. */ "Old database" = "Vanha tietokanta"; -/* No comment provided by engineer. */ -"Old database archive" = "Vanha tietokanta-arkisto"; - /* group pref value */ "on" = "päällä"; @@ -2538,9 +2517,6 @@ /* No comment provided by engineer. */ "Save and update group profile" = "Tallenna ja päivitä ryhmäprofiili"; -/* No comment provided by engineer. */ -"Save archive" = "Tallenna arkisto"; - /* No comment provided by engineer. */ "Save group profile" = "Tallenna ryhmäprofiili"; @@ -2830,9 +2806,6 @@ /* No comment provided by engineer. */ "Stop" = "Lopeta"; -/* No comment provided by engineer. */ -"Stop chat to enable database actions" = "Pysäytä keskustelu tietokantatoimien mahdollistamiseksi"; - /* No comment provided by engineer. */ "Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." = "Pysäytä keskustelut viedäksesi, tuodaksesi tai poistaaksesi keskustelujen tietokannan. Et voi vastaanottaa ja lähettää viestejä, kun keskustelut on pysäytetty."; diff --git a/apps/ios/fr.lproj/Localizable.strings b/apps/ios/fr.lproj/Localizable.strings index 239d425973..273fb76d6e 100644 --- a/apps/ios/fr.lproj/Localizable.strings +++ b/apps/ios/fr.lproj/Localizable.strings @@ -870,9 +870,6 @@ /* chat item text */ "changing address…" = "changement d'adresse…"; -/* No comment provided by engineer. */ -"Chat archive" = "Archives du chat"; - /* No comment provided by engineer. */ "Chat colors" = "Couleurs de chat"; @@ -1251,9 +1248,6 @@ /* copied message info */ "Created at: %@" = "Créé à : %@"; -/* No comment provided by engineer. */ -"Created on %@" = "Créé le %@"; - /* No comment provided by engineer. */ "Creating archive link" = "Création d'un lien d'archive"; @@ -1400,12 +1394,6 @@ /* No comment provided by engineer. */ "Delete and notify contact" = "Supprimer et en informer le contact"; -/* No comment provided by engineer. */ -"Delete archive" = "Supprimer l'archive"; - -/* No comment provided by engineer. */ -"Delete chat archive?" = "Supprimer l'archive du chat ?"; - /* No comment provided by engineer. */ "Delete chat profile" = "Supprimer le profil de chat"; @@ -1884,9 +1872,6 @@ /* No comment provided by engineer. */ "Error accepting contact request" = "Erreur de validation de la demande de contact"; -/* No comment provided by engineer. */ -"Error accessing database file" = "Erreur d'accès au fichier de la base de données"; - /* No comment provided by engineer. */ "Error adding member(s)" = "Erreur lors de l'ajout de membre·s"; @@ -3024,9 +3009,6 @@ /* notification */ "New contact:" = "Nouveau contact :"; -/* No comment provided by engineer. */ -"New database archive" = "Nouvelle archive de base de données"; - /* No comment provided by engineer. */ "New desktop app!" = "Nouvelle application de bureau !"; @@ -3167,9 +3149,6 @@ /* No comment provided by engineer. */ "Old database" = "Ancienne base de données"; -/* No comment provided by engineer. */ -"Old database archive" = "Archives de l'ancienne base de données"; - /* group pref value */ "on" = "on"; @@ -3796,9 +3775,6 @@ /* No comment provided by engineer. */ "Save and update group profile" = "Enregistrer et mettre à jour le profil du groupe"; -/* No comment provided by engineer. */ -"Save archive" = "Enregistrer l'archive"; - /* No comment provided by engineer. */ "Save group profile" = "Enregistrer le profil du groupe"; @@ -4307,9 +4283,6 @@ /* No comment provided by engineer. */ "Stop chat" = "Arrêter le chat"; -/* No comment provided by engineer. */ -"Stop chat to enable database actions" = "Arrêter le chat pour permettre des actions sur la base de données"; - /* No comment provided by engineer. */ "Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." = "Arrêtez le chat pour exporter, importer ou supprimer la base de données du chat. Vous ne pourrez pas recevoir et envoyer de messages pendant que le chat est arrêté."; diff --git a/apps/ios/hu.lproj/Localizable.strings b/apps/ios/hu.lproj/Localizable.strings index 1704389267..b64c75fd1d 100644 --- a/apps/ios/hu.lproj/Localizable.strings +++ b/apps/ios/hu.lproj/Localizable.strings @@ -870,9 +870,6 @@ /* chat item text */ "changing address…" = "cím megváltoztatása…"; -/* No comment provided by engineer. */ -"Chat archive" = "Csevegési archívum"; - /* No comment provided by engineer. */ "Chat colors" = "Csevegés színei"; @@ -1251,9 +1248,6 @@ /* copied message info */ "Created at: %@" = "Létrehozva ekkor: %@"; -/* No comment provided by engineer. */ -"Created on %@" = "Létrehozva %@"; - /* No comment provided by engineer. */ "Creating archive link" = "Archívum hivatkozás létrehozása"; @@ -1400,12 +1394,6 @@ /* No comment provided by engineer. */ "Delete and notify contact" = "Törlés, és az ismerős értesítése"; -/* No comment provided by engineer. */ -"Delete archive" = "Archívum törlése"; - -/* No comment provided by engineer. */ -"Delete chat archive?" = "Csevegési archívum törlése?"; - /* No comment provided by engineer. */ "Delete chat profile" = "Csevegési profil törlése"; @@ -1884,9 +1872,6 @@ /* No comment provided by engineer. */ "Error accepting contact request" = "Hiba történt a kapcsolatkérés elfogadásakor"; -/* No comment provided by engineer. */ -"Error accessing database file" = "Hiba az adatbázisfájl elérésekor"; - /* No comment provided by engineer. */ "Error adding member(s)" = "Hiba a tag(ok) hozzáadásakor"; @@ -3024,9 +3009,6 @@ /* notification */ "New contact:" = "Új kapcsolat:"; -/* No comment provided by engineer. */ -"New database archive" = "Új adatbázis-archívum"; - /* No comment provided by engineer. */ "New desktop app!" = "Új számítógép-alkalmazás!"; @@ -3167,9 +3149,6 @@ /* No comment provided by engineer. */ "Old database" = "Régi adatbázis"; -/* No comment provided by engineer. */ -"Old database archive" = "Régi adatbázis-archívum"; - /* group pref value */ "on" = "bekapcsolva"; @@ -3796,9 +3775,6 @@ /* No comment provided by engineer. */ "Save and update group profile" = "Mentés és a csoportprofil frissítése"; -/* No comment provided by engineer. */ -"Save archive" = "Archívum mentése"; - /* No comment provided by engineer. */ "Save group profile" = "Csoportprofil mentése"; @@ -4307,9 +4283,6 @@ /* No comment provided by engineer. */ "Stop chat" = "Csevegési szolgáltatás megállítása"; -/* No comment provided by engineer. */ -"Stop chat to enable database actions" = "Csevegés megállítása az adatbázis-műveletek engedélyezéséhez"; - /* No comment provided by engineer. */ "Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." = "A csevegés megállítása a csevegési adatbázis exportálásához, importálásához vagy törléséhez. A csevegés megállításakor nem tud üzeneteket fogadni és küldeni."; diff --git a/apps/ios/it.lproj/Localizable.strings b/apps/ios/it.lproj/Localizable.strings index c041228706..06d78256c7 100644 --- a/apps/ios/it.lproj/Localizable.strings +++ b/apps/ios/it.lproj/Localizable.strings @@ -870,9 +870,6 @@ /* chat item text */ "changing address…" = "cambio indirizzo…"; -/* No comment provided by engineer. */ -"Chat archive" = "Archivio chat"; - /* No comment provided by engineer. */ "Chat colors" = "Colori della chat"; @@ -1251,9 +1248,6 @@ /* copied message info */ "Created at: %@" = "Creato il: %@"; -/* No comment provided by engineer. */ -"Created on %@" = "Creato il %@"; - /* No comment provided by engineer. */ "Creating archive link" = "Creazione link dell'archivio"; @@ -1400,12 +1394,6 @@ /* No comment provided by engineer. */ "Delete and notify contact" = "Elimina e avvisa il contatto"; -/* No comment provided by engineer. */ -"Delete archive" = "Elimina archivio"; - -/* No comment provided by engineer. */ -"Delete chat archive?" = "Eliminare l'archivio della chat?"; - /* No comment provided by engineer. */ "Delete chat profile" = "Elimina il profilo di chat"; @@ -1884,9 +1872,6 @@ /* No comment provided by engineer. */ "Error accepting contact request" = "Errore nell'accettazione della richiesta di contatto"; -/* No comment provided by engineer. */ -"Error accessing database file" = "Errore nell'accesso al file del database"; - /* No comment provided by engineer. */ "Error adding member(s)" = "Errore di aggiunta membro/i"; @@ -3024,9 +3009,6 @@ /* notification */ "New contact:" = "Nuovo contatto:"; -/* No comment provided by engineer. */ -"New database archive" = "Nuovo archivio database"; - /* No comment provided by engineer. */ "New desktop app!" = "Nuova app desktop!"; @@ -3167,9 +3149,6 @@ /* No comment provided by engineer. */ "Old database" = "Database vecchio"; -/* No comment provided by engineer. */ -"Old database archive" = "Vecchio archivio del database"; - /* group pref value */ "on" = "on"; @@ -3796,9 +3775,6 @@ /* No comment provided by engineer. */ "Save and update group profile" = "Salva e aggiorna il profilo del gruppo"; -/* No comment provided by engineer. */ -"Save archive" = "Salva archivio"; - /* No comment provided by engineer. */ "Save group profile" = "Salva il profilo del gruppo"; @@ -4307,9 +4283,6 @@ /* No comment provided by engineer. */ "Stop chat" = "Ferma la chat"; -/* No comment provided by engineer. */ -"Stop chat to enable database actions" = "Ferma la chat per attivare le azioni del database"; - /* No comment provided by engineer. */ "Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." = "Ferma la chat per esportare, importare o eliminare il database della chat. Non potrai ricevere e inviare messaggi mentre la chat è ferma."; diff --git a/apps/ios/ja.lproj/Localizable.strings b/apps/ios/ja.lproj/Localizable.strings index d8756cc788..7e1d7f0527 100644 --- a/apps/ios/ja.lproj/Localizable.strings +++ b/apps/ios/ja.lproj/Localizable.strings @@ -660,9 +660,6 @@ /* chat item text */ "changing address…" = "アドレスを変更しています…"; -/* No comment provided by engineer. */ -"Chat archive" = "チャットのアーカイブ"; - /* No comment provided by engineer. */ "Chat console" = "チャットのコンソール"; @@ -876,9 +873,6 @@ /* No comment provided by engineer. */ "Create your profile" = "プロフィールを作成する"; -/* No comment provided by engineer. */ -"Created on %@" = "%@ によって作成されました"; - /* No comment provided by engineer. */ "creator" = "作成者"; @@ -992,12 +986,6 @@ /* No comment provided by engineer. */ "Delete all files" = "ファイルを全て削除"; -/* No comment provided by engineer. */ -"Delete archive" = "アーカイブを削除"; - -/* No comment provided by engineer. */ -"Delete chat archive?" = "チャットのアーカイブを削除しますか?"; - /* No comment provided by engineer. */ "Delete chat profile" = "チャットのプロフィールを削除する"; @@ -1343,9 +1331,6 @@ /* No comment provided by engineer. */ "Error accepting contact request" = "連絡先リクエストの承諾にエラー発生"; -/* No comment provided by engineer. */ -"Error accessing database file" = "データベースファイルへのアクセスエラー"; - /* No comment provided by engineer. */ "Error adding member(s)" = "メンバー追加にエラー発生"; @@ -2108,9 +2093,6 @@ /* notification */ "New contact:" = "新しい連絡先:"; -/* No comment provided by engineer. */ -"New database archive" = "新しいデータベースのアーカイブ"; - /* No comment provided by engineer. */ "New desktop app!" = "新しいデスクトップアプリ!"; @@ -2215,9 +2197,6 @@ /* No comment provided by engineer. */ "Old database" = "古いデータベース"; -/* No comment provided by engineer. */ -"Old database archive" = "過去のデータベースアーカイブ"; - /* group pref value */ "on" = "オン"; @@ -2619,9 +2598,6 @@ /* No comment provided by engineer. */ "Save and update group profile" = "グループプロファイルの保存と更新"; -/* No comment provided by engineer. */ -"Save archive" = "アーカイブを保存"; - /* No comment provided by engineer. */ "Save group profile" = "グループプロフィールの保存"; @@ -2893,9 +2869,6 @@ /* No comment provided by engineer. */ "Stop" = "停止"; -/* No comment provided by engineer. */ -"Stop chat to enable database actions" = "チャットを停止してデータベースアクションを有効にします"; - /* No comment provided by engineer. */ "Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." = "データベースのエクスポート、読み込み、削除するにはチャットを閉じてからです。チャットを閉じると送受信ができなくなります。"; diff --git a/apps/ios/nl.lproj/Localizable.strings b/apps/ios/nl.lproj/Localizable.strings index 4729e50d46..aa324a2ee0 100644 --- a/apps/ios/nl.lproj/Localizable.strings +++ b/apps/ios/nl.lproj/Localizable.strings @@ -870,9 +870,6 @@ /* chat item text */ "changing address…" = "adres wijzigen…"; -/* No comment provided by engineer. */ -"Chat archive" = "Gesprek archief"; - /* No comment provided by engineer. */ "Chat colors" = "Chat kleuren"; @@ -1251,9 +1248,6 @@ /* copied message info */ "Created at: %@" = "Aangemaakt op: %@"; -/* No comment provided by engineer. */ -"Created on %@" = "Gemaakt op %@"; - /* No comment provided by engineer. */ "Creating archive link" = "Archief link maken"; @@ -1400,12 +1394,6 @@ /* No comment provided by engineer. */ "Delete and notify contact" = "Verwijderen en contact op de hoogte stellen"; -/* No comment provided by engineer. */ -"Delete archive" = "Archief verwijderen"; - -/* No comment provided by engineer. */ -"Delete chat archive?" = "Chat archief verwijderen?"; - /* No comment provided by engineer. */ "Delete chat profile" = "Chatprofiel verwijderen"; @@ -1884,9 +1872,6 @@ /* No comment provided by engineer. */ "Error accepting contact request" = "Fout bij het accepteren van een contactverzoek"; -/* No comment provided by engineer. */ -"Error accessing database file" = "Fout bij toegang tot database bestand"; - /* No comment provided by engineer. */ "Error adding member(s)" = "Fout bij het toevoegen van leden"; @@ -3024,9 +3009,6 @@ /* notification */ "New contact:" = "Nieuw contact:"; -/* No comment provided by engineer. */ -"New database archive" = "Nieuw database archief"; - /* No comment provided by engineer. */ "New desktop app!" = "Nieuwe desktop app!"; @@ -3167,9 +3149,6 @@ /* No comment provided by engineer. */ "Old database" = "Oude database"; -/* No comment provided by engineer. */ -"Old database archive" = "Oud database archief"; - /* group pref value */ "on" = "aan"; @@ -3796,9 +3775,6 @@ /* No comment provided by engineer. */ "Save and update group profile" = "Groep profiel opslaan en bijwerken"; -/* No comment provided by engineer. */ -"Save archive" = "Bewaar archief"; - /* No comment provided by engineer. */ "Save group profile" = "Groep profiel opslaan"; @@ -4307,9 +4283,6 @@ /* No comment provided by engineer. */ "Stop chat" = "Stop chat"; -/* No comment provided by engineer. */ -"Stop chat to enable database actions" = "Stop de chat om database acties mogelijk te maken"; - /* No comment provided by engineer. */ "Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." = "Stop de chat om de chat database te exporteren, importeren of verwijderen. U kunt geen berichten ontvangen en verzenden terwijl de chat is gestopt."; diff --git a/apps/ios/pl.lproj/Localizable.strings b/apps/ios/pl.lproj/Localizable.strings index 644ba366f6..6650b1d8c8 100644 --- a/apps/ios/pl.lproj/Localizable.strings +++ b/apps/ios/pl.lproj/Localizable.strings @@ -855,9 +855,6 @@ /* chat item text */ "changing address…" = "zmiana adresu…"; -/* No comment provided by engineer. */ -"Chat archive" = "Archiwum czatu"; - /* No comment provided by engineer. */ "Chat colors" = "Kolory czatu"; @@ -1236,9 +1233,6 @@ /* copied message info */ "Created at: %@" = "Utworzony o: %@"; -/* No comment provided by engineer. */ -"Created on %@" = "Utworzony w dniu %@"; - /* No comment provided by engineer. */ "Creating archive link" = "Tworzenie linku archiwum"; @@ -1382,12 +1376,6 @@ /* No comment provided by engineer. */ "Delete and notify contact" = "Usuń i powiadom kontakt"; -/* No comment provided by engineer. */ -"Delete archive" = "Usuń archiwum"; - -/* No comment provided by engineer. */ -"Delete chat archive?" = "Usunąć archiwum czatu?"; - /* No comment provided by engineer. */ "Delete chat profile" = "Usuń profil czatu"; @@ -1863,9 +1851,6 @@ /* No comment provided by engineer. */ "Error accepting contact request" = "Błąd przyjmowania prośby o kontakt"; -/* No comment provided by engineer. */ -"Error accessing database file" = "Błąd dostępu do pliku bazy danych"; - /* No comment provided by engineer. */ "Error adding member(s)" = "Błąd dodawania członka(ów)"; @@ -2997,9 +2982,6 @@ /* notification */ "New contact:" = "Nowy kontakt:"; -/* No comment provided by engineer. */ -"New database archive" = "Nowe archiwum bazy danych"; - /* No comment provided by engineer. */ "New desktop app!" = "Nowa aplikacja desktopowa!"; @@ -3140,9 +3122,6 @@ /* No comment provided by engineer. */ "Old database" = "Stara baza danych"; -/* No comment provided by engineer. */ -"Old database archive" = "Stare archiwum bazy danych"; - /* group pref value */ "on" = "włączone"; @@ -3769,9 +3748,6 @@ /* No comment provided by engineer. */ "Save and update group profile" = "Zapisz i zaktualizuj profil grupowy"; -/* No comment provided by engineer. */ -"Save archive" = "Zapisz archiwum"; - /* No comment provided by engineer. */ "Save group profile" = "Zapisz profil grupy"; @@ -4277,9 +4253,6 @@ /* No comment provided by engineer. */ "Stop chat" = "Zatrzymaj czat"; -/* No comment provided by engineer. */ -"Stop chat to enable database actions" = "Zatrzymaj czat, aby umożliwić działania na bazie danych"; - /* No comment provided by engineer. */ "Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." = "Zatrzymaj czat, aby wyeksportować, zaimportować lub usunąć bazę danych czatu. Podczas zatrzymania chatu nie będzie można odbierać ani wysyłać wiadomości."; diff --git a/apps/ios/ru.lproj/Localizable.strings b/apps/ios/ru.lproj/Localizable.strings index 536bbb62a8..6db8181e8d 100644 --- a/apps/ios/ru.lproj/Localizable.strings +++ b/apps/ios/ru.lproj/Localizable.strings @@ -873,9 +873,6 @@ /* chat item text */ "changing address…" = "смена адреса…"; -/* No comment provided by engineer. */ -"Chat archive" = "Архив чата"; - /* No comment provided by engineer. */ "Chat colors" = "Цвета чата"; @@ -1254,9 +1251,6 @@ /* copied message info */ "Created at: %@" = "Создано: %@"; -/* No comment provided by engineer. */ -"Created on %@" = "Дата создания %@"; - /* No comment provided by engineer. */ "Creating archive link" = "Создание ссылки на архив"; @@ -1403,12 +1397,6 @@ /* No comment provided by engineer. */ "Delete and notify contact" = "Удалить и уведомить контакт"; -/* No comment provided by engineer. */ -"Delete archive" = "Удалить архив"; - -/* No comment provided by engineer. */ -"Delete chat archive?" = "Удалить архив чата?"; - /* No comment provided by engineer. */ "Delete chat profile" = "Удалить профиль чата"; @@ -1887,9 +1875,6 @@ /* No comment provided by engineer. */ "Error accepting contact request" = "Ошибка при принятии запроса на соединение"; -/* No comment provided by engineer. */ -"Error accessing database file" = "Ошибка при доступе к данным чата"; - /* No comment provided by engineer. */ "Error adding member(s)" = "Ошибка при добавлении членов группы"; @@ -3027,9 +3012,6 @@ /* notification */ "New contact:" = "Новый контакт:"; -/* No comment provided by engineer. */ -"New database archive" = "Новый архив чата"; - /* No comment provided by engineer. */ "New desktop app!" = "Приложение для компьютера!"; @@ -3170,9 +3152,6 @@ /* No comment provided by engineer. */ "Old database" = "Предыдущая версия данных чата"; -/* No comment provided by engineer. */ -"Old database archive" = "Старый архив чата"; - /* group pref value */ "on" = "да"; @@ -3799,9 +3778,6 @@ /* No comment provided by engineer. */ "Save and update group profile" = "Сохранить сообщение и обновить группу"; -/* No comment provided by engineer. */ -"Save archive" = "Сохранить архив"; - /* No comment provided by engineer. */ "Save group profile" = "Сохранить профиль группы"; @@ -4310,9 +4286,6 @@ /* No comment provided by engineer. */ "Stop chat" = "Остановить чат"; -/* No comment provided by engineer. */ -"Stop chat to enable database actions" = "Остановите чат, чтобы разблокировать операции с архивом чата"; - /* No comment provided by engineer. */ "Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." = "Остановите чат, чтобы экспортировать или импортировать архив чата или удалить данные чата. Вы не сможете получать и отправлять сообщения, пока чат остановлен."; diff --git a/apps/ios/th.lproj/Localizable.strings b/apps/ios/th.lproj/Localizable.strings index b50986cf17..1b3dec5ee1 100644 --- a/apps/ios/th.lproj/Localizable.strings +++ b/apps/ios/th.lproj/Localizable.strings @@ -564,9 +564,6 @@ /* chat item text */ "changing address…" = "กำลังเปลี่ยนที่อยู่…"; -/* No comment provided by engineer. */ -"Chat archive" = "ที่เก็บแชทถาวร"; - /* No comment provided by engineer. */ "Chat console" = "คอนโซลแชท"; @@ -771,9 +768,6 @@ /* No comment provided by engineer. */ "Create your profile" = "สร้างโปรไฟล์ของคุณ"; -/* No comment provided by engineer. */ -"Created on %@" = "สร้างเมื่อ %@"; - /* No comment provided by engineer. */ "creator" = "ผู้สร้าง"; @@ -887,12 +881,6 @@ /* No comment provided by engineer. */ "Delete all files" = "ลบไฟล์ทั้งหมด"; -/* No comment provided by engineer. */ -"Delete archive" = "ลบที่เก็บถาวร"; - -/* No comment provided by engineer. */ -"Delete chat archive?" = "ลบที่เก็บแชทถาวร?"; - /* No comment provided by engineer. */ "Delete chat profile" = "ลบโปรไฟล์แชท"; @@ -1223,9 +1211,6 @@ /* No comment provided by engineer. */ "Error accepting contact request" = "เกิดข้อผิดพลาดในการรับคำขอติดต่อ"; -/* No comment provided by engineer. */ -"Error accessing database file" = "เกิดข้อผิดพลาดในการเข้าถึงไฟล์ฐานข้อมูล"; - /* No comment provided by engineer. */ "Error adding member(s)" = "เกิดข้อผิดพลาดในการเพิ่มสมาชิก"; @@ -1970,9 +1955,6 @@ /* notification */ "New contact:" = "คำขอติดต่อใหม่:"; -/* No comment provided by engineer. */ -"New database archive" = "ฐานข้อมูลใหม่สำหรับการเก็บถาวร"; - /* No comment provided by engineer. */ "New display name" = "ชื่อที่แสดงใหม่"; @@ -2071,9 +2053,6 @@ /* No comment provided by engineer. */ "Old database" = "ฐานข้อมูลเก่า"; -/* No comment provided by engineer. */ -"Old database archive" = "คลังฐานข้อมูลเก่า"; - /* group pref value */ "on" = "เปิด"; @@ -2469,9 +2448,6 @@ /* No comment provided by engineer. */ "Save and update group profile" = "บันทึกและอัปเดตโปรไฟล์กลุ่ม"; -/* No comment provided by engineer. */ -"Save archive" = "บันทึกไฟล์เก็บถาวร"; - /* No comment provided by engineer. */ "Save group profile" = "บันทึกโปรไฟล์กลุ่ม"; @@ -2749,9 +2725,6 @@ /* No comment provided by engineer. */ "Stop" = "หยุด"; -/* No comment provided by engineer. */ -"Stop chat to enable database actions" = "หยุดการแชทเพื่อเปิดใช้งานการดำเนินการกับฐานข้อมูล"; - /* No comment provided by engineer. */ "Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." = "หยุดแชทเพื่อส่งออก นำเข้า หรือลบฐานข้อมูลแชท คุณจะไม่สามารถรับและส่งข้อความได้ในขณะที่การแชทหยุดลง"; diff --git a/apps/ios/tr.lproj/Localizable.strings b/apps/ios/tr.lproj/Localizable.strings index 1583f01cda..b849dda85a 100644 --- a/apps/ios/tr.lproj/Localizable.strings +++ b/apps/ios/tr.lproj/Localizable.strings @@ -870,9 +870,6 @@ /* chat item text */ "changing address…" = "adres değiştiriliyor…"; -/* No comment provided by engineer. */ -"Chat archive" = "Sohbet arşivi"; - /* No comment provided by engineer. */ "Chat colors" = "Sohbet renkleri"; @@ -1251,9 +1248,6 @@ /* copied message info */ "Created at: %@" = "Şurada oluşturuldu: %@"; -/* No comment provided by engineer. */ -"Created on %@" = "%@ de oluşturuldu"; - /* No comment provided by engineer. */ "Creating archive link" = "Arşiv bağlantısı oluşturuluyor"; @@ -1400,12 +1394,6 @@ /* No comment provided by engineer. */ "Delete and notify contact" = "Sil ve kişiye bildir"; -/* No comment provided by engineer. */ -"Delete archive" = "Arşivi sil"; - -/* No comment provided by engineer. */ -"Delete chat archive?" = "Sohbet arşivi silinsin mi?"; - /* No comment provided by engineer. */ "Delete chat profile" = "Sohbet profilini sil"; @@ -1884,9 +1872,6 @@ /* No comment provided by engineer. */ "Error accepting contact request" = "Bağlantı isteği kabul edilirken hata oluştu"; -/* No comment provided by engineer. */ -"Error accessing database file" = "Veritabanı dosyasına erişilirken hata oluştu"; - /* No comment provided by engineer. */ "Error adding member(s)" = "Üye(ler) eklenirken hata oluştu"; @@ -3024,9 +3009,6 @@ /* notification */ "New contact:" = "Yeni kişi:"; -/* No comment provided by engineer. */ -"New database archive" = "Yeni veritabanı arşivi"; - /* No comment provided by engineer. */ "New desktop app!" = "Yeni bilgisayar uygulaması!"; @@ -3167,9 +3149,6 @@ /* No comment provided by engineer. */ "Old database" = "Eski veritabanı"; -/* No comment provided by engineer. */ -"Old database archive" = "Eski veritabanı arşivi"; - /* group pref value */ "on" = "açık"; @@ -3796,9 +3775,6 @@ /* No comment provided by engineer. */ "Save and update group profile" = "Kaydet ve grup profilini güncelle"; -/* No comment provided by engineer. */ -"Save archive" = "Arşivi kaydet"; - /* No comment provided by engineer. */ "Save group profile" = "Grup profilini kaydet"; @@ -4307,9 +4283,6 @@ /* No comment provided by engineer. */ "Stop chat" = "Sohbeti kes"; -/* No comment provided by engineer. */ -"Stop chat to enable database actions" = "Veritabanı eylemlerini etkinleştirmek için sohbeti durdur"; - /* No comment provided by engineer. */ "Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." = "Sohbet veritabanını dışa aktarmak, içe aktarmak veya silmek için sohbeti durdurun. Sohbet durdurulduğunda mesaj alamaz ve gönderemezsiniz."; diff --git a/apps/ios/uk.lproj/Localizable.strings b/apps/ios/uk.lproj/Localizable.strings index 01a3196c03..9af4581140 100644 --- a/apps/ios/uk.lproj/Localizable.strings +++ b/apps/ios/uk.lproj/Localizable.strings @@ -840,9 +840,6 @@ /* chat item text */ "changing address…" = "змінює адресу…"; -/* No comment provided by engineer. */ -"Chat archive" = "Архів чату"; - /* No comment provided by engineer. */ "Chat colors" = "Кольори чату"; @@ -1215,9 +1212,6 @@ /* copied message info */ "Created at: %@" = "Створено за адресою: %@"; -/* No comment provided by engineer. */ -"Created on %@" = "Створено %@"; - /* No comment provided by engineer. */ "Creating archive link" = "Створення архівного посилання"; @@ -1361,12 +1355,6 @@ /* No comment provided by engineer. */ "Delete and notify contact" = "Видалити та повідомити контакт"; -/* No comment provided by engineer. */ -"Delete archive" = "Видалити архів"; - -/* No comment provided by engineer. */ -"Delete chat archive?" = "Видалити архів чату?"; - /* No comment provided by engineer. */ "Delete chat profile" = "Видалити профіль чату"; @@ -1836,9 +1824,6 @@ /* No comment provided by engineer. */ "Error accepting contact request" = "Помилка при прийнятті запиту на контакт"; -/* No comment provided by engineer. */ -"Error accessing database file" = "Помилка доступу до файлу бази даних"; - /* No comment provided by engineer. */ "Error adding member(s)" = "Помилка додавання користувача(ів)"; @@ -2934,9 +2919,6 @@ /* notification */ "New contact:" = "Новий контакт:"; -/* No comment provided by engineer. */ -"New database archive" = "Новий архів бази даних"; - /* No comment provided by engineer. */ "New desktop app!" = "Новий десктопний додаток!"; @@ -3062,9 +3044,6 @@ /* No comment provided by engineer. */ "Old database" = "Стара база даних"; -/* No comment provided by engineer. */ -"Old database archive" = "Старий архів бази даних"; - /* group pref value */ "on" = "увімкненo"; @@ -3676,9 +3655,6 @@ /* No comment provided by engineer. */ "Save and update group profile" = "Збереження та оновлення профілю групи"; -/* No comment provided by engineer. */ -"Save archive" = "Зберегти архів"; - /* No comment provided by engineer. */ "Save group profile" = "Зберегти профіль групи"; @@ -4160,9 +4136,6 @@ /* No comment provided by engineer. */ "Stop chat" = "Припинити чат"; -/* No comment provided by engineer. */ -"Stop chat to enable database actions" = "Зупиніть чат, щоб увімкнути дії з базою даних"; - /* No comment provided by engineer. */ "Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." = "Зупиніть чат, щоб експортувати, імпортувати або видалити базу даних чату. Ви не зможете отримувати та надсилати повідомлення, поки чат зупинено."; diff --git a/apps/ios/zh-Hans.lproj/Localizable.strings b/apps/ios/zh-Hans.lproj/Localizable.strings index ba8dcd6e2c..a524b5739d 100644 --- a/apps/ios/zh-Hans.lproj/Localizable.strings +++ b/apps/ios/zh-Hans.lproj/Localizable.strings @@ -831,9 +831,6 @@ /* chat item text */ "changing address…" = "更改地址…"; -/* No comment provided by engineer. */ -"Chat archive" = "聊天档案"; - /* No comment provided by engineer. */ "Chat colors" = "聊天颜色"; @@ -1206,9 +1203,6 @@ /* copied message info */ "Created at: %@" = "创建于:%@"; -/* No comment provided by engineer. */ -"Created on %@" = "创建于 %@"; - /* No comment provided by engineer. */ "Creating archive link" = "正在创建存档链接"; @@ -1352,12 +1346,6 @@ /* No comment provided by engineer. */ "Delete and notify contact" = "删除并通知联系人"; -/* No comment provided by engineer. */ -"Delete archive" = "删除档案"; - -/* No comment provided by engineer. */ -"Delete chat archive?" = "删除聊天档案?"; - /* No comment provided by engineer. */ "Delete chat profile" = "删除聊天资料"; @@ -1827,9 +1815,6 @@ /* No comment provided by engineer. */ "Error accepting contact request" = "接受联系人请求错误"; -/* No comment provided by engineer. */ -"Error accessing database file" = "访问数据库文件错误"; - /* No comment provided by engineer. */ "Error adding member(s)" = "添加成员错误"; @@ -2925,9 +2910,6 @@ /* notification */ "New contact:" = "新联系人:"; -/* No comment provided by engineer. */ -"New database archive" = "新数据库存档"; - /* No comment provided by engineer. */ "New desktop app!" = "全新桌面应用!"; @@ -3053,9 +3035,6 @@ /* No comment provided by engineer. */ "Old database" = "旧的数据库"; -/* No comment provided by engineer. */ -"Old database archive" = "旧数据库存档"; - /* group pref value */ "on" = "开启"; @@ -3667,9 +3646,6 @@ /* No comment provided by engineer. */ "Save and update group profile" = "保存和更新组配置文件"; -/* No comment provided by engineer. */ -"Save archive" = "保存存档"; - /* No comment provided by engineer. */ "Save group profile" = "保存群组资料"; @@ -4151,9 +4127,6 @@ /* No comment provided by engineer. */ "Stop chat" = "停止聊天程序"; -/* No comment provided by engineer. */ -"Stop chat to enable database actions" = "停止聊天以启用数据库操作"; - /* No comment provided by engineer. */ "Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." = "停止聊天以便导出、导入或删除聊天数据库。在聊天停止期间,您将无法收发消息。"; From 94377d0b7ae08cf4d9f9876d4c9da2e3d3fd64f4 Mon Sep 17 00:00:00 2001 From: Diogo Date: Sat, 30 Nov 2024 18:21:48 +0000 Subject: [PATCH 090/167] android, desktop: bottom bar and update texts in onboarding (#5279) * android, desktop: remove one hand ui bar from onboarding and design matching latest ios * padding before text * stop reserving space in conditions view * notifications view * revert unwanted * update heading * translations for new how it works * how it works redone * show create profile in how it works * revert * conditions of use same padding bottom * unused str * swapped instant and off notifications order --------- Co-authored-by: Evgeny Poberezkin --- .../platform/ScrollableColumn.android.kt | 3 +- .../chat/simplex/common/views/WelcomeView.kt | 11 +++-- .../simplex/common/views/helpers/ModalView.kt | 3 +- .../views/onboarding/ChooseServerOperators.kt | 28 +++++------ .../common/views/onboarding/HowItWorks.kt | 6 +-- .../views/onboarding/SetNotificationsMode.kt | 44 ++++++++++++----- .../common/views/onboarding/SimpleXInfo.kt | 49 +++++++++++-------- .../commonMain/resources/MR/ar/strings.xml | 3 -- .../commonMain/resources/MR/base/strings.xml | 19 ++++--- .../commonMain/resources/MR/bg/strings.xml | 3 -- .../commonMain/resources/MR/cs/strings.xml | 3 -- .../commonMain/resources/MR/de/strings.xml | 3 -- .../commonMain/resources/MR/es/strings.xml | 7 +-- .../commonMain/resources/MR/fa/strings.xml | 3 -- .../commonMain/resources/MR/fi/strings.xml | 3 -- .../commonMain/resources/MR/fr/strings.xml | 3 -- .../commonMain/resources/MR/hu/strings.xml | 3 -- .../commonMain/resources/MR/in/strings.xml | 2 - .../commonMain/resources/MR/it/strings.xml | 3 -- .../commonMain/resources/MR/iw/strings.xml | 3 -- .../commonMain/resources/MR/ja/strings.xml | 3 -- .../commonMain/resources/MR/ko/strings.xml | 2 - .../commonMain/resources/MR/lt/strings.xml | 3 -- .../commonMain/resources/MR/nl/strings.xml | 3 -- .../commonMain/resources/MR/pl/strings.xml | 3 -- .../resources/MR/pt-rBR/strings.xml | 7 +-- .../commonMain/resources/MR/pt/strings.xml | 2 +- .../commonMain/resources/MR/ru/strings.xml | 3 -- .../commonMain/resources/MR/th/strings.xml | 3 -- .../commonMain/resources/MR/tr/strings.xml | 3 -- .../commonMain/resources/MR/uk/strings.xml | 3 -- .../commonMain/resources/MR/vi/strings.xml | 1 - .../resources/MR/zh-rCN/strings.xml | 3 -- .../resources/MR/zh-rTW/strings.xml | 3 -- .../platform/ScrollableColumn.desktop.kt | 3 +- 35 files changed, 101 insertions(+), 146 deletions(-) diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/ScrollableColumn.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/ScrollableColumn.android.kt index d70177ffb9..cf95604504 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/ScrollableColumn.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/ScrollableColumn.android.kt @@ -14,6 +14,7 @@ import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.ui.theme.DEFAULT_PADDING import chat.simplex.common.views.chatlist.NavigationBarBackground import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.onboarding.OnboardingStage import kotlinx.coroutines.flow.filter import kotlin.math.absoluteValue @@ -124,7 +125,7 @@ actual fun ColumnWithScrollBar( } } } - val oneHandUI = remember { appPrefs.oneHandUI.state } + val oneHandUI = remember { derivedStateOf { if (appPrefs.onboardingStage.state.value == OnboardingStage.OnboardingComplete) appPrefs.oneHandUI.state.value else false } } Box(Modifier.fillMaxHeight()) { Column( if (maxIntrinsicSize) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt index 15d38c5490..024929030e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt @@ -117,14 +117,15 @@ fun CreateFirstProfile(chatModel: ChatModel, close: () -> Unit) { ColumnWithScrollBar { 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)) { + Column(if (appPlatform.isAndroid) Modifier.fillMaxSize().padding(start = DEFAULT_PADDING * 2, end = DEFAULT_PADDING * 2, bottom = 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) + AppBarTitle(stringResource(MR.strings.create_your_profile), bottomPadding = DEFAULT_PADDING, withPadding = false) } - ProfileNameField(displayName, stringResource(MR.strings.display_name), { it.trim() == mkValidName(it) }, focusRequester) + ReadableText(MR.strings.your_profile_is_stored_on_your_device, TextAlign.Center, padding = PaddingValues(), style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.secondary)) 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)) + ReadableText(MR.strings.profile_is_only_shared_with_your_contacts, TextAlign.Center, style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.secondary)) + Spacer(Modifier.height(DEFAULT_PADDING)) + ProfileNameField(displayName, stringResource(MR.strings.display_name), { it.trim() == mkValidName(it) }, focusRequester) } Spacer(Modifier.fillMaxHeight().weight(1f)) Column(Modifier.widthIn(max = if (appPlatform.isAndroid) 450.dp else 1000.dp).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt index c181f74e99..819efcdd9a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt @@ -14,6 +14,7 @@ import chat.simplex.common.model.ChatModel import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chatlist.StatusBarBackground +import chat.simplex.common.views.onboarding.OnboardingStage import kotlinx.coroutines.flow.MutableStateFlow import java.util.concurrent.atomic.AtomicBoolean import kotlin.math.min @@ -36,7 +37,7 @@ fun ModalView( if (showClose && showAppBar) { BackHandler(enabled = enableClose, onBack = close) } - val oneHandUI = remember { appPrefs.oneHandUI.state } + val oneHandUI = remember { derivedStateOf { if (appPrefs.onboardingStage.state.value == OnboardingStage.OnboardingComplete) appPrefs.oneHandUI.state.value else false } } Surface(Modifier.fillMaxSize(), contentColor = LocalContentColor.current) { Box(if (background != Color.Unspecified) Modifier.background(background) else Modifier.themedBackground(bgLayerSize = LocalAppBarHandler.current?.backgroundGraphicsLayerSize, bgLayer = LocalAppBarHandler.current?.backgroundGraphicsLayer)) { Box(modifier = modifier) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt index 8b383e0146..84ed7d1651 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt @@ -31,13 +31,8 @@ fun ModalData.ChooseServerOperators( LaunchedEffect(Unit) { prepareChatBeforeFinishingOnboarding() } - CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) { - ModalView({}, showClose = false, endButtons = { - IconButton({ modalManager.showModal { ChooseServerOperatorsInfoView() } }) { - Icon(painterResource(MR.images.ic_info), null, Modifier.size(28.dp), tint = MaterialTheme.colors.primary) - } - }) { + ModalView({}, showClose = false) { val serverOperators = remember { derivedStateOf { chatModel.conditions.value.serverOperators } } val selectedOperatorIds = remember { stateGetOrPut("selectedOperatorIds") { serverOperators.value.filter { it.enabled }.map { it.operatorId }.toSet() } } val selectedOperators = remember { derivedStateOf { serverOperators.value.filter { selectedOperatorIds.value.contains(it.operatorId) } } } @@ -48,15 +43,16 @@ fun ModalData.ChooseServerOperators( maxIntrinsicSize = true ) { Box(Modifier.align(Alignment.CenterHorizontally)) { - AppBarTitle(stringResource(MR.strings.onboarding_choose_server_operators)) + AppBarTitle(stringResource(MR.strings.onboarding_choose_server_operators), bottomPadding = DEFAULT_PADDING) } - Column(( - if (appPlatform.isDesktop) Modifier.width(600.dp).align(Alignment.CenterHorizontally) else Modifier) - .padding(horizontal = DEFAULT_PADDING) - ) { - Text(stringResource(MR.strings.onboarding_select_network_operators_to_use)) - Spacer(Modifier.height(DEFAULT_PADDING)) + + Column(Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING), horizontalAlignment = Alignment.CenterHorizontally) { + OnboardingInformationButton( + stringResource(MR.strings.how_it_helps_privacy), + onClick = { modalManager.showModal { ChooseServerOperatorsInfoView() } } + ) } + Spacer(Modifier.weight(1f)) Column(( if (appPlatform.isDesktop) Modifier.width(600.dp).align(Alignment.CenterHorizontally) else Modifier) @@ -93,7 +89,7 @@ fun ModalData.ChooseServerOperators( currUserServers = remember { mutableStateOf(emptyList()) }, userServers = remember { mutableStateOf(emptyList()) }, close = close, - rhId = null + rhId = null, ) } } @@ -249,10 +245,8 @@ private fun ReviewConditionsView( Column(modifier = Modifier.weight(1f).padding(top = DEFAULT_PADDING_HALF)) { ConditionsTextView(chatModel.remoteHostId()) } - Column(Modifier.padding(top = DEFAULT_PADDING).widthIn(max = if (appPlatform.isAndroid) 450.dp else 1000.dp).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) { + Column(Modifier.padding(vertical = DEFAULT_PADDING).widthIn(max = if (appPlatform.isAndroid) 450.dp else 1000.dp).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) { AcceptConditionsButton(onboarding, selectedOperators, selectedOperatorIds, close) - // Reserve space - TextButtonBelowOnboardingButton("", null) } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/HowItWorks.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/HowItWorks.kt index 34b6209ffe..f7e7f456bb 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/HowItWorks.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/HowItWorks.kt @@ -25,14 +25,11 @@ import dev.icerock.moko.resources.StringResource fun HowItWorks(user: User?, onboardingStage: SharedPreference? = null) { ColumnWithScrollBar(Modifier.padding(DEFAULT_PADDING)) { AppBarTitle(stringResource(MR.strings.how_simplex_works), withPadding = false) - ReadableText(MR.strings.many_people_asked_how_can_it_deliver) ReadableText(MR.strings.to_protect_privacy_simplex_has_ids_for_queues) - ReadableText(MR.strings.you_control_servers_to_receive_your_contacts_to_send) ReadableText(MR.strings.only_client_devices_store_contacts_groups_e2e_encrypted_messages) + ReadableText(MR.strings.all_message_and_files_e2e_encrypted) if (onboardingStage == null) { ReadableTextWithLink(MR.strings.read_more_in_github_with_link, "https://github.com/simplex-chat/simplex-chat#readme") - } else { - ReadableText(MR.strings.read_more_in_github) } Spacer(Modifier.fillMaxHeight().weight(1f)) @@ -41,7 +38,6 @@ fun HowItWorks(user: User?, onboardingStage: SharedPreference? Box(Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING), contentAlignment = Alignment.Center) { OnboardingActionButton(user, onboardingStage, onclick = { ModalManager.fullscreen.closeModal() }) } - Spacer(Modifier.fillMaxHeight().weight(1f)) } Spacer(Modifier.height(DEFAULT_PADDING)) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetNotificationsMode.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetNotificationsMode.kt index 49c91813dc..9e6287771f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetNotificationsMode.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetNotificationsMode.kt @@ -32,23 +32,28 @@ fun SetNotificationsMode(m: ChatModel) { ModalView({}, showClose = false) { ColumnWithScrollBar(Modifier.themedBackground(bgLayerSize = LocalAppBarHandler.current?.backgroundGraphicsLayerSize, bgLayer = LocalAppBarHandler.current?.backgroundGraphicsLayer)) { Box(Modifier.align(Alignment.CenterHorizontally)) { - AppBarTitle(stringResource(MR.strings.onboarding_notifications_mode_title)) + AppBarTitle(stringResource(MR.strings.onboarding_notifications_mode_title), bottomPadding = DEFAULT_PADDING) } val currentMode = rememberSaveable { mutableStateOf(NotificationsMode.default) } - Column(Modifier.padding(horizontal = DEFAULT_PADDING * 1f)) { - Text(stringResource(MR.strings.onboarding_notifications_mode_subtitle), Modifier.fillMaxWidth(), textAlign = TextAlign.Center) - Spacer(Modifier.height(DEFAULT_PADDING * 2f)) - SelectableCard(currentMode, NotificationsMode.OFF, stringResource(MR.strings.onboarding_notifications_mode_off), annotatedStringResource(MR.strings.onboarding_notifications_mode_off_desc)) { - currentMode.value = NotificationsMode.OFF - } - SelectableCard(currentMode, NotificationsMode.PERIODIC, stringResource(MR.strings.onboarding_notifications_mode_periodic), annotatedStringResource(MR.strings.onboarding_notifications_mode_periodic_desc)) { - currentMode.value = NotificationsMode.PERIODIC - } - SelectableCard(currentMode, NotificationsMode.SERVICE, stringResource(MR.strings.onboarding_notifications_mode_service), annotatedStringResource(MR.strings.onboarding_notifications_mode_service_desc)) { + Column(Modifier.padding(horizontal = DEFAULT_PADDING).fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { + OnboardingInformationButton( + stringResource(MR.strings.onboarding_notifications_mode_subtitle), + onClick = { ModalManager.fullscreen.showModalCloseable { NotificationBatteryUsageInfo() } } + ) + } + Spacer(Modifier.weight(1f)) + Column(Modifier.padding(horizontal = DEFAULT_PADDING)) { + SelectableCard(currentMode, NotificationsMode.SERVICE, stringResource(MR.strings.onboarding_notifications_mode_service), annotatedStringResource(MR.strings.onboarding_notifications_mode_service_desc_short)) { currentMode.value = NotificationsMode.SERVICE } + SelectableCard(currentMode, NotificationsMode.PERIODIC, stringResource(MR.strings.onboarding_notifications_mode_periodic), annotatedStringResource(MR.strings.onboarding_notifications_mode_periodic_desc_short)) { + currentMode.value = NotificationsMode.PERIODIC + } + SelectableCard(currentMode, NotificationsMode.OFF, stringResource(MR.strings.onboarding_notifications_mode_off), annotatedStringResource(MR.strings.onboarding_notifications_mode_off_desc_short)) { + currentMode.value = NotificationsMode.OFF + } } - Spacer(Modifier.fillMaxHeight().weight(1f)) + Spacer(Modifier.weight(1f)) Column(Modifier.widthIn(max = if (appPlatform.isAndroid) 450.dp else 1000.dp).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) { OnboardingActionButton( modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_PADDING * 2).fillMaxWidth() else Modifier, @@ -99,6 +104,21 @@ fun SelectableCard(currentValue: State, newValue: T, title: String, descr Spacer(Modifier.height(14.dp)) } +@Composable +private fun NotificationBatteryUsageInfo() { + ColumnWithScrollBar(Modifier.padding(DEFAULT_PADDING)) { + AppBarTitle(stringResource(MR.strings.onboarding_notifications_mode_battery), withPadding = false) + Text(stringResource(MR.strings.onboarding_notifications_mode_service), style = MaterialTheme.typography.h3, color = MaterialTheme.colors.secondary) + ReadableText(MR.strings.onboarding_notifications_mode_service_desc) + Spacer(Modifier.height(DEFAULT_PADDING_HALF)) + Text(stringResource(MR.strings.onboarding_notifications_mode_periodic), style = MaterialTheme.typography.h3, color = MaterialTheme.colors.secondary) + ReadableText(MR.strings.onboarding_notifications_mode_periodic_desc) + Spacer(Modifier.height(DEFAULT_PADDING_HALF)) + Text(stringResource(MR.strings.onboarding_notifications_mode_off), style = MaterialTheme.typography.h3, color = MaterialTheme.colors.secondary) + ReadableText(MR.strings.onboarding_notifications_mode_off_desc) + } +} + fun prepareChatBeforeFinishingOnboarding() { // No visible users but may have hidden. In this case chat should be started anyway because it's stopped on this stage with hidden users if (chatModel.users.any { u -> !u.user.hidden }) return diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt index b133ae27d4..a77c25dd1d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt @@ -32,11 +32,7 @@ import dev.icerock.moko.resources.StringResource fun SimpleXInfo(chatModel: ChatModel, onboarding: Boolean = true) { if (onboarding) { CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) { - ModalView({}, showClose = false, endButtons = { - IconButton({ ModalManager.fullscreen.showModal { HowItWorks(chatModel.currentUser.value, null) } }) { - Icon(painterResource(MR.images.ic_info), null, Modifier.size(28.dp), tint = MaterialTheme.colors.primary) - } - }) { + ModalView({}, showClose = false, showAppBar = false) { SimpleXInfoLayout( user = chatModel.currentUser.value, onboardingStage = chatModel.controller.appPrefs.onboardingStage @@ -56,22 +52,14 @@ fun SimpleXInfoLayout( user: User?, onboardingStage: SharedPreference? ) { - ColumnWithScrollBar( - Modifier - .padding(horizontal = DEFAULT_PADDING), - horizontalAlignment = Alignment.CenterHorizontally - ) { + ColumnWithScrollBar(Modifier.padding(horizontal = DEFAULT_PADDING), horizontalAlignment = Alignment.CenterHorizontally) { Box(Modifier.widthIn(max = if (appPlatform.isAndroid) 250.dp else 500.dp).padding(top = DEFAULT_PADDING + 8.dp), contentAlignment = Alignment.Center) { SimpleXLogo() } - Spacer(Modifier.weight(1f)) - - Text( + OnboardingInformationButton( stringResource(MR.strings.next_generation_of_private_messaging), - style = MaterialTheme.typography.h3, - color = MaterialTheme.colors.secondary, - textAlign = TextAlign.Center + onClick = { ModalManager.fullscreen.showModal { HowItWorks(user, onboardingStage) } }, ) Spacer(Modifier.weight(1f)) @@ -82,10 +70,10 @@ fun SimpleXInfoLayout( InfoRow(painterResource(if (isInDarkTheme()) MR.images.decentralized_light else MR.images.decentralized), MR.strings.decentralized, MR.strings.opensource_protocol_and_code_anybody_can_run_servers) } - Spacer(Modifier.fillMaxHeight().weight(1f)) + Column(Modifier.fillMaxHeight().weight(1f)) { } if (onboardingStage != null) { - Column(Modifier.padding(horizontal = DEFAULT_PADDING).widthIn(max = if (appPlatform.isAndroid) 450.dp else 1000.dp).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) { + Column(Modifier.padding(horizontal = DEFAULT_PADDING).widthIn(max = if (appPlatform.isAndroid) 450.dp else 1000.dp).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally,) { OnboardingActionButton(user, onboardingStage) TextButtonBelowOnboardingButton(stringResource(MR.strings.migrate_from_another_device)) { chatModel.migrationState.value = MigrationToState.PasteOrScanLink @@ -165,8 +153,8 @@ fun OnboardingActionButton( fun TextButtonBelowOnboardingButton(text: String, onClick: (() -> Unit)?) { val state = getKeyboardState() val enabled = onClick != null - val topPadding by animateDpAsState(if (appPlatform.isAndroid && state.value == KeyboardState.Opened) 0.dp else DEFAULT_PADDING) - val bottomPadding by animateDpAsState(if (appPlatform.isAndroid && state.value == KeyboardState.Opened) 0.dp else DEFAULT_PADDING * 2) + val topPadding by animateDpAsState(if (appPlatform.isAndroid && state.value == KeyboardState.Opened) 0.dp else DEFAULT_PADDING_HALF) + val bottomPadding by animateDpAsState(if (appPlatform.isAndroid && state.value == KeyboardState.Opened) 0.dp else DEFAULT_PADDING_HALF) if ((appPlatform.isAndroid && state.value == KeyboardState.Closed) || topPadding > 0.dp) { TextButton({ onClick?.invoke() }, Modifier.padding(top = topPadding, bottom = bottomPadding).clip(CircleShape), enabled = enabled) { Text( @@ -183,6 +171,27 @@ fun TextButtonBelowOnboardingButton(text: String, onClick: (() -> Unit)?) { } } +@Composable +fun OnboardingInformationButton( + text: String, + onClick: () -> Unit, +) { + Box( + modifier = Modifier + .clip(CircleShape) + .clickable { onClick() } + ) { + Row(Modifier.padding(8.dp), horizontalArrangement = Arrangement.spacedBy(4.dp) ) { + Icon( + painterResource(MR.images.ic_info), + null, + tint = MaterialTheme.colors.primary + ) + Text(text, style = MaterialTheme.typography.button, color = MaterialTheme.colors.primary) + } + } +} + @Preview/*( uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml index b2cb0acc4b..2f3f245f54 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml @@ -703,7 +703,6 @@ تأكد من أن عناوين خادم WebRTC ICE بالتنسيق الصحيح، وأن تكون مفصولة بأسطر وليست مكررة. علّم تحقق منه خطأ في حفظ كلمة مرور المستخدم - إذا SimpleX ليس لديه معرّفات مستخدم، كيف يمكنه توصيل الرسائل؟]]> خطأ في حفظ ملف تعريف المجموعة رسالة نصية ردود فعل الرسائل @@ -932,7 +931,6 @@ صفّر المنفذ %d خادم محدد مسبقًا - قراءة المزيد في مستودعنا على GitHub. يتم استخدام خادم الترحيل فقط إذا لزم الأمر. يمكن لطرف آخر مراقبة عنوان IP الخاص بك. حفظ وإشعار جهة الاتصال إعادة التشغيل @@ -1215,7 +1213,6 @@ غادرت يجب عليك استخدام أحدث إصدار من قاعدة بيانات الدردشة الخاصة بك على جهاز واحد فقط، وإلا فقد تتوقف عن تلقي الرسائل من بعض جهات الاتصال. سيتم استلام الفيديو عندما تكون جهة اتصالك متصلة بالإنترنت، يرجى الانتظار أو التحقق لاحقًا! - لاستلام الرسائل وجهات اتصالك - الخوادم التي تستخدمها لمراسلتهم.]]> يمكنك مشاركة هذا العنوان مع جهات اتصالك للسماح لهم بالاتصال بـ%s. أُزيلت %1$s تحديث diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 6d0200256c..1c036d76b2 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -1026,7 +1026,7 @@ Error initializing WebView. Make sure you have WebView installed and it\'s supported architecture is arm64.\nError: %s - The next generation\nof private messaging + The future of messaging Privacy redefined No user identifiers. Immune to spam @@ -1040,23 +1040,25 @@ How SimpleX works - if SimpleX has no user identifiers, how can it deliver messages?]]> - To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts. - to receive the messages, your contacts – the servers you use to message them.]]> - 2-layer end-to-end encryption.]]> - Read more in our GitHub repository. + To protect your privacy, SimpleX uses separate IDs for each of your contacts. + Only client devices store user profiles, contacts, groups, and messages. + end-to-end encrypted, with post-quantum security in direct messages.]]> GitHub repository.]]> Use chat Private notifications - It can be changed later via settings. + How it affects battery When app is running Periodic Instant Best for battery. You will receive notifications only when the app is running (NO background service).]]> + No background service Good for battery. App checks messages every 10 minutes. You may miss calls or urgent messages.]]> + Check messages every 10 minutes Uses more battery! App always runs in background – notifications are shown instantly.]]> + App always runs in background + Notifications and battery Setup database passphrase @@ -1064,11 +1066,12 @@ Use random passphrase - Choose operators + Server operators Network operators When more than one network operator is enabled, the app will use the servers of different operators for each conversation. For example, if you receive messages via SimpleX Chat server, the app will use one of Flux servers for private routing. Select network operators to use. + How it helps privacy You can configure servers via settings. Conditions will be accepted for enabled operators after 30 days. You can configure operators in Network & servers settings. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/bg/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/bg/strings.xml index 042089e226..748c264918 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/bg/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/bg/strings.xml @@ -982,9 +982,7 @@ Протокол и код с отворен код – всеки може да оперира собствени сървъри. Хората могат да се свържат с вас само чрез ликовете, които споделяте. Поверителността преосмислена - Прочетете повече в нашето хранилище в GitHub. Добави поверителна връзка - ако SimpleX няма потребителски идентификатори, как може да доставя съобщения\?]]> Отвори Реле сървър се използва само ако е необходимо. Друга страна може да наблюдава вашия IP адрес. Заключване след @@ -1220,7 +1218,6 @@ Когато са налични Вашият профил, контакти и доставени съобщения се съхраняват на вашето устройство. Можете да използвате markdown за форматиране на съобщенията: - да получавате съобщенията, вашите контакти – сървърите, които използвате, за да им изпращате съобщения.]]> Използвай чата Актуализация Трябва да въвеждате парола при всяко стартиране на приложението - тя не се съхранява на устройството. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml index 64a87736c4..548de53a21 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml @@ -667,9 +667,6 @@ Bez uživatelských identifikátorů Odolná vůči spamu K ochraně soukromí, místo uživatelských ID užívaných všemi ostatními platformami, SimpleX používá identifikátory pro fronty zpráv, zvlášť pro každý z vašich kontaktů. - když SimpleX nemá žádný identifikátor uživatelů, jak může doručovat zprávy\?]]> - přijímat zprávy, vaše kontakty – servery, které používáte k zasílání zpráv.]]> - Další informace najdete v našem repozitáři na GitHubu. úložišti GitHub.]]> Použijte chat Lze změnit později v nastavení. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml index 75f6ac2c29..05ed6366a1 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml @@ -475,11 +475,8 @@ Wie es funktioniert Wie SimpleX funktioniert - Wie kann SimpleX Nachrichten zustellen, wenn es keine Benutzerkennungen gibt?]]> Zum Schutz Ihrer Privatsphäre verwendet SimpleX anstelle von Benutzerkennungen, die von allen anderen Plattformen verwendet werden, Kennungen für Nachrichtenwarteschlangen, die für jeden Ihrer Kontakte individuell sind. - empfangen und an Ihre Kontakte senden wollen.]]> zweischichtige Ende-zu-Ende-Verschlüsselung gesendet werden.]]> - Erfahren Sie in unserem GitHub-Repository mehr dazu. GitHub-Repository mehr dazu.]]> Fügen Sie den erhaltenen Link ein diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml index 1cde9ed7c7..0f31210a9a 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml @@ -442,7 +442,7 @@ Asegúrate de que las direcciones del servidor SMP tienen el formato correcto, están separadas por líneas y no están duplicadas. Notificación instantánea Configuración avanzada - cifrado de extremo a extremo de 2 capas .]]> + Sólo los dispositivos cliente almacenan perfiles de usuario, contactos, grupos y mensajes. Puedes cambiar estos ajustes más tarde en Configuración. Instantánea Unirte @@ -505,7 +505,6 @@ Se requieren hosts .onion para la conexión \nRecuerda: no podrás conectarte a servidores que no tengan dirección .onion. Inmune al spam - si SimpleX no tiene identificadores de usuario, ¿cómo puede entregar los mensajes\?]]> Videollamada entrante has salido has cambiado de servidor @@ -651,7 +650,6 @@ confirmación recibida… Periódico Privacidad redefinida - Conoce más en nuestro repositorio GitHub. Rechazar Abrir Llamada pendiente @@ -751,7 +749,7 @@ Inciar chat nuevo Para exportar, importar o eliminar la base de datos debes parar SimpleX. Mientra tanto no podrás recibir o enviar mensajes. Gracias por instalar SimpleX Chat! - Para proteger tu privacidad, en lugar de los identificadores de usuario que usan el resto de plataformas, SimpleX dispone de identificadores para las colas de mensajes, independientes para cada uno de tus contactos. + Para proteger tu privacidad, SimpleX dispone de identificadores para las colas de mensajes, independientes para cada uno de tus contactos. Para proteger tu información, activa el Bloqueo SimpleX. \nSe te pedirá que completes la autenticación antes de activar esta función. Al actualizar la configuración el cliente se reconectará a todos los servidores. @@ -843,7 +841,6 @@ Has sido invitado al grupo Mensajes de voz Tus contactos pueden permitir la eliminación completa de mensajes. - recibes los mensajes. Tus contactos controlan a través de qué servidor(es) envías tus mensajes.]]> Mensajes de voz Los mensajes de voz no están permitidos en este grupo. Comprobar la seguridad de la conexión diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml index dc4552a33e..23e2392fdc 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml @@ -733,8 +733,6 @@ نامتمرکز نمایه خود را ایجاد کنید SimpleX چگونه کار می‌کند - اگر SimpleX هیچ شناسه کاربری ندارد، چگونه می‌تواند پیام‌ها را تحویل دهد؟]]> - مطالعه بیشتر در مخزن GitHub ما. مخزن GitHub ما.]]> استفاده از گپ بهترین گزینه برای باتری. شما اعلان‌ها را فقط وقتی دریافت می‌کنید که برنامه در حال اجراست (بدون سرویس پس‌زمینه).]]> @@ -777,7 +775,6 @@ بلوتوث وارد کردن پایگاه داده اشخاص فقط از طریق لینک‌هایی که به اشتراک می‌گذارید می‌توانند به شما متصل شوند. - دریافت شوند و از چه سرورهایی به مخاطبان خود پیام می‌فرستید.]]> تماس از پیش پایان یافته! هش پیام ناصحیح پذیرفتن خودکار تصاویر diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/fi/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/fi/strings.xml index 682854f9dd..28b29c59af 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/fi/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/fi/strings.xml @@ -657,7 +657,6 @@ PING-väli Profiili- ja palvelinyhteydet Aseta ryhmän asetukset - jos SimpleX ei sisällä käyttäjätunnuksia, kuinka se voi toimittaa viestejä\?]]> PALVELIMET Tallenna ja ilmoita kontaktille Tallenna ja ilmoita kontakteille @@ -835,7 +834,6 @@ GitHub-arkistostamme.]]> Säännölliset 2-kerroksisella päästä päähän -salauksella.]]> - Lue lisää GitHub-tietovarastostamme. Liitä vastaanotettu linkki Välityspalvelin suojaa IP-osoitteesi, mutta se voi tarkkailla puhelun kestoa. Avaa SimpleX Chat hyväksyäksesi puhelun @@ -1051,7 +1049,6 @@ Odottaa tiedostoa Aloita uusi keskustelu Käyttää SimpleX Chat -palvelimia. - vastaanotetaan, kontaktiesi – palvelimet, joita käytät viestien lähettämiseen.]]> Yksityisyytesi poistit %1$s kyllä diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml index 3bef77138e..07b99ebe1d 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml @@ -321,7 +321,6 @@ Assurez-vous que les adresses des serveurs WebRTC ICE sont au bon format et ne sont pas dupliquées, un par ligne. Accéder aux serveurs via un proxy SOCKS sur le port %d \? Le proxy doit être démarré avant d\'activer cette option. Utiliser les hôtes .onions - transmettre ainsi que par quel·s serveur·s vous pouvez recevoir les messages de vos contacts.]]> Vos paramètres SimpleX Lock Console du chat @@ -372,7 +371,6 @@ connexion… N\'importe qui peut heberger un serveur. Pour protéger votre vie privée, au lieu d\'IDs utilisés par toutes les autres plateformes, SimpleX possède des IDs pour les queues de messages, distinctes pour chacun de vos contacts. - Plus d\'informations sur notre GitHub. Collez le lien que vous avez reçu Utiliser le chat Notifications privées @@ -451,7 +449,6 @@ Établir une connexion privée Comment ça fonctionne Comment SimpleX fonctionne - si SimpleX n\'a pas d\'identifiant d\'utilisateur, comment peut-il transmettre des messages \?]]> chiffrement de bout en bout à deux couches.]]> GitHub repository.]]> Batterie peu utilisée. L\'app vérifie les messages toutes les 10 minutes. Vous risquez de manquer des appels ou des messages urgents.]]> diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml index f0355d51ac..216b666100 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml @@ -814,7 +814,6 @@ ajánlott %s Csoport elhagyása Az összes %s által írt üzenet megjelenik! - Ha a SimpleX Chatnek nincs felhasználó-azonosítója, hogyan lehet mégis üzeneteket küldeni?]]> Ez akkor fordulhat elő, ha:\n1. Az üzenetek 2 nap után, vagy a kiszolgálón 30 nap után lejártak.\n2. Az üzenet visszafejtése sikertelen volt, mert Ön, vagy az ismerőse régebbi adatbázis biztonsági mentést használt.\n3. A kapcsolat sérült. megfigyelő inkognitó a csoporthivatkozáson keresztül @@ -1391,7 +1390,6 @@ Rejtett profilja felfedéséhez írja be a teljes jelszavát a keresőmezőbe a „Csevegési profilok” menüben. Fejlesztés és a csevegés megnyitása Engedélyeznie kell a hangüzenetek küldését az ismerőse számára, hogy hangüzeneteket küldhessenek egymásnak. - fogadja az üzeneteket, ismerősöket – a kiszolgálók, amelyeket az üzenetküldéshez használ.]]> %1$s nevű csoport tagja.]]> cím megváltoztatva Az ismerősei engedélyezhetik a teljes üzenet törlést. @@ -1463,7 +1461,6 @@ Ennek a csoportnak több mint %1$d tagja van, a kézbesítési jelentések nem kerülnek elküldésre. A második jelölés, amit kihagytunk! ✅ A közvetítő-kiszolgáló megvédi az IP-címet, de megfigyelheti a hívás időtartamát. - További információ a GitHub tárolónkban. Az utolsó üzenet tervezetének megőrzése a mellékletekkel együtt. A mentett WebRTC ICE-kiszolgálók eltávolításra kerülnek. A kézbesítési jelentések engedélyezve vannak %d csoportban diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml index dec20e2a36..caeec02deb 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml @@ -618,7 +618,6 @@ Terdesentralisasi Kesalahan saat menginisialisasi WebView. Pastikan Anda telah menginstal WebView dan arsitektur yang didukung adalah arm64.\nKesalahan: %s Gunakan obrolan - Jika SimpleX tidak memiliki pengenal pengguna, bagaimana ia dapat menyampaikan pesan?]]> Bagaimana caranya Cara kerja SimpleX Berkala @@ -1093,7 +1092,6 @@ Pindah ke perangkat lain melalui kode QR. Hapus hingga 20 pesan sekaligus. Periksa pembaruan - Baca selengkapnya di repositori GitHub kami. Unduh %s (%s) simplexmq: v%s (%2s) Gunakan routing pribadi dengan server yang tak dikenal. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml index e699714a6a..74c2397edd 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml @@ -650,7 +650,6 @@ Istantaneo Può essere cambiato in seguito via impostazioni. Crea una connessione privata - se SimpleX non ha identificatori utente, come può recapitare i messaggi\?]]> crittografia end-to-end a 2 livelli.]]> Chiunque può installare i server. Incolla il link che hai ricevuto @@ -659,7 +658,6 @@ Privacy ridefinita Notifiche private repository GitHub.]]> - Maggiori informazioni nel nostro repository GitHub. Rifiuta Nessun identificatore utente. La nuova generazione @@ -670,7 +668,6 @@ videochiamata (non crittografata e2e) Quando l\'app è in esecuzione %1$s vuole connettersi con te via - ricevere i messaggi, i tuoi contatti quali server usi per inviare loro i messaggi.]]> Può accadere quando: \n1. I messaggi sono scaduti sul client mittente dopo 2 giorni o sul server dopo 30 giorni. \n2. La decifrazione del messaggio è fallita, perché tu o il tuo contatto avete usato un backup del database vecchio. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/iw/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/iw/strings.xml index 64f86a6ecb..c163458097 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/iw/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/iw/strings.xml @@ -587,7 +587,6 @@ למדו עוד עזרה במרקדאון בואו נדבר ב־Simplex Chat - אם ל־SimpleX אין מזהי משתמש, איך ניתן להעביר הודעות\?]]> שגיאת Keychain הצטרף עם זהות נסתרת לעזוב קבוצה\? @@ -792,7 +791,6 @@ דחיה מדריך למשתמש.]]> דרגו את האפליקציה - קראו עוד ב־GitHub repository שלנו. GitHub repository שלנו.]]> יבוצע שימוש בשרת ממסר רק במידת הצורך. גורם אחר יכול לצפות בכתובת ה־IP שלך. שרת ממסר מגן על כתובת ה־IP שלך, אך הוא יכול לראות את משך השיחה. @@ -1133,7 +1131,6 @@ עליכם לאפשר לאיש הקשר שלכם לשלוח הודעות קוליות כדי שתוכלו לשלוח אותן. סרטון להתחבר למפתחי SimpleX Chat כדי לשאול כל שאלה ולקבל עדכונים.]]> - לקבל את ההודעות, אנשי הקשר שלכם – השרתים דרכם אתם שולחים להם הודעות.]]> שרתי WebRTC ICE %1$d הודעות שדולגו שבועות diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml index 632e62ef09..edd22933ec 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml @@ -154,7 +154,6 @@ 即時通知 SMPサーバのアドレスを正しく1行ずつに分けて、重複しないように、形式もご確認ください。 WebRTC ICEサーバのアドレスを正しく1行ずつに分けて、重複しないように、形式もご確認ください。 - if SimpleX にユーザIDがなければ、メッセージをどうやって届けるのでしょうかと。]]> SimpleX の仕様 通話中 電池消費がより高い!非アクティブ時でもバックグラウンドのサービスが常に稼働します(着信してすぐに通知が出ます)。]]> @@ -328,7 +327,6 @@ プライベートな接続をする プライベートな通知 GitHubリポジトリで詳細をご確認ください。]]> - GitHubリポジトリで詳細をご確認ください。 エンドツーエンド暗号化済みビデオ通話 無効にする エンドツーエンド暗号化がありません @@ -743,7 +741,6 @@ 取り消し線 接続中… 次世代のプライベートメッセンジャー - 受信サーバを決められます。あなたの連絡先が同じく、自分に対する受信サーバを決められます。]]> ビデオ通話 アプリが稼働中に WebRTC ICEサーバ diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ko/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ko/strings.xml index 6af84d88c1..9d129847e4 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ko/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ko/strings.xml @@ -665,7 +665,6 @@ TCP 연결 유지 활성화 %s의 새로운 기능 마크다운 도움말 - SimpleX에는 사용자 식별자가 없는데도 어떻게 메시지를 전달할 수 있어요\?]]> 그룹에서 나갈까요\? 앱 버전보다 최신 버전의 데이터베이스를 사용하고 있지만 데이터베이스를 다운그레이드할 수 없습니다: %s 멤버가 그룹에서 제거됩니다. 이 결정은 되돌릴 수 없습니다! @@ -688,7 +687,6 @@ 거절 비밀번호 표시 2계층 종단 간 암호화 로 전송된 사용자 프로필, 연락처, 그룹 및 메시지를 저장되어요.]]> - 자세한 내용은 GitHub에서 확인해 주세요. 개인 정보 보호 및 보안 알림은 앱이 중지되기 전까지만 전달될 거예요! 당신만 사라지는 메시지를 보낼 수 있습니다. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/lt/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/lt/strings.xml index c3505460ae..da7738f49e 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/lt/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/lt/strings.xml @@ -348,7 +348,6 @@ Kas naujo Įrašyti ir pranešti grupės nariams gautas patvirtinimas… - Išsamiau skaitykite mūsų „GitHub“ saugykloje Praleistas skambutis POKALBIAI APIPAVIDALINIMAI @@ -1531,7 +1530,6 @@ Patvirtinti duomenų bazės slaptafrazę Patvirtinti slaptafrazę Nutildyti - gauti žinutes, jūsų kontaktai - serverius kuriuos naudojate siųsti jiems žinutes.]]> Tarpinis serveris apsaugo jūsų IP adresą, bet jis gali stebėti skambučio trukmę. nėra visapusio šifravimo Naujas duomenų bazės archyvas @@ -1600,7 +1598,6 @@ pakeitėte adresą %s Išplėsti rolių pasirinkimą %1$s.]]> - jei SimpleX neturi naudotojų identifikatorių, kaip jis gali pristatyti žinutes?]]> dviejų sluoksnių visapusiu šifravimu.]]> Kad apsaugoti privatumą, vietoj naudotojų ID naudojamų visose kitose platformose, SimpleX turi identifikatorius žinučių eilėms, skirtingus kiekvienam jūsų kontaktui. Žinutės juodraštis diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml index 649f586620..2b71c3243b 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml @@ -426,7 +426,6 @@ Hoe het werkt gemiste oproep Hoe SimpleX werkt - als SimpleX geen gebruikers-ID\'s heeft, hoe kan het dan berichten bezorgen\?]]> Inkomende audio oproep Inkomend video gesprek Negeren @@ -612,7 +611,6 @@ Jij beheert je gesprek! Uw profiel, contacten en afgeleverde berichten worden op uw apparaat opgeslagen. beginnen… - ontvangt, uw contacten de servers die u gebruikt om ze berichten te sturen.]]> Video aan Deze actie kan niet ongedaan worden gemaakt. Uw profiel, contacten, berichten en bestanden gaan onomkeerbaar verloren. Deze instelling is van toepassing op berichten in uw huidige chatprofiel @@ -931,7 +929,6 @@ je hebt een eenmalige link incognito gedeeld Tik op de knop GitHub repository.]]> - Lees meer in onze GitHub repository. %1$d bericht(en) overgeslagen gemodereerd gemodereerd door %s diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml index e793ff2a72..c679651e7d 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml @@ -432,15 +432,12 @@ Natychmiastowy Można to później zmienić w ustawieniach. Nawiąż prywatne połączenie - jeśli SimpleX nie ma identyfikatora użytkownika, jak może dostarczać wiadomości\?]]> dwuwarstwowego szyfrowania end-to-end.]]> Okresowo Prywatne powiadomienia repozytorium GitHub.]]> - Przeczytaj więcej na naszym repozytorium GitHub. Użyj czatu Gdy aplikacja jest uruchomiona - odbierać wiadomości, Twoje kontakty - serwery, których używasz do wysyłania im wiadomości.]]> Zużywa więcej baterii! Aplikacja zawsze działa w tle - powiadomienia są wyświetlane natychmiastowo.]]> Przychodzące połączenie audio Przychodzące połączenie wideo diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/pt-rBR/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/pt-rBR/strings.xml index 140b208db9..8b16e01b4e 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/pt-rBR/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/pt-rBR/strings.xml @@ -469,7 +469,6 @@ Compilação do aplicativo: %s Salvar e notificar contato resposta recebida… - Leia mais no nosso repositório do GitHub. Cole o link que você recebeu Quando o aplicativo está em execução Periódico @@ -587,7 +586,6 @@ Markdown em mensagens Servidores SMP Endereço do servidor pré-definido - se SimpleX não tem identificadores de usuários, como ele pode mandar mensagens\?]]> Rejeitar %1$d mensagem(s) ignorada(s) Proteger a tela do aplicativo @@ -889,7 +887,7 @@ você é um observador Mensagem de voz (%1$s) Compartilhar link - Para proteger a privacidade, em vez dos IDs de usuário usados por todas as outras plataformas, SimpleX tem identificadores para filas de mensagens, separados para cada um de seus contatos. + Para proteger a privacidade, SimpleX usa identificadores separados para cada um de seus contatos. chamada de vídeo Mostrar Servidores ICE WebRTC @@ -1010,11 +1008,10 @@ APOIE SIMPLEX CHAT Esta ação não pode ser desfeita - as mensagens enviadas e recebidas antes do selecionado serão excluídas. Pode levar vários minutos. Confirme as atualizações do banco de dados - criptografia de ponta a ponta em duas camadas.]]> + Somente o cliente dos dispositivos armazenam perfis de usuários, contatos, grupos e mensagens. Obrigado por instalar o SimpleX Chat! A plataforma de mensagens que protege sua privacidade e segurança. Você está tentando convidar um contato com quem compartilhou um perfil anônimo para o grupo no qual está usando seu perfil principal - receber as mensagens, seus contatos controlam os servidores que você usa para enviar mensagens.]]> Fila segura imagem de perfil temporária Erro ao carregar servidores SMP diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/pt/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/pt/strings.xml index ee5b82d490..18c20eba50 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/pt/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/pt/strings.xml @@ -635,7 +635,7 @@ Para verificar a encriptação de ponta a ponta com o seu contato, compare (ou leia) o código nos seus dispositivos. Ler o código de segurança a partir da aplicação do seu contacto. Ler o código QR do servidor - encriptação de ponta a ponta de 2 camadas.]]> + Apenas dispositivos pessoais armazenam perfis de utilizador, contatos, grupos e mensagens. o contacto tem encriptação ponta a ponta sem encriptação ponta a ponta criador diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml index 10d1de26ae..3d1e92f83f 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml @@ -475,11 +475,8 @@ Как это работает Как SimpleX работает - как SimpleX доставляет сообщения без идентификаторов пользователей?]]> Чтобы защитить Вашу конфиденциальность, вместо ID пользователей, которые есть в других платформах, SimpleX использует ID для очередей сообщений, разные для каждого контакта. - получаете сообщения, Ваши контакты - серверы, которые Вы используете для отправки.]]> с двухуровневым end-to-end шифрованием.]]> - Узнайте больше из нашего GitHub репозитория. GitHub репозитория.]]> Использовать чат diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/th/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/th/strings.xml index c1f88cf3b1..2487c7d5cd 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/th/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/th/strings.xml @@ -746,7 +746,6 @@ เป็นไปได้มากว่าผู้ติดต่อนี้ได้ลบการเชื่อมต่อกับคุณ ข้อผิดพลาดในการส่งข้อความ ตรวจสอบให้แน่ใจว่าที่อยู่เซิร์ฟเวอร์ SMP อยู่ในรูปแบบที่ถูกต้อง แยกบรรทัดและไม่ซ้ำกัน - ถ้า SimpleX ไม่มีตัวระบุผู้ใช้ จะส่งข้อความได้อย่างไร\?]]> โฮสต์หัวหอมจะถูกใช้เมื่อมี ผู้ติดต่อของคุณเท่านั้นที่สามารถส่งข้อความเสียงได้ การเปิดลิงก์ในเบราว์เซอร์อาจลดความเป็นส่วนตัวและความปลอดภัยของการเชื่อมต่อ ลิงก์ SimpleX ที่ไม่น่าเชื่อถือจะเป็นสีแดง @@ -768,7 +767,6 @@ ได้รับการยืนยัน… นิยามความเป็นส่วนตัวใหม่ GitHub repository ของเรา]]> - อ่านเพิ่มเติมใน GitHub repository ของเรา การแจ้งเตือนส่วนตัว โปรดรายงานไปยังผู้พัฒนาแอป ความเป็นส่วนตัวและความปลอดภัย @@ -1149,7 +1147,6 @@ โปรไฟล์ รายชื่อผู้ติดต่อ และข้อความที่ส่งของคุณจะถูกจัดเก็บไว้ในอุปกรณ์ของคุณ คุณสามารถใช้มาร์กดาวน์เพื่อจัดรูปแบบข้อความ: รอคำตอบ… - รับข้อความและผู้ติดต่อของคุณ – เซิร์ฟเวอร์ที่คุณใช้เพื่อส่งข้อความถึงพวกเขา]]> ใช้แชท การสนทนาทางวิดีโอ การโทรของคุณ diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml index 9dcc45e6c4..48e26132c8 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml @@ -851,7 +851,6 @@ ICE sunucularınız Nasıl Mevcut profiliniz - alınacağını siz kontrol edersiniz, kişileriniz - onlara mesaj göndermek için kullandığınız sunucular.]]> video arama (uçtan uca şifreli değil) ICE sunucularınız Video kapalı @@ -985,7 +984,6 @@ Yeni bir sohbet başlatmak için Kimin bağlanabileceğine siz karar verirsiniz. Gizlilik yeniden tanımlanıyor - GitHub repomuzda daha fazlasını okuyun. Periyodik Gizli bildirimler Aldığın bağlantıyı yapıştır @@ -1462,7 +1460,6 @@ Bir sonraki mesajın kimliği yanlış (bir öncekinden az veya aynı). \nBazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir. Kişi gizlendi: - eğer SimpleX’in hiç kullanıcı tanımlayıcısı yok, nasıl mesajları gönderiyor? ]]> Şu durumlarda gerçekleşebilir: \n1. Mesajların süresi, gönderen istemcide 2 gün sonra veya sunucuda 30 gün sonra sona erdi. \n2. Siz veya kişiniz eski veritabanı yedeğini kullandığınız için mesajın şifresini çözme işlemi başarısız oldu. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml index 732f85e473..a37d13bd51 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml @@ -410,7 +410,6 @@ Приватність перевизначена Ви вирішуєте, хто може під\'єднатися. Як працює SimpleX - Докладніше читайте в нашому репозиторії на GitHub. зашифрований e2e аудіовиклик Відкрийте SimpleX Chat для прийняття виклику e2e зашифровано @@ -651,8 +650,6 @@ Ви можете використовувати markdown для форматування повідомлень: Створіть свій профіль Створіть приватне підключення - як в SimpleX можливо доставляти повідомлення, якщо він не має ідентифікаторів користувачів?]]> - отримувати повідомлення, ваші контакти – сервери, які ви використовуєте для надсилання повідомлень їм.]]> шифрування на двох рівнях.]]> Приватні сповіщення Споживає більше акумулятора! Додаток завжди працює у фоновому режимі – сповіщення відображаються миттєво.]]> diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml index d12db70de6..cefaa982a5 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml @@ -1020,7 +1020,6 @@ Tạo kết nối riêng tư Tạo hồ sơ riêng tư! Đảm bảo địa chỉ máy chủ WebRTC ICE ở đúng định dạng, dòng được phân tách và không bị trùng lặp. - nếu SimpleX không có thông tin định danh người dùng, thì làm thế nào mà nó có thể chuyển tin nhắn đi được?]]> Nó có thể xảy ra khi bạn hoặc liên hệ của bạn sử dụng bản sao lưu cơ sở dữ liệu cũ. Chế độ khóa Giao diện tiếng Ý diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml index ed60383eb1..4c1c60e247 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml @@ -628,7 +628,6 @@ %d 秒 SimpleX 是如何工作的 确保 WebRTC ICE 服务器地址格式正确、每行分开且不重复。 - 如果SimpleX没有用户标识符,它是怎样传递信息的?]]> 确保 SMP 服务器地址格式正确、每行分开且不重复。 Markdown 帮助 标记为已验证 @@ -694,7 +693,6 @@ 必须 保存并通知联系人 保存并通知联系人 - 在我们的 GitHub 仓库中阅读更多内容。 拒绝 为了保护隐私,而不是所有其他平台使用的用户 ID,SimpleX 具有消息队列的标识符,每个联系人都是分开的。 TCP 连接超时 @@ -865,7 +863,6 @@ SimpleX 团队 %1$s 成员 - 接收消息,您的联系人 - 您用来向他们发送消息的服务器。]]> 您将在组主设备上线时连接到该群组,请稍等或稍后再检查! 当您启动应用或在应用程序驻留后台超过30 秒后,您将需要进行身份验证。 连接到 SimpleX Chat 开发者提出任何问题并接收更新 。]]> diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml index 58372bfa7a..65c16db612 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml @@ -554,7 +554,6 @@ 透過群組連結使用匿名聊天模式 一個使用了匿名聊天模式的人透過連結加入了群組 透過使用一次性連結匿名聊天模式連接 - 如果 SimpleX 沒有任何的用戶標識符,它如何傳送訊息?]]> 即時 定期的 關閉 @@ -787,7 +786,6 @@ %ds 私人通知 GitHub內查看更多。]]> - 於 GitHub 儲存庫內查看更多。 視訊通話來電 掛斷電話來電 點對點 @@ -874,7 +872,6 @@ 更新傳輸隔離模式? 為了保護隱私,而不像是其他平台般需要提取和存儲用戶的 IDs 資料, SimpleX 平台有自家佇列的標識符,這對於你的每個聯絡人也是獨一無二的。 當應用程式是運行中 - 來接收 你的聯絡人訊息 – 這些伺服器用來接收他們傳送給你的訊息。]]> 透過設定啟用於上鎖畫面顯示來電通知。 這操作不能還原 - 你目前的個人檔案,聯絡人,訊息和檔案將不可逆地遺失。 你必須在裝置上使用最新版本的對話數據庫,否則你可能會停止接收某些聯絡人的訊息。 diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/ScrollableColumn.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/ScrollableColumn.desktop.kt index a294f1cc60..fc806feb2b 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/ScrollableColumn.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/ScrollableColumn.desktop.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.unit.dp import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.ui.theme.DEFAULT_PADDING import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.onboarding.OnboardingStage import kotlinx.coroutines.* import kotlinx.coroutines.flow.filter import kotlin.math.* @@ -206,7 +207,7 @@ actual fun ColumnWithScrollBar( } val modifier = if (fillMaxSize) Modifier.fillMaxSize().then(modifier) else modifier Box(Modifier.nestedScroll(connection)) { - val oneHandUI = remember { appPrefs.oneHandUI.state } + val oneHandUI = remember { derivedStateOf { if (appPrefs.onboardingStage.state.value == OnboardingStage.OnboardingComplete) appPrefs.oneHandUI.state.value else false } } val padding = if (oneHandUI.value) PaddingValues(bottom = AppBarHeight * fontSizeSqrtMultiplier) else PaddingValues(top = AppBarHeight * fontSizeSqrtMultiplier) Column( if (maxIntrinsicSize) { From 2c0de36439164c97451a74f75bdff50c9c3c6d1f Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sat, 30 Nov 2024 18:44:59 +0000 Subject: [PATCH 091/167] ios: large Conditions screen heading during onboarding --- apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift b/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift index fb3db2b585..14e08ff219 100644 --- a/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift +++ b/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift @@ -328,12 +328,12 @@ struct ChooseServerOperators: View { Text("Conditions will be accepted for operator(s): **\(acceptForOperators.map { $0.legalName_ }.joined(separator: ", "))**.") } ConditionsTextView() + .frame(maxHeight: .infinity) acceptConditionsButton() .padding(.bottom) .padding(.bottom) } .padding(.horizontal, 25) - .frame(maxHeight: .infinity) } private func acceptConditionsButton() -> some View { From a9e7635e00444ee716ad95e9edfddc642599c438 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sat, 30 Nov 2024 20:15:11 +0000 Subject: [PATCH 092/167] core: disable Flux XFTP servers to prevent unknown server warning for the previous version users --- src/Simplex/Chat.hs | 2 +- tests/ChatTests/Direct.hs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 2555582fe9..cc06d1b677 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -166,7 +166,7 @@ operatorFlux = conditionsAcceptance = CARequired Nothing, enabled = False, smpRoles = ServerRoles {storage = False, proxy = True}, - xftpRoles = allRoles + xftpRoles = ServerRoles {storage = False, proxy = True} } defaultChatConfig :: ChatConfig diff --git a/tests/ChatTests/Direct.hs b/tests/ChatTests/Direct.hs index d305055d94..25bcc8659b 100644 --- a/tests/ChatTests/Direct.hs +++ b/tests/ChatTests/Direct.hs @@ -1249,9 +1249,9 @@ testOperators = alice <##. "1 (simplex). SimpleX Chat (SimpleX Chat Ltd), domains: simplex.im, servers: enabled, conditions: accepted (" alice <##. "2 (flux). Flux (InFlux Technologies Limited), domains: simplexonflux.com, servers: disabled, conditions: accepted (" -- update operators - alice ##> "/operators 2:on:smp=proxy" + alice ##> "/operators 2:on:smp=proxy:xftp=off" alice <##. "1 (simplex). SimpleX Chat (SimpleX Chat Ltd), domains: simplex.im, servers: enabled, conditions: accepted (" - alice <##. "2 (flux). Flux (InFlux Technologies Limited), domains: simplexonflux.com, servers: SMP enabled proxy, XFTP enabled, conditions: accepted (" + alice <##. "2 (flux). Flux (InFlux Technologies Limited), domains: simplexonflux.com, servers: SMP enabled proxy, XFTP disabled (servers known), conditions: accepted (" where opts' = testOpts {coreOptions = testCoreOpts {smpServers = [], xftpServers = []}} From 79d5573169443a01d73ce9b2b95253e8999ee040 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Sat, 30 Nov 2024 20:51:35 +0000 Subject: [PATCH 093/167] cli: fix option --yes-migrate to confirm up migrations automatically, closes #5200 (#5286) * fix(cli): option to confirm up migrations didn't work fix #5200 * diff * import --------- Co-authored-by: mervyn <6359152+reply2future@users.noreply.github.com> --- src/Simplex/Chat/Core.hs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Simplex/Chat/Core.hs b/src/Simplex/Chat/Core.hs index ad2f1367da..94af3a9dad 100644 --- a/src/Simplex/Chat/Core.hs +++ b/src/Simplex/Chat/Core.hs @@ -25,21 +25,22 @@ import Simplex.Chat.Options (ChatOpts (..), CoreChatOpts (..)) import Simplex.Chat.Store.Profiles import Simplex.Chat.Types import Simplex.Chat.View (serializeChatResponse) -import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore, withTransaction) +import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore, withTransaction, MigrationConfirmation (..)) import System.Exit (exitFailure) import System.IO (hFlush, stdout) import Text.Read (readMaybe) import UnliftIO.Async simplexChatCore :: ChatConfig -> ChatOpts -> (User -> ChatController -> IO ()) -> IO () -simplexChatCore cfg@ChatConfig {confirmMigrations, testView} opts@ChatOpts {coreOptions = CoreChatOpts {dbFilePrefix, dbKey, logAgent}} chat = +simplexChatCore cfg@ChatConfig {confirmMigrations, testView} opts@ChatOpts {coreOptions = CoreChatOpts {dbFilePrefix, dbKey, logAgent, yesToUpMigrations}} chat = case logAgent of Just level -> do setLogLevel level withGlobalLogging logCfg initRun _ -> initRun where - initRun = createChatDatabase dbFilePrefix dbKey False confirmMigrations >>= either exit run + initRun = createChatDatabase dbFilePrefix dbKey False confirm' >>= either exit run + confirm' = if confirmMigrations == MCConsole && yesToUpMigrations then MCYesUp else confirmMigrations exit e = do putStrLn $ "Error opening database: " <> show e exitFailure From 8f32c6a61ac3fc82b4c27d3674af30e58c947d8f Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sat, 30 Nov 2024 20:54:39 +0000 Subject: [PATCH 094/167] core: 6.2.0.2 --- package.yaml | 2 +- simplex-chat.cabal | 2 +- src/Simplex/Chat/Remote.hs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.yaml b/package.yaml index 8e45db71e2..a4073f9df0 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: simplex-chat -version: 6.2.0.1 +version: 6.2.0.2 #synopsis: #description: homepage: https://github.com/simplex-chat/simplex-chat#readme diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 23071423b8..7ede1f99fc 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplex-chat -version: 6.2.0.1 +version: 6.2.0.2 category: Web, System, Services, Cryptography homepage: https://github.com/simplex-chat/simplex-chat#readme author: simplex.chat diff --git a/src/Simplex/Chat/Remote.hs b/src/Simplex/Chat/Remote.hs index f818c8ea3a..729ee502e8 100644 --- a/src/Simplex/Chat/Remote.hs +++ b/src/Simplex/Chat/Remote.hs @@ -73,11 +73,11 @@ import UnliftIO.Directory (copyFile, createDirectoryIfMissing, doesDirectoryExis -- when acting as host minRemoteCtrlVersion :: AppVersion -minRemoteCtrlVersion = AppVersion [6, 2, 0, 1] +minRemoteCtrlVersion = AppVersion [6, 2, 0, 2] -- when acting as controller minRemoteHostVersion :: AppVersion -minRemoteHostVersion = AppVersion [6, 2, 0, 1] +minRemoteHostVersion = AppVersion [6, 2, 0, 2] currentAppVersion :: AppVersion currentAppVersion = AppVersion SC.version From 98a3437f43085ebac80f4eb4c17a06615d45b582 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sat, 30 Nov 2024 22:26:05 +0000 Subject: [PATCH 095/167] 6.2-beta.2: ios 248, android 253, desktop 77 --- apps/ios/SimpleX.xcodeproj/project.pbxproj | 36 +++++++++++----------- apps/multiplatform/gradle.properties | 8 ++--- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 8d2d05489d..4fd5527ea7 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -148,11 +148,11 @@ 6419EC562AB8BC8B004A607A /* ContextInvitingContactMemberView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6419EC552AB8BC8B004A607A /* ContextInvitingContactMemberView.swift */; }; 6419EC582AB97507004A607A /* CIMemberCreatedContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6419EC572AB97507004A607A /* CIMemberCreatedContactView.swift */; }; 642BA82D2CE50495005E9412 /* NewServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 642BA82C2CE50495005E9412 /* NewServerView.swift */; }; - 642BA8332CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.1-3FFlorLJSLlCbWWiG2Vp14.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 642BA82E2CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.1-3FFlorLJSLlCbWWiG2Vp14.a */; }; + 642BA8332CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.2-5kRGnUsa36hjwDTjUoXRa.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 642BA82E2CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.2-5kRGnUsa36hjwDTjUoXRa.a */; }; 642BA8342CEB3D4B005E9412 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 642BA82F2CEB3D4B005E9412 /* libffi.a */; }; 642BA8352CEB3D4B005E9412 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 642BA8302CEB3D4B005E9412 /* libgmp.a */; }; 642BA8362CEB3D4B005E9412 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 642BA8312CEB3D4B005E9412 /* libgmpxx.a */; }; - 642BA8372CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.1-3FFlorLJSLlCbWWiG2Vp14-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 642BA8322CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.1-3FFlorLJSLlCbWWiG2Vp14-ghc9.6.3.a */; }; + 642BA8372CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.2-5kRGnUsa36hjwDTjUoXRa-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 642BA8322CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.2-5kRGnUsa36hjwDTjUoXRa-ghc9.6.3.a */; }; 6432857C2925443C00FBE5C8 /* GroupPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */; }; 643B3B4E2CCFD6400083A2CF /* OperatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 643B3B4D2CCFD6400083A2CF /* OperatorView.swift */; }; 6440CA00288857A10062C672 /* CIEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440C9FF288857A10062C672 /* CIEventView.swift */; }; @@ -496,11 +496,11 @@ 6419EC552AB8BC8B004A607A /* ContextInvitingContactMemberView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextInvitingContactMemberView.swift; sourceTree = ""; }; 6419EC572AB97507004A607A /* CIMemberCreatedContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIMemberCreatedContactView.swift; sourceTree = ""; }; 642BA82C2CE50495005E9412 /* NewServerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewServerView.swift; sourceTree = ""; }; - 642BA82E2CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.1-3FFlorLJSLlCbWWiG2Vp14.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.2.0.1-3FFlorLJSLlCbWWiG2Vp14.a"; sourceTree = ""; }; + 642BA82E2CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.2-5kRGnUsa36hjwDTjUoXRa.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.2.0.2-5kRGnUsa36hjwDTjUoXRa.a"; sourceTree = ""; }; 642BA82F2CEB3D4B005E9412 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; 642BA8302CEB3D4B005E9412 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; 642BA8312CEB3D4B005E9412 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; - 642BA8322CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.1-3FFlorLJSLlCbWWiG2Vp14-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.2.0.1-3FFlorLJSLlCbWWiG2Vp14-ghc9.6.3.a"; sourceTree = ""; }; + 642BA8322CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.2-5kRGnUsa36hjwDTjUoXRa-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.2.0.2-5kRGnUsa36hjwDTjUoXRa-ghc9.6.3.a"; sourceTree = ""; }; 6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupPreferencesView.swift; sourceTree = ""; }; 643B3B4D2CCFD6400083A2CF /* OperatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperatorView.swift; sourceTree = ""; }; 6440C9FF288857A10062C672 /* CIEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIEventView.swift; sourceTree = ""; }; @@ -672,8 +672,8 @@ CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */, 642BA8342CEB3D4B005E9412 /* libffi.a in Frameworks */, 642BA8352CEB3D4B005E9412 /* libgmp.a in Frameworks */, - 642BA8372CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.1-3FFlorLJSLlCbWWiG2Vp14-ghc9.6.3.a in Frameworks */, - 642BA8332CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.1-3FFlorLJSLlCbWWiG2Vp14.a in Frameworks */, + 642BA8372CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.2-5kRGnUsa36hjwDTjUoXRa-ghc9.6.3.a in Frameworks */, + 642BA8332CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.2-5kRGnUsa36hjwDTjUoXRa.a in Frameworks */, 642BA8362CEB3D4B005E9412 /* libgmpxx.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -754,8 +754,8 @@ 642BA82F2CEB3D4B005E9412 /* libffi.a */, 642BA8302CEB3D4B005E9412 /* libgmp.a */, 642BA8312CEB3D4B005E9412 /* libgmpxx.a */, - 642BA8322CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.1-3FFlorLJSLlCbWWiG2Vp14-ghc9.6.3.a */, - 642BA82E2CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.1-3FFlorLJSLlCbWWiG2Vp14.a */, + 642BA8322CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.2-5kRGnUsa36hjwDTjUoXRa-ghc9.6.3.a */, + 642BA82E2CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.2-5kRGnUsa36hjwDTjUoXRa.a */, ); path = Libraries; sourceTree = ""; @@ -1931,7 +1931,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 247; + CURRENT_PROJECT_VERSION = 248; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -1980,7 +1980,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 247; + CURRENT_PROJECT_VERSION = 248; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -2021,7 +2021,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 247; + CURRENT_PROJECT_VERSION = 248; DEVELOPMENT_TEAM = 5NN7GUYB6T; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; @@ -2041,7 +2041,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 247; + CURRENT_PROJECT_VERSION = 248; DEVELOPMENT_TEAM = 5NN7GUYB6T; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; @@ -2066,7 +2066,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 247; + CURRENT_PROJECT_VERSION = 248; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GCC_OPTIMIZATION_LEVEL = s; @@ -2103,7 +2103,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 247; + CURRENT_PROJECT_VERSION = 248; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_CODE_COVERAGE = NO; @@ -2140,7 +2140,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 247; + CURRENT_PROJECT_VERSION = 248; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -2191,7 +2191,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 247; + CURRENT_PROJECT_VERSION = 248; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -2242,7 +2242,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 247; + CURRENT_PROJECT_VERSION = 248; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -2276,7 +2276,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 247; + CURRENT_PROJECT_VERSION = 248; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; diff --git a/apps/multiplatform/gradle.properties b/apps/multiplatform/gradle.properties index b2d7875074..08056080ea 100644 --- a/apps/multiplatform/gradle.properties +++ b/apps/multiplatform/gradle.properties @@ -24,11 +24,11 @@ android.nonTransitiveRClass=true kotlin.mpp.androidSourceSetLayoutVersion=2 kotlin.jvm.target=11 -android.version_name=6.2-beta.1 -android.version_code=252 +android.version_name=6.2-beta.2 +android.version_code=253 -desktop.version_name=6.2-beta.1 -desktop.version_code=76 +desktop.version_name=6.2-beta.2 +desktop.version_code=77 kotlin.version=1.9.23 gradle.plugin.version=8.2.0 From b8442d92a4817f95ff8fde6664e2c9312fc58f05 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Sun, 1 Dec 2024 13:11:30 +0000 Subject: [PATCH 096/167] core: improve performance of marking chat items as read (#5290) * core: improve performance of marking chat items as read * fix tests --- src/Simplex/Chat.hs | 18 +-- src/Simplex/Chat/Controller.hs | 2 +- src/Simplex/Chat/Store/Messages.hs | 223 +++++++++++++--------------- src/Simplex/Chat/Terminal/Output.hs | 3 +- tests/ChatTests/Direct.hs | 2 - tests/ChatTests/Groups.hs | 3 - tests/ChatTests/Local.hs | 2 +- 7 files changed, 117 insertions(+), 136 deletions(-) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index cc06d1b677..cc7fc992fb 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -1250,13 +1250,13 @@ processChatCommand' vr = \case when (size' > 0) $ copyChunks r w size' APIUserRead userId -> withUserId userId $ \user -> withFastStore' (`setUserChatsRead` user) >> ok user UserRead -> withUser $ \User {userId} -> processChatCommand $ APIUserRead userId - APIChatRead chatRef@(ChatRef cType chatId) fromToIds -> withUser $ \_ -> case cType of + APIChatRead chatRef@(ChatRef cType chatId) -> withUser $ \_ -> case cType of CTDirect -> do user <- withFastStore $ \db -> getUserByContactId db chatId ts <- liftIO getCurrentTime timedItems <- withFastStore' $ \db -> do - timedItems <- getDirectUnreadTimedItems db user chatId fromToIds - updateDirectChatItemsRead db user chatId fromToIds + timedItems <- getDirectUnreadTimedItems db user chatId + updateDirectChatItemsRead db user chatId setDirectChatItemsDeleteAt db user chatId timedItems ts forM_ timedItems $ \(itemId, deleteAt) -> startProximateTimedItemThread user (chatRef, itemId) deleteAt ok user @@ -1264,14 +1264,14 @@ processChatCommand' vr = \case user <- withFastStore $ \db -> getUserByGroupId db chatId ts <- liftIO getCurrentTime timedItems <- withFastStore' $ \db -> do - timedItems <- getGroupUnreadTimedItems db user chatId fromToIds - updateGroupChatItemsRead db user chatId fromToIds + timedItems <- getGroupUnreadTimedItems db user chatId + updateGroupChatItemsRead db user chatId setGroupChatItemsDeleteAt db user chatId timedItems ts forM_ timedItems $ \(itemId, deleteAt) -> startProximateTimedItemThread user (chatRef, itemId) deleteAt ok user CTLocal -> do user <- withFastStore $ \db -> getUserByNoteFolderId db chatId - withFastStore' $ \db -> updateLocalChatItemsRead db user chatId fromToIds + withFastStore' $ \db -> updateLocalChatItemsRead db user chatId ok user CTContactRequest -> pure $ chatCmdError Nothing "not supported" CTContactConnection -> pure $ chatCmdError Nothing "not supported" @@ -1471,7 +1471,7 @@ processChatCommand' vr = \case withCurrentCall contactId $ \user ct Call {chatItemId, callState} -> case callState of CallInvitationReceived {} -> do let aciContent = ACIContent SMDRcv $ CIRcvCall CISCallRejected 0 - withFastStore' $ \db -> updateDirectChatItemsRead db user contactId $ Just (chatItemId, chatItemId) + withFastStore' $ \db -> setDirectChatItemRead db user contactId chatItemId timed_ <- contactCITimed ct updateDirectChatItemView user ct chatItemId aciContent False False timed_ Nothing forM_ (timed_ >>= timedDeleteAt') $ @@ -1487,7 +1487,7 @@ processChatCommand' vr = \case callState' = CallOfferSent {localCallType = callType, peerCallType, localCallSession = rtcSession, sharedKey} aciContent = ACIContent SMDRcv $ CIRcvCall CISCallAccepted 0 (SndMessage {msgId}, _) <- sendDirectContactMessage user ct (XCallOffer callId offer) - withFastStore' $ \db -> updateDirectChatItemsRead db user contactId $ Just (chatItemId, chatItemId) + withFastStore' $ \db -> setDirectChatItemRead db user contactId chatItemId updateDirectChatItemView user ct chatItemId aciContent False False Nothing $ Just msgId pure $ Just call {callState = callState'} _ -> throwChatError . CECallState $ callStateTag callState @@ -8277,7 +8277,7 @@ chatCommandP = "/_forward " *> (APIForwardChatItems <$> chatRefP <* A.space <*> chatRefP <*> _strP <*> sendMessageTTLP), "/_read user " *> (APIUserRead <$> A.decimal), "/read user" $> UserRead, - "/_read chat " *> (APIChatRead <$> chatRefP <*> optional (A.space *> ((,) <$> ("from=" *> A.decimal) <* A.space <*> ("to=" *> A.decimal)))), + "/_read chat " *> (APIChatRead <$> chatRefP), "/_read chat items " *> (APIChatItemsRead <$> chatRefP <*> _strP), "/_unread chat " *> (APIChatUnread <$> chatRefP <* A.space <*> onOffP), "/_delete " *> (APIDeleteChat <$> chatRefP <*> chatDeleteMode), diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index b6f8d5e093..d208efce77 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -309,7 +309,7 @@ data ChatCommand | APIForwardChatItems {toChatRef :: ChatRef, fromChatRef :: ChatRef, chatItemIds :: NonEmpty ChatItemId, ttl :: Maybe Int} | APIUserRead UserId | UserRead - | APIChatRead ChatRef (Maybe (ChatItemId, ChatItemId)) + | APIChatRead ChatRef | APIChatItemsRead ChatRef (NonEmpty ChatItemId) | APIChatUnread ChatRef Bool | APIDeleteChat ChatRef ChatDeleteMode -- currently delete mode settings are only applied to direct chats diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs index f94cbbd81d..cff7f6b785 100644 --- a/src/Simplex/Chat/Store/Messages.hs +++ b/src/Simplex/Chat/Store/Messages.hs @@ -62,6 +62,7 @@ module Simplex.Chat.Store.Messages updateDirectChatItemsRead, getDirectUnreadTimedItems, updateDirectChatItemsReadList, + setDirectChatItemRead, setDirectChatItemsDeleteAt, updateGroupChatItemsRead, getGroupUnreadTimedItems, @@ -1670,61 +1671,61 @@ toChatItemRef = \case (itemId, Nothing, Nothing, Just folderId) -> Right (ChatRef CTLocal folderId, itemId) (itemId, _, _, _) -> Left $ SEBadChatItem itemId Nothing -updateDirectChatItemsRead :: DB.Connection -> User -> ContactId -> Maybe (ChatItemId, ChatItemId) -> IO () -updateDirectChatItemsRead db User {userId} contactId itemsRange_ = do +updateDirectChatItemsRead :: DB.Connection -> User -> ContactId -> IO () +updateDirectChatItemsRead db User {userId} contactId = do currentTs <- getCurrentTime - case itemsRange_ of - Just (fromItemId, toItemId) -> - DB.execute - db - [sql| - UPDATE chat_items SET item_status = ?, updated_at = ? - WHERE user_id = ? AND contact_id = ? AND chat_item_id >= ? AND chat_item_id <= ? AND item_status = ? - |] - (CISRcvRead, currentTs, userId, contactId, fromItemId, toItemId, CISRcvNew) - _ -> - DB.execute - db - [sql| - UPDATE chat_items SET item_status = ?, updated_at = ? - WHERE user_id = ? AND contact_id = ? AND item_status = ? - |] - (CISRcvRead, currentTs, userId, contactId, CISRcvNew) + DB.execute + db + [sql| + UPDATE chat_items SET item_status = ?, updated_at = ? + WHERE user_id = ? AND contact_id = ? AND item_status = ? + |] + (CISRcvRead, currentTs, userId, contactId, CISRcvNew) -getDirectUnreadTimedItems :: DB.Connection -> User -> ContactId -> Maybe (ChatItemId, ChatItemId) -> IO [(ChatItemId, Int)] -getDirectUnreadTimedItems db User {userId} contactId itemsRange_ = case itemsRange_ of - Just (fromItemId, toItemId) -> - DB.query - db - [sql| - SELECT chat_item_id, timed_ttl - FROM chat_items - WHERE user_id = ? AND contact_id = ? - AND chat_item_id >= ? AND chat_item_id <= ? - AND item_status = ? - AND timed_ttl IS NOT NULL AND timed_delete_at IS NULL - AND (item_live IS NULL OR item_live = ?) - |] - (userId, contactId, fromItemId, toItemId, CISRcvNew, False) - _ -> - DB.query - db - [sql| - SELECT chat_item_id, timed_ttl - FROM chat_items - WHERE user_id = ? AND contact_id = ? AND item_status = ? AND timed_ttl IS NOT NULL AND timed_delete_at IS NULL - |] - (userId, contactId, CISRcvNew) +getDirectUnreadTimedItems :: DB.Connection -> User -> ContactId -> IO [(ChatItemId, Int)] +getDirectUnreadTimedItems db User {userId} contactId = + DB.query + db + [sql| + SELECT chat_item_id, timed_ttl + FROM chat_items + WHERE user_id = ? AND contact_id = ? AND item_status = ? AND timed_ttl IS NOT NULL AND timed_delete_at IS NULL + |] + (userId, contactId, CISRcvNew) updateDirectChatItemsReadList :: DB.Connection -> User -> ContactId -> NonEmpty ChatItemId -> IO [(ChatItemId, Int)] -updateDirectChatItemsReadList db user contactId itemIds = do - catMaybes . L.toList <$> mapM getUpdateDirectItem itemIds +updateDirectChatItemsReadList db user@User {userId} contactId itemIds = do + currentTs <- getCurrentTime + catMaybes . L.toList <$> mapM (getUpdateDirectItem currentTs) itemIds where - getUpdateDirectItem chatItemId = do - let itemsRange = Just (chatItemId, chatItemId) - timedItem <- maybeFirstRow id $ getDirectUnreadTimedItems db user contactId itemsRange - updateDirectChatItemsRead db user contactId itemsRange - pure timedItem + getUpdateDirectItem currentTs itemId = do + ttl_ <- maybeFirstRow fromOnly getUnreadTimedItem + setDirectChatItemRead_ db user contactId itemId currentTs + pure $ (itemId,) <$> ttl_ + where + getUnreadTimedItem = + DB.query + db + [sql| + SELECT timed_ttl + FROM chat_items + WHERE user_id = ? AND contact_id = ? AND item_status = ? AND chat_item_id = ? AND timed_ttl IS NOT NULL AND timed_delete_at IS NULL + |] + (userId, contactId, CISRcvNew, itemId) + +setDirectChatItemRead :: DB.Connection -> User -> ContactId -> ChatItemId -> IO () +setDirectChatItemRead db user contactId itemId = + setDirectChatItemRead_ db user contactId itemId =<< getCurrentTime + +setDirectChatItemRead_ :: DB.Connection -> User -> ContactId -> ChatItemId -> UTCTime -> IO () +setDirectChatItemRead_ db User {userId} contactId itemId currentTs = + DB.execute + db + [sql| + UPDATE chat_items SET item_status = ?, updated_at = ? + WHERE user_id = ? AND contact_id = ? AND item_status = ? AND chat_item_id = ? + |] + (CISRcvRead, currentTs, userId, contactId, CISRcvNew, itemId) setDirectChatItemsDeleteAt :: DB.Connection -> User -> ContactId -> [(ChatItemId, Int)] -> UTCTime -> IO [(ChatItemId, UTCTime)] setDirectChatItemsDeleteAt db User {userId} contactId itemIds currentTs = forM itemIds $ \(chatItemId, ttl) -> do @@ -1735,61 +1736,55 @@ setDirectChatItemsDeleteAt db User {userId} contactId itemIds currentTs = forM i (deleteAt, userId, contactId, chatItemId) pure (chatItemId, deleteAt) -updateGroupChatItemsRead :: DB.Connection -> User -> GroupId -> Maybe (ChatItemId, ChatItemId) -> IO () -updateGroupChatItemsRead db User {userId} groupId itemsRange_ = do +updateGroupChatItemsRead :: DB.Connection -> User -> GroupId -> IO () +updateGroupChatItemsRead db User {userId} groupId = do currentTs <- getCurrentTime - case itemsRange_ of - Just (fromItemId, toItemId) -> - DB.execute - db - [sql| - UPDATE chat_items SET item_status = ?, updated_at = ? - WHERE user_id = ? AND group_id = ? AND chat_item_id >= ? AND chat_item_id <= ? AND item_status = ? - |] - (CISRcvRead, currentTs, userId, groupId, fromItemId, toItemId, CISRcvNew) - _ -> - DB.execute - db - [sql| - UPDATE chat_items SET item_status = ?, updated_at = ? - WHERE user_id = ? AND group_id = ? AND item_status = ? - |] - (CISRcvRead, currentTs, userId, groupId, CISRcvNew) + DB.execute + db + [sql| + UPDATE chat_items SET item_status = ?, updated_at = ? + WHERE user_id = ? AND group_id = ? AND item_status = ? + |] + (CISRcvRead, currentTs, userId, groupId, CISRcvNew) -getGroupUnreadTimedItems :: DB.Connection -> User -> GroupId -> Maybe (ChatItemId, ChatItemId) -> IO [(ChatItemId, Int)] -getGroupUnreadTimedItems db User {userId} groupId itemsRange_ = case itemsRange_ of - Just (fromItemId, toItemId) -> - DB.query - db - [sql| - SELECT chat_item_id, timed_ttl - FROM chat_items - WHERE user_id = ? AND group_id = ? - AND chat_item_id >= ? AND chat_item_id <= ? - AND item_status = ? - AND timed_ttl IS NOT NULL AND timed_delete_at IS NULL - AND (item_live IS NULL OR item_live = ?) - |] - (userId, groupId, fromItemId, toItemId, CISRcvNew, False) - _ -> - DB.query - db - [sql| - SELECT chat_item_id, timed_ttl - FROM chat_items - WHERE user_id = ? AND group_id = ? AND item_status = ? AND timed_ttl IS NOT NULL AND timed_delete_at IS NULL - |] - (userId, groupId, CISRcvNew) +getGroupUnreadTimedItems :: DB.Connection -> User -> GroupId -> IO [(ChatItemId, Int)] +getGroupUnreadTimedItems db User {userId} groupId = + DB.query + db + [sql| + SELECT chat_item_id, timed_ttl + FROM chat_items + WHERE user_id = ? AND group_id = ? AND item_status = ? AND timed_ttl IS NOT NULL AND timed_delete_at IS NULL + |] + (userId, groupId, CISRcvNew) updateGroupChatItemsReadList :: DB.Connection -> User -> GroupId -> NonEmpty ChatItemId -> IO [(ChatItemId, Int)] -updateGroupChatItemsReadList db user groupId itemIds = do - catMaybes . L.toList <$> mapM getUpdateGroupItem itemIds +updateGroupChatItemsReadList db User {userId} groupId itemIds = do + currentTs <- getCurrentTime + catMaybes . L.toList <$> mapM (getUpdateGroupItem currentTs) itemIds where - getUpdateGroupItem chatItemId = do - let itemsRange = Just (chatItemId, chatItemId) - timedItem <- maybeFirstRow id $ getGroupUnreadTimedItems db user groupId itemsRange - updateGroupChatItemsRead db user groupId itemsRange - pure timedItem + getUpdateGroupItem currentTs itemId = do + ttl_ <- maybeFirstRow fromOnly getUnreadTimedItem + setItemRead + pure $ (itemId,) <$> ttl_ + where + getUnreadTimedItem = + DB.query + db + [sql| + SELECT timed_ttl + FROM chat_items + WHERE user_id = ? AND group_id = ? AND item_status = ? AND chat_item_id = ? AND timed_ttl IS NOT NULL AND timed_delete_at IS NULL + |] + (userId, groupId, CISRcvNew, itemId) + setItemRead = + DB.execute + db + [sql| + UPDATE chat_items SET item_status = ?, updated_at = ? + WHERE user_id = ? AND group_id = ? AND item_status = ? AND chat_item_id = ? + |] + (CISRcvRead, currentTs, userId, groupId, CISRcvNew, itemId) setGroupChatItemsDeleteAt :: DB.Connection -> User -> GroupId -> [(ChatItemId, Int)] -> UTCTime -> IO [(ChatItemId, UTCTime)] setGroupChatItemsDeleteAt db User {userId} groupId itemIds currentTs = forM itemIds $ \(chatItemId, ttl) -> do @@ -1800,26 +1795,16 @@ setGroupChatItemsDeleteAt db User {userId} groupId itemIds currentTs = forM item (deleteAt, userId, groupId, chatItemId) pure (chatItemId, deleteAt) -updateLocalChatItemsRead :: DB.Connection -> User -> NoteFolderId -> Maybe (ChatItemId, ChatItemId) -> IO () -updateLocalChatItemsRead db User {userId} noteFolderId itemsRange_ = do +updateLocalChatItemsRead :: DB.Connection -> User -> NoteFolderId -> IO () +updateLocalChatItemsRead db User {userId} noteFolderId = do currentTs <- getCurrentTime - case itemsRange_ of - Just (fromItemId, toItemId) -> - DB.execute - db - [sql| - UPDATE chat_items SET item_status = ?, updated_at = ? - WHERE user_id = ? AND note_folder_id = ? AND chat_item_id >= ? AND chat_item_id <= ? AND item_status = ? - |] - (CISRcvRead, currentTs, userId, noteFolderId, fromItemId, toItemId, CISRcvNew) - _ -> - DB.execute - db - [sql| - UPDATE chat_items SET item_status = ?, updated_at = ? - WHERE user_id = ? AND note_folder_id = ? AND item_status = ? - |] - (CISRcvRead, currentTs, userId, noteFolderId, CISRcvNew) + DB.execute + db + [sql| + UPDATE chat_items SET item_status = ?, updated_at = ? + WHERE user_id = ? AND note_folder_id = ? AND item_status = ? + |] + (CISRcvRead, currentTs, userId, noteFolderId, CISRcvNew) type MaybeCIFIleRow = (Maybe Int64, Maybe String, Maybe Integer, Maybe FilePath, Maybe C.SbKey, Maybe C.CbNonce, Maybe ACIFileStatus, Maybe FileProtocol) diff --git a/src/Simplex/Chat/Terminal/Output.hs b/src/Simplex/Chat/Terminal/Output.hs index 0ead850b86..37c5c039c1 100644 --- a/src/Simplex/Chat/Terminal/Output.hs +++ b/src/Simplex/Chat/Terminal/Output.hs @@ -3,6 +3,7 @@ {-# LANGUAGE GADTs #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedLists #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} @@ -164,7 +165,7 @@ runTerminalOutput ct cc@ChatController {outputQ, showLiveItems, logFilePath} Cha (True, CISRcvNew) -> do let itemId = chatItemId' ci chatRef = chatInfoToRef chat - void $ runReaderT (runExceptT $ processChatCommand (APIChatRead chatRef (Just (itemId, itemId)))) cc + void $ runReaderT (runExceptT $ processChatCommand (APIChatItemsRead chatRef [itemId])) cc _ -> pure () logResponse path s = withFile path AppendMode $ \h -> mapM_ (hPutStrLn h . unStyle) s getRemoteUser rhId = diff --git a/tests/ChatTests/Direct.hs b/tests/ChatTests/Direct.hs index 25bcc8659b..72a28c3ada 100644 --- a/tests/ChatTests/Direct.hs +++ b/tests/ChatTests/Direct.hs @@ -217,8 +217,6 @@ testAddContact = versionTestMatrix2 runTestAddContact -- search alice #$> ("/_get chat @2 count=100 search=ello ther", chat, [(1, "hello there 🙂"), (0, "hello there")]) -- read messages - alice #$> ("/_read chat @2 from=1 to=100", id, "ok") - bob #$> ("/_read chat @2 from=1 to=100", id, "ok") alice #$> ("/_read chat @2", id, "ok") bob #$> ("/_read chat @2", id, "ok") alice #$> ("/read user", id, "ok") diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index a1d9951088..89462b2b61 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -353,9 +353,6 @@ testGroupShared alice bob cath checkMessages directConnections = do bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "added cath (Catherine)"), (0, "connected"), (0, "hello"), (1, "hi there"), (0, "hey team")]) cath @@@ [("@bob", "hey"), ("#team", "hey team"), ("@alice", "received invitation to join group team as admin")] cath #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "connected"), (0, "hello"), (0, "hi there"), (1, "hey team")]) - alice #$> ("/_read chat #1 from=1 to=100", id, "ok") - bob #$> ("/_read chat #1 from=1 to=100", id, "ok") - cath #$> ("/_read chat #1 from=1 to=100", id, "ok") alice #$> ("/_read chat #1", id, "ok") bob #$> ("/_read chat #1", id, "ok") cath #$> ("/_read chat #1", id, "ok") diff --git a/tests/ChatTests/Local.hs b/tests/ChatTests/Local.hs index 40df02252d..c17b893be1 100644 --- a/tests/ChatTests/Local.hs +++ b/tests/ChatTests/Local.hs @@ -41,7 +41,7 @@ testNotes tmp = withNewTestChat tmp "alice" aliceProfile $ \alice -> do alice ##> "/? keep" alice <# "* keep in mind" - alice #$> ("/_read chat *1 from=1 to=100", id, "ok") + alice #$> ("/_read chat *1", id, "ok") alice ##> "/_unread chat *1 on" alice <## "ok" From 3143cc960e44ebee4a9b201234e53720fd66cd5d Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sun, 1 Dec 2024 13:18:57 +0000 Subject: [PATCH 097/167] core: 6.2.0.3 --- package.yaml | 2 +- simplex-chat.cabal | 2 +- src/Simplex/Chat/Remote.hs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.yaml b/package.yaml index a4073f9df0..98571e1342 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: simplex-chat -version: 6.2.0.2 +version: 6.2.0.3 #synopsis: #description: homepage: https://github.com/simplex-chat/simplex-chat#readme diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 7ede1f99fc..1b1a0b9753 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplex-chat -version: 6.2.0.2 +version: 6.2.0.3 category: Web, System, Services, Cryptography homepage: https://github.com/simplex-chat/simplex-chat#readme author: simplex.chat diff --git a/src/Simplex/Chat/Remote.hs b/src/Simplex/Chat/Remote.hs index 729ee502e8..3a7d450691 100644 --- a/src/Simplex/Chat/Remote.hs +++ b/src/Simplex/Chat/Remote.hs @@ -73,11 +73,11 @@ import UnliftIO.Directory (copyFile, createDirectoryIfMissing, doesDirectoryExis -- when acting as host minRemoteCtrlVersion :: AppVersion -minRemoteCtrlVersion = AppVersion [6, 2, 0, 2] +minRemoteCtrlVersion = AppVersion [6, 2, 0, 3] -- when acting as controller minRemoteHostVersion :: AppVersion -minRemoteHostVersion = AppVersion [6, 2, 0, 2] +minRemoteHostVersion = AppVersion [6, 2, 0, 3] currentAppVersion :: AppVersion currentAppVersion = AppVersion SC.version From c488c4fcd52c5716fbf7b4bcab86262575dc8d35 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sun, 1 Dec 2024 19:09:53 +0000 Subject: [PATCH 098/167] 6.2-beta.3: ios 249, android 254, desktop 78 --- apps/ios/SimpleX.xcodeproj/project.pbxproj | 36 +++++++++++----------- apps/multiplatform/gradle.properties | 8 ++--- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 4fd5527ea7..31917f1ab9 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -148,11 +148,11 @@ 6419EC562AB8BC8B004A607A /* ContextInvitingContactMemberView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6419EC552AB8BC8B004A607A /* ContextInvitingContactMemberView.swift */; }; 6419EC582AB97507004A607A /* CIMemberCreatedContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6419EC572AB97507004A607A /* CIMemberCreatedContactView.swift */; }; 642BA82D2CE50495005E9412 /* NewServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 642BA82C2CE50495005E9412 /* NewServerView.swift */; }; - 642BA8332CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.2-5kRGnUsa36hjwDTjUoXRa.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 642BA82E2CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.2-5kRGnUsa36hjwDTjUoXRa.a */; }; + 642BA8332CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.3-ELfYrsBTXJJ5vBEIgQ1y2B.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 642BA82E2CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.3-ELfYrsBTXJJ5vBEIgQ1y2B.a */; }; 642BA8342CEB3D4B005E9412 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 642BA82F2CEB3D4B005E9412 /* libffi.a */; }; 642BA8352CEB3D4B005E9412 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 642BA8302CEB3D4B005E9412 /* libgmp.a */; }; 642BA8362CEB3D4B005E9412 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 642BA8312CEB3D4B005E9412 /* libgmpxx.a */; }; - 642BA8372CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.2-5kRGnUsa36hjwDTjUoXRa-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 642BA8322CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.2-5kRGnUsa36hjwDTjUoXRa-ghc9.6.3.a */; }; + 642BA8372CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.3-ELfYrsBTXJJ5vBEIgQ1y2B-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 642BA8322CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.3-ELfYrsBTXJJ5vBEIgQ1y2B-ghc9.6.3.a */; }; 6432857C2925443C00FBE5C8 /* GroupPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */; }; 643B3B4E2CCFD6400083A2CF /* OperatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 643B3B4D2CCFD6400083A2CF /* OperatorView.swift */; }; 6440CA00288857A10062C672 /* CIEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440C9FF288857A10062C672 /* CIEventView.swift */; }; @@ -496,11 +496,11 @@ 6419EC552AB8BC8B004A607A /* ContextInvitingContactMemberView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextInvitingContactMemberView.swift; sourceTree = ""; }; 6419EC572AB97507004A607A /* CIMemberCreatedContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIMemberCreatedContactView.swift; sourceTree = ""; }; 642BA82C2CE50495005E9412 /* NewServerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewServerView.swift; sourceTree = ""; }; - 642BA82E2CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.2-5kRGnUsa36hjwDTjUoXRa.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.2.0.2-5kRGnUsa36hjwDTjUoXRa.a"; sourceTree = ""; }; + 642BA82E2CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.3-ELfYrsBTXJJ5vBEIgQ1y2B.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.2.0.3-ELfYrsBTXJJ5vBEIgQ1y2B.a"; sourceTree = ""; }; 642BA82F2CEB3D4B005E9412 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; 642BA8302CEB3D4B005E9412 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; 642BA8312CEB3D4B005E9412 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; - 642BA8322CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.2-5kRGnUsa36hjwDTjUoXRa-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.2.0.2-5kRGnUsa36hjwDTjUoXRa-ghc9.6.3.a"; sourceTree = ""; }; + 642BA8322CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.3-ELfYrsBTXJJ5vBEIgQ1y2B-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.2.0.3-ELfYrsBTXJJ5vBEIgQ1y2B-ghc9.6.3.a"; sourceTree = ""; }; 6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupPreferencesView.swift; sourceTree = ""; }; 643B3B4D2CCFD6400083A2CF /* OperatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperatorView.swift; sourceTree = ""; }; 6440C9FF288857A10062C672 /* CIEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIEventView.swift; sourceTree = ""; }; @@ -672,8 +672,8 @@ CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */, 642BA8342CEB3D4B005E9412 /* libffi.a in Frameworks */, 642BA8352CEB3D4B005E9412 /* libgmp.a in Frameworks */, - 642BA8372CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.2-5kRGnUsa36hjwDTjUoXRa-ghc9.6.3.a in Frameworks */, - 642BA8332CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.2-5kRGnUsa36hjwDTjUoXRa.a in Frameworks */, + 642BA8372CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.3-ELfYrsBTXJJ5vBEIgQ1y2B-ghc9.6.3.a in Frameworks */, + 642BA8332CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.3-ELfYrsBTXJJ5vBEIgQ1y2B.a in Frameworks */, 642BA8362CEB3D4B005E9412 /* libgmpxx.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -754,8 +754,8 @@ 642BA82F2CEB3D4B005E9412 /* libffi.a */, 642BA8302CEB3D4B005E9412 /* libgmp.a */, 642BA8312CEB3D4B005E9412 /* libgmpxx.a */, - 642BA8322CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.2-5kRGnUsa36hjwDTjUoXRa-ghc9.6.3.a */, - 642BA82E2CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.2-5kRGnUsa36hjwDTjUoXRa.a */, + 642BA8322CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.3-ELfYrsBTXJJ5vBEIgQ1y2B-ghc9.6.3.a */, + 642BA82E2CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.3-ELfYrsBTXJJ5vBEIgQ1y2B.a */, ); path = Libraries; sourceTree = ""; @@ -1931,7 +1931,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 248; + CURRENT_PROJECT_VERSION = 249; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -1980,7 +1980,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 248; + CURRENT_PROJECT_VERSION = 249; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -2021,7 +2021,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 248; + CURRENT_PROJECT_VERSION = 249; DEVELOPMENT_TEAM = 5NN7GUYB6T; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; @@ -2041,7 +2041,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 248; + CURRENT_PROJECT_VERSION = 249; DEVELOPMENT_TEAM = 5NN7GUYB6T; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; @@ -2066,7 +2066,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 248; + CURRENT_PROJECT_VERSION = 249; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GCC_OPTIMIZATION_LEVEL = s; @@ -2103,7 +2103,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 248; + CURRENT_PROJECT_VERSION = 249; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_CODE_COVERAGE = NO; @@ -2140,7 +2140,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 248; + CURRENT_PROJECT_VERSION = 249; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -2191,7 +2191,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 248; + CURRENT_PROJECT_VERSION = 249; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -2242,7 +2242,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 248; + CURRENT_PROJECT_VERSION = 249; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -2276,7 +2276,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 248; + CURRENT_PROJECT_VERSION = 249; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; diff --git a/apps/multiplatform/gradle.properties b/apps/multiplatform/gradle.properties index 08056080ea..0893c75520 100644 --- a/apps/multiplatform/gradle.properties +++ b/apps/multiplatform/gradle.properties @@ -24,11 +24,11 @@ android.nonTransitiveRClass=true kotlin.mpp.androidSourceSetLayoutVersion=2 kotlin.jvm.target=11 -android.version_name=6.2-beta.2 -android.version_code=253 +android.version_name=6.2-beta.3 +android.version_code=254 -desktop.version_name=6.2-beta.2 -desktop.version_code=77 +desktop.version_name=6.2-beta.3 +desktop.version_code=78 kotlin.version=1.9.23 gradle.plugin.version=8.2.0 From 5f01dc1a3f6b922a3025a60f933942ea2fe4294d Mon Sep 17 00:00:00 2001 From: Evgeny Date: Mon, 2 Dec 2024 14:01:23 +0000 Subject: [PATCH 099/167] core: support business addresses and chats (#5272) * core: support business addresses and chats * types * connect plan, add link type * ios: toggle on address UI * make compile * todo * fix migration * types * comments * fix * remove * fix schema * comment * simplify * remove diff * comment * comment * diff * acceptBusinessJoinRequestAsync wip * comment * update * simplify types * remove business * wip * read/write columns * createBusinessRequestGroup * remove comments * read/write business_address column * validate that business address is not set to be incognito * replace contact card * update simplexmq * refactor * event when accepting business address request * sendGroupAutoReply * delete contact request earlier * test, fix * refactor * refactor2 --------- Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> --- .../Views/UserSettings/UserAddressView.swift | 37 +++- apps/ios/SimpleX.xcodeproj/project.pbxproj | 4 + apps/ios/SimpleXChat/APITypes.swift | 4 +- cabal.project | 2 +- docs/rfcs/2024-11-28-business-address.md | 29 +++ scripts/nix/sha256map.nix | 2 +- simplex-chat.cabal | 1 + src/Simplex/Chat.hs | 165 +++++++++++++----- src/Simplex/Chat/Bot.hs | 2 +- src/Simplex/Chat/Controller.hs | 2 + .../Migrations/M20241128_business_chats.hs | 22 +++ src/Simplex/Chat/Migrations/chat_schema.sql | 5 +- src/Simplex/Chat/Protocol.hs | 7 +- src/Simplex/Chat/Store/Connections.hs | 2 +- src/Simplex/Chat/Store/Groups.hs | 90 ++++++++-- src/Simplex/Chat/Store/Migrations.hs | 4 +- src/Simplex/Chat/Store/Profiles.hs | 31 ++-- src/Simplex/Chat/Types.hs | 39 ++++- src/Simplex/Chat/Types/Preferences.hs | 27 +++ src/Simplex/Chat/View.hs | 14 +- tests/ChatTests/Profiles.hs | 47 ++++- tests/ChatTests/Utils.hs | 3 + tests/ProtocolTests.hs | 12 +- 23 files changed, 454 insertions(+), 97 deletions(-) create mode 100644 docs/rfcs/2024-11-28-business-address.md create mode 100644 src/Simplex/Chat/Migrations/M20241128_business_chats.hs diff --git a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift index 28301c5ddb..6bc3a221b2 100644 --- a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift +++ b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift @@ -16,6 +16,8 @@ struct UserAddressView: View { @EnvironmentObject var theme: AppTheme @State var shareViaProfile = false @State var autoCreate = false + @State private var aas = AutoAcceptState() + @State private var savedAAS = AutoAcceptState() @State private var showMailView = false @State private var mailViewResult: Result? = nil @State private var alert: UserAddressAlert? @@ -55,7 +57,15 @@ struct UserAddressView: View { if chatModel.userAddress == nil, autoCreate { createAddress() } + if let userAddress = chatModel.userAddress { + aas = AutoAcceptState(userAddress: userAddress) + savedAAS = aas + } } + .onChange(of: aas.enable) { aasEnabled in + if !aasEnabled { aas = AutoAcceptState() } + } + } private func userAddressView() -> some View { @@ -135,10 +145,23 @@ struct UserAddressView: View { // if MFMailComposeViewController.canSendMail() { // shareViaEmailButton(userAddress) // } + settingsRow("hand.wave", color: theme.colors.secondary) { + Toggle("Business address", isOn: $aas.business) + .onChange(of: aas.business) { ba in + if ba { + aas.enable = true + aas.incognito = false + } + } + } addressSettingsButton(userAddress) } header: { Text("For social media") .foregroundColor(theme.colors.secondary) + } footer: { + if aas.business { + Text("Add your team members to the conversations").foregroundColor(theme.colors.secondary) + } } Section { @@ -276,11 +299,13 @@ struct UserAddressView: View { private struct AutoAcceptState: Equatable { var enable = false var incognito = false + var business = false var welcomeText = "" - init(enable: Bool = false, incognito: Bool = false, welcomeText: String = "") { + init(enable: Bool = false, incognito: Bool = false, business: Bool = false, welcomeText: String = "") { self.enable = enable self.incognito = incognito + self.business = business self.welcomeText = welcomeText } @@ -288,6 +313,7 @@ private struct AutoAcceptState: Equatable { if let aa = userAddress.autoAccept { enable = true incognito = aa.acceptIncognito + business = aa.businessAddress == true if let msg = aa.autoReply { welcomeText = msg.text } else { @@ -296,6 +322,7 @@ private struct AutoAcceptState: Equatable { } else { enable = false incognito = false + business = false welcomeText = "" } } @@ -305,7 +332,7 @@ private struct AutoAcceptState: Equatable { var autoReply: MsgContent? = nil let s = welcomeText.trimmingCharacters(in: .whitespacesAndNewlines) if s != "" { autoReply = .text(s) } - return AutoAccept(acceptIncognito: incognito, autoReply: autoReply) + return AutoAccept(businessAddress: business, acceptIncognito: incognito, autoReply: autoReply) } return nil } @@ -373,7 +400,7 @@ struct UserAddressSettingsView: View { List { Section { shareWithContactsButton() - autoAcceptToggle() + autoAcceptToggle().disabled(aas.business) } if aas.enable { @@ -450,7 +477,9 @@ struct UserAddressSettingsView: View { private func autoAcceptSection() -> some View { Section { - acceptIncognitoToggle() + if !aas.business { + acceptIncognitoToggle() + } welcomeMessageEditor() saveAASButton() .disabled(aas == savedAAS) diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 31917f1ab9..7e5a48013b 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -229,6 +229,7 @@ D741547A29AF90B00022400A /* PushKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D741547929AF90B00022400A /* PushKit.framework */; }; D77B92DC2952372200A5A1CC /* SwiftyGif in Frameworks */ = {isa = PBXBuildFile; productRef = D77B92DB2952372200A5A1CC /* SwiftyGif */; }; D7F0E33929964E7E0068AF69 /* LZString in Frameworks */ = {isa = PBXBuildFile; productRef = D7F0E33829964E7E0068AF69 /* LZString */; }; + E504516F2CFA3BFB00DE3F74 /* ContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = E504516E2CFA3BFB00DE3F74 /* ContextMenu.swift */; }; E51CC1E62C62085600DB91FE /* OneHandUICard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E51CC1E52C62085600DB91FE /* OneHandUICard.swift */; }; E5DCF8DB2C56FAC1007928CC /* SimpleXChat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; }; E5DCF9712C590272007928CC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E5DCF96F2C590272007928CC /* Localizable.strings */; }; @@ -575,6 +576,7 @@ D741547729AF89AF0022400A /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/StoreKit.framework; sourceTree = DEVELOPER_DIR; }; 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; }; + E504516E2CFA3BFB00DE3F74 /* ContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenu.swift; sourceTree = ""; }; E51CC1E52C62085600DB91FE /* OneHandUICard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneHandUICard.swift; sourceTree = ""; }; E5DCF9702C590272007928CC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; E5DCF9722C590274007928CC /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Localizable.strings; sourceTree = ""; }; @@ -791,6 +793,7 @@ 5C971E1F27AEBF7000C8A3CE /* Helpers */ = { isa = PBXGroup; children = ( + E504516E2CFA3BFB00DE3F74 /* ContextMenu.swift */, 5C971E2027AEBF8300C8A3CE /* ChatInfoImage.swift */, 5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */, 5CC1C99427A6CF7F000D9FF6 /* ShareSheet.swift */, @@ -1445,6 +1448,7 @@ CE984D4B2C36C5D500E3AEFF /* ChatItemClipShape.swift in Sources */, 64D0C2C629FAC1EC00B38D5F /* AddContactLearnMore.swift in Sources */, 5C3A88D127DF57800060F1C2 /* FramedItemView.swift in Sources */, + E504516F2CFA3BFB00DE3F74 /* ContextMenu.swift in Sources */, 5C65F343297D45E100B67AF3 /* VersionView.swift in Sources */, 64F1CC3B28B39D8600CD1FB1 /* IncognitoHelp.swift in Sources */, 5CB0BA90282713D900B3292C /* SimpleXInfo.swift in Sources */, diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 83c74178ba..954022c312 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -2103,10 +2103,12 @@ public struct UserContactLink: Decodable, Hashable { } public struct AutoAccept: Codable, Hashable { + public var businessAddress: Bool? // make not nullable public var acceptIncognito: Bool public var autoReply: MsgContent? - public init(acceptIncognito: Bool, autoReply: MsgContent? = nil) { + public init(businessAddress: Bool, acceptIncognito: Bool, autoReply: MsgContent? = nil) { + self.businessAddress = businessAddress self.acceptIncognito = acceptIncognito self.autoReply = autoReply } diff --git a/cabal.project b/cabal.project index 2246cfeb1d..b89dc764cb 100644 --- a/cabal.project +++ b/cabal.project @@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 601620bdde612ebdd33da2637d99b15ff32170c9 + tag: 38ad3c046e1bd5eb1ffe696dd24b10dd69001ba2 source-repository-package type: git diff --git a/docs/rfcs/2024-11-28-business-address.md b/docs/rfcs/2024-11-28-business-address.md new file mode 100644 index 0000000000..e41c904457 --- /dev/null +++ b/docs/rfcs/2024-11-28-business-address.md @@ -0,0 +1,29 @@ +# Business address + +## Problem + +When business uses a communication system for support and other business scenarios, it's important for the customer: +- to be able to talk to multiple people in the business, and know who they are. +- potentially, add friends or relatives to the conversation if this is about a group purchase. + +It's important for the business: +- to have bot accept incoming requests. +- to be able to add other people to the coversation, as transfer and as escalation. + +This is how all messaging support system works, and how WeChat business accounts work, but no messenger provides it. + +## Solution + +Make current contact addresses to support business mode. We already have all the elements for that. + +- connection requests will be accepted automatically (non-optionally), and auto-reply will be sent (if provided). +- the request sender will be made member, can be made admin later manually. +- the new group with the customer will be created on each request instead of direct conversation. + +Group will function differently from a normal group: +- Show business name and avatar to customer, customer name and avatar to business. +- Use different icon for customer and for the business if the avatar is not provided. +- Possibly, a sub-icon on business avatar for customers. +- Members added by business are marked as business, by customer as customer (not MVP). + +This functionality allows to develop support bots that automatically reply, potentially answer some questions, and add support agents as required, who can escalate further. diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 72d2ddd59b..7a812dbc6e 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."601620bdde612ebdd33da2637d99b15ff32170c9" = "0lgiphb9sf5i29d378pah24mhf7m8df75jk6asvw8ns527g4amj1"; + "https://github.com/simplex-chat/simplexmq.git"."38ad3c046e1bd5eb1ffe696dd24b10dd69001ba2" = "0nq2a2lklbxpc049zjxa5w8c63l9l9nf08jb7pny42nmah0mlc20"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 1b1a0b9753..4e339fc5da 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -153,6 +153,7 @@ library Simplex.Chat.Migrations.M20241023_chat_item_autoincrement_id Simplex.Chat.Migrations.M20241027_server_operators Simplex.Chat.Migrations.M20241125_indexes + Simplex.Chat.Migrations.M20241128_business_chats Simplex.Chat.Mobile Simplex.Chat.Mobile.File Simplex.Chat.Mobile.Shared diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index cc7fc992fb..92ec04d11b 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -2065,6 +2065,8 @@ processChatCommand' vr = \case SetProfileAddress onOff -> withUser $ \User {userId} -> processChatCommand $ APISetProfileAddress userId onOff APIAddressAutoAccept userId autoAccept_ -> withUserId userId $ \user -> do + forM_ autoAccept_ $ \AutoAccept {businessAddress, acceptIncognito} -> + when (businessAddress && acceptIncognito) $ throwChatError $ CECommandError "requests to business address cannot be accepted incognito" contactLink <- withFastStore (\db -> updateUserAddressAutoAccept db user autoAccept_) pure $ CRUserContactLinkUpdated user contactLink AddressAutoAccept autoAccept_ -> withUser $ \User {userId} -> @@ -3007,7 +3009,7 @@ processChatCommand' vr = \case groupMemberId <- getGroupMemberIdByName db user groupId groupMemberName pure (groupId, groupMemberId) sendGrpInvitation :: User -> Contact -> GroupInfo -> GroupMember -> ConnReqInvitation -> CM () - sendGrpInvitation user ct@Contact {contactId, localDisplayName} gInfo@GroupInfo {groupId, groupProfile, membership} GroupMember {groupMemberId, memberId, memberRole = memRole} cReq = do + sendGrpInvitation user ct@Contact {contactId, localDisplayName} gInfo@GroupInfo {groupId, groupProfile, membership, businessChat} GroupMember {groupMemberId, memberId, memberRole = memRole} cReq = do currentMemCount <- withStore' $ \db -> getGroupCurrentMembersCount db user gInfo let GroupMember {memberRole = userRole, memberId = userMemberId} = membership groupInv = @@ -3016,6 +3018,7 @@ processChatCommand' vr = \case invitedMember = MemberIdRole memberId memRole, connRequest = cReq, groupProfile, + businessChat, groupLinkId = Nothing, groupSize = Just currentMemCount } @@ -3972,12 +3975,14 @@ acceptContactRequestAsync user cReq@UserContactRequest {agentInvitationId = Agen acceptGroupJoinRequestAsync :: User -> GroupInfo -> UserContactRequest -> GroupMemberRole -> Maybe IncognitoProfile -> CM GroupMember acceptGroupJoinRequestAsync user - gInfo@GroupInfo {groupProfile, membership} + gInfo@GroupInfo {groupProfile, membership, businessChat} ucr@UserContactRequest {agentInvitationId = AgentInvId invId, cReqChatVRange} gLinkMemRole incognitoProfile = do gVar <- asks random - (groupMemberId, memberId) <- withStore $ \db -> createAcceptedMember db gVar user gInfo ucr gLinkMemRole + (groupMemberId, memberId) <- withStore $ \db -> do + liftIO $ deleteContactRequestRec db user ucr + createAcceptedMember db gVar user gInfo ucr gLinkMemRole currentMemCount <- withStore' $ \db -> getGroupCurrentMembersCount db user gInfo let Profile {displayName} = profileToSendOnAccept user incognitoProfile True GroupMember {memberRole = userRole, memberId = userMemberId} = membership @@ -3988,6 +3993,7 @@ acceptGroupJoinRequestAsync fromMemberName = displayName, invitedMember = MemberIdRole memberId gLinkMemRole, groupProfile, + businessChat, groupSize = Just currentMemCount } subMode <- chatReadVar subscriptionMode @@ -3998,6 +4004,43 @@ acceptGroupJoinRequestAsync liftIO $ createAcceptedMemberConnection db user connIds chatV ucr groupMemberId subMode getGroupMemberById db vr user groupMemberId +acceptBusinessJoinRequestAsync :: User -> UserContactRequest -> CM GroupInfo +acceptBusinessJoinRequestAsync + user + ucr@UserContactRequest {agentInvitationId = AgentInvId invId, cReqChatVRange} = do + vr <- chatVersionRange + gVar <- asks random + let userProfile@Profile {displayName, preferences} = profileToSendOnAccept user Nothing True + groupPreferences = maybe defaultBusinessGroupPrefs businessGroupPrefs preferences + (gInfo, clientMember) <- withStore $ \db -> do + liftIO $ deleteContactRequestRec db user ucr + createBusinessRequestGroup db vr gVar user ucr groupPreferences + let GroupInfo {membership} = gInfo + GroupMember {memberRole = userRole, memberId = userMemberId} = membership + GroupMember {groupMemberId, memberId} = clientMember + msg = + XGrpLinkInv $ + GroupLinkInvitation + { fromMember = MemberIdRole userMemberId userRole, + fromMemberName = displayName, + invitedMember = MemberIdRole memberId GRMember, + groupProfile = businessGroupProfile userProfile groupPreferences, + -- This refers to the "title member" that defines the group name and profile. + -- This coincides with fromMember to be current user when accepting the connecting user, + -- but it will be different when inviting somebody else. + businessChat = Just $ BusinessChatInfo userMemberId BCBusiness, + groupSize = Just 1 + } + subMode <- chatReadVar subscriptionMode + let chatV = vr `peerConnChatVersion` cReqChatVRange + connIds <- agentAcceptContactAsync user True invId msg subMode PQSupportOff chatV + withStore' $ \db -> createAcceptedMemberConnection db user connIds chatV ucr groupMemberId subMode + pure gInfo + where + businessGroupProfile :: Profile -> GroupPreferences -> GroupProfile + businessGroupProfile Profile {displayName, fullName, image} groupPreferences = + GroupProfile {displayName, fullName, description = Nothing, image, groupPreferences = Just groupPreferences} + profileToSendOnAccept :: User -> Maybe IncognitoProfile -> Bool -> Profile profileToSendOnAccept user ip = userProfileToSend user (getIncognitoProfile <$> ip) Nothing where @@ -4683,15 +4726,14 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = CONF confId pqSupport _ connInfo -> do conn' <- processCONFpqSupport conn pqSupport -- [incognito] send saved profile + (conn'', inGroup) <- saveConnInfo conn' connInfo incognitoProfile <- forM customUserProfileId $ \profileId -> withStore (\db -> getProfileById db userId profileId) - let profileToSend = userProfileToSend user (fromLocalProfile <$> incognitoProfile) Nothing False - conn'' <- saveConnInfo conn' connInfo + let profileToSend = userProfileToSend user (fromLocalProfile <$> incognitoProfile) Nothing inGroup -- [async agent commands] no continuation needed, but command should be asynchronous for stability allowAgentConnectionAsync user conn'' confId $ XInfo profileToSend INFO pqSupport connInfo -> do processINFOpqSupport conn pqSupport - _conn' <- saveConnInfo conn connInfo - pure () + void $ saveConnInfo conn connInfo MSG meta _msgFlags _msgBody -> -- We are not saving message (saveDirectRcvMSG) as contact hasn't been created yet, -- chat item is also not created here @@ -4806,6 +4848,16 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = let p = userProfileToSend user (fromLocalProfile <$> incognitoProfile) (Just ct') False allowAgentConnectionAsync user conn'' confId $ XInfo p void $ withStore' $ \db -> resetMemberContactFields db ct' + XGrpLinkInv glInv -> do + -- XGrpLinkInv here means we are connecting via business contact card, so we replace contact with group + (gInfo, host) <- withStore $ \db -> do + liftIO $ deleteContactCardKeepConn db connId ct + createGroupInvitedViaLink db vr user conn'' glInv + -- [incognito] send saved profile + incognitoProfile <- forM customUserProfileId $ \pId -> withStore (\db -> getProfileById db userId pId) + let profileToSend = userProfileToSend user (fromLocalProfile <$> incognitoProfile) Nothing True + allowAgentConnectionAsync user conn'' confId $ XInfo profileToSend + toView $ CRBusinessLinkConnecting user gInfo host ct _ -> messageError "CONF for existing contact must have x.grp.mem.info or x.info" INFO pqSupport connInfo -> do processINFOpqSupport conn pqSupport @@ -4936,7 +4988,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = _ -> pure () processGroupMessage :: AEvent e -> ConnectionEntity -> Connection -> GroupInfo -> GroupMember -> CM () - processGroupMessage agentMsg connEntity conn@Connection {connId, connectionCode} gInfo@GroupInfo {groupId, groupProfile, membership, chatSettings} m = case agentMsg of + processGroupMessage agentMsg connEntity conn@Connection {connId, connChatVersion, connectionCode} gInfo@GroupInfo {groupId, groupProfile, membership, chatSettings} m = case agentMsg of INV (ACR _ cReq) -> withCompletedCommand conn agentMsg $ \CommandData {cmdFunction} -> case cReq of @@ -4977,6 +5029,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = invitedMember = MemberIdRole memberId memRole, connRequest = cReq, groupProfile, + businessChat = Nothing, groupLinkId = groupLinkId, groupSize = Just currentMemCount } @@ -5049,6 +5102,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = void . sendGroupMessage user gInfo members . XGrpMemNew $ memberInfo m sendIntroductions members when (groupFeatureAllowed SGFHistory gInfo) sendHistory + when (connChatVersion < batchSend2Version) $ sendGroupAutoReply members where sendXGrpLinkMem = do let profileMode = ExistingIncognito <$> incognitoMembershipProfile gInfo @@ -5311,9 +5365,12 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = OK -> -- [async agent commands] continuation on receiving OK when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () - JOINED _ -> + JOINED sqSecured -> -- [async agent commands] continuation on receiving JOINED - when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () + when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> + when sqSecured $ do + members <- withStore' $ \db -> getGroupMembers db vr user gInfo + when (connChatVersion >= batchSend2Version) $ sendGroupAutoReply members QCONT -> do continued <- continueSending connEntity conn when continued $ sendPendingGroupMessages user m conn @@ -5341,6 +5398,23 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = updateGroupItemsErrorStatus db msgId groupMemberId newStatus = do itemIds <- getChatItemIdsByAgentMsgId db connId msgId forM_ itemIds $ \itemId -> updateGroupMemSndStatus' db itemId groupMemberId newStatus + sendGroupAutoReply members = autoReplyMC >>= mapM_ send + where + autoReplyMC = do + let GroupInfo {businessChat} = gInfo + GroupMember {memberId = joiningMemberId} = m + case businessChat of + Just BusinessChatInfo {memberId, chatType = BCCustomer} + | joiningMemberId == memberId -> useReply <$> withStore (`getUserAddress` user) + where + useReply UserContactLink {autoAccept} = case autoAccept of + Just AutoAccept {businessAddress, autoReply} | businessAddress -> autoReply + _ -> Nothing + _ -> pure Nothing + send mc = do + msg <- sendGroupMessage' user gInfo members (XMsgNew $ MCSimple (extMsgContent mc Nothing)) + ci <- saveSndChatItem user (CDGroupSnd gInfo) msg (CISndMsgContent mc) + toView $ CRNewChatItems user [AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci] agentMsgDecryptError :: AgentCryptoError -> (MsgDecryptError, Word32) agentMsgDecryptError = \case @@ -5525,26 +5599,37 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = CORContact contact -> toView $ CRContactRequestAlreadyAccepted user contact CORRequest cReq -> do ucl <- withStore $ \db -> getUserContactLinkById db userId userContactLinkId - let (UserContactLink {autoAccept}, groupId_, gLinkMemRole) = ucl + let (UserContactLink {connReqContact, autoAccept}, groupId_, gLinkMemRole) = ucl + isSimplexTeam = sameConnReqContact connReqContact adminContactReq + v = maxVersion chatVRange case autoAccept of - Just AutoAccept {acceptIncognito} -> case groupId_ of - Nothing -> do - -- [incognito] generate profile to send, create connection with incognito profile - incognitoProfile <- if acceptIncognito then Just . NewIncognito <$> liftIO generateRandomProfile else pure Nothing - ct <- acceptContactRequestAsync user cReq incognitoProfile True reqPQSup - toView $ CRAcceptingContactRequest user ct - Just groupId -> do - gInfo <- withStore $ \db -> getGroupInfo db vr user groupId - let profileMode = ExistingIncognito <$> incognitoMembershipProfile gInfo - if maxVersion chatVRange >= groupFastLinkJoinVersion - then do - mem <- acceptGroupJoinRequestAsync user gInfo cReq gLinkMemRole profileMode - createInternalChatItem user (CDGroupRcv gInfo mem) (CIRcvGroupEvent RGEInvitedViaGroupLink) Nothing - toView $ CRAcceptingGroupJoinRequestMember user gInfo mem - else do - -- TODO v5.7 remove old API (or v6.0?) - ct <- acceptContactRequestAsync user cReq profileMode False PQSupportOff - toView $ CRAcceptingGroupJoinRequest user gInfo ct + Just AutoAccept {acceptIncognito, businessAddress} + | businessAddress -> + if v < groupFastLinkJoinVersion || (isSimplexTeam && v < businessChatsVersion) + then do + ct <- acceptContactRequestAsync user cReq Nothing True reqPQSup + toView $ CRAcceptingContactRequest user ct + else do + gInfo <- acceptBusinessJoinRequestAsync user cReq + toView $ CRAcceptingBusinessRequest user gInfo + | otherwise -> case groupId_ of + Nothing -> do + -- [incognito] generate profile to send, create connection with incognito profile + incognitoProfile <- if acceptIncognito then Just . NewIncognito <$> liftIO generateRandomProfile else pure Nothing + ct <- acceptContactRequestAsync user cReq incognitoProfile True reqPQSup + toView $ CRAcceptingContactRequest user ct + Just groupId -> do + gInfo <- withStore $ \db -> getGroupInfo db vr user groupId + let profileMode = ExistingIncognito <$> incognitoMembershipProfile gInfo + if v >= groupFastLinkJoinVersion + then do + mem <- acceptGroupJoinRequestAsync user gInfo cReq gLinkMemRole profileMode + createInternalChatItem user (CDGroupRcv gInfo mem) (CIRcvGroupEvent RGEInvitedViaGroupLink) Nothing + toView $ CRAcceptingGroupJoinRequestMember user gInfo mem + else do + -- TODO v5.7 remove old API (or v6.0?) + ct <- acceptContactRequestAsync user cReq profileMode False PQSupportOff + toView $ CRAcceptingGroupJoinRequest user gInfo ct _ -> toView $ CRReceivedContactRequest user cReq memberCanSend :: GroupMember -> CM () -> CM () @@ -6353,9 +6438,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = xInfoMember gInfo m p' brokerTs = void $ processMemberProfileUpdate gInfo m p' True (Just brokerTs) xGrpLinkMem :: GroupInfo -> GroupMember -> Connection -> Profile -> CM () - xGrpLinkMem gInfo@GroupInfo {membership} m@GroupMember {groupMemberId, memberCategory} Connection {viaGroupLink} p' = do + xGrpLinkMem gInfo@GroupInfo {membership, businessChat} m@GroupMember {groupMemberId, memberCategory} Connection {viaGroupLink} p' = do xGrpLinkMemReceived <- withStore $ \db -> getXGrpLinkMemReceived db groupMemberId - if viaGroupLink && isNothing (memberContactId m) && memberCategory == GCHostMember && not xGrpLinkMemReceived + if (viaGroupLink || isJust businessChat) && isNothing (memberContactId m) && memberCategory == GCHostMember && not xGrpLinkMemReceived then do m' <- processMemberProfileUpdate gInfo m p' False Nothing withStore' $ \db -> setXGrpLinkMemReceived db groupMemberId True @@ -6652,7 +6737,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = toView $ CRContactAndMemberAssociated user c2 g m1 c2' pure c2' - saveConnInfo :: Connection -> ConnInfo -> CM Connection + saveConnInfo :: Connection -> ConnInfo -> CM (Connection, Bool) saveConnInfo activeConn connInfo = do ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage activeConn connInfo conn' <- updatePeerChatVRange activeConn chatVRange @@ -6661,13 +6746,13 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = let contactUsed = connDirect activeConn ct <- withStore $ \db -> createDirectContact db user conn' p contactUsed toView $ CRContactConnecting user ct - pure conn' + pure (conn', False) XGrpLinkInv glInv -> do (gInfo, host) <- withStore $ \db -> createGroupInvitedViaLink db vr user conn' glInv toView $ CRGroupLinkConnecting user gInfo host - pure conn' + pure (conn', True) -- TODO show/log error, other events in SMP confirmation - _ -> pure conn' + _ -> pure (conn', False) xGrpMemNew :: GroupInfo -> GroupMember -> MemberInfo -> RcvMessage -> UTCTime -> CM () xGrpMemNew gInfo m memInfo@(MemberInfo memId memRole _ _) msg brokerTs = do @@ -8683,11 +8768,11 @@ chatCommandP = dbKeyP = nonEmptyKey <$?> strP nonEmptyKey k@(DBEncryptionKey s) = if BA.null s then Left "empty key" else Right k dbEncryptionConfig currentKey newKey = DBEncryptionConfig {currentKey, newKey, keepKey = Just False} - autoAcceptP = - ifM - onOffP - (Just <$> (AutoAccept <$> (" incognito=" *> onOffP <|> pure False) <*> optional (A.space *> msgContentP))) - (pure Nothing) + autoAcceptP = ifM onOffP (Just <$> (businessAA <|> addressAA)) (pure Nothing) + where + addressAA = AutoAccept False <$> (" incognito=" *> onOffP <|> pure False) <*> autoReply + businessAA = AutoAccept True <$> (" business" *> pure False) <*> autoReply + autoReply = optional (A.space *> msgContentP) rcCtrlAddressP = RCCtrlAddress <$> ("addr=" *> strP) <*> (" iface=" *> (jsonP <|> text1P)) text1P = safeDecodeUtf8 <$> A.takeTill (== ' ') char_ = optional . A.char diff --git a/src/Simplex/Chat/Bot.hs b/src/Simplex/Chat/Bot.hs index 8c0978a98f..2f7e2f2abd 100644 --- a/src/Simplex/Chat/Bot.hs +++ b/src/Simplex/Chat/Bot.hs @@ -56,7 +56,7 @@ initializeBotAddress' logAddress cc = do where showBotAddress uri = do when logAddress $ putStrLn $ "Bot's contact address is: " <> B.unpack (strEncode uri) - void $ sendChatCmd cc $ AddressAutoAccept $ Just AutoAccept {acceptIncognito = False, autoReply = Nothing} + void $ sendChatCmd cc $ AddressAutoAccept $ Just AutoAccept {businessAddress = False, acceptIncognito = False, autoReply = Nothing} sendMessage :: ChatController -> Contact -> Text -> IO () sendMessage cc ct = sendComposedMessage cc ct Nothing . MCText diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index d208efce77..0ab3ba5652 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -639,6 +639,7 @@ data ChatResponse | CRContactRequestRejected {user :: User, contactRequest :: UserContactRequest} | CRUserAcceptedGroupSent {user :: User, groupInfo :: GroupInfo, hostContact :: Maybe Contact} | CRGroupLinkConnecting {user :: User, groupInfo :: GroupInfo, hostMember :: GroupMember} + | CRBusinessLinkConnecting {user :: User, groupInfo :: GroupInfo, hostMember :: GroupMember, fromContact :: Contact} | CRUserDeletedMember {user :: User, groupInfo :: GroupInfo, member :: GroupMember} | CRGroupsList {user :: User, groups :: [(GroupInfo, GroupSummary)]} | CRSentGroupInvitation {user :: User, groupInfo :: GroupInfo, contact :: Contact, member :: GroupMember} @@ -665,6 +666,7 @@ data ChatResponse | CRUserContactLinkDeleted {user :: User} | CRReceivedContactRequest {user :: User, contactRequest :: UserContactRequest} | CRAcceptingContactRequest {user :: User, contact :: Contact} + | CRAcceptingBusinessRequest {user :: User, groupInfo :: GroupInfo} | CRContactAlreadyExists {user :: User, contact :: Contact} | CRContactRequestAlreadyAccepted {user :: User, contact :: Contact} | CRLeftMemberUser {user :: User, groupInfo :: GroupInfo} diff --git a/src/Simplex/Chat/Migrations/M20241128_business_chats.hs b/src/Simplex/Chat/Migrations/M20241128_business_chats.hs new file mode 100644 index 0000000000..f068b1bd81 --- /dev/null +++ b/src/Simplex/Chat/Migrations/M20241128_business_chats.hs @@ -0,0 +1,22 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Migrations.M20241128_business_chats where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20241128_business_chats :: Query +m20241128_business_chats = + [sql| +ALTER TABLE user_contact_links ADD business_address INTEGER DEFAULT 0; +ALTER TABLE groups ADD COLUMN business_member_id BLOB NULL; +ALTER TABLE groups ADD COLUMN business_chat TEXT NULL; +|] + +down_m20241128_business_chats :: Query +down_m20241128_business_chats = + [sql| +ALTER TABLE user_contact_links DROP COLUMN business_address; +ALTER TABLE groups DROP COLUMN business_member_id; +ALTER TABLE groups DROP COLUMN business_chat; +|] diff --git a/src/Simplex/Chat/Migrations/chat_schema.sql b/src/Simplex/Chat/Migrations/chat_schema.sql index 6f944157c1..460b348b4d 100644 --- a/src/Simplex/Chat/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Migrations/chat_schema.sql @@ -127,7 +127,9 @@ CREATE TABLE groups( via_group_link_uri_hash BLOB, user_member_profile_sent_at TEXT, custom_data BLOB, - ui_themes TEXT, -- received + ui_themes TEXT, + business_member_id BLOB NULL, + business_chat TEXT NULL, -- received FOREIGN KEY(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name) ON DELETE CASCADE @@ -309,6 +311,7 @@ CREATE TABLE user_contact_links( auto_accept_incognito INTEGER DEFAULT 0 CHECK(auto_accept_incognito NOT NULL), group_link_id BLOB, group_link_member_role TEXT NULL, + business_address INTEGER DEFAULT 0, UNIQUE(user_id, local_display_name) ); CREATE TABLE contact_requests( diff --git a/src/Simplex/Chat/Protocol.hs b/src/Simplex/Chat/Protocol.hs index ea39293b9f..8afefdc850 100644 --- a/src/Simplex/Chat/Protocol.hs +++ b/src/Simplex/Chat/Protocol.hs @@ -66,12 +66,13 @@ import Simplex.Messaging.Version hiding (version) -- 7 - update member profiles (1/15/2024) -- 8 - compress messages and PQ e2e encryption (2024-03-08) -- 9 - batch sending in direct connections (2024-07-24) +-- 10 - business chats (2024-11-29) -- This should not be used directly in code, instead use `maxVersion chatVRange` from ChatConfig. -- This indirection is needed for backward/forward compatibility testing. -- Testing with real app versions is still needed, as tests use the current code with different version ranges, not the old code. currentChatVersion :: VersionChat -currentChatVersion = VersionChat 9 +currentChatVersion = VersionChat 10 -- This should not be used directly in code, instead use `chatVRange` from ChatConfig (see comment above) supportedChatVRange :: VersionRangeChat @@ -110,6 +111,10 @@ pqEncryptionCompressionVersion = VersionChat 8 batchSend2Version :: VersionChat batchSend2Version = VersionChat 9 +-- supports differentiating business chats when joining contact addresses +businessChatsVersion :: VersionChat +businessChatsVersion = VersionChat 10 + agentToChatVersion :: VersionSMPA -> VersionChat agentToChatVersion v | v < pqdrSMPAgentVersion = initialChatVersion diff --git a/src/Simplex/Chat/Store/Connections.hs b/src/Simplex/Chat/Store/Connections.hs index 2c7543f08a..fe52c6d7b7 100644 --- a/src/Simplex/Chat/Store/Connections.hs +++ b/src/Simplex/Chat/Store/Connections.hs @@ -123,7 +123,7 @@ getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do -- GroupInfo g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, - g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.ui_themes, g.custom_data, + g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_member_id, g.business_chat, g.ui_themes, g.custom_data, -- GroupInfo {membership} mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index 142c702f77..b07adf407b 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -30,6 +30,7 @@ module Simplex.Chat.Store.Groups getGroupAndMember, createNewGroup, createGroupInvitation, + deleteContactCardKeepConn, createGroupInvitedViaLink, setViaGroupLinkHash, setGroupInvitationChatItemId, @@ -62,6 +63,7 @@ module Simplex.Chat.Store.Groups createNewContactMemberAsync, createAcceptedMember, createAcceptedMemberConnection, + createBusinessRequestGroup, getContactViaMember, setNewContactMemberConnRequest, getMemberInvitation, @@ -153,19 +155,20 @@ import Simplex.Messaging.Util (eitherToMaybe, ($>>=), (<$$>)) import Simplex.Messaging.Version import UnliftIO.STM -type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe Text, Maybe ImageData, Maybe ProfileId, Maybe MsgFilter, Maybe Bool, Bool, Maybe GroupPreferences) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime, Maybe UIThemeEntityOverrides, Maybe CustomData) :. GroupMemberRow +type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe Text, Maybe ImageData, Maybe ProfileId, Maybe MsgFilter, Maybe Bool, Bool, Maybe GroupPreferences) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime, Maybe MemberId, Maybe BusinessChatType, Maybe UIThemeEntityOverrides, Maybe CustomData) :. GroupMemberRow type GroupMemberRow = ((Int64, Int64, MemberId, VersionChat, VersionChat, GroupMemberRole, GroupMemberCategory, GroupMemberStatus, Bool, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, ContactName, Maybe ContactId, ProfileId, ProfileId, ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Maybe Preferences)) type MaybeGroupMemberRow = ((Maybe Int64, Maybe Int64, Maybe MemberId, Maybe VersionChat, Maybe VersionChat, Maybe GroupMemberRole, Maybe GroupMemberCategory, Maybe GroupMemberStatus, Maybe Bool, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, Maybe ContactName, Maybe ContactId, Maybe ProfileId, Maybe ProfileId, Maybe ContactName, Maybe Text, Maybe ImageData, Maybe ConnReqContact, Maybe LocalAlias, Maybe Preferences)) toGroupInfo :: VersionRangeChat -> Int64 -> GroupInfoRow -> GroupInfo -toGroupInfo vr userContactId ((groupId, localDisplayName, displayName, fullName, description, image, hostConnCustomUserProfileId, enableNtfs_, sendRcpts, favorite, groupPreferences) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt, uiThemes, customData) :. userMemberRow) = +toGroupInfo vr userContactId ((groupId, localDisplayName, displayName, fullName, description, image, hostConnCustomUserProfileId, enableNtfs_, sendRcpts, favorite, groupPreferences) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt, businessMemberId, businessChatType, uiThemes, customData) :. userMemberRow) = let membership = (toGroupMember userContactId userMemberRow) {memberChatVRange = vr} chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts, favorite} fullGroupPreferences = mergeGroupPreferences groupPreferences groupProfile = GroupProfile {displayName, fullName, description, image, groupPreferences} - in GroupInfo {groupId, localDisplayName, groupProfile, fullGroupPreferences, membership, hostConnCustomUserProfileId, chatSettings, createdAt, updatedAt, chatTs, userMemberProfileSentAt, uiThemes, customData} + businessChat = BusinessChatInfo <$> businessMemberId <*> businessChatType + in GroupInfo {groupId, localDisplayName, groupProfile, businessChat, fullGroupPreferences, membership, hostConnCustomUserProfileId, chatSettings, createdAt, updatedAt, chatTs, userMemberProfileSentAt, uiThemes, customData} toGroupMember :: Int64 -> GroupMemberRow -> GroupMember toGroupMember userContactId ((groupMemberId, groupId, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, showMessages, memberRestriction_) :. (invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId, profileId, displayName, fullName, image, contactLink, localAlias, preferences)) = @@ -276,7 +279,7 @@ getGroupAndMember db User {userId, userContactId} groupMemberId vr = -- GroupInfo g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, - g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.ui_themes, g.custom_data, + g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_member_id, g.business_chat, g.ui_themes, g.custom_data, -- GroupInfo {membership} mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, @@ -342,6 +345,7 @@ createNewGroup db vr gVar user@User {userId} groupProfile incognitoProfile = Exc { groupId, localDisplayName = ldn, groupProfile, + businessChat = Nothing, fullGroupPreferences, membership, hostConnCustomUserProfileId = Nothing, @@ -357,7 +361,7 @@ createNewGroup db vr gVar user@User {userId} groupProfile incognitoProfile = Exc -- | creates a new group record for the group the current user was invited to, or returns an existing one createGroupInvitation :: DB.Connection -> VersionRangeChat -> User -> Contact -> GroupInvitation -> Maybe ProfileId -> ExceptT StoreError IO (GroupInfo, GroupMemberId) createGroupInvitation _ _ _ Contact {localDisplayName, activeConn = Nothing} _ _ = throwError $ SEContactNotReady localDisplayName -createGroupInvitation db vr user@User {userId} contact@Contact {contactId, activeConn = Just Connection {customUserProfileId, peerChatVRange}} GroupInvitation {fromMember, invitedMember, connRequest, groupProfile} incognitoProfileId = do +createGroupInvitation db vr user@User {userId} contact@Contact {contactId, activeConn = Just Connection {customUserProfileId, peerChatVRange}} GroupInvitation {fromMember, invitedMember, connRequest, groupProfile, businessChat} incognitoProfileId = do liftIO getInvitationGroupId_ >>= \case Nothing -> createGroupInvitation_ Just gId -> do @@ -395,10 +399,10 @@ createGroupInvitation db vr user@User {userId} contact@Contact {contactId, activ [sql| INSERT INTO groups (group_profile_id, local_display_name, inv_queue_info, host_conn_custom_user_profile_id, user_id, enable_ntfs, - created_at, updated_at, chat_ts, user_member_profile_sent_at) - VALUES (?,?,?,?,?,?,?,?,?,?) + created_at, updated_at, chat_ts, user_member_profile_sent_at, business_member_id, business_chat) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?) |] - (profileId, localDisplayName, connRequest, customUserProfileId, userId, True, currentTs, currentTs, currentTs, currentTs) + ((profileId, localDisplayName, connRequest, customUserProfileId, userId, True, currentTs, currentTs, currentTs, currentTs) :. businessChatTuple businessChat) insertedRowId db let hostVRange = adjustedMemberVRange vr peerChatVRange GroupMember {groupMemberId} <- createContactMemberInv_ db user groupId Nothing contact fromMember GCHostMember GSMemInvited IBUnknown Nothing currentTs hostVRange @@ -409,6 +413,7 @@ createGroupInvitation db vr user@User {userId} contact@Contact {contactId, activ { groupId, localDisplayName, groupProfile, + businessChat = Nothing, fullGroupPreferences, membership, hostConnCustomUserProfileId = customUserProfileId, @@ -423,6 +428,11 @@ createGroupInvitation db vr user@User {userId} contact@Contact {contactId, activ groupMemberId ) +businessChatTuple :: Maybe BusinessChatInfo -> (Maybe MemberId, Maybe BusinessChatType) +businessChatTuple = \case + Just BusinessChatInfo {memberId, chatType} -> (Just memberId, Just chatType) + Nothing -> (Nothing, Nothing) + adjustedMemberVRange :: VersionRangeChat -> VersionRangeChat -> VersionRangeChat adjustedMemberVRange chatVR vr@(VersionRange minV maxV) = let maxV' = min maxV (maxVersion chatVR) @@ -497,13 +507,19 @@ createContactMemberInv_ db User {userId, userContactId} groupId invitedByGroupMe ) pure $ Right incognitoLdn +deleteContactCardKeepConn :: DB.Connection -> Int64 -> Contact -> IO () +deleteContactCardKeepConn db connId Contact {contactId, profile = LocalProfile {profileId}} = do + DB.execute db "UPDATE connections SET contact_id = NULL WHERE connection_id = ?" (Only connId) + DB.execute db "DELETE FROM contacts WHERE contact_id = ?" (Only contactId) + DB.execute db "DELETE FROM contact_profiles WHERE contact_profile_id = ?" (Only profileId) + createGroupInvitedViaLink :: DB.Connection -> VersionRangeChat -> User -> Connection -> GroupLinkInvitation -> ExceptT StoreError IO (GroupInfo, GroupMember) createGroupInvitedViaLink db vr user@User {userId, userContactId} Connection {connId, customUserProfileId} - GroupLinkInvitation {fromMember, fromMemberName, invitedMember, groupProfile} = do + GroupLinkInvitation {fromMember, fromMemberName, invitedMember, groupProfile, businessChat} = do currentTs <- liftIO getCurrentTime groupId <- insertGroup_ currentTs hostMemberId <- insertHost_ currentTs groupId @@ -527,10 +543,10 @@ createGroupInvitedViaLink [sql| INSERT INTO groups (group_profile_id, local_display_name, host_conn_custom_user_profile_id, user_id, enable_ntfs, - created_at, updated_at, chat_ts, user_member_profile_sent_at) - VALUES (?,?,?,?,?,?,?,?,?) + created_at, updated_at, chat_ts, user_member_profile_sent_at, business_member_id, business_chat) + VALUES (?,?,?,?,?,?,?,?,?,?,?) |] - (profileId, localDisplayName, customUserProfileId, userId, True, currentTs, currentTs, currentTs, currentTs) + ((profileId, localDisplayName, customUserProfileId, userId, True, currentTs, currentTs, currentTs, currentTs) :. businessChatTuple businessChat) insertedRowId db insertHost_ currentTs groupId = do let fromMemberProfile = profileFromName fromMemberName @@ -637,7 +653,7 @@ getUserGroupDetails db vr User {userId, userContactId} _contactId_ search_ = SELECT g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, - g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.ui_themes, g.custom_data, + g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_member_id, g.business_chat, g.ui_themes, g.custom_data, mu.group_member_id, g.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences FROM groups g @@ -879,9 +895,7 @@ createAcceptedMember User {userId, userContactId} GroupInfo {groupId, membership} UserContactRequest {cReqChatVRange, localDisplayName, profileId} - memberRole = do - liftIO $ - DB.execute db "DELETE FROM contact_requests WHERE user_id = ? AND local_display_name = ?" (userId, localDisplayName) + memberRole = createWithRandomId gVar $ \memId -> do createdAt <- liftIO getCurrentTime insertMember_ (MemberId memId) createdAt @@ -917,6 +931,46 @@ createAcceptedMemberConnection Connection {connId} <- createConnection_ db userId ConnMember (Just groupMemberId) agentConnId ConnNew chatV cReqChatVRange Nothing (Just userContactLinkId) Nothing 0 createdAt subMode PQSupportOff setCommandConnId db user cmdId connId +createBusinessRequestGroup :: DB.Connection -> VersionRangeChat -> TVar ChaChaDRG -> User -> UserContactRequest -> GroupPreferences -> ExceptT StoreError IO (GroupInfo, GroupMember) +createBusinessRequestGroup + db + vr + gVar + user@User {userId} + ucr@UserContactRequest {profile} + groupPreferences = do + currentTs <- liftIO getCurrentTime + groupInfo <- insertGroup_ currentTs + (groupMemberId, memberId) <- createAcceptedMember db gVar user groupInfo ucr GRMember + liftIO $ setBusinessMemberId groupInfo memberId + acceptedMember <- getGroupMemberById db vr user groupMemberId + pure (groupInfo, acceptedMember) + where + insertGroup_ currentTs = ExceptT $ do + let Profile {displayName, fullName, image} = profile + withLocalDisplayName db userId displayName $ \localDisplayName -> runExceptT $ do + groupId <- liftIO $ do + DB.execute + db + "INSERT INTO group_profiles (display_name, full_name, image, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?)" + (displayName, fullName, image, userId, groupPreferences, currentTs, currentTs) + profileId <- insertedRowId db + DB.execute + db + [sql| + INSERT INTO groups + (group_profile_id, local_display_name, user_id, enable_ntfs, + created_at, updated_at, chat_ts, user_member_profile_sent_at, business_chat) + VALUES (?,?,?,?,?,?,?,?,?) + |] + (profileId, localDisplayName, userId, True, currentTs, currentTs, currentTs, currentTs, BCCustomer) + insertedRowId db + memberId <- liftIO $ encodedRandomBytes gVar 12 + void $ createContactMemberInv_ db user groupId Nothing user (MemberIdRole (MemberId memberId) GROwner) GCUserMember GSMemCreator IBUser Nothing currentTs vr + getGroupInfo db vr user groupId + setBusinessMemberId GroupInfo {groupId} businessMemberId = do + DB.execute db "UPDATE groups SET business_member_id = ? WHERE group_id = ?" (businessMemberId, groupId) + getContactViaMember :: DB.Connection -> VersionRangeChat -> User -> GroupMember -> ExceptT StoreError IO Contact getContactViaMember db vr user@User {userId} GroupMember {groupMemberId} = do contactId <- @@ -1315,7 +1369,7 @@ getViaGroupMember db vr User {userId, userContactId} Contact {contactId} = -- GroupInfo g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, - g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.ui_themes, g.custom_data, + g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_member_id, g.business_chat, g.ui_themes, g.custom_data, -- GroupInfo {membership} mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, @@ -1411,7 +1465,7 @@ getGroupInfo db vr User {userId, userContactId} groupId = -- GroupInfo g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, - g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.ui_themes, g.custom_data, + g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_member_id, g.business_chat, g.ui_themes, g.custom_data, -- GroupMember - membership mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, diff --git a/src/Simplex/Chat/Store/Migrations.hs b/src/Simplex/Chat/Store/Migrations.hs index 9a91c7f970..6654dec034 100644 --- a/src/Simplex/Chat/Store/Migrations.hs +++ b/src/Simplex/Chat/Store/Migrations.hs @@ -117,6 +117,7 @@ import Simplex.Chat.Migrations.M20241010_contact_requests_contact_id import Simplex.Chat.Migrations.M20241023_chat_item_autoincrement_id import Simplex.Chat.Migrations.M20241027_server_operators import Simplex.Chat.Migrations.M20241125_indexes +import Simplex.Chat.Migrations.M20241128_business_chats import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..)) schemaMigrations :: [(String, Query, Maybe Query)] @@ -233,7 +234,8 @@ schemaMigrations = ("20241010_contact_requests_contact_id", m20241010_contact_requests_contact_id, Just down_m20241010_contact_requests_contact_id), ("20241023_chat_item_autoincrement_id", m20241023_chat_item_autoincrement_id, Just down_m20241023_chat_item_autoincrement_id), ("20241027_server_operators", m20241027_server_operators, Just down_m20241027_server_operators), - ("20241125_indexes", m20241125_indexes, Just down_m20241125_indexes) + ("20241125_indexes", m20241125_indexes, Just down_m20241125_indexes), + ("20241128_business_chats", m20241128_business_chats, Just down_m20241128_business_chats) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs index ec657fd6f7..e88cf39feb 100644 --- a/src/Simplex/Chat/Store/Profiles.hs +++ b/src/Simplex/Chat/Store/Profiles.hs @@ -445,7 +445,8 @@ data UserContactLink = UserContactLink deriving (Show) data AutoAccept = AutoAccept - { acceptIncognito :: IncognitoEnabled, + { businessAddress :: Bool, -- possibly, it can be wrapped together with acceptIncognito, or AutoAccept made sum type + acceptIncognito :: IncognitoEnabled, autoReply :: Maybe MsgContent } deriving (Show) @@ -454,10 +455,10 @@ $(J.deriveJSON defaultJSON ''AutoAccept) $(J.deriveJSON defaultJSON ''UserContactLink) -toUserContactLink :: (ConnReqContact, Bool, IncognitoEnabled, Maybe MsgContent) -> UserContactLink -toUserContactLink (connReq, autoAccept, acceptIncognito, autoReply) = +toUserContactLink :: (ConnReqContact, Bool, Bool, IncognitoEnabled, Maybe MsgContent) -> UserContactLink +toUserContactLink (connReq, autoAccept, businessAddress, acceptIncognito, autoReply) = UserContactLink connReq $ - if autoAccept then Just AutoAccept {acceptIncognito, autoReply} else Nothing + if autoAccept then Just AutoAccept {businessAddress, acceptIncognito, autoReply} else Nothing getUserAddress :: DB.Connection -> User -> ExceptT StoreError IO UserContactLink getUserAddress db User {userId} = @@ -465,7 +466,7 @@ getUserAddress db User {userId} = DB.query db [sql| - SELECT conn_req_contact, auto_accept, auto_accept_incognito, auto_reply_msg_content + SELECT conn_req_contact, auto_accept, business_address, auto_accept_incognito, auto_reply_msg_content FROM user_contact_links WHERE user_id = ? AND local_display_name = '' AND group_id IS NULL |] @@ -477,7 +478,7 @@ getUserContactLinkById db userId userContactLinkId = DB.query db [sql| - SELECT conn_req_contact, auto_accept, auto_accept_incognito, auto_reply_msg_content, group_id, group_link_member_role + SELECT conn_req_contact, auto_accept, business_address, auto_accept_incognito, auto_reply_msg_content, group_id, group_link_member_role FROM user_contact_links WHERE user_id = ? AND user_contact_link_id = ? @@ -490,7 +491,7 @@ getUserContactLinkByConnReq db User {userId} (cReqSchema1, cReqSchema2) = DB.query db [sql| - SELECT conn_req_contact, auto_accept, auto_accept_incognito, auto_reply_msg_content + SELECT conn_req_contact, auto_accept, business_address, auto_accept_incognito, auto_reply_msg_content FROM user_contact_links WHERE user_id = ? AND conn_req_contact IN (?,?) |] @@ -522,13 +523,13 @@ updateUserAddressAutoAccept db user@User {userId} autoAccept = do db [sql| UPDATE user_contact_links - SET auto_accept = ?, auto_accept_incognito = ?, auto_reply_msg_content = ? + SET auto_accept = ?, business_address = ?, auto_accept_incognito = ?, auto_reply_msg_content = ? WHERE user_id = ? AND local_display_name = '' AND group_id IS NULL |] (ucl :. Only userId) ucl = case autoAccept of - Just AutoAccept {acceptIncognito, autoReply} -> (True, acceptIncognito, autoReply) - _ -> (False, False, Nothing) + Just AutoAccept {businessAddress, acceptIncognito, autoReply} -> (True, businessAddress, acceptIncognito, autoReply) + _ -> (False, False, False, Nothing) getProtocolServers :: forall p. ProtocolTypeI p => DB.Connection -> SProtocolType p -> User -> IO [UserServer p] getProtocolServers db p User {userId} = @@ -589,7 +590,7 @@ getServerOperators db = do let conditionsAction = usageConditionsAction ops currentConditions now pure ServerOperatorConditions {serverOperators = ops, currentConditions, conditionsAction} -getUserServers :: DB.Connection -> User -> ExceptT StoreError IO ([Maybe ServerOperator], [UserServer 'PSMP], [UserServer 'PXFTP]) +getUserServers :: DB.Connection -> User -> ExceptT StoreError IO ([Maybe ServerOperator], [UserServer 'PSMP], [UserServer 'PXFTP]) getUserServers db user = (,,) <$> (map Just . serverOperators <$> getServerOperators db) @@ -620,7 +621,8 @@ getUpdateServerOperators db presetOps newUser = do mapM_ insertConditions condsToAdd latestAcceptedConds_ <- getLatestAcceptedConditions db ops <- updatedServerOperators presetOps <$> getServerOperators_ db - forM ops $ traverse $ mapM $ \(ASO _ op) -> -- traverse for tuple, mapM for Maybe + forM ops $ traverse $ mapM $ \(ASO _ op) -> + -- traverse for tuple, mapM for Maybe case operatorId op of DBNewEntity -> do op' <- insertOperator op @@ -765,8 +767,9 @@ acceptConditions db condId opIds acceptedAt = do liftIO $ forM_ operators $ \op -> acceptConditions_ db op conditionsCommit ts where getServerOperator_ opId = - ExceptT $ firstRow toServerOperator (SEOperatorNotFound opId) $ - DB.query db (serverOperatorQuery <> " WHERE server_operator_id = ?") (Only opId) + ExceptT $ + firstRow toServerOperator (SEOperatorNotFound opId) $ + DB.query db (serverOperatorQuery <> " WHERE server_operator_id = ?") (Only opId) acceptConditions_ :: DB.Connection -> ServerOperator -> Text -> Maybe UTCTime -> IO () acceptConditions_ db ServerOperator {operatorId, operatorTag} conditionsCommit acceptedAt = diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index 36bf9edb52..cc98b4101d 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -52,7 +52,7 @@ import Simplex.Messaging.Agent.Protocol (ACorrId, AEventTag (..), AEvtTag (..), import Simplex.Messaging.Crypto.File (CryptoFileArgs (..)) import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport, pattern PQEncOff) import Simplex.Messaging.Encoding.String -import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, fromTextField_, sumTypeJSON, taggedObjectJSON) +import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, fromTextField_, sumTypeJSON) import Simplex.Messaging.Util (safeDecodeUtf8, (<$?>)) import Simplex.Messaging.Version import Simplex.Messaging.Version.Internal @@ -371,6 +371,7 @@ data GroupInfo = GroupInfo { groupId :: GroupId, localDisplayName :: GroupName, groupProfile :: GroupProfile, + businessChat :: Maybe BusinessChatInfo, fullGroupPreferences :: FullGroupPreferences, membership :: GroupMember, hostConnCustomUserProfileId :: Maybe ProfileId, @@ -384,6 +385,24 @@ data GroupInfo = GroupInfo } deriving (Eq, Show) +data BusinessChatType + = BCBusiness -- used on the customer side + | BCCustomer -- used on the business side + deriving (Eq, Show) + +instance TextEncoding BusinessChatType where + textEncode = \case + BCBusiness -> "business" + BCCustomer -> "customer" + textDecode = \case + "business" -> Just BCBusiness + "customer" -> Just BCCustomer + _ -> Nothing + +instance FromField BusinessChatType where fromField = fromTextField_ textDecode + +instance ToField BusinessChatType where toField = toField . textEncode + groupName' :: GroupInfo -> GroupName groupName' GroupInfo {localDisplayName = g} = g @@ -598,6 +617,7 @@ data GroupInvitation = GroupInvitation invitedMember :: MemberIdRole, connRequest :: ConnReqInvitation, groupProfile :: GroupProfile, + businessChat :: Maybe BusinessChatInfo, groupLinkId :: Maybe GroupLinkId, groupSize :: Maybe Int } @@ -608,6 +628,7 @@ data GroupLinkInvitation = GroupLinkInvitation fromMemberName :: ContactName, invitedMember :: MemberIdRole, groupProfile :: GroupProfile, + businessChat :: Maybe BusinessChatInfo, groupSize :: Maybe Int } deriving (Eq, Show) @@ -632,6 +653,12 @@ data MemberInfo = MemberInfo } deriving (Eq, Show) +data BusinessChatInfo = BusinessChatInfo + { memberId :: MemberId, + chatType :: BusinessChatType + } + deriving (Eq, Show) + memberInfo :: GroupMember -> MemberInfo memberInfo GroupMember {memberId, memberRole, memberProfile, activeConn} = MemberInfo @@ -1696,6 +1723,10 @@ $(JQ.deriveJSON (enumJSON $ dropPrefix "MF") ''MsgFilter) $(JQ.deriveJSON defaultJSON ''ChatSettings) +$(JQ.deriveJSON (enumJSON $ dropPrefix "BC") ''BusinessChatType) + +$(JQ.deriveJSON defaultJSON ''BusinessChatInfo) + $(JQ.deriveJSON defaultJSON ''GroupInfo) $(JQ.deriveJSON defaultJSON ''Group) @@ -1706,18 +1737,18 @@ instance FromField MsgFilter where fromField = fromIntField_ msgFilterIntP instance ToField MsgFilter where toField = toField . msgFilterInt -$(JQ.deriveJSON (taggedObjectJSON $ dropPrefix "CRData") ''CReqClientData) +$(JQ.deriveJSON defaultJSON ''CReqClientData) $(JQ.deriveJSON defaultJSON ''MemberIdRole) +$(JQ.deriveJSON defaultJSON ''MemberInfo) + $(JQ.deriveJSON defaultJSON ''GroupInvitation) $(JQ.deriveJSON defaultJSON ''GroupLinkInvitation) $(JQ.deriveJSON defaultJSON ''IntroInvitation) -$(JQ.deriveJSON defaultJSON ''MemberInfo) - $(JQ.deriveJSON defaultJSON ''MemberRestrictions) $(JQ.deriveJSON defaultJSON ''GroupMemberRef) diff --git a/src/Simplex/Chat/Types/Preferences.hs b/src/Simplex/Chat/Types/Preferences.hs index bccfd4bdce..8465caeee0 100644 --- a/src/Simplex/Chat/Types/Preferences.hs +++ b/src/Simplex/Chat/Types/Preferences.hs @@ -390,6 +390,33 @@ defaultGroupPrefs = emptyGroupPrefs :: GroupPreferences emptyGroupPrefs = GroupPreferences Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing +businessGroupPrefs :: Preferences -> GroupPreferences +businessGroupPrefs Preferences {timedMessages, fullDelete, reactions, voice} = + defaultBusinessGroupPrefs + { timedMessages = Just TimedMessagesGroupPreference {enable = maybe FEOff enableFeature timedMessages, ttl = maybe Nothing prefParam timedMessages}, + fullDelete = Just FullDeleteGroupPreference {enable = maybe FEOff enableFeature fullDelete}, + reactions = Just ReactionsGroupPreference {enable = maybe FEOn enableFeature reactions}, + voice = Just VoiceGroupPreference {enable = maybe FEOff enableFeature voice, role = Nothing} + } + where + enableFeature :: FeatureI f => FeaturePreference f -> GroupFeatureEnabled + enableFeature p = case getField @"allow" p of + FANo -> FEOff + _ -> FEOn + +defaultBusinessGroupPrefs :: GroupPreferences +defaultBusinessGroupPrefs = + GroupPreferences + { timedMessages = Just $ TimedMessagesGroupPreference FEOff Nothing, + directMessages = Just $ DirectMessagesGroupPreference FEOff Nothing, + fullDelete = Just $ FullDeleteGroupPreference FEOff, + reactions = Just $ ReactionsGroupPreference FEOn, + voice = Just $ VoiceGroupPreference FEOff Nothing, + files = Just $ FilesGroupPreference FEOn Nothing, + simplexLinks = Just $ SimplexLinksGroupPreference FEOn Nothing, + history = Just $ HistoryGroupPreference FEOn + } + data TimedMessagesPreference = TimedMessagesPreference { allow :: FeatureAllowed, ttl :: Maybe Int diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 093d750a42..4458f8ee7b 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -204,12 +204,14 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe CRContactDeletedByContact u c -> ttyUser u [ttyFullContact c <> " deleted contact with you"] CRChatCleared u chatInfo -> ttyUser u $ viewChatCleared chatInfo CRAcceptingContactRequest u c -> ttyUser u $ viewAcceptingContactRequest c + CRAcceptingBusinessRequest u g -> ttyUser u $ viewAcceptingBusinessRequest g CRContactAlreadyExists u c -> ttyUser u [ttyFullContact c <> ": contact already exists"] CRContactRequestAlreadyAccepted u c -> ttyUser u [ttyFullContact c <> ": sent you a duplicate contact request, but you are already connected, no action needed"] CRUserContactLinkCreated u cReq -> ttyUser u $ connReqContact_ "Your new chat address is created!" cReq CRUserContactLinkDeleted u -> ttyUser u viewUserContactLinkDeleted CRUserAcceptedGroupSent u _g _ -> ttyUser u [] -- [ttyGroup' g <> ": joining the group..."] CRGroupLinkConnecting u g _ -> ttyUser u [ttyGroup' g <> ": joining the group..."] + CRBusinessLinkConnecting u g _ _ -> ttyUser u [ttyGroup' g <> ": joining the group..."] CRUserDeletedMember u g m -> ttyUser u [ttyGroup' g <> ": you removed " <> ttyMember m <> " from the group"] CRLeftMemberUser u g -> ttyUser u $ [ttyGroup' g <> ": you left the group"] <> groupPreserved g CRUnknownMemberCreated u g fwdM um -> ttyUser u [ttyGroup' g <> ": " <> ttyMember fwdM <> " forwarded a message from an unknown member, creating unknown member record " <> ttyMember um] @@ -979,9 +981,14 @@ simplexChatContact (CRContactUri crData) = CRContactUri crData {crScheme = simpl autoAcceptStatus_ :: Maybe AutoAccept -> [StyledString] autoAcceptStatus_ = \case - Just AutoAccept {acceptIncognito, autoReply} -> - ("auto_accept on" <> if acceptIncognito then ", incognito" else "") + Just AutoAccept {businessAddress, acceptIncognito, autoReply} -> + ("auto_accept on" <> aaInfo) : maybe [] ((["auto reply:"] <>) . ttyMsgContent) autoReply + where + aaInfo + | businessAddress = ", business" + | acceptIncognito = ", incognito" + | otherwise = "" _ -> ["auto_accept off"] groupLink_ :: StyledString -> GroupInfo -> ConnReqContact -> GroupMemberRole -> [StyledString] @@ -1017,6 +1024,9 @@ viewAcceptingContactRequest ct | contactReady ct = [ttyFullContact ct <> ": accepting contact request, you can send messages to contact"] | otherwise = [ttyFullContact ct <> ": accepting contact request..."] +viewAcceptingBusinessRequest :: GroupInfo -> [StyledString] +viewAcceptingBusinessRequest g = [ttyFullGroup g <> ": accepting business address request..."] + viewReceivedContactRequest :: ContactName -> Profile -> [StyledString] viewReceivedContactRequest c Profile {fullName} = [ ttyFullName c fullName <> " wants to connect to you!", diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index 3ff8808541..b2db679f29 100644 --- a/tests/ChatTests/Profiles.hs +++ b/tests/ChatTests/Profiles.hs @@ -47,6 +47,8 @@ chatProfileTests = do it "delete connection requests when contact link deleted" testDeleteConnectionRequests it "auto-reply message" testAutoReplyMessage it "auto-reply message in incognito" testAutoReplyMessageInIncognito + describe "business address" $ do + it "create and connect via business address" testBusinessAddress describe "contact address connection plan" $ do it "contact address ok to connect; known contact" testPlanAddressOkKnown it "own contact address" testPlanAddressOwn @@ -677,6 +679,49 @@ testAutoReplyMessageInIncognito = testChat2 aliceProfile bobProfile $ alice <## "use /i bob to print out this incognito profile again" ] +testBusinessAddress :: HasCallStack => FilePath -> IO () +testBusinessAddress = testChat3 businessProfile aliceProfile {fullName = "Alice @ Biz"} bobProfile $ + \biz alice bob -> do + biz ##> "/ad" + cLink <- getContactLink biz True + biz ##> "/auto_accept on business" + biz <## "auto_accept on, business" + bob ##> ("/c " <> cLink) + bob <## "connection request sent!" + biz <## "#bob_1 (Bob): accepting business address request..." + biz <## "#bob_1: bob joined the group" + bob <## "#biz: joining the group..." + bob <## "#biz: you joined the group" + biz #> "#bob_1 hi" + bob <# "#biz biz_1> hi" + bob #> "#biz hello" + biz <# "#bob_1 bob> hello" + connectUsers biz alice + biz <##> alice + biz ##> "/a #bob_1 alice" + biz <## "invitation to join the group #bob_1 sent to alice" + alice <## "#bob (Bob): biz invites you to join the group as member" + alice <## "use /j bob to accept" + alice ##> "/j bob" + concurrentlyN_ + [ do + alice <## "#bob: you joined the group" + alice <### [WithTime "#bob biz> hi [>>]", WithTime "#bob bob_1> hello [>>]"] + alice <## "#bob: member bob_1 (Bob) is connected", + biz <## "#bob_1: alice joined the group", + do + bob <## "#biz: biz_1 added alice (Alice @ Biz) to the group (connecting...)" + bob <## "#biz: new member alice is connected" + ] + alice #> "#bob hey" + concurrently_ + (bob <# "#biz alice> hey") + (biz <# "#bob_1 alice> hey") + bob #> "#biz hey there" + concurrently_ + (alice <# "#bob bob_1> hey there") + (biz <# "#bob_1 bob> hey there") + testPlanAddressOkKnown :: HasCallStack => FilePath -> IO () testPlanAddressOkKnown = testChat2 aliceProfile bobProfile $ @@ -2380,7 +2425,7 @@ testSetUITheme = a <## "you've shared main profile with this contact" a <## "connection not verified, use /code command to see security code" a <## "quantum resistant end-to-end encryption" - a <## "peer chat protocol version range: (Version 1, Version 9)" + a <## "peer chat protocol version range: (Version 1, Version 10)" groupInfo a = do a <## "group ID: 1" a <## "current members: 1" diff --git a/tests/ChatTests/Utils.hs b/tests/ChatTests/Utils.hs index 8c022d5bfd..6459522134 100644 --- a/tests/ChatTests/Utils.hs +++ b/tests/ChatTests/Utils.hs @@ -64,6 +64,9 @@ cathProfile = Profile {displayName = "cath", fullName = "Catherine", image = Not danProfile :: Profile danProfile = Profile {displayName = "dan", fullName = "Daniel", image = Nothing, contactLink = Nothing, preferences = defaultPrefs} +businessProfile :: Profile +businessProfile = Profile {displayName = "biz", fullName = "Biz Inc", image = Nothing, contactLink = Nothing, preferences = defaultPrefs} + it :: HasCallStack => String -> (FilePath -> Expectation) -> SpecWith (Arg (FilePath -> Expectation)) it name test = Hspec.it name $ \tmp -> timeout t (test tmp) >>= maybe (error "test timed out") pure diff --git a/tests/ProtocolTests.hs b/tests/ProtocolTests.hs index f64efe108f..2eb946d731 100644 --- a/tests/ProtocolTests.hs +++ b/tests/ProtocolTests.hs @@ -133,7 +133,7 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew (MCSimple (extMsgContent (MCText "hello") Nothing))) it "x.msg.new chat message with chat version range" $ - "{\"v\":\"1-9\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" + "{\"v\":\"1-10\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" ##==## ChatMessage supportedChatVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew (MCSimple (extMsgContent (MCText "hello") Nothing))) it "x.msg.new quote" $ "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello to you too\",\"type\":\"text\"},\"quote\":{\"content\":{\"text\":\"hello there!\",\"type\":\"text\"},\"msgRef\":{\"msgId\":\"BQYHCA==\",\"sent\":true,\"sentAt\":\"1970-01-01T00:00:01.000000001Z\"}}}}" @@ -232,10 +232,10 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do ==# XContact testProfile Nothing it "x.grp.inv" $ "{\"v\":\"1\",\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\",\"groupPreferences\":{\"reactions\":{\"enable\":\"on\"},\"voice\":{\"enable\":\"on\"}}},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}}}}" - #==# XGrpInv GroupInvitation {fromMember = MemberIdRole (MemberId "\1\2\3\4") GRAdmin, invitedMember = MemberIdRole (MemberId "\5\6\7\8") GRMember, connRequest = testConnReq, groupProfile = testGroupProfile, groupLinkId = Nothing, groupSize = Nothing} + #==# XGrpInv GroupInvitation {fromMember = MemberIdRole (MemberId "\1\2\3\4") GRAdmin, invitedMember = MemberIdRole (MemberId "\5\6\7\8") GRMember, connRequest = testConnReq, groupProfile = testGroupProfile, businessChat = Nothing, groupLinkId = Nothing, groupSize = Nothing} it "x.grp.inv with group link id" $ "{\"v\":\"1\",\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\",\"groupPreferences\":{\"reactions\":{\"enable\":\"on\"},\"voice\":{\"enable\":\"on\"}}},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}, \"groupLinkId\":\"AQIDBA==\"}}}" - #==# XGrpInv GroupInvitation {fromMember = MemberIdRole (MemberId "\1\2\3\4") GRAdmin, invitedMember = MemberIdRole (MemberId "\5\6\7\8") GRMember, connRequest = testConnReq, groupProfile = testGroupProfile, groupLinkId = Just $ GroupLinkId "\1\2\3\4", groupSize = Nothing} + #==# XGrpInv GroupInvitation {fromMember = MemberIdRole (MemberId "\1\2\3\4") GRAdmin, invitedMember = MemberIdRole (MemberId "\5\6\7\8") GRMember, connRequest = testConnReq, groupProfile = testGroupProfile, businessChat = Nothing, groupLinkId = Just $ GroupLinkId "\1\2\3\4", groupSize = Nothing} it "x.grp.acpt without incognito profile" $ "{\"v\":\"1\",\"event\":\"x.grp.acpt\",\"params\":{\"memberId\":\"AQIDBA==\"}}" #==# XGrpAcpt (MemberId "\1\2\3\4") @@ -243,13 +243,13 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do "{\"v\":\"1\",\"event\":\"x.grp.mem.new\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemNew MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile} it "x.grp.mem.new with member chat version range" $ - "{\"v\":\"1\",\"event\":\"x.grp.mem.new\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-9\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" + "{\"v\":\"1\",\"event\":\"x.grp.mem.new\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-10\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemNew MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile} it "x.grp.mem.intro" $ "{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemIntro MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile} Nothing it "x.grp.mem.intro with member chat version range" $ - "{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-9\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" + "{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-10\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemIntro MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile} Nothing it "x.grp.mem.intro with member restrictions" $ "{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberRestrictions\":{\"restriction\":\"blocked\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" @@ -264,7 +264,7 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do "{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"directConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemFwd MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile} IntroInvitation {groupConnReq = testConnReq, directConnReq = Just testConnReq} it "x.grp.mem.fwd with member chat version range and w/t directConnReq" $ - "{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-9\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" + "{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-10\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemFwd MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile} IntroInvitation {groupConnReq = testConnReq, directConnReq = Nothing} it "x.grp.mem.info" $ "{\"v\":\"1\",\"event\":\"x.grp.mem.info\",\"params\":{\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}" From 5a59fdd91c01fe1b420d7e45f5b467207b14f05e Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Mon, 2 Dec 2024 22:28:58 +0700 Subject: [PATCH 100/167] android, desktop: fix Can't represent a width ... and a height ... in Constraints (#5293) --- .../common/views/chat/item/FramedItemView.kt | 49 ++++++++++--------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt index c2df11a8ea..e955428031 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt @@ -23,7 +23,7 @@ import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.res.MR -import kotlin.math.min +import kotlin.math.ceil @Composable fun FramedItemView( @@ -330,23 +330,16 @@ const val CHAT_IMAGE_LAYOUT_ID = "chatImage" const val CHAT_BUBBLE_LAYOUT_ID = "chatBubble" const val CHAT_COMPOSE_LAYOUT_ID = "chatCompose" const val CONSOLE_COMPOSE_LAYOUT_ID = "consoleCompose" -/** - * Equal to [androidx.compose.ui.unit.Constraints.MaxFocusMask], which is 0x3FFFF - 1 - * Other values make a crash `java.lang.IllegalArgumentException: Can't represent a width of 123456 and height of 9909 in Constraints` - * See [androidx.compose.ui.unit.Constraints.createConstraints] - * */ -const val MAX_SAFE_WIDTH = 0x3FFFF - 1 /** - * Limiting max value for height + width in order to not crash the app, see [androidx.compose.ui.unit.Constraints.createConstraints] - * */ -private fun maxSafeHeight(width: Int) = when { // width bits + height bits should be <= 31 - width < 0x1FFF /*MaxNonFocusMask*/ -> 0x3FFFF - 1 /* MaxFocusMask */ // 13 bits width + 18 bits height - width < 0x7FFF /*MinNonFocusMask*/ -> 0xFFFF - 1 /* MinFocusMask */ // 15 bits width + 16 bits height - width < 0xFFFF /*MinFocusMask*/ -> 0x7FFF - 1 /* MinFocusMask */ // 16 bits width + 15 bits height - width < 0x3FFFF /*MaxFocusMask*/ -> 0x1FFF - 1 /* MaxNonFocusMask */ // 18 bits width + 13 bits height - else -> 0x1FFF // shouldn't happen since width is limited already -} + * Compose shows "Can't represent a width of ... and height ... in Constraints" even when using built-in method for measuring max + * available size. It seems like padding around such layout prevents showing them in parent layout when such child layouts are placed. + * So calculating the expected padding here based on the values Compose printed in the exception (removing some pixels from + * [Constraints.fitPrioritizingHeight] result makes it working well) +*/ +private fun horizontalPaddingAroundCustomLayouts(density: Float): Int = + // currently, it's 18. Doubling it just to cover possible changes in the future + 36 * ceil(density).toInt() @Composable fun PriorityLayout( @@ -365,11 +358,15 @@ fun PriorityLayout( if (it.layoutId == priorityLayoutId) imagePlaceable!! else - it.measure(constraints.copy(maxWidth = imagePlaceable?.width ?: min(MAX_SAFE_WIDTH, constraints.maxWidth))) } + it.measure(constraints.copy(maxWidth = imagePlaceable?.width ?: constraints.maxWidth)) } // Limit width for every other element to width of important element and height for a sum of all elements. - val width = imagePlaceable?.measuredWidth ?: min(MAX_SAFE_WIDTH, placeables.maxOf { it.width }) - val height = minOf(maxSafeHeight(width), placeables.sumOf { it.height }) - layout(width, height) { + val width = imagePlaceable?.measuredWidth ?: placeables.maxOf { it.width } + val height = placeables.sumOf { it.height } + val adjustedConstraints = Constraints.fitPrioritizingHeight(constraints.minWidth, width, constraints.minHeight, height) + layout( + if (width > adjustedConstraints.maxWidth) adjustedConstraints.maxWidth - horizontalPaddingAroundCustomLayouts(density) else adjustedConstraints.maxWidth, + adjustedConstraints.maxHeight + ) { var y = 0 placeables.forEach { it.place(0, y) @@ -396,10 +393,14 @@ fun DependentLayout( if (it.layoutId == mainLayoutId) mainPlaceable!! else - it.measure(constraints.copy(minWidth = mainPlaceable?.width ?: 0, maxWidth = min(MAX_SAFE_WIDTH, constraints.maxWidth))) } - val width = mainPlaceable?.measuredWidth ?: min(MAX_SAFE_WIDTH, placeables.maxOf { it.width }) - val height = minOf(maxSafeHeight(width), placeables.sumOf { it.height }) - layout(width, height) { + it.measure(constraints.copy(minWidth = mainPlaceable?.width ?: 0, maxWidth = constraints.maxWidth)) } + val width = mainPlaceable?.measuredWidth ?: placeables.maxOf { it.width } + val height = placeables.sumOf { it.height } + val adjustedConstraints = Constraints.fitPrioritizingHeight(constraints.minWidth, width, constraints.minHeight, height) + layout( + if (width > adjustedConstraints.maxWidth) adjustedConstraints.maxWidth - horizontalPaddingAroundCustomLayouts(density) else adjustedConstraints.maxWidth, + adjustedConstraints.maxHeight + ) { var y = 0 placeables.forEach { it.place(0, y) From f6b611aa30655ab61ac37c69b3fa961434e4fc7b Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Mon, 2 Dec 2024 22:58:21 +0700 Subject: [PATCH 101/167] android, desktop: check existence before deleting database (#5298) --- .../chat/simplex/common/views/database/DatabaseView.kt | 5 ++++- .../chat/simplex/common/views/helpers/DatabaseUtils.kt | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt index 8185122089..af40aa2e70 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt @@ -719,13 +719,16 @@ private fun deleteChatAlert(onConfirm: () -> Unit) { } private suspend fun deleteChat(m: ChatModel, progressIndicator: MutableState) { + if (!DatabaseUtils.hasAtLeastOneDatabase(dataDir.absolutePath)) { + return + } progressIndicator.value = true try { deleteChatAsync(m) operationEnded(m, progressIndicator) { AlertManager.shared.showAlertMsg(generalGetString(MR.strings.chat_database_deleted), generalGetString(MR.strings.restart_the_app_to_create_a_new_chat_profile)) } - } catch (e: Error) { + } catch (e: Throwable) { operationEnded(m, progressIndicator) { AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_deleting_database), e.toString()) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DatabaseUtils.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DatabaseUtils.kt index c621f186cd..4827e6ae61 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DatabaseUtils.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DatabaseUtils.kt @@ -39,7 +39,7 @@ object DatabaseUtils { } } - private fun hasAtLeastOneDatabase(rootDir: String): Boolean = + fun hasAtLeastOneDatabase(rootDir: String): Boolean = File(rootDir + File.separator + chatDatabaseFileName).exists() || File(rootDir + File.separator + agentDatabaseFileName).exists() fun hasOnlyOneDatabase(rootDir: String): Boolean = From a62ce9168e227bbf159f83f2b3361ddef0b556c0 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Mon, 2 Dec 2024 20:30:05 +0400 Subject: [PATCH 102/167] core: fix names created for business request group and member (#5296) --- src/Simplex/Chat.hs | 4 ++-- src/Simplex/Chat/Store/Groups.hs | 40 +++++++++++++++++++++++++------- tests/ChatTests/Profiles.hs | 18 +++++++------- 3 files changed, 43 insertions(+), 19 deletions(-) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 92ec04d11b..7a48914abf 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -4007,13 +4007,13 @@ acceptGroupJoinRequestAsync acceptBusinessJoinRequestAsync :: User -> UserContactRequest -> CM GroupInfo acceptBusinessJoinRequestAsync user - ucr@UserContactRequest {agentInvitationId = AgentInvId invId, cReqChatVRange} = do + ucr@UserContactRequest {contactRequestId, agentInvitationId = AgentInvId invId, cReqChatVRange} = do vr <- chatVersionRange gVar <- asks random let userProfile@Profile {displayName, preferences} = profileToSendOnAccept user Nothing True groupPreferences = maybe defaultBusinessGroupPrefs businessGroupPrefs preferences (gInfo, clientMember) <- withStore $ \db -> do - liftIO $ deleteContactRequestRec db user ucr + liftIO $ deleteContactRequest db user contactRequestId createBusinessRequestGroup db vr gVar user ucr groupPreferences let GroupInfo {membership} = gInfo GroupMember {memberRole = userRole, memberId = userMemberId} = membership diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index b07adf407b..bb5252ddb0 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -155,7 +155,7 @@ import Simplex.Messaging.Util (eitherToMaybe, ($>>=), (<$$>)) import Simplex.Messaging.Version import UnliftIO.STM -type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe Text, Maybe ImageData, Maybe ProfileId, Maybe MsgFilter, Maybe Bool, Bool, Maybe GroupPreferences) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime, Maybe MemberId, Maybe BusinessChatType, Maybe UIThemeEntityOverrides, Maybe CustomData) :. GroupMemberRow +type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe Text, Maybe ImageData, Maybe ProfileId, Maybe MsgFilter, Maybe Bool, Bool, Maybe GroupPreferences) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime, Maybe MemberId, Maybe BusinessChatType, Maybe UIThemeEntityOverrides, Maybe CustomData) :. GroupMemberRow type GroupMemberRow = ((Int64, Int64, MemberId, VersionChat, VersionChat, GroupMemberRole, GroupMemberCategory, GroupMemberStatus, Bool, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, ContactName, Maybe ContactId, ProfileId, ProfileId, ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Maybe Preferences)) @@ -936,18 +936,17 @@ createBusinessRequestGroup db vr gVar - user@User {userId} - ucr@UserContactRequest {profile} + user@User {userId, userContactId} + UserContactRequest {cReqChatVRange, profile = Profile {displayName, fullName, image, contactLink, preferences}} groupPreferences = do currentTs <- liftIO getCurrentTime groupInfo <- insertGroup_ currentTs - (groupMemberId, memberId) <- createAcceptedMember db gVar user groupInfo ucr GRMember + (groupMemberId, memberId) <- insertClientMember_ currentTs groupInfo liftIO $ setBusinessMemberId groupInfo memberId - acceptedMember <- getGroupMemberById db vr user groupMemberId - pure (groupInfo, acceptedMember) + clientMember <- getGroupMemberById db vr user groupMemberId + pure (groupInfo, clientMember) where - insertGroup_ currentTs = ExceptT $ do - let Profile {displayName, fullName, image} = profile + insertGroup_ currentTs = ExceptT $ withLocalDisplayName db userId displayName $ \localDisplayName -> runExceptT $ do groupId <- liftIO $ do DB.execute @@ -968,6 +967,31 @@ createBusinessRequestGroup memberId <- liftIO $ encodedRandomBytes gVar 12 void $ createContactMemberInv_ db user groupId Nothing user (MemberIdRole (MemberId memberId) GROwner) GCUserMember GSMemCreator IBUser Nothing currentTs vr getGroupInfo db vr user groupId + VersionRange minV maxV = cReqChatVRange + insertClientMember_ currentTs GroupInfo {groupId, membership} = ExceptT $ do + withLocalDisplayName db userId displayName $ \localDisplayName -> runExceptT $ do + liftIO $ + DB.execute + db + "INSERT INTO contact_profiles (display_name, full_name, image, contact_link, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)" + (displayName, fullName, image, contactLink, userId, preferences, currentTs, currentTs) + profileId <- liftIO $ insertedRowId db + createWithRandomId gVar $ \memId -> do + DB.execute + db + [sql| + INSERT INTO group_members + ( group_id, member_id, member_role, member_category, member_status, invited_by, invited_by_group_member_id, + user_id, local_display_name, contact_id, contact_profile_id, created_at, updated_at, + peer_chat_min_version, peer_chat_max_version) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + |] + ( (groupId, MemberId memId, GRMember, GCInviteeMember, GSMemAccepted, fromInvitedBy userContactId IBUser, groupMemberId' membership) + :. (userId, localDisplayName, Nothing :: (Maybe Int64), profileId, currentTs, currentTs) + :. (minV, maxV) + ) + groupMemberId <- liftIO $ insertedRowId db + pure (groupMemberId, MemberId memId) setBusinessMemberId GroupInfo {groupId} businessMemberId = do DB.execute db "UPDATE groups SET business_member_id = ? WHERE group_id = ?" (businessMemberId, groupId) diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index b2db679f29..3ffe57ba2e 100644 --- a/tests/ChatTests/Profiles.hs +++ b/tests/ChatTests/Profiles.hs @@ -688,18 +688,18 @@ testBusinessAddress = testChat3 businessProfile aliceProfile {fullName = "Alice biz <## "auto_accept on, business" bob ##> ("/c " <> cLink) bob <## "connection request sent!" - biz <## "#bob_1 (Bob): accepting business address request..." - biz <## "#bob_1: bob joined the group" + biz <## "#bob (Bob): accepting business address request..." + biz <## "#bob: bob_1 joined the group" bob <## "#biz: joining the group..." bob <## "#biz: you joined the group" - biz #> "#bob_1 hi" + biz #> "#bob hi" bob <# "#biz biz_1> hi" bob #> "#biz hello" - biz <# "#bob_1 bob> hello" + biz <# "#bob bob_1> hello" connectUsers biz alice biz <##> alice - biz ##> "/a #bob_1 alice" - biz <## "invitation to join the group #bob_1 sent to alice" + biz ##> "/a #bob alice" + biz <## "invitation to join the group #bob sent to alice" alice <## "#bob (Bob): biz invites you to join the group as member" alice <## "use /j bob to accept" alice ##> "/j bob" @@ -708,7 +708,7 @@ testBusinessAddress = testChat3 businessProfile aliceProfile {fullName = "Alice alice <## "#bob: you joined the group" alice <### [WithTime "#bob biz> hi [>>]", WithTime "#bob bob_1> hello [>>]"] alice <## "#bob: member bob_1 (Bob) is connected", - biz <## "#bob_1: alice joined the group", + biz <## "#bob: alice joined the group", do bob <## "#biz: biz_1 added alice (Alice @ Biz) to the group (connecting...)" bob <## "#biz: new member alice is connected" @@ -716,11 +716,11 @@ testBusinessAddress = testChat3 businessProfile aliceProfile {fullName = "Alice alice #> "#bob hey" concurrently_ (bob <# "#biz alice> hey") - (biz <# "#bob_1 alice> hey") + (biz <# "#bob alice> hey") bob #> "#biz hey there" concurrently_ (alice <# "#bob bob_1> hey there") - (biz <# "#bob_1 bob> hey there") + (biz <# "#bob bob_1> hey there") testPlanAddressOkKnown :: HasCallStack => FilePath -> IO () testPlanAddressOkKnown = From 665501026dde8542898418f45afbef14fda5f414 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Mon, 2 Dec 2024 23:32:04 +0700 Subject: [PATCH 103/167] android: show info for Xiaomi users about autostart restrictions (#5295) * android: show info for Xiaomi users about autostart restrictions * text and placement * update strings --------- Co-authored-by: Evgeny Poberezkin --- .../src/main/java/chat/simplex/app/SimplexApp.kt | 2 ++ .../src/main/java/chat/simplex/app/SimplexService.kt | 8 +++++++- .../kotlin/chat/simplex/common/platform/Platform.kt | 1 + .../views/usersettings/NotificationsSettingsView.kt | 10 ++++++++-- .../src/commonMain/resources/MR/base/strings.xml | 5 +++-- 5 files changed, 21 insertions(+), 5 deletions(-) diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt index f46ed4775f..0ebd9ca2c6 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt @@ -332,6 +332,8 @@ class SimplexApp: Application(), LifecycleEventObserver { NetworkObserver.shared.restartNetworkObserver() } + override fun androidIsXiaomiDevice(): Boolean = setOf("xiaomi", "redmi", "poco").contains(Build.BRAND.lowercase()) + @SuppressLint("SourceLockedOrientationActivity") @Composable override fun androidLockPortraitOrientation() { diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexService.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexService.kt index ce3f0825b8..33799087a7 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexService.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexService.kt @@ -497,6 +497,12 @@ class SimplexService: Service() { Modifier.padding(bottom = 8.dp) ) Text(annotatedStringResource(MR.strings.turn_off_battery_optimization)) + + if (platform.androidIsXiaomiDevice() && (mode == NotificationsMode.PERIODIC || mode == NotificationsMode.SERVICE)) { + Text(annotatedStringResource(MR.strings.xiaomi_ignore_battery_optimization), + Modifier.padding(top = 8.dp) + ) + } } }, dismissButton = { @@ -623,7 +629,7 @@ class SimplexService: Service() { fun isBackgroundAllowed(): Boolean = isIgnoringBatteryOptimizations() && !isBackgroundRestricted() - fun isIgnoringBatteryOptimizations(): Boolean { + private fun isIgnoringBatteryOptimizations(): Boolean { val powerManager = androidAppContext.getSystemService(Application.POWER_SERVICE) as PowerManager return powerManager.isIgnoringBatteryOptimizations(androidAppContext.packageName) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt index 23ab450cb6..e0a9e22f71 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt @@ -26,6 +26,7 @@ interface PlatformInterface { fun androidPictureInPictureAllowed(): Boolean = true fun androidCallEnded() {} fun androidRestartNetworkObserver() {} + fun androidIsXiaomiDevice(): Boolean = false val androidApiLevel: Int? get() = null @Composable fun androidLockPortraitOrientation() {} suspend fun androidAskToAllowBackgroundCalls(): Boolean = true diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NotificationsSettingsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NotificationsSettingsView.kt index 60bde83c17..66b518e9aa 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NotificationsSettingsView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NotificationsSettingsView.kt @@ -1,11 +1,10 @@ package chat.simplex.common.views.usersettings import SectionBottomSpacer +import SectionTextFooter import SectionView import SectionViewSelectable import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier @@ -16,6 +15,7 @@ import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.style.TextOverflow import chat.simplex.common.model.* import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF import chat.simplex.common.views.helpers.* import chat.simplex.res.MR import kotlin.collections.ArrayList @@ -77,6 +77,9 @@ fun NotificationsSettingsLayout( color = MaterialTheme.colors.secondary ) } + if (platform.androidIsXiaomiDevice() && (notificationsMode.value == NotificationsMode.PERIODIC || notificationsMode.value == NotificationsMode.SERVICE)) { + SectionTextFooter(stringResource(MR.strings.xiaomi_ignore_battery_optimization)) + } } SectionBottomSpacer() } @@ -91,6 +94,9 @@ fun NotificationsModeView( ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.settings_notifications_mode_title).lowercase().capitalize(Locale.current)) SectionViewSelectable(null, notificationsMode, modes, onNotificationsModeSelected) + if (platform.androidIsXiaomiDevice() && (notificationsMode.value == NotificationsMode.PERIODIC || notificationsMode.value == NotificationsMode.SERVICE)) { + SectionTextFooter(stringResource(MR.strings.xiaomi_ignore_battery_optimization)) + } } } diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 1c036d76b2..c390096ee2 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -191,9 +191,9 @@ Instant notifications Instant notifications! Instant notifications are disabled! - SimpleX background service – it uses a few percent of the battery per day.]]> + SimpleX runs in background instead of using push notifications.]]> It can be disabled via settings – notifications will still be shown while the app is running.]]> - allow SimpleX to run in background in the next dialog. Otherwise, the notifications will be disabled.]]> + Allow it in the next dialog to receive notifications instantly.]]> Battery optimization is active, turning off background service and periodic requests for new messages. You can re-enable them via settings. Periodic notifications Periodic notifications are disabled! @@ -210,6 +210,7 @@ To receive notifications, please, enter the database passphrase Can\'t initialize the database The database is not working correctly. Tap to learn more + Xiaomi devices: please enable Autostart in the system settings for notifications to work.]]> SimpleX Chat service From a588e7003d79ff540c8ea4c4c3915c0814274dd1 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Mon, 2 Dec 2024 23:33:22 +0700 Subject: [PATCH 104/167] android: alert round corners (#5299) Co-authored-by: Evgeny Poberezkin --- .../java/chat/simplex/app/SimplexService.kt | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexService.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexService.kt index 33799087a7..3b9f2ade26 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexService.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexService.kt @@ -10,6 +10,8 @@ import android.os.* import android.os.SystemClock import android.provider.Settings import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CornerSize +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier @@ -462,7 +464,8 @@ class SimplexService: Service() { }, confirmButton = { TextButton(onClick = AlertManager.shared::hideAlert) { Text(stringResource(MR.strings.ok)) } - } + }, + shape = RoundedCornerShape(corner = CornerSize(25.dp)) ) } @@ -510,7 +513,8 @@ class SimplexService: Service() { }, confirmButton = { TextButton(onClick = ignoreOptimization) { Text(stringResource(MR.strings.turn_off_battery_optimization_button)) } - } + }, + shape = RoundedCornerShape(corner = CornerSize(25.dp)) ) } @@ -552,7 +556,8 @@ class SimplexService: Service() { }, confirmButton = { TextButton(onClick = unrestrict) { Text(stringResource(MR.strings.turn_off_system_restriction_button)) } - } + }, + shape = RoundedCornerShape(corner = CornerSize(25.dp)) ) } @@ -579,7 +584,8 @@ class SimplexService: Service() { }, confirmButton = { TextButton(onClick = unrestrict) { Text(stringResource(MR.strings.turn_off_system_restriction_button)) } - } + }, + shape = RoundedCornerShape(corner = CornerSize(25.dp)) ) val scope = rememberCoroutineScope() DisposableEffect(Unit) { @@ -623,7 +629,8 @@ class SimplexService: Service() { }, confirmButton = { TextButton(onClick = AlertManager.shared::hideAlert) { Text(stringResource(MR.strings.ok)) } - } + }, + shape = RoundedCornerShape(corner = CornerSize(25.dp)) ) } From bc960001310cb454ceddc3650a6d155721d48988 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Mon, 2 Dec 2024 21:40:22 +0400 Subject: [PATCH 105/167] ios: support business addresses and chats (#5300) * ios: support business addresses and chats * improve * words * fix --------- Co-authored-by: Evgeny Poberezkin --- apps/ios/Shared/Model/SimpleXAPI.swift | 7 +++ .../Views/Chat/Group/GroupChatInfoView.swift | 24 ++++++---- .../Chat/Group/GroupPreferencesView.swift | 6 +-- .../Views/Chat/Group/GroupWelcomeView.swift | 2 +- .../Views/UserSettings/UserAddressView.swift | 42 +++++++++--------- apps/ios/SimpleX.xcodeproj/project.pbxproj | 44 +++++++++---------- apps/ios/SimpleXChat/APITypes.swift | 12 ++++- apps/ios/SimpleXChat/ChatTypes.swift | 13 +++++- apps/ios/SimpleXChat/ChatUtils.swift | 7 ++- 9 files changed, 96 insertions(+), 61 deletions(-) diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 459ece32da..5f29a848ef 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -2014,6 +2014,13 @@ func processReceivedMsg(_ res: ChatResponse) async { m.removeChat(hostConn.id) } } + case let .businessLinkConnecting(user, groupInfo, hostMember, fromContact): + if !active(user) { return } + + await MainActor.run { + m.updateGroup(groupInfo) + m.removeChat(fromContact.id) + } case let .joinedGroupMemberConnecting(user, groupInfo, _, member): if active(user) { await MainActor.run { diff --git a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift index 59df52df9f..89f0fcbedf 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift @@ -81,10 +81,10 @@ struct GroupChatInfoView: View { .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) Section { - if groupInfo.canEdit { + if groupInfo.isOwner && groupInfo.businessChat == nil { editGroupButton() } - if groupInfo.groupProfile.description != nil || groupInfo.canEdit { + if groupInfo.groupProfile.description != nil || (groupInfo.isOwner && groupInfo.businessChat == nil) { addOrEditWelcomeMessage() } groupPreferencesButton($groupInfo) @@ -107,7 +107,9 @@ struct GroupChatInfoView: View { Section(header: Text("\(members.count + 1) members").foregroundColor(theme.colors.secondary)) { if groupInfo.canAddMembers { - groupLinkButton() + if groupInfo.businessChat == nil { + groupLinkButton() + } if (chat.chatInfo.incognito) { Label("Invite members", systemImage: "plus") .foregroundColor(Color(uiColor: .tertiaryLabel)) @@ -276,10 +278,15 @@ struct GroupChatInfoView: View { } private func addMembersButton() -> some View { - NavigationLink { + let label: LocalizedStringKey = switch groupInfo.businessChat?.chatType { + case .customer: "Add team members" + case .business: "Add friends" + case .none: "Invite members" + } + return NavigationLink { addMembersDestinationView() } label: { - Label("Invite members", systemImage: "plus") + Label(label, systemImage: "plus") } } @@ -625,21 +632,22 @@ struct GroupChatInfoView: View { } func groupPreferencesButton(_ groupInfo: Binding, _ creatingGroup: Bool = false) -> some View { - NavigationLink { + let label: LocalizedStringKey = groupInfo.wrappedValue.businessChat == nil ? "Group preferences" : "Chat preferences" + return NavigationLink { GroupPreferencesView( groupInfo: groupInfo, preferences: groupInfo.wrappedValue.fullGroupPreferences, currentPreferences: groupInfo.wrappedValue.fullGroupPreferences, creatingGroup: creatingGroup ) - .navigationBarTitle("Group preferences") + .navigationBarTitle(label) .modifier(ThemedBackground(grouped: true)) .navigationBarTitleDisplayMode(.large) } label: { if creatingGroup { Text("Set group preferences") } else { - Label("Group preferences", systemImage: "switch.2") + Label(label, systemImage: "switch.2") } } } diff --git a/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift b/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift index 2b0d05375b..bbbbe4d4c3 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift @@ -38,7 +38,7 @@ struct GroupPreferencesView: View { featureSection(.simplexLinks, $preferences.simplexLinks.enable, $preferences.simplexLinks.role) featureSection(.history, $preferences.history.enable) - if groupInfo.canEdit { + if groupInfo.isOwner { Section { Button("Reset") { preferences = currentPreferences } Button(saveText) { savePreferences() } @@ -77,7 +77,7 @@ struct GroupPreferencesView: View { let color: Color = enableFeature.wrappedValue == .on ? .green : theme.colors.secondary let icon = enableFeature.wrappedValue == .on ? feature.iconFilled : feature.icon let timedOn = feature == .timedMessages && enableFeature.wrappedValue == .on - if groupInfo.canEdit { + if groupInfo.isOwner { let enable = Binding( get: { enableFeature.wrappedValue == .on }, set: { on, _ in enableFeature.wrappedValue = on ? .on : .off } @@ -123,7 +123,7 @@ struct GroupPreferencesView: View { } } } footer: { - Text(feature.enableDescription(enableFeature.wrappedValue, groupInfo.canEdit)) + Text(feature.enableDescription(enableFeature.wrappedValue, groupInfo.isOwner)) .foregroundColor(theme.colors.secondary) } .onChange(of: enableFeature.wrappedValue) { enabled in diff --git a/apps/ios/Shared/Views/Chat/Group/GroupWelcomeView.swift b/apps/ios/Shared/Views/Chat/Group/GroupWelcomeView.swift index 9a9002f9dc..8dfc32f6ea 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupWelcomeView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupWelcomeView.swift @@ -23,7 +23,7 @@ struct GroupWelcomeView: View { var body: some View { VStack { - if groupInfo.canEdit { + if groupInfo.isOwner && groupInfo.businessChat == nil { editorView() .modifier(BackButton(disabled: Binding.constant(false)) { if welcomeTextUnchanged() { diff --git a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift index 6bc3a221b2..7965215b49 100644 --- a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift +++ b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift @@ -57,21 +57,17 @@ struct UserAddressView: View { if chatModel.userAddress == nil, autoCreate { createAddress() } - if let userAddress = chatModel.userAddress { - aas = AutoAcceptState(userAddress: userAddress) - savedAAS = aas - } } - .onChange(of: aas.enable) { aasEnabled in - if !aasEnabled { aas = AutoAcceptState() } - } - } private func userAddressView() -> some View { List { if let userAddress = chatModel.userAddress { existingAddressView(userAddress) + .onAppear { + aas = AutoAcceptState(userAddress: userAddress) + savedAAS = aas + } } else { Section { createAddressButton() @@ -145,13 +141,14 @@ struct UserAddressView: View { // if MFMailComposeViewController.canSendMail() { // shareViaEmailButton(userAddress) // } - settingsRow("hand.wave", color: theme.colors.secondary) { + settingsRow("briefcase", color: theme.colors.secondary) { Toggle("Business address", isOn: $aas.business) .onChange(of: aas.business) { ba in if ba { aas.enable = true aas.incognito = false } + saveAAS($aas, $savedAAS) } } addressSettingsButton(userAddress) @@ -160,7 +157,8 @@ struct UserAddressView: View { .foregroundColor(theme.colors.secondary) } footer: { if aas.business { - Text("Add your team members to the conversations").foregroundColor(theme.colors.secondary) + Text("Add your team members to the conversations.") + .foregroundColor(theme.colors.secondary) } } @@ -313,7 +311,7 @@ private struct AutoAcceptState: Equatable { if let aa = userAddress.autoAccept { enable = true incognito = aa.acceptIncognito - business = aa.businessAddress == true + business = aa.businessAddress if let msg = aa.autoReply { welcomeText = msg.text } else { @@ -382,7 +380,7 @@ struct UserAddressSettingsView: View { title: NSLocalizedString("Auto-accept settings", comment: "alert title"), message: NSLocalizedString("Settings were changed.", comment: "alert message"), buttonTitle: NSLocalizedString("Save", comment: "alert button"), - buttonAction: saveAAS, + buttonAction: { saveAAS($aas, $savedAAS) }, cancelButton: true ) } @@ -470,7 +468,7 @@ struct UserAddressSettingsView: View { settingsRow("checkmark", color: theme.colors.secondary) { Toggle("Auto-accept", isOn: $aas.enable) .onChange(of: aas.enable) { _ in - saveAAS() + saveAAS($aas, $savedAAS) } } } @@ -519,22 +517,24 @@ struct UserAddressSettingsView: View { private func saveAASButton() -> some View { Button { keyboardVisible = false - saveAAS() + saveAAS($aas, $savedAAS) } label: { Text("Save") } } +} - private func saveAAS() { - Task { - do { - if let address = try await userAddressAutoAccept(aas.autoAccept) { +private func saveAAS(_ aas: Binding, _ savedAAS: Binding) { + Task { + do { + if let address = try await userAddressAutoAccept(aas.wrappedValue.autoAccept) { + await MainActor.run { ChatModel.shared.userAddress = address - savedAAS = aas + savedAAS.wrappedValue = aas.wrappedValue } - } catch let error { - logger.error("userAddressAutoAccept error: \(responseError(error))") } + } catch let error { + logger.error("userAddressAutoAccept error: \(responseError(error))") } } } diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 7e5a48013b..8d4e4fe5c4 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -148,11 +148,6 @@ 6419EC562AB8BC8B004A607A /* ContextInvitingContactMemberView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6419EC552AB8BC8B004A607A /* ContextInvitingContactMemberView.swift */; }; 6419EC582AB97507004A607A /* CIMemberCreatedContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6419EC572AB97507004A607A /* CIMemberCreatedContactView.swift */; }; 642BA82D2CE50495005E9412 /* NewServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 642BA82C2CE50495005E9412 /* NewServerView.swift */; }; - 642BA8332CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.3-ELfYrsBTXJJ5vBEIgQ1y2B.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 642BA82E2CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.3-ELfYrsBTXJJ5vBEIgQ1y2B.a */; }; - 642BA8342CEB3D4B005E9412 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 642BA82F2CEB3D4B005E9412 /* libffi.a */; }; - 642BA8352CEB3D4B005E9412 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 642BA8302CEB3D4B005E9412 /* libgmp.a */; }; - 642BA8362CEB3D4B005E9412 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 642BA8312CEB3D4B005E9412 /* libgmpxx.a */; }; - 642BA8372CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.3-ELfYrsBTXJJ5vBEIgQ1y2B-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 642BA8322CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.3-ELfYrsBTXJJ5vBEIgQ1y2B-ghc9.6.3.a */; }; 6432857C2925443C00FBE5C8 /* GroupPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */; }; 643B3B4E2CCFD6400083A2CF /* OperatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 643B3B4D2CCFD6400083A2CF /* OperatorView.swift */; }; 6440CA00288857A10062C672 /* CIEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440C9FF288857A10062C672 /* CIEventView.swift */; }; @@ -171,6 +166,11 @@ 647F090E288EA27B00644C40 /* GroupMemberInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647F090D288EA27B00644C40 /* GroupMemberInfoView.swift */; }; 648010AB281ADD15009009B9 /* CIFileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648010AA281ADD15009009B9 /* CIFileView.swift */; }; 648679AB2BC96A74006456E7 /* ChatItemForwardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648679AA2BC96A74006456E7 /* ChatItemForwardingView.swift */; }; + 649B28DD2CFE07CF00536B68 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 649B28D82CFE07CF00536B68 /* libffi.a */; }; + 649B28DE2CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.3-ELfYrsBTXJJ5vBEIgQ1y2B-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 649B28D92CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.3-ELfYrsBTXJJ5vBEIgQ1y2B-ghc9.6.3.a */; }; + 649B28DF2CFE07CF00536B68 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 649B28DA2CFE07CF00536B68 /* libgmpxx.a */; }; + 649B28E02CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.3-ELfYrsBTXJJ5vBEIgQ1y2B.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 649B28DB2CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.3-ELfYrsBTXJJ5vBEIgQ1y2B.a */; }; + 649B28E12CFE07CF00536B68 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 649B28DC2CFE07CF00536B68 /* libgmp.a */; }; 649BCDA0280460FD00C3A862 /* ComposeImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649BCD9F280460FD00C3A862 /* ComposeImageView.swift */; }; 649BCDA22805D6EF00C3A862 /* CIImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649BCDA12805D6EF00C3A862 /* CIImageView.swift */; }; 64AA1C6927EE10C800AC7277 /* ContextItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6827EE10C800AC7277 /* ContextItemView.swift */; }; @@ -229,7 +229,6 @@ D741547A29AF90B00022400A /* PushKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D741547929AF90B00022400A /* PushKit.framework */; }; D77B92DC2952372200A5A1CC /* SwiftyGif in Frameworks */ = {isa = PBXBuildFile; productRef = D77B92DB2952372200A5A1CC /* SwiftyGif */; }; D7F0E33929964E7E0068AF69 /* LZString in Frameworks */ = {isa = PBXBuildFile; productRef = D7F0E33829964E7E0068AF69 /* LZString */; }; - E504516F2CFA3BFB00DE3F74 /* ContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = E504516E2CFA3BFB00DE3F74 /* ContextMenu.swift */; }; E51CC1E62C62085600DB91FE /* OneHandUICard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E51CC1E52C62085600DB91FE /* OneHandUICard.swift */; }; E5DCF8DB2C56FAC1007928CC /* SimpleXChat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; }; E5DCF9712C590272007928CC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E5DCF96F2C590272007928CC /* Localizable.strings */; }; @@ -497,11 +496,6 @@ 6419EC552AB8BC8B004A607A /* ContextInvitingContactMemberView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextInvitingContactMemberView.swift; sourceTree = ""; }; 6419EC572AB97507004A607A /* CIMemberCreatedContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIMemberCreatedContactView.swift; sourceTree = ""; }; 642BA82C2CE50495005E9412 /* NewServerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewServerView.swift; sourceTree = ""; }; - 642BA82E2CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.3-ELfYrsBTXJJ5vBEIgQ1y2B.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.2.0.3-ELfYrsBTXJJ5vBEIgQ1y2B.a"; sourceTree = ""; }; - 642BA82F2CEB3D4B005E9412 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 642BA8302CEB3D4B005E9412 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; - 642BA8312CEB3D4B005E9412 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; - 642BA8322CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.3-ELfYrsBTXJJ5vBEIgQ1y2B-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.2.0.3-ELfYrsBTXJJ5vBEIgQ1y2B-ghc9.6.3.a"; sourceTree = ""; }; 6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupPreferencesView.swift; sourceTree = ""; }; 643B3B4D2CCFD6400083A2CF /* OperatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperatorView.swift; sourceTree = ""; }; 6440C9FF288857A10062C672 /* CIEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIEventView.swift; sourceTree = ""; }; @@ -521,6 +515,11 @@ 648010AA281ADD15009009B9 /* CIFileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIFileView.swift; sourceTree = ""; }; 648679AA2BC96A74006456E7 /* ChatItemForwardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemForwardingView.swift; sourceTree = ""; }; 6493D667280ED77F007A76FB /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + 649B28D82CFE07CF00536B68 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; + 649B28D92CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.3-ELfYrsBTXJJ5vBEIgQ1y2B-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.2.0.3-ELfYrsBTXJJ5vBEIgQ1y2B-ghc9.6.3.a"; sourceTree = ""; }; + 649B28DA2CFE07CF00536B68 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; + 649B28DB2CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.3-ELfYrsBTXJJ5vBEIgQ1y2B.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.2.0.3-ELfYrsBTXJJ5vBEIgQ1y2B.a"; sourceTree = ""; }; + 649B28DC2CFE07CF00536B68 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; 649BCD9F280460FD00C3A862 /* ComposeImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeImageView.swift; sourceTree = ""; }; 649BCDA12805D6EF00C3A862 /* CIImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIImageView.swift; sourceTree = ""; }; 64AA1C6827EE10C800AC7277 /* ContextItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextItemView.swift; sourceTree = ""; }; @@ -576,7 +575,6 @@ D741547729AF89AF0022400A /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/StoreKit.framework; sourceTree = DEVELOPER_DIR; }; 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; }; - E504516E2CFA3BFB00DE3F74 /* ContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenu.swift; sourceTree = ""; }; E51CC1E52C62085600DB91FE /* OneHandUICard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneHandUICard.swift; sourceTree = ""; }; E5DCF9702C590272007928CC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; E5DCF9722C590274007928CC /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Localizable.strings; sourceTree = ""; }; @@ -669,14 +667,14 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 649B28DF2CFE07CF00536B68 /* libgmpxx.a in Frameworks */, 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, + 649B28E12CFE07CF00536B68 /* libgmp.a in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, + 649B28E02CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.3-ELfYrsBTXJJ5vBEIgQ1y2B.a in Frameworks */, CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */, - 642BA8342CEB3D4B005E9412 /* libffi.a in Frameworks */, - 642BA8352CEB3D4B005E9412 /* libgmp.a in Frameworks */, - 642BA8372CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.3-ELfYrsBTXJJ5vBEIgQ1y2B-ghc9.6.3.a in Frameworks */, - 642BA8332CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.3-ELfYrsBTXJJ5vBEIgQ1y2B.a in Frameworks */, - 642BA8362CEB3D4B005E9412 /* libgmpxx.a in Frameworks */, + 649B28DE2CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.3-ELfYrsBTXJJ5vBEIgQ1y2B-ghc9.6.3.a in Frameworks */, + 649B28DD2CFE07CF00536B68 /* libffi.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -753,11 +751,11 @@ 5C764E5C279C70B7000C6508 /* Libraries */ = { isa = PBXGroup; children = ( - 642BA82F2CEB3D4B005E9412 /* libffi.a */, - 642BA8302CEB3D4B005E9412 /* libgmp.a */, - 642BA8312CEB3D4B005E9412 /* libgmpxx.a */, - 642BA8322CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.3-ELfYrsBTXJJ5vBEIgQ1y2B-ghc9.6.3.a */, - 642BA82E2CEB3D4B005E9412 /* libHSsimplex-chat-6.2.0.3-ELfYrsBTXJJ5vBEIgQ1y2B.a */, + 649B28D82CFE07CF00536B68 /* libffi.a */, + 649B28DC2CFE07CF00536B68 /* libgmp.a */, + 649B28DA2CFE07CF00536B68 /* libgmpxx.a */, + 649B28D92CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.3-ELfYrsBTXJJ5vBEIgQ1y2B-ghc9.6.3.a */, + 649B28DB2CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.3-ELfYrsBTXJJ5vBEIgQ1y2B.a */, ); path = Libraries; sourceTree = ""; @@ -793,7 +791,6 @@ 5C971E1F27AEBF7000C8A3CE /* Helpers */ = { isa = PBXGroup; children = ( - E504516E2CFA3BFB00DE3F74 /* ContextMenu.swift */, 5C971E2027AEBF8300C8A3CE /* ChatInfoImage.swift */, 5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */, 5CC1C99427A6CF7F000D9FF6 /* ShareSheet.swift */, @@ -1448,7 +1445,6 @@ CE984D4B2C36C5D500E3AEFF /* ChatItemClipShape.swift in Sources */, 64D0C2C629FAC1EC00B38D5F /* AddContactLearnMore.swift in Sources */, 5C3A88D127DF57800060F1C2 /* FramedItemView.swift in Sources */, - E504516F2CFA3BFB00DE3F74 /* ContextMenu.swift in Sources */, 5C65F343297D45E100B67AF3 /* VersionView.swift in Sources */, 64F1CC3B28B39D8600CD1FB1 /* IncognitoHelp.swift in Sources */, 5CB0BA90282713D900B3292C /* SimpleXInfo.swift in Sources */, diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 954022c312..07095ed5e1 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -640,6 +640,7 @@ public enum ChatResponse: Decodable, Error { case sentGroupInvitation(user: UserRef, groupInfo: GroupInfo, contact: Contact, member: GroupMember) case userAcceptedGroupSent(user: UserRef, groupInfo: GroupInfo, hostContact: Contact?) case groupLinkConnecting(user: UserRef, groupInfo: GroupInfo, hostMember: GroupMember) + case businessLinkConnecting(user: UserRef, groupInfo: GroupInfo, hostMember: GroupMember, fromContact: Contact) case userDeletedMember(user: UserRef, groupInfo: GroupInfo, member: GroupMember) case leftMemberUser(user: UserRef, groupInfo: GroupInfo) case groupMembers(user: UserRef, group: Group) @@ -816,6 +817,7 @@ public enum ChatResponse: Decodable, Error { case .sentGroupInvitation: return "sentGroupInvitation" case .userAcceptedGroupSent: return "userAcceptedGroupSent" case .groupLinkConnecting: return "groupLinkConnecting" + case .businessLinkConnecting: return "businessLinkConnecting" case .userDeletedMember: return "userDeletedMember" case .leftMemberUser: return "leftMemberUser" case .groupMembers: return "groupMembers" @@ -998,6 +1000,7 @@ public enum ChatResponse: Decodable, Error { case let .sentGroupInvitation(u, groupInfo, contact, member): return withUser(u, "groupInfo: \(groupInfo)\ncontact: \(contact)\nmember: \(member)") case let .userAcceptedGroupSent(u, groupInfo, hostContact): return withUser(u, "groupInfo: \(groupInfo)\nhostContact: \(String(describing: hostContact))") case let .groupLinkConnecting(u, groupInfo, hostMember): return withUser(u, "groupInfo: \(groupInfo)\nhostMember: \(String(describing: hostMember))") + case let .businessLinkConnecting(u, groupInfo, hostMember, fromContact): return withUser(u, "groupInfo: \(groupInfo)\nhostMember: \(String(describing: hostMember))\nfromContact: \(String(describing: fromContact))") case let .userDeletedMember(u, groupInfo, member): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)") case let .leftMemberUser(u, groupInfo): return withUser(u, String(describing: groupInfo)) case let .groupMembers(u, group): return withUser(u, String(describing: group)) @@ -2103,7 +2106,7 @@ public struct UserContactLink: Decodable, Hashable { } public struct AutoAccept: Codable, Hashable { - public var businessAddress: Bool? // make not nullable + public var businessAddress: Bool public var acceptIncognito: Bool public var autoReply: MsgContent? @@ -2115,7 +2118,12 @@ public struct AutoAccept: Codable, Hashable { static func cmdString(_ autoAccept: AutoAccept?) -> String { guard let autoAccept = autoAccept else { return "off" } - let s = "on" + (autoAccept.acceptIncognito ? " incognito=on" : "") + var s = "on" + if autoAccept.acceptIncognito { + s += " incognito=on" + } else if autoAccept.businessAddress { + s += " business" + } guard let msg = autoAccept.autoReply else { return s } return s + " " + msg.cmdString } diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index de671ee203..b2532c1dc1 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -1890,6 +1890,7 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat, Hashable { public var groupId: Int64 var localDisplayName: GroupName public var groupProfile: GroupProfile + public var businessChat: BusinessChatInfo? public var fullGroupPreferences: FullGroupPreferences public var membership: GroupMember public var hostConnCustomUserProfileId: Int64? @@ -1908,7 +1909,7 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat, Hashable { public var image: String? { get { groupProfile.image } } public var localAlias: String { "" } - public var canEdit: Bool { + public var isOwner: Bool { return membership.memberRole == .owner && membership.memberCurrent } @@ -1960,6 +1961,16 @@ public struct GroupProfile: Codable, NamedChat, Hashable { ) } +public struct BusinessChatInfo: Decodable, Hashable { + public var memberId: String + public var chatType: BusinessChatType +} + +public enum BusinessChatType: String, Codable, Hashable { + case business + case customer +} + public struct GroupMember: Identifiable, Decodable, Hashable { public var groupMemberId: Int64 public var groupId: Int64 diff --git a/apps/ios/SimpleXChat/ChatUtils.swift b/apps/ios/SimpleXChat/ChatUtils.swift index 5f56180918..2bf861f437 100644 --- a/apps/ios/SimpleXChat/ChatUtils.swift +++ b/apps/ios/SimpleXChat/ChatUtils.swift @@ -93,7 +93,12 @@ private func canForwardToChat(_ cInfo: ChatInfo) -> Bool { public func chatIconName(_ cInfo: ChatInfo) -> String { switch cInfo { case .direct: "person.crop.circle.fill" - case .group: "person.2.circle.fill" + case let .group(groupInfo): + switch groupInfo.businessChat?.chatType { + case .none: "person.2.circle.fill" + case .business: "briefcase.circle.fill" + case .customer: "person.crop.circle.fill" + } case .local: "folder.circle.fill" case .contactRequest: "person.crop.circle.fill" default: "circle.fill" From 92967dfe0c57c5281583c243956cd30ae087d390 Mon Sep 17 00:00:00 2001 From: Diogo Date: Mon, 2 Dec 2024 20:14:26 +0000 Subject: [PATCH 106/167] ios: disable autocorrect add group member search (#5301) --- apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift b/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift index 859d2dfd27..691bda39a6 100644 --- a/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift +++ b/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift @@ -231,6 +231,7 @@ func searchFieldView(text: Binding, focussed: FocusState.Binding, .focused(focussed) .foregroundColor(onBackgroundColor) .frame(maxWidth: .infinity) + .autocorrectionDisabled(true) Image(systemName: "xmark.circle.fill") .resizable() .scaledToFit() From c04e952620ac3b6a3a01724d88607485837121d3 Mon Sep 17 00:00:00 2001 From: Diogo Date: Mon, 2 Dec 2024 21:00:55 +0000 Subject: [PATCH 107/167] desktop: onboarding improvements (#5294) * consistent space to bottom on future of messaging * consistent button suze on server operators * updated setup database passphrase screen * ability to cancel random passphrase * reduce conditions padding to header * show scrollbar in desktop * EOLs * EOL * fix random passphrase param when deleting database and recreating new one --------- Co-authored-by: Evgeny Co-authored-by: Avently <7953703+avently@users.noreply.github.com> --- .../networkAndServers/OperatorView.android.kt | 19 ++++ .../common/views/database/DatabaseView.kt | 1 + .../views/onboarding/ChooseServerOperators.kt | 8 +- .../common/views/onboarding/HowItWorks.kt | 8 +- .../onboarding/SetupDatabasePassphrase.kt | 94 +++++++++---------- .../networkAndServers/NetworkAndServers.kt | 2 +- .../networkAndServers/OperatorView.kt | 27 +++--- .../networkAndServers/OperatorView.desktop.kt | 51 ++++++++++ 8 files changed, 141 insertions(+), 69 deletions(-) create mode 100644 apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.android.kt create mode 100644 apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.desktop.kt diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.android.kt new file mode 100644 index 0000000000..e52515b345 --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.android.kt @@ -0,0 +1,19 @@ +package chat.simplex.common.views.usersettings.networkAndServers + +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +actual fun ConditionsBox(modifier: Modifier, scrollState: ScrollState, content: @Composable() (BoxScope.() -> Unit)){ + Box( + modifier = modifier + .verticalScroll(scrollState) + .padding(8.dp) + ) { + content() + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt index af40aa2e70..e1f53760e5 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt @@ -525,6 +525,7 @@ fun deleteChatDatabaseFilesAndState() { wallpapersDir.mkdirs() DatabaseUtils.ksDatabasePassword.remove() appPrefs.newDatabaseInitialized.set(false) + chatModel.desktopOnboardingRandomPassword.value = false controller.appPrefs.storeDBPassphrase.set(true) controller.ctrl = null diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt index 84ed7d1651..782f51c205 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt @@ -169,7 +169,7 @@ private fun ReviewConditionsButton( modalManager: ModalManager ) { OnboardingActionButton( - modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_PADDING * 2).fillMaxWidth() else Modifier, + modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_PADDING * 2).fillMaxWidth() else Modifier.widthIn(min = 300.dp), labelId = MR.strings.operator_review_conditions, onboarding = null, enabled = enabled, @@ -184,7 +184,7 @@ private fun ReviewConditionsButton( @Composable private fun SetOperatorsButton(enabled: Boolean, onboarding: Boolean, serverOperators: State>, selectedOperatorIds: State>, close: () -> Unit) { OnboardingActionButton( - modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_PADDING * 2).fillMaxWidth() else Modifier, + modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_PADDING * 2).fillMaxWidth() else Modifier.widthIn(min = 300.dp), labelId = MR.strings.onboarding_network_operators_update, onboarding = null, enabled = enabled, @@ -206,7 +206,7 @@ private fun SetOperatorsButton(enabled: Boolean, onboarding: Boolean, serverOper @Composable private fun ContinueButton(enabled: Boolean, onboarding: Boolean, close: () -> Unit) { OnboardingActionButton( - modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_PADDING * 2).fillMaxWidth() else Modifier, + modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_PADDING * 2).fillMaxWidth() else Modifier.widthIn(min = 300.dp), labelId = MR.strings.onboarding_network_operators_continue, onboarding = null, enabled = enabled, @@ -235,7 +235,7 @@ private fun ReviewConditionsView( val operatorsWithConditionsAccepted = remember { chatModel.conditions.value.serverOperators.filter { it.conditionsAcceptance.conditionsAccepted } } val acceptForOperators = remember { selectedOperators.value.filter { !it.conditionsAcceptance.conditionsAccepted } } ColumnWithScrollBar(modifier = Modifier.fillMaxSize().padding(horizontal = DEFAULT_PADDING)) { - AppBarTitle(stringResource(MR.strings.operator_conditions_of_use), withPadding = false, enableAlphaChanges = false) + AppBarTitle(stringResource(MR.strings.operator_conditions_of_use), withPadding = false, enableAlphaChanges = false, bottomPadding = DEFAULT_PADDING) if (operatorsWithConditionsAccepted.isNotEmpty()) { ReadableText(MR.strings.operator_conditions_accepted_for_some, args = operatorsWithConditionsAccepted.joinToString(", ") { it.legalName_ }) ReadableText(MR.strings.operator_same_conditions_will_apply_to_operators, args = acceptForOperators.joinToString(", ") { it.legalName_ }) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/HowItWorks.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/HowItWorks.kt index f7e7f456bb..aff02e90f5 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/HowItWorks.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/HowItWorks.kt @@ -1,6 +1,7 @@ package chat.simplex.common.views.onboarding import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.material.* @@ -23,7 +24,7 @@ import dev.icerock.moko.resources.StringResource @Composable fun HowItWorks(user: User?, onboardingStage: SharedPreference? = null) { - ColumnWithScrollBar(Modifier.padding(DEFAULT_PADDING)) { + ColumnWithScrollBar(Modifier.padding(horizontal = DEFAULT_PADDING)) { AppBarTitle(stringResource(MR.strings.how_simplex_works), withPadding = false) ReadableText(MR.strings.to_protect_privacy_simplex_has_ids_for_queues) ReadableText(MR.strings.only_client_devices_store_contacts_groups_e2e_encrypted_messages) @@ -35,11 +36,12 @@ fun HowItWorks(user: User?, onboardingStage: SharedPreference? Spacer(Modifier.fillMaxHeight().weight(1f)) if (onboardingStage != null) { - Box(Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING), contentAlignment = Alignment.Center) { + Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { OnboardingActionButton(user, onboardingStage, onclick = { ModalManager.fullscreen.closeModal() }) + // Reserve space + TextButtonBelowOnboardingButton("", null) } } - Spacer(Modifier.height(DEFAULT_PADDING)) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetupDatabasePassphrase.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetupDatabasePassphrase.kt index f20cb38dad..e7db51c768 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetupDatabasePassphrase.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetupDatabasePassphrase.kt @@ -1,6 +1,5 @@ package chat.simplex.common.views.onboarding -import SectionTextFooter import androidx.compose.foundation.layout.* import androidx.compose.foundation.text.KeyboardActions import androidx.compose.material.* @@ -12,7 +11,6 @@ import androidx.compose.ui.focus.* import androidx.compose.ui.input.key.* import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.text.input.ImeAction -import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @@ -106,14 +104,31 @@ private fun SetupDatabasePassphraseLayout( CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) { ModalView({}, showClose = false) { ColumnWithScrollBar( - Modifier.themedBackground(bgLayerSize = LocalAppBarHandler.current?.backgroundGraphicsLayerSize, bgLayer = LocalAppBarHandler.current?.backgroundGraphicsLayer).padding(bottom = DEFAULT_PADDING * 2), + Modifier.themedBackground(bgLayerSize = LocalAppBarHandler.current?.backgroundGraphicsLayerSize, bgLayer = LocalAppBarHandler.current?.backgroundGraphicsLayer).padding(horizontal = DEFAULT_PADDING), horizontalAlignment = Alignment.CenterHorizontally, ) { AppBarTitle(stringResource(MR.strings.setup_database_passphrase)) - Spacer(Modifier.weight(1f)) + val onClickUpdate = { + // Don't do things concurrently. Shouldn't be here concurrently, just in case + if (!progressIndicator.value) { + encryptDatabaseAlert(onConfirmEncrypt) + } + } + val disabled = currentKey.value == newKey.value || + newKey.value != confirmNewKey.value || + newKey.value.isEmpty() || + !validKey(currentKey.value) || + !validKey(newKey.value) || + progressIndicator.value + + Column(Modifier.width(600.dp), horizontalAlignment = Alignment.CenterHorizontally) { + val textStyle = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.secondary) + ReadableText(MR.strings.you_have_to_enter_passphrase_every_time, TextAlign.Center, padding = PaddingValues(), style = textStyle ) + Spacer(Modifier.height(DEFAULT_PADDING)) + ReadableText(MR.strings.impossible_to_recover_passphrase, TextAlign.Center, padding = PaddingValues(), style = textStyle) + Spacer(Modifier.height(DEFAULT_PADDING)) - Column(Modifier.width(600.dp)) { val focusRequester = remember { FocusRequester() } val focusManager = LocalFocusManager.current LaunchedEffect(Unit) { @@ -138,18 +153,6 @@ private fun SetupDatabasePassphraseLayout( isValid = ::validKey, keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }), ) - val onClickUpdate = { - // Don't do things concurrently. Shouldn't be here concurrently, just in case - if (!progressIndicator.value) { - encryptDatabaseAlert(onConfirmEncrypt) - } - } - val disabled = currentKey.value == newKey.value || - newKey.value != confirmNewKey.value || - newKey.value.isEmpty() || - !validKey(currentKey.value) || - !validKey(newKey.value) || - progressIndicator.value PassphraseField( confirmNewKey, @@ -167,21 +170,17 @@ private fun SetupDatabasePassphraseLayout( isValid = { confirmNewKey.value == "" || newKey.value == confirmNewKey.value }, keyboardActions = KeyboardActions(onDone = { defaultKeyboardAction(ImeAction.Done) }), ) - - Box(Modifier.align(Alignment.CenterHorizontally).padding(vertical = DEFAULT_PADDING)) { - SetPassphraseButton(disabled, onClickUpdate) - } - - Column { - SectionTextFooter(generalGetString(MR.strings.you_have_to_enter_passphrase_every_time)) - SectionTextFooter(annotatedStringResource(MR.strings.impossible_to_recover_passphrase)) - } } - Spacer(Modifier.weight(1f)) - SkipButton(progressIndicator.value) { - chatModel.desktopOnboardingRandomPassword.value = true - nextStep() + + Column(Modifier.widthIn(max = if (appPlatform.isAndroid) 450.dp else 1000.dp), horizontalAlignment = Alignment.CenterHorizontally) { + SetPassphraseButton(disabled, onClickUpdate) + SkipButton(progressIndicator.value) { + randomPassphraseAlert { + chatModel.desktopOnboardingRandomPassword.value = true + nextStep() + } + } } } } @@ -190,30 +189,18 @@ private fun SetupDatabasePassphraseLayout( @Composable private fun SetPassphraseButton(disabled: Boolean, onClick: () -> Unit) { - SimpleButtonIconEnded( - stringResource(MR.strings.set_database_passphrase), - painterResource(MR.images.ic_check), - style = MaterialTheme.typography.h2, - color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary, - disabled = disabled, - click = onClick + OnboardingActionButton( + if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_PADDING * 2).fillMaxWidth() else Modifier.widthIn(min = 300.dp), + labelId = MR.strings.set_database_passphrase, + onboarding = null, + onclick = onClick, + enabled = !disabled ) } @Composable private fun SkipButton(disabled: Boolean, onClick: () -> Unit) { - SimpleButtonIconEnded(stringResource(MR.strings.use_random_passphrase), painterResource(MR.images.ic_chevron_right), color = - if (disabled) MaterialTheme.colors.secondary else WarningOrange, disabled = disabled, click = onClick) - Text( - stringResource(MR.strings.you_can_change_it_later), - Modifier - .fillMaxWidth() - .padding(horizontal = DEFAULT_PADDING * 3) - .padding(top = DEFAULT_PADDING, bottom = DEFAULT_PADDING - 5.dp), - style = MaterialTheme.typography.subtitle1, - color = MaterialTheme.colors.secondary, - textAlign = TextAlign.Center, - ) + TextButtonBelowOnboardingButton(stringResource(MR.strings.use_random_passphrase), onClick = if (disabled) null else onClick) } @Composable @@ -238,3 +225,12 @@ private suspend fun startChat(key: String?) { m.chatDbChanged.value = false m.chatRunning.value = true } + +private fun randomPassphraseAlert(onConfirm: () -> Unit) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.use_random_passphrase), + text = generalGetString(MR.strings.you_can_change_it_later), + confirmText = generalGetString(MR.strings.ok), + onConfirm = onConfirm, + ) +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NetworkAndServers.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NetworkAndServers.kt index 6b1dcec5d8..835e01ec27 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NetworkAndServers.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NetworkAndServers.kt @@ -734,7 +734,7 @@ fun UsageConditionsView( } ColumnWithScrollBar(modifier = Modifier.fillMaxSize().padding(horizontal = DEFAULT_PADDING)) { - AppBarTitle(stringResource(MR.strings.operator_conditions_of_use), enableAlphaChanges = false, withPadding = false) + AppBarTitle(stringResource(MR.strings.operator_conditions_of_use), enableAlphaChanges = false, withPadding = false, bottomPadding = DEFAULT_PADDING) when (val conditionsAction = chatModel.conditions.value.conditionsAction) { is UsageConditionsAction.Review -> { if (conditionsAction.operators.isNotEmpty()) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt index 4e0fc6ec79..3836d5b6df 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt @@ -14,6 +14,7 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.* import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.UriHandler @@ -615,7 +616,6 @@ fun ConditionsTextView( val failedToLoad = remember { mutableStateOf(false) } val defaultConditionsLink = "https://github.com/simplex-chat/simplex-chat/blob/stable/PRIVACY.md" val scope = rememberCoroutineScope() - // can show conditions when animation between modals finishes to prevent glitches val canShowConditionsAt = remember { System.currentTimeMillis() + 300 } LaunchedEffect(Unit) { @@ -645,18 +645,18 @@ fun ConditionsTextView( if (conditionsText != null) { val scrollState = rememberScrollState() - Box( - modifier = Modifier - .fillMaxSize() - .border(border = BorderStroke(1.dp, CurrentColors.value.colors.secondary.copy(alpha = 0.6f)), shape = RoundedCornerShape(12.dp)) - .verticalScroll(scrollState) - .padding(8.dp) - ) { - val parentUriHandler = LocalUriHandler.current - CompositionLocalProvider(LocalUriHandler provides remember { internalUriHandler(parentUriHandler) }) { - ConditionsMarkdown(conditionsText) - } + ConditionsBox( + Modifier + .fillMaxSize() + .border(border = BorderStroke(1.dp, CurrentColors.value.colors.secondary.copy(alpha = 0.6f)), shape = RoundedCornerShape(12.dp)) + .clip(shape = RoundedCornerShape(12.dp)), + scrollState + ) { + val parentUriHandler = LocalUriHandler.current + CompositionLocalProvider(LocalUriHandler provides remember { internalUriHandler(parentUriHandler) }) { + ConditionsMarkdown(conditionsText) } + } } else { val conditionsLink = "https://github.com/simplex-chat/simplex-chat/blob/${usageConditions.conditionsCommit}/PRIVACY.md" ConditionsLinkView(conditionsLink) @@ -668,6 +668,9 @@ fun ConditionsTextView( } } +@Composable +expect fun ConditionsBox(modifier: Modifier, scrollState: ScrollState, content: @Composable() (BoxScope.() -> Unit)) + @Composable private fun ConditionsMarkdown(text: String) { Markdown(text, diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.desktop.kt new file mode 100644 index 0000000000..c9f62a5b79 --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.desktop.kt @@ -0,0 +1,51 @@ +package chat.simplex.common.views.usersettings.networkAndServers + +import androidx.compose.animation.core.Animatable +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.unit.dp +import chat.simplex.common.platform.DesktopScrollBar +import chat.simplex.common.views.helpers.detectCursorMove +import kotlinx.coroutines.* + +@Composable +actual fun ConditionsBox(modifier: Modifier, scrollState: ScrollState, content: @Composable() (BoxScope.() -> Unit)) { + val scope = rememberCoroutineScope() + val scrollBarAlpha = remember { Animatable(0f) } + val scrollJob: MutableState = remember { mutableStateOf(Job()) } + val scrollBarDraggingState = remember { mutableStateOf(false) } + val scrollModifier = remember { + Modifier + .pointerInput(Unit) { + detectCursorMove { + scope.launch { + scrollBarAlpha.animateTo(1f) + } + scrollJob.value.cancel() + scrollJob.value = scope.launch { + delay(1000L) + scrollBarAlpha.animateTo(0f) + } + } + } + } + + Box(modifier = modifier) { + Box( + Modifier + .fillMaxSize() + .verticalScroll(scrollState) + .padding(8.dp) + .then(scrollModifier) + ) { + content() + } + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.CenterEnd) { + DesktopScrollBar(rememberScrollbarAdapter(scrollState), Modifier.fillMaxHeight(), scrollBarAlpha, scrollJob, false, scrollBarDraggingState) + } + } +} From e61babdc8f4646a00bf66b43903c9845fce944c2 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Tue, 3 Dec 2024 06:36:48 +0700 Subject: [PATCH 108/167] android, desktop: preserving long message when failed to send (#5297) * android, desktop: preserving long message when failed to send * forwarding * unused code * strings --------- Co-authored-by: Evgeny Poberezkin --- .../chat/simplex/common/model/SimpleXAPI.kt | 41 +++++++++++++++++-- .../simplex/common/views/chat/ComposeView.kt | 38 +++++++++++++---- .../commonMain/resources/MR/base/strings.xml | 4 ++ 3 files changed, 71 insertions(+), 12 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index 7ada27d000..89c6f40cfe 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -894,8 +894,27 @@ object ChatController { private suspend fun processSendMessageCmd(rh: Long?, cmd: CC): List? { val r = sendCmd(rh, cmd) - return when (r) { - is CR.NewChatItems -> r.chatItems + return when { + r is CR.NewChatItems -> r.chatItems + r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorStore && r.chatError.storeError is StoreError.LargeMsg && cmd is CC.ApiSendMessages -> { + val mc = cmd.composedMessages.last().msgContent + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.maximum_message_size_title), + if (mc is MsgContent.MCImage || mc is MsgContent.MCVideo || mc is MsgContent.MCLink) { + generalGetString(MR.strings.maximum_message_size_reached_non_text) + } else { + generalGetString(MR.strings.maximum_message_size_reached_text) + } + ) + null + } + r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorStore && r.chatError.storeError is StoreError.LargeMsg && cmd is CC.ApiForwardChatItems -> { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.maximum_message_size_title), + generalGetString(MR.strings.maximum_message_size_reached_forwarding) + ) + null + } else -> { if (!(networkErrorAlert(r))) { apiErrorAlert("processSendMessageCmd", generalGetString(MR.strings.error_sending_message), r) @@ -943,7 +962,21 @@ object ChatController { suspend fun apiUpdateChatItem(rh: Long?, type: ChatType, id: Long, itemId: Long, mc: MsgContent, live: Boolean = false): AChatItem? { val r = sendCmd(rh, CC.ApiUpdateChatItem(type, id, itemId, mc, live)) - if (r is CR.ChatItemUpdated) return r.chatItem + when { + r is CR.ChatItemUpdated -> return r.chatItem + r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorStore && r.chatError.storeError is StoreError.LargeMsg -> { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.maximum_message_size_title), + if (mc is MsgContent.MCImage || mc is MsgContent.MCVideo || mc is MsgContent.MCLink) { + generalGetString(MR.strings.maximum_message_size_reached_non_text) + } else { + generalGetString(MR.strings.maximum_message_size_reached_text) + } + ) + return null + } + } + Log.e(TAG, "apiUpdateChatItem bad response: ${r.responseType} ${r.details}") return null } @@ -6357,6 +6390,7 @@ sealed class StoreError { is HostMemberIdNotFound -> "hostMemberIdNotFound" is ContactNotFoundByFileId -> "contactNotFoundByFileId" is NoGroupSndStatus -> "noGroupSndStatus" + is LargeMsg -> "largeMsg" } @Serializable @SerialName("duplicateName") object DuplicateName: StoreError() @@ -6416,6 +6450,7 @@ sealed class StoreError { @Serializable @SerialName("hostMemberIdNotFound") class HostMemberIdNotFound(val groupId: Long): StoreError() @Serializable @SerialName("contactNotFoundByFileId") class ContactNotFoundByFileId(val fileId: Long): StoreError() @Serializable @SerialName("noGroupSndStatus") class NoGroupSndStatus(val itemId: Long, val groupMemberId: Long): StoreError() + @Serializable @SerialName("largeMsg") object LargeMsg: StoreError() } @Serializable diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt index 6cd46d49a6..3a63cf508e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt @@ -412,6 +412,7 @@ fun ComposeView( val cInfo = chat.chatInfo val cs = composeState.value var sent: List? + var lastMessageFailedToSend: ComposeState? = null val msgText = text ?: cs.message fun sending() { @@ -461,6 +462,19 @@ fun ComposeView( } } + fun constructFailedMessage(cs: ComposeState): ComposeState { + val preview = when (cs.preview) { + is ComposePreview.MediaPreview -> { + ComposePreview.MediaPreview( + if (cs.preview.images.isNotEmpty()) listOf(cs.preview.images.last()) else emptyList(), + if (cs.preview.content.isNotEmpty()) listOf(cs.preview.content.last()) else emptyList() + ) + } + else -> cs.preview + } + return cs.copy(inProgress = false, preview = preview) + } + fun updateMsgContent(msgContent: MsgContent): MsgContent { return when (msgContent) { is MsgContent.MCText -> checkLinkPreview() @@ -517,6 +531,9 @@ fun ComposeView( sent = null } else if (cs.contextItem is ComposeContextItem.ForwardingItems) { sent = forwardItem(chat.remoteHostId, cs.contextItem.chatItems, cs.contextItem.fromChatInfo, ttl = ttl) + if (sent == null) { + lastMessageFailedToSend = constructFailedMessage(cs) + } if (cs.message.isNotEmpty()) { sent?.mapIndexed { index, message -> if (index == sent!!.lastIndex) { @@ -531,6 +548,7 @@ fun ComposeView( val ei = cs.contextItem.chatItem val updatedMessage = updateMessage(ei, chat, live) sent = if (updatedMessage != null) listOf(updatedMessage) else null + lastMessageFailedToSend = if (updatedMessage == null) constructFailedMessage(cs) else null } else if (liveMessage != null && liveMessage.sent) { val updatedMessage = updateMessage(liveMessage.chatItem, chat, live) sent = if (updatedMessage != null) listOf(updatedMessage) else null @@ -631,19 +649,21 @@ fun ComposeView( ttl = ttl ) sent = if (sendResult != null) listOf(sendResult) else null - } - if (sent == null && - (cs.preview is ComposePreview.MediaPreview || - cs.preview is ComposePreview.FilePreview || - cs.preview is ComposePreview.VoicePreview) - ) { - val sendResult = send(chat, MsgContent.MCText(msgText), quotedItemId, null, live, ttl) - sent = if (sendResult != null) listOf(sendResult) else null + if (sent == null && index == msgs.lastIndex && cs.liveMessage == null) { + constructFailedMessage(cs) + // it's the last message in the series so if it fails, restore it in ComposeView for editing + lastMessageFailedToSend = constructFailedMessage(cs) + } } } val wasForwarding = cs.forwarding val forwardingFromChatId = (cs.contextItem as? ComposeContextItem.ForwardingItems)?.fromChatInfo?.id - clearState(live) + val lastFailed = lastMessageFailedToSend + if (lastFailed == null) { + clearState(live) + } else { + composeState.value = lastFailed + } val draft = chatModel.draft.value if (wasForwarding && chatModel.draftChatId.value == chat.chatInfo.id && forwardingFromChatId != chat.chatInfo.id && draft != null) { composeState.value = draft diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index c390096ee2..d931abab70 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -441,6 +441,10 @@ Files and media not allowed Voice messages not allowed Message + Message is too large! + Please reduce the message size and send again. + Please reduce the message size or remove media and send again. + You can copy and reduce the message size to send it. Image From 9d992735f41bcfd5d5fd8c3203e4571ac19aba5a Mon Sep 17 00:00:00 2001 From: Evgeny Date: Tue, 3 Dec 2024 12:11:38 +0000 Subject: [PATCH 109/167] core, ios: improve business address (connection plan, repeat requests, feature items) (#5303) * core, ios: connection plan for business address * core: store xcontact_id on business groups to prevent duplicate contact requests * core: create feature items in new groups and in business groups * fix tests * error message --- .../Shared/Views/NewChat/NewChatView.swift | 15 +++++- apps/ios/SimpleXChat/APITypes.swift | 3 -- .../chat/simplex/common/model/SimpleXAPI.kt | 3 -- src/Simplex/Chat.hs | 45 ++++++++++------- src/Simplex/Chat/Controller.hs | 1 + .../Migrations/M20241128_business_chats.hs | 6 +++ src/Simplex/Chat/Migrations/chat_schema.sql | 4 +- src/Simplex/Chat/Store/Direct.hs | 20 ++++++-- src/Simplex/Chat/Store/Groups.hs | 48 ++----------------- src/Simplex/Chat/Store/Shared.hs | 43 +++++++++++++++++ src/Simplex/Chat/Types.hs | 2 +- src/Simplex/Chat/View.hs | 8 +++- tests/ChatTests/ChatList.hs | 18 +++---- tests/ChatTests/Direct.hs | 4 +- tests/ChatTests/Groups.hs | 43 +++++++++-------- tests/ChatTests/Profiles.hs | 34 ++++++++----- tests/ChatTests/Utils.hs | 30 +++++++----- 17 files changed, 195 insertions(+), 132 deletions(-) diff --git a/apps/ios/Shared/Views/NewChat/NewChatView.swift b/apps/ios/Shared/Views/NewChat/NewChatView.swift index 19e810d034..e18d932278 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatView.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatView.swift @@ -916,11 +916,17 @@ func planAndConnectAlert(_ alert: PlanAndConnectAlert, dismiss: Bool, cleanup: ( ) case let .groupLinkConnecting(_, groupInfo): if let groupInfo = groupInfo { - return Alert( + return groupInfo.businessChat == nil + ? Alert( title: Text("Group already exists!"), message: Text("You are already joining the group \(groupInfo.displayName)."), dismissButton: .default(Text("OK")) { cleanup?() } ) + : Alert( + title: Text("Chat already exists!"), + message: Text("You are already connecting to \(groupInfo.displayName)."), + dismissButton: .default(Text("OK")) { cleanup?() } + ) } else { return Alert( title: Text("Already joining the group!"), @@ -1237,10 +1243,15 @@ func contactAlreadyConnectingAlert(_ contact: Contact) -> Alert { } func groupAlreadyExistsAlert(_ groupInfo: GroupInfo) -> Alert { - mkAlert( + groupInfo.businessChat == nil + ? mkAlert( title: "Group already exists", message: "You are already in group \(groupInfo.displayName)." ) + : mkAlert( + title: "Chat already exists", + message: "You are already connected with \(groupInfo.displayName)." + ) } enum ConnReqType: Equatable { diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 07095ed5e1..ca9bb70ea4 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -598,7 +598,6 @@ public enum ChatResponse: Decodable, Error { case sentInvitation(user: UserRef, connection: PendingContactConnection) case sentInvitationToContact(user: UserRef, contact: Contact, customUserProfile: Profile?) case contactAlreadyExists(user: UserRef, contact: Contact) - case contactRequestAlreadyAccepted(user: UserRef, contact: Contact) case contactDeleted(user: UserRef, contact: Contact) case contactDeletedByContact(user: UserRef, contact: Contact) case chatCleared(user: UserRef, chatInfo: ChatInfo) @@ -776,7 +775,6 @@ public enum ChatResponse: Decodable, Error { case .sentInvitation: return "sentInvitation" case .sentInvitationToContact: return "sentInvitationToContact" case .contactAlreadyExists: return "contactAlreadyExists" - case .contactRequestAlreadyAccepted: return "contactRequestAlreadyAccepted" case .contactDeleted: return "contactDeleted" case .contactDeletedByContact: return "contactDeletedByContact" case .chatCleared: return "chatCleared" @@ -952,7 +950,6 @@ public enum ChatResponse: Decodable, Error { case let .sentInvitation(u, connection): return withUser(u, String(describing: connection)) case let .sentInvitationToContact(u, contact, _): return withUser(u, String(describing: contact)) case let .contactAlreadyExists(u, contact): return withUser(u, String(describing: contact)) - case let .contactRequestAlreadyAccepted(u, contact): return withUser(u, String(describing: contact)) case let .contactDeleted(u, contact): return withUser(u, String(describing: contact)) case let .contactDeletedByContact(u, contact): return withUser(u, String(describing: contact)) case let .chatCleared(u, chatInfo): return withUser(u, String(describing: chatInfo)) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index 89c6f40cfe..d9c7df9d81 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -5439,7 +5439,6 @@ sealed class CR { @Serializable @SerialName("sentInvitation") class SentInvitation(val user: UserRef, val connection: PendingContactConnection): CR() @Serializable @SerialName("sentInvitationToContact") class SentInvitationToContact(val user: UserRef, val contact: Contact, val customUserProfile: Profile?): CR() @Serializable @SerialName("contactAlreadyExists") class ContactAlreadyExists(val user: UserRef, val contact: Contact): CR() - @Serializable @SerialName("contactRequestAlreadyAccepted") class ContactRequestAlreadyAccepted(val user: UserRef, val contact: Contact): CR() @Serializable @SerialName("contactDeleted") class ContactDeleted(val user: UserRef, val contact: Contact): CR() @Serializable @SerialName("contactDeletedByContact") class ContactDeletedByContact(val user: UserRef, val contact: Contact): CR() @Serializable @SerialName("chatCleared") class ChatCleared(val user: UserRef, val chatInfo: ChatInfo): CR() @@ -5623,7 +5622,6 @@ sealed class CR { is SentInvitation -> "sentInvitation" is SentInvitationToContact -> "sentInvitationToContact" is ContactAlreadyExists -> "contactAlreadyExists" - is ContactRequestAlreadyAccepted -> "contactRequestAlreadyAccepted" is ContactDeleted -> "contactDeleted" is ContactDeletedByContact -> "contactDeletedByContact" is ChatCleared -> "chatCleared" @@ -5797,7 +5795,6 @@ sealed class CR { is SentInvitation -> withUser(user, json.encodeToString(connection)) is SentInvitationToContact -> withUser(user, json.encodeToString(contact)) is ContactAlreadyExists -> withUser(user, json.encodeToString(contact)) - is ContactRequestAlreadyAccepted -> withUser(user, json.encodeToString(contact)) is ContactDeleted -> withUser(user, json.encodeToString(contact)) is ContactDeletedByContact -> withUser(user, json.encodeToString(contact)) is ChatCleared -> withUser(user, json.encodeToString(chatInfo)) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 7a48914abf..31862253dd 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -2208,9 +2208,11 @@ processChatCommand' vr = \case gVar <- asks random -- [incognito] generate incognito profile for group membership incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing - groupInfo <- withFastStore $ \db -> createNewGroup db vr gVar user gProfile incognitoProfile - createInternalChatItem user (CDGroupSnd groupInfo) (CISndGroupE2EEInfo $ E2EInfo {pqEnabled = PQEncOff}) Nothing - pure $ CRGroupCreated user groupInfo + gInfo <- withFastStore $ \db -> createNewGroup db vr gVar user gProfile incognitoProfile + let cd = CDGroupSnd gInfo + createInternalChatItem user cd (CISndGroupE2EEInfo E2EInfo {pqEnabled = PQEncOff}) Nothing + createGroupFeatureItems user cd CISndGroupFeature gInfo + pure $ CRGroupCreated user gInfo NewGroup incognito gProfile -> withUser $ \User {userId} -> processChatCommand $ APINewGroup userId incognito gProfile APIAddMember groupId contactId memRole -> withUser $ \user -> withGroupLock "addMember" groupId $ do @@ -3137,7 +3139,8 @@ processChatCommand' vr = \case | not (contactReady ct) && contactActive ct -> pure $ CPContactAddress (CAPConnectingProhibit ct) | contactDeleted ct -> pure $ CPContactAddress CAPOk | otherwise -> pure $ CPContactAddress (CAPKnown ct) - Just _ -> throwChatError $ CECommandError "found connection entity is not RcvDirectMsgConnection" + Just (RcvGroupMsgConnection _ gInfo _) -> groupPlan gInfo + Just _ -> throwChatError $ CECommandError "found connection entity is not RcvDirectMsgConnection or RcvGroupMsgConnection" -- group link Just _ -> withFastStore' (\db -> getGroupInfoByUserContactLinkConnReq db vr user cReqSchemas) >>= \case @@ -3152,12 +3155,13 @@ processChatCommand' vr = \case | not (contactReady ct) && contactActive ct -> pure $ CPGroupLink (GLPConnectingProhibit gInfo_) | otherwise -> pure $ CPGroupLink GLPOk (Nothing, Just _) -> throwChatError $ CECommandError "found connection entity is not RcvDirectMsgConnection" - (Just gInfo@GroupInfo {membership}, _) - | not (memberActive membership) && not (memberRemoved membership) -> - pure $ CPGroupLink (GLPConnectingProhibit gInfo_) - | memberActive membership -> pure $ CPGroupLink (GLPKnown gInfo) - | otherwise -> pure $ CPGroupLink GLPOk + (Just gInfo, _) -> groupPlan gInfo where + groupPlan gInfo@GroupInfo {membership} + | not (memberActive membership) && not (memberRemoved membership) = + pure $ CPGroupLink (GLPConnectingProhibit $ Just gInfo) + | memberActive membership = pure $ CPGroupLink (GLPKnown gInfo) + | otherwise = pure $ CPGroupLink GLPOk cReqSchemas :: (ConnReqContact, ConnReqContact) cReqSchemas = ( CRContactUri crData {crScheme = SSSimplex}, @@ -4035,6 +4039,9 @@ acceptBusinessJoinRequestAsync let chatV = vr `peerConnChatVersion` cReqChatVRange connIds <- agentAcceptContactAsync user True invId msg subMode PQSupportOff chatV withStore' $ \db -> createAcceptedMemberConnection db user connIds chatV ucr groupMemberId subMode + let cd = CDGroupSnd gInfo + createInternalChatItem user cd (CISndGroupE2EEInfo E2EInfo {pqEnabled = PQEncOff}) Nothing + createGroupFeatureItems user cd CISndGroupFeature gInfo pure gInfo where businessGroupProfile :: Profile -> GroupPreferences -> GroupProfile @@ -5086,8 +5093,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = case memberCategory m of GCHostMember -> do toView $ CRUserJoinedGroup user gInfo {membership = membership {memberStatus = GSMemConnected}} m {memberStatus = GSMemConnected} - createInternalChatItem user (CDGroupRcv gInfo m) (CIRcvGroupE2EEInfo $ E2EInfo {pqEnabled = PQEncOff}) Nothing - createGroupFeatureItems gInfo m + let cd = CDGroupRcv gInfo m + createInternalChatItem user cd (CIRcvGroupE2EEInfo E2EInfo {pqEnabled = PQEncOff}) Nothing + createGroupFeatureItems user cd CIRcvGroupFeature gInfo let GroupInfo {groupProfile = GroupProfile {description}} = gInfo memberConnectedChatItem gInfo m unless expectHistory $ forM_ description $ groupDescriptionChatItem gInfo m @@ -5597,6 +5605,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = profileContactRequest invId chatVRange p xContactId_ reqPQSup = do withStore (\db -> createOrUpdateContactRequest db vr user userContactLinkId invId chatVRange p xContactId_ reqPQSup) >>= \case CORContact contact -> toView $ CRContactRequestAlreadyAccepted user contact + CORGroup gInfo -> toView $ CRBusinessRequestAlreadyAccepted user gInfo CORRequest cReq -> do ucl <- withStore $ \db -> getUserContactLinkById db userId userContactLinkId let (UserContactLink {connReqContact, autoAccept}, groupId_, gLinkMemRole) = ucl @@ -6487,13 +6496,6 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = let state = featureState $ getContactUserPreference f mergedPreferences createInternalChatItem user (CDDirectRcv ct) (uncurry (CIRcvChatFeature $ chatFeature f) state) Nothing - createGroupFeatureItems :: GroupInfo -> GroupMember -> CM () - createGroupFeatureItems g@GroupInfo {fullGroupPreferences} m = - forM_ allGroupFeatures $ \(AGF f) -> do - let p = getGroupPreference f fullGroupPreferences - (_, param, role) = groupFeatureState p - createInternalChatItem user (CDGroupRcv g m) (CIRcvGroupFeature (toGroupFeature f) (toGroupPreference p) param role) Nothing - xInfoProbe :: ContactOrMember -> Probe -> CM () xInfoProbe cgm2 probe = do contactMerge <- readTVarIO =<< asks contactMergeEnabled @@ -8179,6 +8181,13 @@ createGroupFeatureChangedItems user cd ciContent GroupInfo {fullGroupPreferences sameGroupProfileInfo :: GroupProfile -> GroupProfile -> Bool sameGroupProfileInfo p p' = p {groupPreferences = Nothing} == p' {groupPreferences = Nothing} +createGroupFeatureItems :: MsgDirectionI d => User -> ChatDirection 'CTGroup d -> (GroupFeature -> GroupPreference -> Maybe Int -> Maybe GroupMemberRole -> CIContent d) -> GroupInfo -> CM () +createGroupFeatureItems user cd ciContent GroupInfo {fullGroupPreferences} = + forM_ allGroupFeatures $ \(AGF f) -> do + let p = getGroupPreference f fullGroupPreferences + (_, param, role) = groupFeatureState p + createInternalChatItem user cd (ciContent (toGroupFeature f) (toGroupPreference p) param role) Nothing + createInternalChatItem :: (ChatTypeI c, MsgDirectionI d) => User -> ChatDirection c d -> CIContent d -> Maybe UTCTime -> CM () createInternalChatItem user cd content itemTs_ = lift (createInternalItemsForChats user itemTs_ [(cd, [content])]) >>= \case diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 0ab3ba5652..593c328d0c 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -669,6 +669,7 @@ data ChatResponse | CRAcceptingBusinessRequest {user :: User, groupInfo :: GroupInfo} | CRContactAlreadyExists {user :: User, contact :: Contact} | CRContactRequestAlreadyAccepted {user :: User, contact :: Contact} + | CRBusinessRequestAlreadyAccepted {user :: User, groupInfo :: GroupInfo} | CRLeftMemberUser {user :: User, groupInfo :: GroupInfo} | CRGroupDeletedUser {user :: User, groupInfo :: GroupInfo} | CRForwardPlan {user :: User, itemsCount :: Int, chatItemIds :: [ChatItemId], forwardConfirmation :: Maybe ForwardConfirmation} diff --git a/src/Simplex/Chat/Migrations/M20241128_business_chats.hs b/src/Simplex/Chat/Migrations/M20241128_business_chats.hs index f068b1bd81..2b3be38030 100644 --- a/src/Simplex/Chat/Migrations/M20241128_business_chats.hs +++ b/src/Simplex/Chat/Migrations/M20241128_business_chats.hs @@ -11,12 +11,18 @@ m20241128_business_chats = ALTER TABLE user_contact_links ADD business_address INTEGER DEFAULT 0; ALTER TABLE groups ADD COLUMN business_member_id BLOB NULL; ALTER TABLE groups ADD COLUMN business_chat TEXT NULL; +ALTER TABLE groups ADD COLUMN business_xcontact_id BLOB NULL; + +CREATE INDEX idx_groups_business_xcontact_id ON groups(business_xcontact_id); |] down_m20241128_business_chats :: Query down_m20241128_business_chats = [sql| +DROP INDEX idx_groups_business_xcontact_id; + ALTER TABLE user_contact_links DROP COLUMN business_address; ALTER TABLE groups DROP COLUMN business_member_id; ALTER TABLE groups DROP COLUMN business_chat; +ALTER TABLE groups DROP COLUMN business_xcontact_id; |] diff --git a/src/Simplex/Chat/Migrations/chat_schema.sql b/src/Simplex/Chat/Migrations/chat_schema.sql index 460b348b4d..621c28f91e 100644 --- a/src/Simplex/Chat/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Migrations/chat_schema.sql @@ -129,7 +129,8 @@ CREATE TABLE groups( custom_data BLOB, ui_themes TEXT, business_member_id BLOB NULL, - business_chat TEXT NULL, -- received + business_chat TEXT NULL, + business_xcontact_id BLOB NULL, -- received FOREIGN KEY(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name) ON DELETE CASCADE @@ -926,3 +927,4 @@ CREATE INDEX idx_chat_items_notes ON chat_items( item_status, created_at ); +CREATE INDEX idx_groups_business_xcontact_id ON groups(business_xcontact_id); diff --git a/src/Simplex/Chat/Store/Direct.hs b/src/Simplex/Chat/Store/Direct.hs index 4d33fa113d..b7759f0905 100644 --- a/src/Simplex/Chat/Store/Direct.hs +++ b/src/Simplex/Chat/Store/Direct.hs @@ -102,6 +102,7 @@ import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow) import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB import Simplex.Messaging.Crypto.Ratchet (PQSupport) import Simplex.Messaging.Protocol (SubscriptionMode (..)) +import Simplex.Messaging.Util ((<$$>)) import Simplex.Messaging.Version getPendingContactConnection :: DB.Connection -> UserId -> Int64 -> ExceptT StoreError IO PendingContactConnection @@ -591,13 +592,17 @@ getUserContacts db vr user@User {userId} = do contacts <- rights <$> mapM (runExceptT . getContact db vr user) contactIds pure $ filter (\Contact {activeConn} -> isJust activeConn) contacts -createOrUpdateContactRequest :: DB.Connection -> VersionRangeChat -> User -> Int64 -> InvitationId -> VersionRangeChat -> Profile -> Maybe XContactId -> PQSupport -> ExceptT StoreError IO ContactOrRequest -createOrUpdateContactRequest db vr user@User {userId} userContactLinkId invId (VersionRange minV maxV) Profile {displayName, fullName, image, contactLink, preferences} xContactId_ pqSup = - liftIO (maybeM getContact' xContactId_) >>= \case - Just contact -> pure $ CORContact contact +createOrUpdateContactRequest :: DB.Connection -> VersionRangeChat -> User -> Int64 -> InvitationId -> VersionRangeChat -> Profile -> Maybe XContactId -> PQSupport -> ExceptT StoreError IO ChatOrRequest +createOrUpdateContactRequest db vr user@User {userId, userContactId} userContactLinkId invId (VersionRange minV maxV) Profile {displayName, fullName, image, contactLink, preferences} xContactId_ pqSup = + liftIO (maybeM getContactOrGroup xContactId_) >>= \case + Just cr -> pure cr Nothing -> CORRequest <$> createOrUpdate_ where maybeM = maybe (pure Nothing) + getContactOrGroup xContactId = + getContact' xContactId >>= \case + Just ct -> pure $ Just $ CORContact ct + Nothing -> CORGroup <$$> getGroupInfo' xContactId createOrUpdate_ :: ExceptT StoreError IO UserContactRequest createOrUpdate_ = do cReqId <- @@ -651,6 +656,13 @@ createOrUpdateContactRequest db vr user@User {userId} userContactLinkId invId (V LIMIT 1 |] (userId, xContactId) + getGroupInfo' :: XContactId -> IO (Maybe GroupInfo) + getGroupInfo' xContactId = + maybeFirstRow (toGroupInfo vr userContactId) $ + DB.query + db + (groupInfoQuery <> " WHERE g.business_xcontact_id = ? AND g.user_id = ? AND mu.contact_id = ?") + (xContactId, userId, userContactId) getContactRequestByXContactId :: XContactId -> IO (Maybe UserContactRequest) getContactRequestByXContactId xContactId = maybeFirstRow toContactRequest $ diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index bb5252ddb0..2c938ecc44 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -155,31 +155,8 @@ import Simplex.Messaging.Util (eitherToMaybe, ($>>=), (<$$>)) import Simplex.Messaging.Version import UnliftIO.STM -type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe Text, Maybe ImageData, Maybe ProfileId, Maybe MsgFilter, Maybe Bool, Bool, Maybe GroupPreferences) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime, Maybe MemberId, Maybe BusinessChatType, Maybe UIThemeEntityOverrides, Maybe CustomData) :. GroupMemberRow - -type GroupMemberRow = ((Int64, Int64, MemberId, VersionChat, VersionChat, GroupMemberRole, GroupMemberCategory, GroupMemberStatus, Bool, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, ContactName, Maybe ContactId, ProfileId, ProfileId, ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Maybe Preferences)) - type MaybeGroupMemberRow = ((Maybe Int64, Maybe Int64, Maybe MemberId, Maybe VersionChat, Maybe VersionChat, Maybe GroupMemberRole, Maybe GroupMemberCategory, Maybe GroupMemberStatus, Maybe Bool, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, Maybe ContactName, Maybe ContactId, Maybe ProfileId, Maybe ProfileId, Maybe ContactName, Maybe Text, Maybe ImageData, Maybe ConnReqContact, Maybe LocalAlias, Maybe Preferences)) -toGroupInfo :: VersionRangeChat -> Int64 -> GroupInfoRow -> GroupInfo -toGroupInfo vr userContactId ((groupId, localDisplayName, displayName, fullName, description, image, hostConnCustomUserProfileId, enableNtfs_, sendRcpts, favorite, groupPreferences) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt, businessMemberId, businessChatType, uiThemes, customData) :. userMemberRow) = - let membership = (toGroupMember userContactId userMemberRow) {memberChatVRange = vr} - chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts, favorite} - fullGroupPreferences = mergeGroupPreferences groupPreferences - groupProfile = GroupProfile {displayName, fullName, description, image, groupPreferences} - businessChat = BusinessChatInfo <$> businessMemberId <*> businessChatType - in GroupInfo {groupId, localDisplayName, groupProfile, businessChat, fullGroupPreferences, membership, hostConnCustomUserProfileId, chatSettings, createdAt, updatedAt, chatTs, userMemberProfileSentAt, uiThemes, customData} - -toGroupMember :: Int64 -> GroupMemberRow -> GroupMember -toGroupMember userContactId ((groupMemberId, groupId, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, showMessages, memberRestriction_) :. (invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId, profileId, displayName, fullName, image, contactLink, localAlias, preferences)) = - let memberProfile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias} - memberSettings = GroupMemberSettings {showMessages} - blockedByAdmin = maybe False mrsBlocked memberRestriction_ - invitedBy = toInvitedBy userContactId invitedById - activeConn = Nothing - memberChatVRange = fromMaybe (versionToRange maxVer) $ safeVersionRange minVer maxVer - in GroupMember {..} - toMaybeGroupMember :: Int64 -> MaybeGroupMemberRow -> Maybe GroupMember toMaybeGroupMember userContactId ((Just groupMemberId, Just groupId, Just memberId, Just minVer, Just maxVer, Just memberRole, Just memberCategory, Just memberStatus, Just showMessages, memberBlocked) :. (invitedById, invitedByGroupMemberId, Just localDisplayName, memberContactId, Just memberContactProfileId, Just profileId, Just displayName, Just fullName, image, contactLink, Just localAlias, contactPreferences)) = Just $ toGroupMember userContactId ((groupMemberId, groupId, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, showMessages, memberBlocked) :. (invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId, profileId, displayName, fullName, image, contactLink, localAlias, contactPreferences)) @@ -937,7 +914,7 @@ createBusinessRequestGroup vr gVar user@User {userId, userContactId} - UserContactRequest {cReqChatVRange, profile = Profile {displayName, fullName, image, contactLink, preferences}} + UserContactRequest {cReqChatVRange, xContactId, profile = Profile {displayName, fullName, image, contactLink, preferences}} groupPreferences = do currentTs <- liftIO getCurrentTime groupInfo <- insertGroup_ currentTs @@ -959,10 +936,10 @@ createBusinessRequestGroup [sql| INSERT INTO groups (group_profile_id, local_display_name, user_id, enable_ntfs, - created_at, updated_at, chat_ts, user_member_profile_sent_at, business_chat) - VALUES (?,?,?,?,?,?,?,?,?) + created_at, updated_at, chat_ts, user_member_profile_sent_at, business_chat, business_xcontact_id) + VALUES (?,?,?,?,?,?,?,?,?,?) |] - (profileId, localDisplayName, userId, True, currentTs, currentTs, currentTs, currentTs, BCCustomer) + (profileId, localDisplayName, userId, True, currentTs, currentTs, currentTs, currentTs, BCCustomer, xContactId) insertedRowId db memberId <- liftIO $ encodedRandomBytes gVar 12 void $ createContactMemberInv_ db user groupId Nothing user (MemberIdRole (MemberId memberId) GROwner) GCUserMember GSMemCreator IBUser Nothing currentTs vr @@ -1484,22 +1461,7 @@ getGroupInfo db vr User {userId, userContactId} groupId = ExceptT . firstRow (toGroupInfo vr userContactId) (SEGroupNotFound groupId) $ DB.query db - [sql| - SELECT - -- GroupInfo - g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image, - g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, - g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_member_id, g.business_chat, g.ui_themes, g.custom_data, - -- GroupMember - membership - mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, - mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, - pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences - FROM groups g - JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id - JOIN group_members mu ON mu.group_id = g.group_id - JOIN contact_profiles pu ON pu.contact_profile_id = COALESCE(mu.member_profile_id, mu.contact_profile_id) - WHERE g.group_id = ? AND g.user_id = ? AND mu.contact_id = ? - |] + (groupInfoQuery <> " WHERE g.group_id = ? AND g.user_id = ? AND mu.contact_id = ?") (groupId, userId, userContactId) getGroupInfoByUserContactLinkConnReq :: DB.Connection -> VersionRangeChat -> User -> (ConnReqContact, ConnReqContact) -> IO (Maybe GroupInfo) diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs index fcd9896917..b00ba5705f 100644 --- a/src/Simplex/Chat/Store/Shared.hs +++ b/src/Simplex/Chat/Store/Shared.hs @@ -5,6 +5,7 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeOperators #-} @@ -33,6 +34,7 @@ import Simplex.Chat.Protocol import Simplex.Chat.Remote.Types import Simplex.Chat.Types import Simplex.Chat.Types.Preferences +import Simplex.Chat.Types.Shared import Simplex.Chat.Types.UITheme import Simplex.Messaging.Agent.Protocol (ConnId, UserId) import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow) @@ -543,3 +545,44 @@ safeDeleteLDN db User {userId} localDisplayName = do AND local_display_name NOT IN (SELECT local_display_name FROM users WHERE user_id = ?) |] (userId, localDisplayName, userId) + +type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe Text, Maybe ImageData, Maybe ProfileId, Maybe MsgFilter, Maybe Bool, Bool, Maybe GroupPreferences) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime, Maybe MemberId, Maybe BusinessChatType, Maybe UIThemeEntityOverrides, Maybe CustomData) :. GroupMemberRow + +type GroupMemberRow = ((Int64, Int64, MemberId, VersionChat, VersionChat, GroupMemberRole, GroupMemberCategory, GroupMemberStatus, Bool, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, ContactName, Maybe ContactId, ProfileId, ProfileId, ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Maybe Preferences)) + +toGroupInfo :: VersionRangeChat -> Int64 -> GroupInfoRow -> GroupInfo +toGroupInfo vr userContactId ((groupId, localDisplayName, displayName, fullName, description, image, hostConnCustomUserProfileId, enableNtfs_, sendRcpts, favorite, groupPreferences) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt, businessMemberId, businessChatType, uiThemes, customData) :. userMemberRow) = + let membership = (toGroupMember userContactId userMemberRow) {memberChatVRange = vr} + chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts, favorite} + fullGroupPreferences = mergeGroupPreferences groupPreferences + groupProfile = GroupProfile {displayName, fullName, description, image, groupPreferences} + businessChat = BusinessChatInfo <$> businessMemberId <*> businessChatType + in GroupInfo {groupId, localDisplayName, groupProfile, businessChat, fullGroupPreferences, membership, hostConnCustomUserProfileId, chatSettings, createdAt, updatedAt, chatTs, userMemberProfileSentAt, uiThemes, customData} + +toGroupMember :: Int64 -> GroupMemberRow -> GroupMember +toGroupMember userContactId ((groupMemberId, groupId, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, showMessages, memberRestriction_) :. (invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId, profileId, displayName, fullName, image, contactLink, localAlias, preferences)) = + let memberProfile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias} + memberSettings = GroupMemberSettings {showMessages} + blockedByAdmin = maybe False mrsBlocked memberRestriction_ + invitedBy = toInvitedBy userContactId invitedById + activeConn = Nothing + memberChatVRange = fromMaybe (versionToRange maxVer) $ safeVersionRange minVer maxVer + in GroupMember {..} + +groupInfoQuery :: Query +groupInfoQuery = + [sql| + SELECT + -- GroupInfo + g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image, + g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, + g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_member_id, g.business_chat, g.ui_themes, g.custom_data, + -- GroupMember - membership + mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, + mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, + pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences + FROM groups g + JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id + JOIN group_members mu ON mu.group_id = g.group_id + JOIN contact_profiles pu ON pu.contact_profile_id = COALESCE(mu.member_profile_id, mu.contact_profile_id) + |] diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index cc98b4101d..ec8f546d2f 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -349,7 +349,7 @@ instance ToJSON ConnReqUriHash where toJSON = strToJSON toEncoding = strToJEncoding -data ContactOrRequest = CORContact Contact | CORRequest UserContactRequest +data ChatOrRequest = CORContact Contact | CORGroup GroupInfo | CORRequest UserContactRequest type UserName = Text diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 4458f8ee7b..8b6a545637 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -207,6 +207,7 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe CRAcceptingBusinessRequest u g -> ttyUser u $ viewAcceptingBusinessRequest g CRContactAlreadyExists u c -> ttyUser u [ttyFullContact c <> ": contact already exists"] CRContactRequestAlreadyAccepted u c -> ttyUser u [ttyFullContact c <> ": sent you a duplicate contact request, but you are already connected, no action needed"] + CRBusinessRequestAlreadyAccepted u g -> ttyUser u [ttyFullGroup g <> ": sent you a duplicate connection request, but you are already connected, no action needed"] CRUserContactLinkCreated u cReq -> ttyUser u $ connReqContact_ "Your new chat address is created!" cReq CRUserContactLinkDeleted u -> ttyUser u viewUserContactLinkDeleted CRUserAcceptedGroupSent u _g _ -> ttyUser u [] -- [ttyGroup' g <> ": joining the group..."] @@ -1670,13 +1671,16 @@ viewConnectionPlan = \case GLPOwnLink g -> [grpLink "own link for group " <> ttyGroup' g] GLPConnectingConfirmReconnect -> [grpLink "connecting, allowed to reconnect"] GLPConnectingProhibit Nothing -> [grpLink "connecting"] - GLPConnectingProhibit (Just g) -> [grpLink ("connecting to group " <> ttyGroup' g)] + GLPConnectingProhibit (Just g) -> [grpOrBiz g <> " link: connecting to " <> grpOrBiz g <> " " <> ttyGroup' g] GLPKnown g -> - [ grpLink ("known group " <> ttyGroup' g), + [ grpOrBiz g <> " link: known " <> grpOrBiz g <> " " <> ttyGroup' g, "use " <> ttyToGroup g <> highlight' "" <> " to send messages" ] where grpLink = ("group link: " <>) + grpOrBiz GroupInfo {businessChat} = case businessChat of + Just _ -> "business" + Nothing -> "group" viewContactUpdated :: Contact -> Contact -> [StyledString] viewContactUpdated diff --git a/tests/ChatTests/ChatList.hs b/tests/ChatTests/ChatList.hs index 7f02fafc2c..de12caf648 100644 --- a/tests/ChatTests/ChatList.hs +++ b/tests/ChatTests/ChatList.hs @@ -199,14 +199,14 @@ testPaginationAllChatTypes = ts7 <- iso8601Show <$> getCurrentTime - getChats_ alice "count=10" [("*", "psst"), ("@dan", "hey"), ("#team", e2eeInfoNoPQStr), (":3", ""), ("<@cath", ""), ("@bob", "hey")] - getChats_ alice "count=3" [("*", "psst"), ("@dan", "hey"), ("#team", e2eeInfoNoPQStr)] + getChats_ alice "count=10" [("*", "psst"), ("@dan", "hey"), ("#team", "Recent history: on"), (":3", ""), ("<@cath", ""), ("@bob", "hey")] + getChats_ alice "count=3" [("*", "psst"), ("@dan", "hey"), ("#team", "Recent history: on")] getChats_ alice ("after=" <> ts2 <> " count=2") [(":3", ""), ("<@cath", "")] - getChats_ alice ("before=" <> ts5 <> " count=2") [("#team", e2eeInfoNoPQStr), (":3", "")] - getChats_ alice ("after=" <> ts3 <> " count=10") [("*", "psst"), ("@dan", "hey"), ("#team", e2eeInfoNoPQStr), (":3", "")] + getChats_ alice ("before=" <> ts5 <> " count=2") [("#team", "Recent history: on"), (":3", "")] + getChats_ alice ("after=" <> ts3 <> " count=10") [("*", "psst"), ("@dan", "hey"), ("#team", "Recent history: on"), (":3", "")] getChats_ alice ("before=" <> ts4 <> " count=10") [(":3", ""), ("<@cath", ""), ("@bob", "hey")] - getChats_ alice ("after=" <> ts1 <> " count=10") [("*", "psst"), ("@dan", "hey"), ("#team", e2eeInfoNoPQStr), (":3", ""), ("<@cath", ""), ("@bob", "hey")] - getChats_ alice ("before=" <> ts7 <> " count=10") [("*", "psst"), ("@dan", "hey"), ("#team", e2eeInfoNoPQStr), (":3", ""), ("<@cath", ""), ("@bob", "hey")] + getChats_ alice ("after=" <> ts1 <> " count=10") [("*", "psst"), ("@dan", "hey"), ("#team", "Recent history: on"), (":3", ""), ("<@cath", ""), ("@bob", "hey")] + getChats_ alice ("before=" <> ts7 <> " count=10") [("*", "psst"), ("@dan", "hey"), ("#team", "Recent history: on"), (":3", ""), ("<@cath", ""), ("@bob", "hey")] getChats_ alice ("after=" <> ts7 <> " count=10") [] getChats_ alice ("before=" <> ts1 <> " count=10") [] @@ -218,11 +218,11 @@ testPaginationAllChatTypes = alice ##> "/_settings #1 {\"enableNtfs\":\"all\",\"favorite\":true}" alice <## "ok" - getChats_ alice queryFavorite [("#team", e2eeInfoNoPQStr), ("@bob", "hey")] + getChats_ alice queryFavorite [("#team", "Recent history: on"), ("@bob", "hey")] getChats_ alice ("before=" <> ts4 <> " count=1 " <> queryFavorite) [("@bob", "hey")] - getChats_ alice ("before=" <> ts5 <> " count=1 " <> queryFavorite) [("#team", e2eeInfoNoPQStr)] + getChats_ alice ("before=" <> ts5 <> " count=1 " <> queryFavorite) [("#team", "Recent history: on")] getChats_ alice ("after=" <> ts1 <> " count=1 " <> queryFavorite) [("@bob", "hey")] - getChats_ alice ("after=" <> ts4 <> " count=1 " <> queryFavorite) [("#team", e2eeInfoNoPQStr)] + getChats_ alice ("after=" <> ts4 <> " count=1 " <> queryFavorite) [("#team", "Recent history: on")] let queryUnread = "{\"type\": \"filters\", \"favorite\": false, \"unread\": true}" diff --git a/tests/ChatTests/Direct.hs b/tests/ChatTests/Direct.hs index 72a28c3ada..95d27801f6 100644 --- a/tests/ChatTests/Direct.hs +++ b/tests/ChatTests/Direct.hs @@ -2620,7 +2620,7 @@ testSwitchGroupMember = bob <## "#team: alice changed address for you" alice <## "#team: you changed address for bob" threadDelay 100000 - alice #$> ("/_get chat #1 count=100", chat, [(1, e2eeInfoNoPQStr), (0, "connected"), (1, "started changing address for bob..."), (1, "you changed address for bob")]) + alice #$> ("/_get chat #1 count=100", chat, sndGroupFeatures <> [(0, "connected"), (1, "started changing address for bob..."), (1, "you changed address for bob")]) bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "started changing address for you..."), (0, "changed address for you")]) alice #> "#team hey" bob <# "#team alice> hey" @@ -2652,7 +2652,7 @@ testAbortSwitchGroupMember tmp = do bob <## "#team: alice changed address for you" alice <## "#team: you changed address for bob" threadDelay 100000 - alice #$> ("/_get chat #1 count=100", chat, [(1, e2eeInfoNoPQStr), (0, "connected"), (1, "started changing address for bob..."), (1, "started changing address for bob..."), (1, "you changed address for bob")]) + alice #$> ("/_get chat #1 count=100", chat, sndGroupFeatures <> [(0, "connected"), (1, "started changing address for bob..."), (1, "started changing address for bob..."), (1, "you changed address for bob")]) bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "started changing address for you..."), (0, "started changing address for you..."), (0, "changed address for you")]) alice #> "#team hey" bob <# "#team alice> hey" diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index 89462b2b61..a185b0038e 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -342,11 +342,11 @@ testGroupShared alice bob cath checkMessages directConnections = do getReadChats :: HasCallStack => String -> String -> IO () getReadChats msgItem1 msgItem2 = do alice @@@ [("#team", "hey team"), ("@cath", "sent invitation to join group team as admin"), ("@bob", "sent invitation to join group team as admin")] - alice #$> ("/_get chat #1 count=100", chat, [(1, e2eeInfoNoPQStr), (0, "connected"), (0, "connected"), (1, "hello"), (0, "hi there"), (0, "hey team")]) + alice #$> ("/_get chat #1 count=100", chat, sndGroupFeatures <> [(0, "connected"), (0, "connected"), (1, "hello"), (0, "hi there"), (0, "hey team")]) -- "before" and "after" define a chat item id across all chats, -- so we take into account group event items as well as sent group invitations in direct chats alice #$> ("/_get chat #1 after=" <> msgItem1 <> " count=100", chat, [(0, "hi there"), (0, "hey team")]) - alice #$> ("/_get chat #1 before=" <> msgItem2 <> " count=100", chat, [(1, e2eeInfoNoPQStr), (0, "connected"), (0, "connected"), (1, "hello"), (0, "hi there")]) + alice #$> ("/_get chat #1 before=" <> msgItem2 <> " count=100", chat, sndGroupFeatures <> [(0, "connected"), (0, "connected"), (1, "hello"), (0, "hi there")]) alice #$> ("/_get chat #1 around=" <> msgItem1 <> " count=2", chat, [(0, "connected"), (0, "connected"), (1, "hello"), (0, "hi there"), (0, "hey team")]) alice #$> ("/_get chat #1 count=100 search=team", chat, [(0, "hey team")]) bob @@@ [("@cath", "hey"), ("#team", "hey team"), ("@alice", "received invitation to join group team as admin")] @@ -565,18 +565,21 @@ testGroup2 = dan <##> cath dan <##> alice -- show last messages - alice ##> "/t #club 9" + alice ##> "/t #club 17" alice -- these strings are expected in any order because of sorting by time and rounding of time for sent - <##? [ ConsoleString ("#club " <> e2eeInfoNoPQStr), - "#club bob> connected", - "#club cath> connected", - "#club bob> added dan (Daniel)", - "#club dan> connected", - "#club hello", - "#club bob> hi there", - "#club cath> hey", - "#club dan> how is it going?" - ] + <##? + ( map (ConsoleString . ("#club " <> )) groupFeatureStrs + <> + [ "#club bob> connected", + "#club cath> connected", + "#club bob> added dan (Daniel)", + "#club dan> connected", + "#club hello", + "#club bob> hi there", + "#club cath> hey", + "#club dan> how is it going?" + ] + ) alice ##> "/t @dan 2" alice <##? [ "dan> hi", @@ -2139,7 +2142,7 @@ testGroupLink = bob <## "#team: you joined the group" ] threadDelay 100000 - alice #$> ("/_get chat #1 count=100", chat, [(1, e2eeInfoNoPQStr), (0, "invited via your group link"), (0, "connected")]) + alice #$> ("/_get chat #1 count=100", chat, sndGroupFeatures <> [(0, "invited via your group link"), (0, "connected")]) -- contacts connected via group link are not in chat previews alice @@@ [("#team", "connected")] bob @@@ [("#team", "connected")] @@ -3000,7 +3003,7 @@ testGroupLinkNoContact = ] threadDelay 100000 - alice #$> ("/_get chat #1 count=100", chat, [(1, e2eeInfoNoPQStr), (1, "Recent history: off"), (0, "invited via your group link"), (0, "connected")]) + alice #$> ("/_get chat #1 count=100", chat, sndGroupFeatures <> [(1, "Recent history: off"), (0, "invited via your group link"), (0, "connected")]) alice @@@ [("#team", "connected")] bob @@@ [("#team", "connected")] @@ -3063,7 +3066,7 @@ testGroupLinkNoContactInviteesWereConnected = ] threadDelay 100000 - alice #$> ("/_get chat #1 count=100", chat, [(1, e2eeInfoNoPQStr), (1, "Recent history: off"), (0, "invited via your group link"), (0, "connected")]) + alice #$> ("/_get chat #1 count=100", chat, sndGroupFeatures <> [(1, "Recent history: off"), (0, "invited via your group link"), (0, "connected")]) alice @@@ [("#team", "connected")] bob @@@ [("#team", "connected"), ("@cath", "hey")] @@ -3144,7 +3147,7 @@ testGroupLinkNoContactAllMembersWereConnected = ] threadDelay 100000 - alice #$> ("/_get chat #1 count=100", chat, [(1, e2eeInfoNoPQStr), (1, "Recent history: off"), (0, "invited via your group link"), (0, "connected")]) + alice #$> ("/_get chat #1 count=100", chat, sndGroupFeatures <> [(1, "Recent history: off"), (0, "invited via your group link"), (0, "connected")]) alice @@@ [("#team", "connected"), ("@bob", "hey"), ("@cath", "hey")] bob @@@ [("#team", "connected"), ("@alice", "hey"), ("@cath", "hey")] @@ -3300,7 +3303,7 @@ testGroupLinkNoContactHostIncognito = ] threadDelay 100000 - alice #$> ("/_get chat #1 count=100", chat, [(1, e2eeInfoNoPQStr), (0, "invited via your group link"), (0, "connected")]) + alice #$> ("/_get chat #1 count=100", chat, sndGroupFeatures <> [(0, "invited via your group link"), (0, "connected")]) alice @@@ [("#team", "connected")] bob @@@ [("#team", "connected")] @@ -3334,7 +3337,7 @@ testGroupLinkNoContactInviteeIncognito = ] threadDelay 100000 - alice #$> ("/_get chat #1 count=100", chat, [(1, e2eeInfoNoPQStr), (0, "invited via your group link"), (0, "connected")]) + alice #$> ("/_get chat #1 count=100", chat, sndGroupFeatures <> [(0, "invited via your group link"), (0, "connected")]) alice @@@ [("#team", "connected")] bob @@@ [("#team", "connected")] @@ -3401,7 +3404,7 @@ testGroupLinkNoContactExistingContactMerged = ] threadDelay 100000 - alice #$> ("/_get chat #1 count=100", chat, [(1, e2eeInfoNoPQStr), (0, "invited via your group link"), (0, "connected")]) + alice #$> ("/_get chat #1 count=100", chat, sndGroupFeatures <> [(0, "invited via your group link"), (0, "connected")]) alice <##> bob diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index 3ffe57ba2e..c5a464d969 100644 --- a/tests/ChatTests/Profiles.hs +++ b/tests/ChatTests/Profiles.hs @@ -686,16 +686,26 @@ testBusinessAddress = testChat3 businessProfile aliceProfile {fullName = "Alice cLink <- getContactLink biz True biz ##> "/auto_accept on business" biz <## "auto_accept on, business" + bob ##> ("/_connect plan 1 " <> cLink) + bob <## "contact address: ok to connect" bob ##> ("/c " <> cLink) bob <## "connection request sent!" + bob ##> ("/_connect plan 1 " <> cLink) + bob <## "contact address: connecting, allowed to reconnect" biz <## "#bob (Bob): accepting business address request..." - biz <## "#bob: bob_1 joined the group" bob <## "#biz: joining the group..." + -- the next command can be prone to race conditions + bob ##> ("/_connect plan 1 " <> cLink) + bob <## "business link: connecting to business #biz" + biz <## "#bob: bob_1 joined the group" bob <## "#biz: you joined the group" biz #> "#bob hi" bob <# "#biz biz_1> hi" bob #> "#biz hello" biz <# "#bob bob_1> hello" + bob ##> ("/_connect plan 1 " <> cLink) + bob <## "business link: known business #biz" + bob <## "use #biz to send messages" connectUsers biz alice biz <##> alice biz ##> "/a #bob alice" @@ -1948,13 +1958,13 @@ testUpdateGroupPrefs = testChat2 aliceProfile bobProfile $ \alice bob -> do createGroup2 "team" alice bob - alice #$> ("/_get chat #1 count=100", chat, [(1, e2eeInfoNoPQStr), (0, "connected")]) + alice #$> ("/_get chat #1 count=100", chat, sndGroupFeatures <> [(0, "connected")]) threadDelay 500000 bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected")]) alice ##> "/_group_profile #1 {\"displayName\": \"team\", \"fullName\": \"\", \"groupPreferences\": {\"fullDelete\": {\"enable\": \"on\"}, \"directMessages\": {\"enable\": \"on\"}, \"history\": {\"enable\": \"on\"}}}" alice <## "updated group preferences:" alice <## "Full deletion: on" - alice #$> ("/_get chat #1 count=100", chat, [(1, e2eeInfoNoPQStr), (0, "connected"), (1, "Full deletion: on")]) + alice #$> ("/_get chat #1 count=100", chat, sndGroupFeatures <> [(0, "connected"), (1, "Full deletion: on")]) bob <## "alice updated group #team:" bob <## "updated group preferences:" bob <## "Full deletion: on" @@ -1964,7 +1974,7 @@ testUpdateGroupPrefs = alice <## "updated group preferences:" alice <## "Full deletion: off" alice <## "Voice messages: off" - alice #$> ("/_get chat #1 count=100", chat, [(1, e2eeInfoNoPQStr), (0, "connected"), (1, "Full deletion: on"), (1, "Full deletion: off"), (1, "Voice messages: off")]) + alice #$> ("/_get chat #1 count=100", chat, sndGroupFeatures <> [(0, "connected"), (1, "Full deletion: on"), (1, "Full deletion: off"), (1, "Voice messages: off")]) bob <## "alice updated group #team:" bob <## "updated group preferences:" bob <## "Full deletion: off" @@ -1974,7 +1984,7 @@ testUpdateGroupPrefs = alice ##> "/set voice #team on" alice <## "updated group preferences:" alice <## "Voice messages: on" - alice #$> ("/_get chat #1 count=100", chat, [(1, e2eeInfoNoPQStr), (0, "connected"), (1, "Full deletion: on"), (1, "Full deletion: off"), (1, "Voice messages: off"), (1, "Voice messages: on")]) + alice #$> ("/_get chat #1 count=100", chat, sndGroupFeatures <> [(0, "connected"), (1, "Full deletion: on"), (1, "Full deletion: off"), (1, "Voice messages: off"), (1, "Voice messages: on")]) bob <## "alice updated group #team:" bob <## "updated group preferences:" bob <## "Voice messages: on" @@ -1984,14 +1994,14 @@ testUpdateGroupPrefs = alice ##> "/_group_profile #1 {\"displayName\": \"team\", \"fullName\": \"\", \"groupPreferences\": {\"fullDelete\": {\"enable\": \"off\"}, \"voice\": {\"enable\": \"on\"}, \"directMessages\": {\"enable\": \"on\"}, \"history\": {\"enable\": \"on\"}}}" -- no update threadDelay 500000 - alice #$> ("/_get chat #1 count=100", chat, [(1, e2eeInfoNoPQStr), (0, "connected"), (1, "Full deletion: on"), (1, "Full deletion: off"), (1, "Voice messages: off"), (1, "Voice messages: on")]) + alice #$> ("/_get chat #1 count=100", chat, sndGroupFeatures <> [(0, "connected"), (1, "Full deletion: on"), (1, "Full deletion: off"), (1, "Voice messages: off"), (1, "Voice messages: on")]) alice #> "#team hey" bob <# "#team alice> hey" threadDelay 1000000 bob #> "#team hi" alice <# "#team bob> hi" threadDelay 500000 - alice #$> ("/_get chat #1 count=100", chat, [(1, e2eeInfoNoPQStr), (0, "connected"), (1, "Full deletion: on"), (1, "Full deletion: off"), (1, "Voice messages: off"), (1, "Voice messages: on"), (1, "hey"), (0, "hi")]) + alice #$> ("/_get chat #1 count=100", chat, sndGroupFeatures <> [(0, "connected"), (1, "Full deletion: on"), (1, "Full deletion: off"), (1, "Voice messages: off"), (1, "Voice messages: on"), (1, "hey"), (0, "hi")]) bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "Full deletion: on"), (0, "Full deletion: off"), (0, "Voice messages: off"), (0, "Voice messages: on"), (0, "hey"), (1, "hi")]) testAllowFullDeletionContact :: HasCallStack => FilePath -> IO () @@ -2031,11 +2041,11 @@ testAllowFullDeletionGroup = bob <## "alice updated group #team:" bob <## "updated group preferences:" bob <## "Full deletion: on" - alice #$> ("/_get chat #1 count=100", chat, [(1, e2eeInfoNoPQStr), (0, "connected"), (1, "hi"), (0, "hey"), (1, "Full deletion: on")]) + alice #$> ("/_get chat #1 count=100", chat, sndGroupFeatures <> [(0, "connected"), (1, "hi"), (0, "hey"), (1, "Full deletion: on")]) bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "hi"), (1, "hey"), (0, "Full deletion: on")]) bob #$> ("/_delete item #1 " <> msgItemId <> " broadcast", id, "message deleted") alice <# "#team bob> [deleted] hey" - alice #$> ("/_get chat #1 count=100", chat, [(1, e2eeInfoNoPQStr), (0, "connected"), (1, "hi"), (1, "Full deletion: on")]) + alice #$> ("/_get chat #1 count=100", chat, sndGroupFeatures <> [(0, "connected"), (1, "hi"), (1, "Full deletion: on")]) bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "hi"), (0, "Full deletion: on")]) testProhibitDirectMessages :: HasCallStack => FilePath -> IO () @@ -2157,12 +2167,12 @@ testEnableTimedMessagesGroup = alice #> "#team hi" bob <# "#team alice> hi" threadDelay 500000 - alice #$> ("/_get chat #1 count=100", chat, [(1, e2eeInfoNoPQStr), (0, "connected"), (1, "Disappearing messages: on (1 sec)"), (1, "hi")]) + alice #$> ("/_get chat #1 count=100", chat, sndGroupFeatures <> [(0, "connected"), (1, "Disappearing messages: on (1 sec)"), (1, "hi")]) bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "Disappearing messages: on (1 sec)"), (0, "hi")]) threadDelay 1000000 alice <## "timed message deleted: hi" bob <## "timed message deleted: hi" - alice #$> ("/_get chat #1 count=100", chat, [(1, e2eeInfoNoPQStr), (0, "connected"), (1, "Disappearing messages: on (1 sec)")]) + alice #$> ("/_get chat #1 count=100", chat, sndGroupFeatures <> [(0, "connected"), (1, "Disappearing messages: on (1 sec)")]) bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "Disappearing messages: on (1 sec)")]) -- turn off, messages are not disappearing alice ##> "/set disappear #team off" @@ -2175,7 +2185,7 @@ testEnableTimedMessagesGroup = alice #> "#team hey" bob <# "#team alice> hey" threadDelay 1500000 - alice #$> ("/_get chat #1 count=100", chat, [(1, e2eeInfoNoPQStr), (0, "connected"), (1, "Disappearing messages: on (1 sec)"), (1, "Disappearing messages: off"), (1, "hey")]) + alice #$> ("/_get chat #1 count=100", chat, sndGroupFeatures <> [(0, "connected"), (1, "Disappearing messages: on (1 sec)"), (1, "Disappearing messages: off"), (1, "hey")]) bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "Disappearing messages: on (1 sec)"), (0, "Disappearing messages: off"), (0, "hey")]) -- test api alice ##> "/set disappear #team on 30s" diff --git a/tests/ChatTests/Utils.hs b/tests/ChatTests/Utils.hs index 6459522134..ce820b12a7 100644 --- a/tests/ChatTests/Utils.hs +++ b/tests/ChatTests/Utils.hs @@ -281,19 +281,25 @@ lastChatFeature :: String lastChatFeature = snd $ last chatFeatures groupFeatures :: [(Int, String)] -groupFeatures = map (\(a, _, _) -> a) groupFeatures'' +groupFeatures = map (\(a, _, _) -> a) $ groupFeatures'' 0 -groupFeatures'' :: [((Int, String), Maybe (Int, String), Maybe String)] -groupFeatures'' = - [ ((0, e2eeInfoNoPQStr), Nothing, Nothing), - ((0, "Disappearing messages: off"), Nothing, Nothing), - ((0, "Direct messages: on"), Nothing, Nothing), - ((0, "Full deletion: off"), Nothing, Nothing), - ((0, "Message reactions: on"), Nothing, Nothing), - ((0, "Voice messages: on"), Nothing, Nothing), - ((0, "Files and media: on"), Nothing, Nothing), - ((0, "SimpleX links: on"), Nothing, Nothing), - ((0, "Recent history: on"), Nothing, Nothing) +sndGroupFeatures :: [(Int, String)] +sndGroupFeatures = map (\(a, _, _) -> a) $ groupFeatures'' 1 + +groupFeatureStrs :: [String] +groupFeatureStrs = map (\(a, _, _) -> snd a) $ groupFeatures'' 0 + +groupFeatures'' :: Int -> [((Int, String), Maybe (Int, String), Maybe String)] +groupFeatures'' dir = + [ ((dir, e2eeInfoNoPQStr), Nothing, Nothing), + ((dir, "Disappearing messages: off"), Nothing, Nothing), + ((dir, "Direct messages: on"), Nothing, Nothing), + ((dir, "Full deletion: off"), Nothing, Nothing), + ((dir, "Message reactions: on"), Nothing, Nothing), + ((dir, "Voice messages: on"), Nothing, Nothing), + ((dir, "Files and media: on"), Nothing, Nothing), + ((dir, "SimpleX links: on"), Nothing, Nothing), + ((dir, "Recent history: on"), Nothing, Nothing) ] itemId :: Int -> String From 85e7a13dba593a8a3ca4680ecfef97d31c1c5c6a Mon Sep 17 00:00:00 2001 From: Diogo Date: Tue, 3 Dec 2024 12:26:50 +0000 Subject: [PATCH 110/167] desktop: show group message reaction on right click (#5304) * desktop: show group message reaction on right click * open on ctrl + click too --- .../common/views/chat/item/ChatItemView.kt | 43 +++++++++++++------ 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt index 82744fdc39..bf871ab626 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt @@ -121,9 +121,33 @@ fun ChatItemView( cItem.reactions.forEach { r -> val showReactionMenu = remember { mutableStateOf(false) } val reactionMembers = remember { mutableStateOf(emptyList()) } + val interactionSource = remember { MutableInteractionSource() } + val enterInteraction = remember { HoverInteraction.Enter() } + KeyChangeEffect(highlighted.value) { + if (highlighted.value) { + interactionSource.emit(enterInteraction) + } else { + interactionSource.emit(HoverInteraction.Exit(enterInteraction)) + } + } var modifier = Modifier.padding(horizontal = 5.dp, vertical = 2.dp).clip(RoundedCornerShape(8.dp)) if (cInfo.featureEnabled(ChatFeature.Reactions)) { + fun showReactionsMenu() { + if (cInfo is ChatInfo.Group) { + withBGApi { + try { + val members = controller.apiGetReactionMembers(rhId, cInfo.groupInfo.groupId, cItem.id, r.reaction) + if (members != null) { + showReactionMenu.value = true + reactionMembers.value = members + } + } catch (e: Exception) { + Log.d(TAG, "hatItemView ChatItemReactions onLongClick: unexpected exception: ${e.stackTraceToString()}") + } + } + } + } modifier = modifier .combinedClickable( onClick = { @@ -132,21 +156,12 @@ fun ChatItemView( } }, onLongClick = { - if (cInfo is ChatInfo.Group) { - withBGApi { - try { - val members = controller.apiGetReactionMembers(rhId, cInfo.groupInfo.groupId, cItem.id, r.reaction) - if (members != null) { - showReactionMenu.value = true - reactionMembers.value = members - } - } catch (e: Exception) { - Log.d(TAG, "hatItemView ChatItemReactions onLongClick: unexpected exception: ${e.stackTraceToString()}") - } - } - } - } + showReactionsMenu() + }, + interactionSource = interactionSource, + indication = LocalIndication.current ) + .onRightClick { showReactionsMenu() } } Row(modifier.padding(2.dp), verticalAlignment = Alignment.CenterVertically) { ReactionIcon(r.reaction.text, fontSize = 12.sp) From 43fa4c43a29672d949361894caa9e8ef38b0d397 Mon Sep 17 00:00:00 2001 From: Narasimha-sc <166327228+Narasimha-sc@users.noreply.github.com> Date: Tue, 3 Dec 2024 12:48:54 +0000 Subject: [PATCH 111/167] docs: update FAQ (#5179) * Update FAQ.md Added: - Why invite links use simplex.chat domain? - I do not know my database passphrase * Update FAQ.md * Add flatpak directory * corrections * correction * invitation --------- Co-authored-by: Evgeny --- docs/FAQ.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/FAQ.md b/docs/FAQ.md index 932a4c33ee..0d0426d7c9 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -18,6 +18,7 @@ revision: 23.04.2024 - [I want to see when my contacts read my messages](#i-want-to-see-when-my-contacts-read-my-messages) - [Can I use the same profile on desktop? Do messages sync cross-platform?](#can-i-use-the-same-profile-on-desktop-do-messages-sync-cross-platform) - [Why cannot I delete messages I sent from my contact's device?](#why-cannot-i-delete-messages-i-sent-from-my-contacts-device) +- [Why invitation links use simplex.chat domain?](#why-invitation-links-use-simplex.chat-domain) [Troubleshooting](#troubleshooting) - [I do not receive messages or message notifications](#i-do-not-receive-messages-or-message-notifications) @@ -27,6 +28,7 @@ revision: 23.04.2024 - [Audio or video calls do not connect](#audio-or-video-calls-do-not-connect) - [Audio or video calls without e2e encryption](#audio-or-video-calls-without-e2e-encryption) - [I clicked the link to connect, but could not connect](#i-clicked-the-link-to-connect-but-could-not-connect) +- [I do not know my database passphrase](#i-do-not-know-my-database-passphrase) [Privacy and security](#privacy-and-security) - [Does SimpleX support post quantum cryptography?](#does-simplex-support-post-quantum-cryptography) @@ -120,6 +122,14 @@ It is also important to remember, that even if your contact enabled "Delete for When "Delete for everyone" is not enabled, you can still mark the sent message as deleted within 24 hours of sending it. In this case the recipient will see it as "deleted message", and will be able to reveal the original message. +### Why invitation links use simplex.chat domain? + +You can replace `https://simplex.chat/` with `simplex:/` or with any other domain - the app never connect with it, ignoring it completely. It is only used to make it easier to connect for the new users who did not install the app yet. + +The invitation links will soon move to servers' domains. The servers already can host the pages that will be used to show QR codes. + +The link itself and the key exchange are not hosted anywhere, and the server that hosts the page to show QR code does not observe the actual connection link, because it is in the hash part of the link. The part after hash character (`#`) is not sent over the internet - the server can only see `https://simplex.chat/contact/` and the rest is processed on user's device in the browser, if you open it as a page. + ## Troubleshooting ### I do not receive messages or message notifications @@ -226,6 +236,20 @@ For connection to complete, your contact has to be online and have the app runni Once the connection is established you don't need to be online at the same time to send messages. +### I do not know my database passphrase + +If you are prompted to enter database passphrase and you do not know it, this could have happened due to: +- You may have forgotten the passphrase. (There is no other way to access your data). +- Migration of app data from one device to another while using unsupported migration process, e.g. via iCloud backup. Use SimpleX Chat's own migration process in the app Settings. + +In the previous desktop app versions it could also happen in case of error during SimpleX Chat installation. + +You can resolve it by deleting the app's database: (WARNING: this results in deletion of all profiles, contacts and messages) +- on Android/iOS, uninstall the app and install it again. +- on Windows, delete folder `C:\AppData\Roaming\SimpleX`, you can find it by pressing Windows key + R and entering `%appdata%`. +- on Linux/Mac, delete directories `~/.local/share/simplex` and `~/.config/simplex`, where `~` represents your home directory (/home/user) +- on Flatpak, delete directory `~/.var/app/chat.simplex.simplex`. + ## Privacy and security ### Does SimpleX support post quantum cryptography? From a0b8cf62be27a9b701f65279a1d772ce544130b8 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Tue, 3 Dec 2024 17:27:00 +0400 Subject: [PATCH 112/167] android, desktop: support business addresses and chats (#5302) --- .../chat/simplex/common/model/ChatModel.kt | 15 ++++- .../chat/simplex/common/model/SimpleXAPI.kt | 20 ++++++- .../views/chat/group/GroupChatInfoView.kt | 33 +++++++---- .../views/chat/group/GroupPreferences.kt | 14 ++--- .../views/chat/group/WelcomeMessageView.kt | 5 +- .../common/views/helpers/ChatInfoImage.kt | 8 ++- .../common/views/newchat/ConnectPlan.kt | 33 ++++++++--- .../common/views/usersettings/Preferences.kt | 4 +- .../common/views/usersettings/SettingsView.kt | 2 + .../views/usersettings/UserAddressView.kt | 55 +++++++++++++++---- .../commonMain/resources/MR/base/strings.xml | 6 ++ .../resources/MR/images/ic_work.svg | 1 + .../MR/images/ic_work_filled_padded.svg | 8 +++ 13 files changed, 155 insertions(+), 49 deletions(-) create mode 100644 apps/multiplatform/common/src/commonMain/resources/MR/images/ic_work.svg create mode 100644 apps/multiplatform/common/src/commonMain/resources/MR/images/ic_work_filled_padded.svg diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index 857d21b966..4d75d37b99 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -1467,6 +1467,7 @@ data class GroupInfo ( val groupId: Long, override val localDisplayName: String, val groupProfile: GroupProfile, + val businessChat: BusinessChatInfo? = null, val fullGroupPreferences: FullGroupPreferences, val membership: GroupMember, val hostConnCustomUserProfileId: Long? = null, @@ -1497,7 +1498,7 @@ data class GroupInfo ( override val image get() = groupProfile.image override val localAlias get() = "" - val canEdit: Boolean + val isOwner: Boolean get() = membership.memberRole == GroupMemberRole.Owner && membership.memberCurrent val canDelete: Boolean @@ -1543,6 +1544,18 @@ data class GroupProfile ( } } +@Serializable +data class BusinessChatInfo ( + val memberId: String, + val chatType: BusinessChatType +) + +@Serializable +enum class BusinessChatType { + @SerialName("business") Business, + @SerialName("customer") Customer, +} + @Serializable data class GroupMember ( val groupMemberId: Long, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index d9c7df9d81..f0f63f2c72 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -2526,6 +2526,14 @@ object ChatController { } } } + is CR.BusinessLinkConnecting -> { + if (!active(r.user)) return + + withChats { + updateGroup(rhId, r.groupInfo) + removeChat(rhId, r.fromContact.id) + } + } is CR.JoinedGroupMemberConnecting -> if (active(r.user)) { withChats { @@ -5484,6 +5492,7 @@ sealed class CR { @Serializable @SerialName("sentGroupInvitation") class SentGroupInvitation(val user: UserRef, val groupInfo: GroupInfo, val contact: Contact, val member: GroupMember): CR() @Serializable @SerialName("userAcceptedGroupSent") class UserAcceptedGroupSent (val user: UserRef, val groupInfo: GroupInfo, val hostContact: Contact? = null): CR() @Serializable @SerialName("groupLinkConnecting") class GroupLinkConnecting (val user: UserRef, val groupInfo: GroupInfo, val hostMember: GroupMember): CR() + @Serializable @SerialName("businessLinkConnecting") class BusinessLinkConnecting (val user: UserRef, val groupInfo: GroupInfo, val hostMember: GroupMember, val fromContact: Contact): CR() @Serializable @SerialName("userDeletedMember") class UserDeletedMember(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember): CR() @Serializable @SerialName("leftMemberUser") class LeftMemberUser(val user: UserRef, val groupInfo: GroupInfo): CR() @Serializable @SerialName("groupMembers") class GroupMembers(val user: UserRef, val group: Group): CR() @@ -5664,6 +5673,7 @@ sealed class CR { is SentGroupInvitation -> "sentGroupInvitation" is UserAcceptedGroupSent -> "userAcceptedGroupSent" is GroupLinkConnecting -> "groupLinkConnecting" + is BusinessLinkConnecting -> "businessLinkConnecting" is UserDeletedMember -> "userDeletedMember" is LeftMemberUser -> "leftMemberUser" is GroupMembers -> "groupMembers" @@ -5837,6 +5847,7 @@ sealed class CR { is SentGroupInvitation -> withUser(user, "groupInfo: $groupInfo\ncontact: $contact\nmember: $member") is UserAcceptedGroupSent -> json.encodeToString(groupInfo) is GroupLinkConnecting -> withUser(user, "groupInfo: $groupInfo\nhostMember: $hostMember") + is BusinessLinkConnecting -> withUser(user, "groupInfo: $groupInfo\nhostMember: $hostMember\nfromContact: $fromContact") is UserDeletedMember -> withUser(user, "groupInfo: $groupInfo\nmember: $member") is LeftMemberUser -> withUser(user, json.encodeToString(groupInfo)) is GroupMembers -> withUser(user, json.encodeToString(group)) @@ -6108,11 +6119,16 @@ class UserContactLinkRec(val connReqContact: String, val autoAccept: AutoAccept? } @Serializable -class AutoAccept(val acceptIncognito: Boolean, val autoReply: MsgContent?) { +class AutoAccept(val businessAddress: Boolean, val acceptIncognito: Boolean, val autoReply: MsgContent?) { companion object { fun cmdString(autoAccept: AutoAccept?): String { if (autoAccept == null) return "off" - val s = "on" + if (autoAccept.acceptIncognito) " incognito=on" else "" + var s = "on" + if (autoAccept.acceptIncognito) { + s += " incognito=on" + } else if (autoAccept.businessAddress) { + s += " business" + } val msg = autoAccept.autoReply ?: return s return s + " " + msg.cmdString } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt index 76f2866950..870df388b2 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt @@ -36,6 +36,7 @@ import chat.simplex.common.views.chat.* import chat.simplex.common.views.chat.item.ItemAction import chat.simplex.common.views.chatlist.* import chat.simplex.res.MR +import dev.icerock.moko.resources.StringResource import kotlinx.coroutines.launch const val SMALL_GROUPS_RCPS_MEM_LIMIT: Int = 20 @@ -328,13 +329,14 @@ fun ModalData.GroupChatInfoLayout( SectionSpacer() SectionView { - if (groupInfo.canEdit) { + if (groupInfo.isOwner && groupInfo.businessChat?.chatType == null) { EditGroupProfileButton(editGroupProfile) } - if (groupInfo.groupProfile.description != null || groupInfo.canEdit) { + if (groupInfo.groupProfile.description != null || (groupInfo.isOwner && groupInfo.businessChat?.chatType == null)) { AddOrEditWelcomeMessage(groupInfo.groupProfile.description, addOrEditWelcomeMessage) } - GroupPreferencesButton(openPreferences) + val prefsTitleId = if (groupInfo.businessChat == null) MR.strings.group_preferences else MR.strings.chat_preferences + GroupPreferencesButton(prefsTitleId, openPreferences) if (members.filter { it.memberCurrent }.size <= SMALL_GROUPS_RCPS_MEM_LIMIT) { SendReceiptsOption(currentUser, sendReceipts, setSendReceipts) } else { @@ -356,14 +358,21 @@ fun ModalData.GroupChatInfoLayout( SectionView(title = String.format(generalGetString(MR.strings.group_info_section_title_num_members), members.count() + 1)) { if (groupInfo.canAddMembers) { - if (groupLink == null) { - CreateGroupLinkButton(manageGroupLink) - } else { - GroupLinkButton(manageGroupLink) + if (groupInfo.businessChat == null) { + if (groupLink == null) { + CreateGroupLinkButton(manageGroupLink) + } else { + GroupLinkButton(manageGroupLink) + } } val onAddMembersClick = if (chat.chatInfo.incognito) ::cantInviteIncognitoAlert else addMembers val tint = if (chat.chatInfo.incognito) MaterialTheme.colors.secondary else MaterialTheme.colors.primary - AddMembersButton(tint, onAddMembersClick) + val addMembersTitleId = when (groupInfo.businessChat?.chatType) { + BusinessChatType.Customer -> MR.strings.button_add_team_members + BusinessChatType.Business -> MR.strings.button_add_friends + null -> MR.strings.button_add_members + } + AddMembersButton(addMembersTitleId, tint, onAddMembersClick) } if (members.size > 8) { SectionItemView(padding = PaddingValues(start = 14.dp, end = DEFAULT_PADDING_HALF)) { @@ -439,10 +448,10 @@ private fun GroupChatInfoHeader(cInfo: ChatInfo) { } @Composable -private fun GroupPreferencesButton(onClick: () -> Unit) { +private fun GroupPreferencesButton(titleId: StringResource, onClick: () -> Unit) { SettingsActionItem( painterResource(MR.images.ic_toggle_on), - stringResource(MR.strings.group_preferences), + stringResource(titleId), click = onClick ) } @@ -479,10 +488,10 @@ fun SendReceiptsOptionDisabled() { } @Composable -private fun AddMembersButton(tint: Color = MaterialTheme.colors.primary, onClick: () -> Unit) { +private fun AddMembersButton(titleId: StringResource, tint: Color = MaterialTheme.colors.primary, onClick: () -> Unit) { SettingsActionItem( painterResource(MR.images.ic_add), - stringResource(MR.strings.button_add_members), + stringResource(titleId), onClick, iconColor = tint, textColor = tint diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt index 128dfe2d97..0a807e1d63 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt @@ -6,12 +6,10 @@ import SectionDividerSpaced import SectionItemView import SectionTextFooter import SectionView -import androidx.compose.foundation.layout.* import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Modifier import dev.icerock.moko.resources.compose.stringResource import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* @@ -20,7 +18,6 @@ import chat.simplex.common.model.* import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.platform.ColumnWithScrollBar import chat.simplex.res.MR -import dev.icerock.moko.resources.compose.painterResource private val featureRoles: List> = listOf( null to generalGetString(MR.strings.feature_roles_all_members), @@ -83,7 +80,8 @@ private fun GroupPreferencesLayout( savePrefs: () -> Unit, ) { ColumnWithScrollBar { - AppBarTitle(stringResource(MR.strings.group_preferences)) + val titleId = if (groupInfo.businessChat == null) MR.strings.group_preferences else MR.strings.chat_preferences + AppBarTitle(stringResource(titleId)) val timedMessages = remember(preferences) { mutableStateOf(preferences.timedMessages.enable) } val onTTLUpdated = { ttl: Int? -> applyPrefs(preferences.copy(timedMessages = preferences.timedMessages.copy(ttl = ttl))) @@ -136,7 +134,7 @@ private fun GroupPreferencesLayout( FeatureSection(GroupFeature.History, enableHistory, null, groupInfo, preferences, onTTLUpdated) { enable, _ -> applyPrefs(preferences.copy(history = GroupPreference(enable = enable))) } - if (groupInfo.canEdit) { + if (groupInfo.isOwner) { SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false) ResetSaveButtons( reset = reset, @@ -163,12 +161,12 @@ private fun FeatureSection( val icon = if (on) feature.iconFilled() else feature.icon val iconTint = if (on) SimplexGreen else MaterialTheme.colors.secondary val timedOn = feature == GroupFeature.TimedMessages && enableFeature.value == GroupFeatureEnabled.ON - if (groupInfo.canEdit) { + if (groupInfo.isOwner) { PreferenceToggleWithIcon( feature.text, icon, iconTint, - enableFeature.value == GroupFeatureEnabled.ON, + checked = enableFeature.value == GroupFeatureEnabled.ON, ) { checked -> onSelected(if (checked) GroupFeatureEnabled.ON else GroupFeatureEnabled.OFF, enableForRole?.value) } @@ -214,7 +212,7 @@ private fun FeatureSection( onSelected(enableFeature.value, null) } } - SectionTextFooter(feature.enableDescription(enableFeature.value, groupInfo.canEdit)) + SectionTextFooter(feature.enableDescription(enableFeature.value, groupInfo.isOwner)) } @Composable diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/WelcomeMessageView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/WelcomeMessageView.kt index 7f0af360e7..6ebd4b13c3 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/WelcomeMessageView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/WelcomeMessageView.kt @@ -7,9 +7,7 @@ import SectionTextFooter import SectionView import TextIconSpaced import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.selection.SelectionContainer -import androidx.compose.foundation.verticalScroll import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable @@ -32,7 +30,6 @@ import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.model.GroupInfo import chat.simplex.common.platform.ColumnWithScrollBar import chat.simplex.common.platform.chatJsonLength -import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF import chat.simplex.res.MR import kotlinx.coroutines.delay @@ -99,7 +96,7 @@ private fun GroupWelcomeLayout( val editMode = remember { mutableStateOf(true) } AppBarTitle(stringResource(MR.strings.group_welcome_title)) val wt = rememberSaveable { welcomeText } - if (groupInfo.canEdit) { + if (groupInfo.isOwner && groupInfo.businessChat?.chatType == null) { if (editMode.value) { val focusRequester = remember { FocusRequester() } TextEditor( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatInfoImage.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatInfoImage.kt index d338c57e61..c3e97dd27b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatInfoImage.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatInfoImage.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.graphics.* import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.InspectableValue import androidx.compose.ui.unit.* +import chat.simplex.common.model.BusinessChatType import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import chat.simplex.common.model.ChatInfo @@ -30,7 +31,12 @@ import kotlin.math.max fun ChatInfoImage(chatInfo: ChatInfo, size: Dp, iconColor: Color = MaterialTheme.colors.secondaryVariant, shadow: Boolean = false) { val icon = when (chatInfo) { - is ChatInfo.Group -> MR.images.ic_supervised_user_circle_filled + is ChatInfo.Group -> + when (chatInfo.groupInfo.businessChat?.chatType) { + BusinessChatType.Business -> MR.images.ic_work_filled_padded + BusinessChatType.Customer -> MR.images.ic_account_circle_filled + null -> MR.images.ic_supervised_user_circle_filled + } is ChatInfo.Local -> MR.images.ic_folder_filled else -> MR.images.ic_account_circle_filled } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt index 7cd272c109..e8190e0767 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt @@ -275,10 +275,17 @@ suspend fun planAndConnect( Log.d(TAG, "planAndConnect, .GroupLink, .ConnectingProhibit, incognito=$incognito") val groupInfo = connectionPlan.groupLinkPlan.groupInfo_ if (groupInfo != null) { - AlertManager.privacySensitive.showAlertMsg( - generalGetString(MR.strings.connect_plan_group_already_exists), - String.format(generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_vName), groupInfo.displayName) + linkText - ) + if (groupInfo.businessChat == null) { + AlertManager.privacySensitive.showAlertMsg( + generalGetString(MR.strings.connect_plan_group_already_exists), + String.format(generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_vName), groupInfo.displayName) + linkText + ) + } else { + AlertManager.privacySensitive.showAlertMsg( + generalGetString(MR.strings.connect_plan_chat_already_exists), + String.format(generalGetString(MR.strings.connect_plan_you_are_already_connecting_to_vName), groupInfo.displayName) + linkText + ) + } } else { AlertManager.privacySensitive.showAlertMsg( generalGetString(MR.strings.connect_plan_already_joining_the_group), @@ -295,11 +302,19 @@ suspend fun planAndConnect( filterKnownGroup(groupInfo) } else { openKnownGroup(chatModel, rhId, close, groupInfo) - AlertManager.privacySensitive.showAlertMsg( - generalGetString(MR.strings.connect_plan_group_already_exists), - String.format(generalGetString(MR.strings.connect_plan_you_are_already_in_group_vName), groupInfo.displayName) + linkText, - hostDevice = hostDevice(rhId), - ) + if (groupInfo.businessChat == null) { + AlertManager.privacySensitive.showAlertMsg( + generalGetString(MR.strings.connect_plan_group_already_exists), + String.format(generalGetString(MR.strings.connect_plan_you_are_already_in_group_vName), groupInfo.displayName) + linkText, + hostDevice = hostDevice(rhId), + ) + } else { + AlertManager.privacySensitive.showAlertMsg( + generalGetString(MR.strings.connect_plan_chat_already_exists), + String.format(generalGetString(MR.strings.connect_plan_you_are_already_connected_with_vName), groupInfo.displayName) + linkText, + hostDevice = hostDevice(rhId), + ) + } cleanup?.invoke() } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Preferences.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Preferences.kt index bc27773ca6..5132516669 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Preferences.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Preferences.kt @@ -123,9 +123,9 @@ private fun TimedMessagesFeatureSection(allowFeature: State, onS ChatFeature.TimedMessages.text, ChatFeature.TimedMessages.icon, MaterialTheme.colors.secondary, - allowFeature.value == FeatureAllowed.ALWAYS || allowFeature.value == FeatureAllowed.YES, + checked = allowFeature.value == FeatureAllowed.ALWAYS || allowFeature.value == FeatureAllowed.YES, extraPadding = false, - onSelected + onChange = onSelected ) } SectionTextFooter(ChatFeature.TimedMessages.allowDescription(allowFeature.value)) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt index ac6431f6fc..51a0ffad8d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt @@ -408,6 +408,7 @@ fun PreferenceToggleWithIcon( text: String, icon: Painter? = null, iconColor: Color? = MaterialTheme.colors.secondary, + disabled: Boolean = false, checked: Boolean, extraPadding: Boolean = false, onChange: (Boolean) -> Unit = {}, @@ -418,6 +419,7 @@ fun PreferenceToggleWithIcon( onCheckedChange = { onChange(it) }, + enabled = !disabled ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt index 836faee49b..7bc35bc0de 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt @@ -196,14 +196,22 @@ private fun UserAddressLayout( LearnMoreButton(learnMore) } } else { + val autoAcceptState = remember { mutableStateOf(AutoAcceptState(userAddress)) } + val autoAcceptStateSaved = remember { mutableStateOf(autoAcceptState.value) } + SectionView(stringResource(MR.strings.for_social_media).uppercase()) { SimpleXLinkQRCode(userAddress.connReqContact) ShareAddressButton { share(simplexChatLink(userAddress.connReqContact)) } // ShareViaEmailButton { sendEmail(userAddress) } + BusinessAddressToggle(autoAcceptState) { saveAas(autoAcceptState.value, autoAcceptStateSaved) } AddressSettingsButton(user, userAddress, shareViaProfile, setProfileAddress, saveAas) + + if (autoAcceptState.value.business) { + SectionTextFooter(stringResource(MR.strings.add_your_team_members_to_conversations)) + } } - SectionDividerSpaced() + SectionDividerSpaced(maxTopPadding = autoAcceptState.value.business) SectionView(generalGetString(MR.strings.or_to_share_privately).uppercase()) { CreateOneTimeLinkButton() } @@ -385,17 +393,37 @@ fun ShareWithContactsButton(shareViaProfile: MutableState, setProfileAd onDismissRequest = { shareViaProfile.value = !on }) + } } +} + +@Composable +private fun BusinessAddressToggle(autoAcceptState: MutableState, saveAas: (AutoAcceptState) -> Unit) { + PreferenceToggleWithIcon( + stringResource(MR.strings.business_address), + painterResource(MR.images.ic_work), + checked = autoAcceptState.value.business, + ) { ba -> + autoAcceptState.value = if (ba) + AutoAcceptState(enable = true, incognito = false, business = true, autoAcceptState.value.welcomeText) + else + AutoAcceptState(autoAcceptState.value.enable, autoAcceptState.value.incognito, business = false, autoAcceptState.value.welcomeText) + saveAas(autoAcceptState.value) } } @Composable private fun AutoAcceptToggle(autoAcceptState: MutableState, saveAas: (AutoAcceptState) -> Unit) { - PreferenceToggleWithIcon(stringResource(MR.strings.auto_accept_contact), painterResource(MR.images.ic_check), checked = autoAcceptState.value.enable) { + PreferenceToggleWithIcon( + stringResource(MR.strings.auto_accept_contact), + painterResource(MR.images.ic_check), + disabled = autoAcceptState.value.business, + checked = autoAcceptState.value.enable + ) { autoAcceptState.value = if (!it) AutoAcceptState() else - AutoAcceptState(it, autoAcceptState.value.incognito, autoAcceptState.value.welcomeText) + AutoAcceptState(it, autoAcceptState.value.incognito, autoAcceptState.value.business, autoAcceptState.value.welcomeText) saveAas(autoAcceptState.value) } } @@ -416,12 +444,15 @@ private class AutoAcceptState { private set var incognito: Boolean = false private set + var business: Boolean = false + private set var welcomeText: String = "" private set - constructor(enable: Boolean = false, incognito: Boolean = false, welcomeText: String = "") { + constructor(enable: Boolean = false, incognito: Boolean = false, business: Boolean = false, welcomeText: String = "") { this.enable = enable this.incognito = incognito + this.business = business this.welcomeText = welcomeText } @@ -429,6 +460,7 @@ private class AutoAcceptState { contactLink.autoAccept?.let { aa -> enable = true incognito = aa.acceptIncognito + business = aa.businessAddress aa.autoReply?.let { msg -> welcomeText = msg.text } ?: run { @@ -445,19 +477,20 @@ private class AutoAcceptState { if (s != "") { autoReply = MsgContent.MCText(s) } - return AutoAccept(incognito, autoReply) + return AutoAccept(business, incognito, autoReply) } return null } override fun equals(other: Any?): Boolean { if (other !is AutoAcceptState) return false - return this.enable == other.enable && this.incognito == other.incognito && this.welcomeText == other.welcomeText + return this.enable == other.enable && this.incognito == other.incognito && this.business == other.business && this.welcomeText == other.welcomeText } override fun hashCode(): Int { var result = enable.hashCode() result = 31 * result + incognito.hashCode() + result = 31 * result + business.hashCode() result = 31 * result + welcomeText.hashCode() return result } @@ -470,7 +503,9 @@ private fun AutoAcceptSection( saveAas: (AutoAcceptState, MutableState) -> Unit ) { SectionView(stringResource(MR.strings.auto_accept_contact).uppercase()) { - AcceptIncognitoToggle(autoAcceptState) + if (!autoAcceptState.value.business) { + AcceptIncognitoToggle(autoAcceptState) + } WelcomeMessageEditor(autoAcceptState) SaveAASButton(autoAcceptState.value == savedAutoAcceptState.value) { saveAas(autoAcceptState.value, savedAutoAcceptState) } } @@ -482,9 +517,9 @@ private fun AcceptIncognitoToggle(autoAcceptState: MutableState stringResource(MR.strings.accept_contact_incognito_button), if (autoAcceptState.value.incognito) painterResource(MR.images.ic_theater_comedy_filled) else painterResource(MR.images.ic_theater_comedy), if (autoAcceptState.value.incognito) Indigo else MaterialTheme.colors.secondary, - autoAcceptState.value.incognito, + checked = autoAcceptState.value.incognito, ) { - autoAcceptState.value = AutoAcceptState(autoAcceptState.value.enable, it, autoAcceptState.value.welcomeText) + autoAcceptState.value = AutoAcceptState(autoAcceptState.value.enable, it, autoAcceptState.value.business, autoAcceptState.value.welcomeText) } } @@ -494,7 +529,7 @@ private fun WelcomeMessageEditor(autoAcceptState: MutableState) TextEditor(welcomeText, Modifier.height(100.dp), placeholder = stringResource(MR.strings.enter_welcome_message_optional)) LaunchedEffect(welcomeText.value) { if (welcomeText.value != autoAcceptState.value.welcomeText) { - autoAcceptState.value = AutoAcceptState(autoAcceptState.value.enable, autoAcceptState.value.incognito, welcomeText.value) + autoAcceptState.value = AutoAcceptState(autoAcceptState.value.enable, autoAcceptState.value.incognito, autoAcceptState.value.business, welcomeText.value) } } } diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index d931abab70..55b7e2b0e7 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -936,6 +936,8 @@ SimpleX address or 1-time link? Create 1-time link Address settings + Business address + Add your team members to the conversations. Continue @@ -1551,6 +1553,8 @@ Invite members + Add team members + Add friends %1$s MEMBERS you: %1$s Delete group @@ -2275,10 +2279,12 @@ Open group Repeat join request? Group already exists! + Chat already exists! %1$s.]]> Already joining the group! You are already joining the group via this link. %1$s.]]> + %1$s.]]> Connect via link? diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_work.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_work.svg new file mode 100644 index 0000000000..4ea483b006 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_work.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_work_filled_padded.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_work_filled_padded.svg new file mode 100644 index 0000000000..3d8c05e2c8 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_work_filled_padded.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file From 29b9abf2411b04c1eddea06901153309b2e0bd67 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Tue, 3 Dec 2024 15:22:41 +0000 Subject: [PATCH 113/167] ui: translations (#5267) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Translated using Weblate (German) Currently translated at 95.6% (2087 of 2182 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/de/ * Translated using Weblate (Russian) Currently translated at 95.6% (2087 of 2182 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ru/ * Translated using Weblate (French) Currently translated at 95.6% (2087 of 2182 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/fr/ * Translated using Weblate (Italian) Currently translated at 95.6% (2087 of 2182 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/it/ * Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 95.6% (2087 of 2182 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/zh_Hans/ * Translated using Weblate (Chinese (Traditional Han script)) Currently translated at 80.7% (1763 of 2182 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/zh_Hant/ * Translated using Weblate (Dutch) Currently translated at 95.6% (2087 of 2182 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/nl/ * Translated using Weblate (Japanese) Currently translated at 88.6% (1935 of 2182 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ja/ * Translated using Weblate (Czech) Currently translated at 91.9% (2006 of 2182 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/cs/ * Translated using Weblate (Arabic) Currently translated at 95.6% (2087 of 2182 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ar/ * Translated using Weblate (Ukrainian) Currently translated at 95.5% (2085 of 2182 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/uk/ * Translated using Weblate (Finnish) Currently translated at 67.5% (1474 of 2182 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/fi/ * Translated using Weblate (Polish) Currently translated at 95.6% (2086 of 2182 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/pl/ * Translated using Weblate (Portuguese) Currently translated at 43.9% (959 of 2182 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/pt/ * Translated using Weblate (Hebrew) Currently translated at 86.2% (1883 of 2182 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/he/ * Translated using Weblate (Bulgarian) Currently translated at 79.1% (1726 of 2182 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/bg/ * Translated using Weblate (Turkish) Currently translated at 95.6% (2087 of 2182 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/tr/ * Translated using Weblate (Persian) Currently translated at 83.3% (1819 of 2182 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/fa/ * Translated using Weblate (Romanian) Currently translated at 33.1% (723 of 2182 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ro/ * Translated using Weblate (Vietnamese) Currently translated at 61.5% (1342 of 2182 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/vi/ * Translated using Weblate (German) Currently translated at 95.6% (2087 of 2182 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/de/ * Translated using Weblate (Russian) Currently translated at 95.6% (2087 of 2182 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ru/ * Translated using Weblate (French) Currently translated at 95.6% (2087 of 2182 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/fr/ * Translated using Weblate (Italian) Currently translated at 95.6% (2087 of 2182 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/it/ * Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 95.6% (2087 of 2182 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/zh_Hans/ * Translated using Weblate (Chinese (Traditional Han script)) Currently translated at 80.7% (1763 of 2182 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/zh_Hant/ * Translated using Weblate (Dutch) Currently translated at 95.6% (2087 of 2182 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/nl/ * Translated using Weblate (Japanese) Currently translated at 88.6% (1935 of 2182 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ja/ * Translated using Weblate (Czech) Currently translated at 91.9% (2006 of 2182 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/cs/ * Translated using Weblate (Arabic) Currently translated at 95.6% (2087 of 2182 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ar/ * Translated using Weblate (Ukrainian) Currently translated at 95.5% (2085 of 2182 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/uk/ * Translated using Weblate (Finnish) Currently translated at 67.5% (1474 of 2182 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/fi/ * Translated using Weblate (Polish) Currently translated at 95.6% (2086 of 2182 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/pl/ * Translated using Weblate (Portuguese) Currently translated at 43.9% (959 of 2182 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/pt/ * Translated using Weblate (Hebrew) Currently translated at 86.2% (1883 of 2182 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/he/ * Translated using Weblate (Bulgarian) Currently translated at 79.1% (1726 of 2182 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/bg/ * Translated using Weblate (Turkish) Currently translated at 95.6% (2087 of 2182 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/tr/ * Translated using Weblate (Persian) Currently translated at 83.3% (1819 of 2182 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/fa/ * Translated using Weblate (Romanian) Currently translated at 33.1% (723 of 2182 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ro/ * Translated using Weblate (Vietnamese) Currently translated at 61.5% (1342 of 2182 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/vi/ * Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (2182 of 2182 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/zh_Hans/ * Translated using Weblate (Dutch) Currently translated at 95.9% (2093 of 2182 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/nl/ * Translated using Weblate (Turkish) Currently translated at 96.1% (2099 of 2182 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/tr/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2182 of 2182 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (German) Currently translated at 100.0% (2182 of 2182 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/de/ * Translated using Weblate (German) Currently translated at 100.0% (1918 of 1918 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/de/ * Translated using Weblate (Italian) Currently translated at 100.0% (2182 of 2182 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/it/ * Translated using Weblate (Italian) Currently translated at 100.0% (1918 of 1918 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/it/ * Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (2182 of 2182 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/zh_Hans/ * Translated using Weblate (Spanish) Currently translated at 98.4% (2148 of 2182 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/es/ * Translated using Weblate (Dutch) Currently translated at 100.0% (2182 of 2182 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/nl/ * Translated using Weblate (Dutch) Currently translated at 99.0% (1900 of 1918 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/nl/ * Translated using Weblate (Arabic) Currently translated at 100.0% (2182 of 2182 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ar/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (2182 of 2182 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/uk/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (2182 of 2182 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/uk/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (1918 of 1918 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/uk/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2182 of 2182 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (1918 of 1918 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hu/ * Translated using Weblate (Vietnamese) Currently translated at 62.4% (1362 of 2182 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/vi/ * Translated using Weblate (Spanish) Currently translated at 99.0% (2161 of 2182 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/es/ * Translated using Weblate (Dutch) Currently translated at 100.0% (1918 of 1918 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/nl/ * Translated using Weblate (Spanish) Currently translated at 100.0% (2182 of 2182 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/es/ * Translated using Weblate (Spanish) Currently translated at 99.5% (1910 of 1918 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/es/ * Translated using Weblate (Spanish) Currently translated at 99.6% (1911 of 1918 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/es/ * Translated using Weblate (Spanish) Currently translated at 100.0% (2178 of 2178 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/es/ * Translated using Weblate (Spanish) Currently translated at 100.0% (1918 of 1918 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/es/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2178 of 2178 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2178 of 2178 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (2178 of 2178 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/uk/ * Translated using Weblate (Italian) Currently translated at 100.0% (2178 of 2178 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/it/ * Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (2178 of 2178 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/zh_Hans/ * Translated using Weblate (Korean) Currently translated at 66.6% (1452 of 2178 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ko/ * Translated using Weblate (Vietnamese) Currently translated at 62.7% (1367 of 2178 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/vi/ * Translated using Weblate (Indonesian) Currently translated at 59.3% (1293 of 2178 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/id/ * Translated using Weblate (Spanish) Currently translated at 100.0% (1918 of 1918 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/es/ * Translated using Weblate (Dutch) Currently translated at 99.9% (2177 of 2178 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/nl/ * Translated using Weblate (Czech) Currently translated at 91.9% (2002 of 2178 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/cs/ * Translated using Weblate (Vietnamese) Currently translated at 62.9% (1372 of 2178 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/vi/ * Translated using Weblate (Vietnamese) Currently translated at 63.6% (1387 of 2178 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/vi/ * Translated using Weblate (German) Currently translated at 100.0% (2178 of 2178 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/de/ * Translated using Weblate (Japanese) Currently translated at 62.5% (1199 of 1918 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/ja/ * fix/process localizations --------- Co-authored-by: Anonymous Co-authored-by: 大王叫我来巡山 Co-authored-by: M1K4 Co-authored-by: Abdullah Koyuncu Co-authored-by: summoner001 Co-authored-by: mlanp Co-authored-by: Random Co-authored-by: No name Co-authored-by: jonnysemon Co-authored-by: Max Co-authored-by: Bezruchenko Simon Co-authored-by: tuananh-ng <158744840+tuananh-ng@users.noreply.github.com> Co-authored-by: Ghost of Sparta Co-authored-by: Максим Горпиніч Co-authored-by: translatorforkr Co-authored-by: Rafi Co-authored-by: zenobit Co-authored-by: Miyu Sakatsuki --- .../bg.xcloc/Localized Contents/bg.xliff | 28 ++ .../cs.xcloc/Localized Contents/cs.xliff | 28 ++ .../de.xcloc/Localized Contents/de.xliff | 131 +++++ .../en.xcloc/Localized Contents/en.xliff | 35 ++ .../es.xcloc/Localized Contents/es.xliff | 148 +++++- .../fi.xcloc/Localized Contents/fi.xliff | 28 ++ .../fr.xcloc/Localized Contents/fr.xliff | 28 ++ .../hu.xcloc/Localized Contents/hu.xliff | 165 +++++- .../it.xcloc/Localized Contents/it.xliff | 131 +++++ .../ja.xcloc/Localized Contents/ja.xliff | 50 ++ .../nl.xcloc/Localized Contents/nl.xliff | 131 +++++ .../pl.xcloc/Localized Contents/pl.xliff | 28 ++ .../ru.xcloc/Localized Contents/ru.xliff | 28 ++ .../th.xcloc/Localized Contents/th.xliff | 28 ++ .../tr.xcloc/Localized Contents/tr.xliff | 28 ++ .../uk.xcloc/Localized Contents/uk.xliff | 195 +++++++ .../Localized Contents/zh-Hans.xliff | 28 ++ .../SimpleX NSE/de.lproj/Localizable.strings | 20 +- .../SimpleX NSE/es.lproj/Localizable.strings | 20 +- .../SimpleX NSE/hu.lproj/Localizable.strings | 20 +- .../SimpleX NSE/it.lproj/Localizable.strings | 20 +- .../SimpleX NSE/nl.lproj/Localizable.strings | 20 +- .../SimpleX NSE/uk.lproj/Localizable.strings | 20 +- apps/ios/de.lproj/Localizable.strings | 291 +++++++++++ apps/ios/es.lproj/Localizable.strings | 310 +++++++++++- apps/ios/hu.lproj/Localizable.strings | 325 +++++++++++- apps/ios/it.lproj/Localizable.strings | 291 +++++++++++ apps/ios/ja.lproj/Localizable.strings | 66 +++ apps/ios/nl.lproj/Localizable.strings | 291 +++++++++++ apps/ios/uk.lproj/Localizable.strings | 474 ++++++++++++++++++ .../commonMain/resources/MR/ar/strings.xml | 112 ++++- .../commonMain/resources/MR/base/strings.xml | 1 - .../commonMain/resources/MR/bg/strings.xml | 3 +- .../commonMain/resources/MR/cs/strings.xml | 5 +- .../commonMain/resources/MR/de/strings.xml | 103 +++- .../commonMain/resources/MR/es/strings.xml | 103 +++- .../commonMain/resources/MR/fa/strings.xml | 3 +- .../commonMain/resources/MR/fi/strings.xml | 3 +- .../commonMain/resources/MR/fr/strings.xml | 3 +- .../commonMain/resources/MR/hu/strings.xml | 205 +++++--- .../commonMain/resources/MR/in/strings.xml | 17 + .../commonMain/resources/MR/it/strings.xml | 100 +++- .../commonMain/resources/MR/iw/strings.xml | 3 +- .../commonMain/resources/MR/ja/strings.xml | 3 +- .../commonMain/resources/MR/ko/strings.xml | 52 +- .../commonMain/resources/MR/nl/strings.xml | 99 +++- .../commonMain/resources/MR/pl/strings.xml | 3 +- .../commonMain/resources/MR/pt/strings.xml | 3 +- .../commonMain/resources/MR/ro/strings.xml | 3 +- .../commonMain/resources/MR/ru/strings.xml | 3 +- .../commonMain/resources/MR/tr/strings.xml | 14 +- .../commonMain/resources/MR/uk/strings.xml | 126 ++++- .../commonMain/resources/MR/vi/strings.xml | 52 +- .../resources/MR/zh-rCN/strings.xml | 100 +++- .../resources/MR/zh-rTW/strings.xml | 3 +- 55 files changed, 4293 insertions(+), 235 deletions(-) diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff index 2964742c85..f1e9ee0f39 100644 --- a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff @@ -618,6 +618,10 @@ Добавете адрес към вашия профил, така че вашите контакти да могат да го споделят с други хора. Актуализацията на профила ще бъде изпратена до вашите контакти. No comment provided by engineer. + + Add friends + No comment provided by engineer. + Add profile Добави профил @@ -633,6 +637,10 @@ Добави сървъри чрез сканиране на QR кодове. No comment provided by engineer. + + Add team members + No comment provided by engineer. + Add to another device Добави към друго устройство @@ -643,6 +651,10 @@ Добави съобщение при посрещане No comment provided by engineer. + + Add your team members to the conversations. + No comment provided by engineer. + Added media & file servers No comment provided by engineer. @@ -1183,6 +1195,10 @@ Български, финландски, тайландски и украински - благодарение на потребителите и [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! No comment provided by engineer. + + Business address + 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). Чрез чат профил (по подразбиране) или [чрез връзка](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (БЕТА). @@ -1318,6 +1334,14 @@ Change user profiles authentication reason + + Chat already exists + No comment provided by engineer. + + + Chat already exists! + No comment provided by engineer. + Chat colors No comment provided by engineer. @@ -7590,6 +7614,10 @@ To connect, please ask your contact to create another connection link and check Вече сте вече свързани с %@. No comment provided by engineer. + + You are already connected with %@. + No comment provided by engineer. + You are already connecting to %@. Вече се свързвате с %@. diff --git a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff index bf9436afe3..a4bff0f321 100644 --- a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff @@ -600,6 +600,10 @@ Přidejte adresu do svého profilu, aby ji vaše kontakty mohly sdílet s dalšími lidmi. Aktualizace profilu bude zaslána vašim kontaktům. No comment provided by engineer. + + Add friends + No comment provided by engineer. + Add profile Přidat profil @@ -615,6 +619,10 @@ Přidejte servery skenováním QR kódů. No comment provided by engineer. + + Add team members + No comment provided by engineer. + Add to another device Přidat do jiného zařízení @@ -625,6 +633,10 @@ Přidat uvítací zprávu No comment provided by engineer. + + Add your team members to the conversations. + No comment provided by engineer. + Added media & file servers No comment provided by engineer. @@ -1145,6 +1157,10 @@ Bulharský, finský, thajský a ukrajinský - díky uživatelům a [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! No comment provided by engineer. + + Business address + 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). Podle chat profilu (výchozí) nebo [podle připojení](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA). @@ -1277,6 +1293,14 @@ Change user profiles authentication reason + + Chat already exists + No comment provided by engineer. + + + Chat already exists! + No comment provided by engineer. + Chat colors No comment provided by engineer. @@ -7338,6 +7362,10 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Již jste připojeni k %@. No comment provided by engineer. + + You are already connected with %@. + No comment provided by engineer. + You are already connecting to %@. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff index 6a92589851..3770207e39 100644 --- a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff +++ b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff @@ -114,10 +114,12 @@ %@ server + %@ Server No comment provided by engineer. %@ servers + %@ Server No comment provided by engineer. @@ -382,6 +384,7 @@ **Scan / Paste link**: to connect via a link you received. + **Link scannen / einfügen**: Um eine Verbindung über den Link herzustellen, den Sie erhalten haben. No comment provided by engineer. @@ -492,10 +495,12 @@ 1-time link + Einmal-Link No comment provided by engineer. 1-time link can be used *with one contact only* - share in person or via any messenger. + Ein Einmal-Link kann *nur mit einem Kontakt* genutzt werden - teilen Sie in nur persönlich oder über einen beliebigen Messenger. No comment provided by engineer. @@ -586,6 +591,7 @@ Accept conditions + Nutzungsbedingungen akzeptieren No comment provided by engineer. @@ -606,6 +612,7 @@ Accepted conditions + Akzeptierte Nutzungsbedingungen No comment provided by engineer. @@ -628,6 +635,10 @@ Fügen Sie die Adresse Ihrem Profil hinzu, damit Ihre Kontakte sie mit anderen Personen teilen können. Es wird eine Profilaktualisierung an Ihre Kontakte gesendet. No comment provided by engineer. + + Add friends + No comment provided by engineer. + Add profile Profil hinzufügen @@ -643,6 +654,10 @@ Fügen Sie Server durch Scannen der QR Codes hinzu. No comment provided by engineer. + + Add team members + No comment provided by engineer. + Add to another device Einem anderen Gerät hinzufügen @@ -653,12 +668,18 @@ Begrüßungsmeldung hinzufügen No comment provided by engineer. + + Add your team members to the conversations. + No comment provided by engineer. + Added media & file servers + Medien- und Dateiserver hinzugefügt No comment provided by engineer. Added message servers + Nachrichtenserver hinzugefügt No comment provided by engineer. @@ -688,10 +709,12 @@ Address or 1-time link? + Adress- oder Einmal-Link? No comment provided by engineer. Address settings + Adress-Einstellungen No comment provided by engineer. @@ -741,6 +764,7 @@ All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages. + Alle Nachrichten und Dateien werden **Ende-zu-Ende verschlüsselt** versendet - in Direkt-Nachrichten mit Post-Quantum-Security. No comment provided by engineer. @@ -1218,6 +1242,10 @@ Bulgarisch, Finnisch, Thailändisch und Ukrainisch - Dank der Nutzer und [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! No comment provided by engineer. + + Business address + 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). Per Chat-Profil (Voreinstellung) oder [per Verbindung](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA). @@ -1357,8 +1385,17 @@ Change user profiles + Chat-Profile wechseln authentication reason + + Chat already exists + No comment provided by engineer. + + + Chat already exists! + No comment provided by engineer. + Chat colors Chat-Farben @@ -1441,10 +1478,12 @@ Check messages every 20 min. + Alle 20min Nachrichten überprüfen. No comment provided by engineer. Check messages when allowed. + Wenn es erlaubt ist, Nachrichten überprüfen. No comment provided by engineer. @@ -1539,38 +1578,47 @@ Conditions accepted on: %@. + Die Nutzungsbedingungen wurden akzeptiert am: %@. No comment provided by engineer. Conditions are accepted for the operator(s): **%@**. + Die Nutzungsbedingungen der/des Betreiber(s) werden akzeptiert: **%@**. No comment provided by engineer. Conditions are already accepted for following operator(s): **%@**. + Die Nutzungsbedingungen der/des folgenden Betreiber(s) wurden schon akzeptiert: **%@**. No comment provided by engineer. Conditions of use + Nutzungsbedingungen No comment provided by engineer. Conditions will be accepted for enabled operators after 30 days. + Die Nutzungsbedingungen der aktivierten Betreiber werden nach 30 Tagen akzeptiert. No comment provided by engineer. Conditions will be accepted for operator(s): **%@**. + Die Nutzungsbedingungen der/des Betreiber(s) werden akzeptiert: **%@**. No comment provided by engineer. Conditions will be accepted for the operator(s): **%@**. + Die Nutzungsbedingungen der/des Betreiber(s) werden akzeptiert: **%@**. No comment provided by engineer. Conditions will be accepted on: %@. + Die Nutzungsbedingungen werden akzeptiert am: %@. No comment provided by engineer. Conditions will be automatically accepted for enabled operators on: %@. + Die Nutzungsbedingungen der aktivierten Betreiber werden automatisch akzeptiert am: %@. No comment provided by engineer. @@ -1769,6 +1817,7 @@ Das ist Ihr eigener Einmal-Link! Connection security + Verbindungs-Sicherheit No comment provided by engineer. @@ -1888,6 +1937,7 @@ Das ist Ihr eigener Einmal-Link! Create 1-time link + Einmal-Link erstellen No comment provided by engineer. @@ -1977,6 +2027,7 @@ Das ist Ihr eigener Einmal-Link! Current conditions text couldn't be loaded, you can review conditions via this link: + Der Text der aktuellen Nutzungsbedingungen konnte nicht geladen werden. Sie können die Nutzungsbedingungen unter diesem Link einsehen: No comment provided by engineer. @@ -2346,6 +2397,7 @@ Das ist Ihr eigener Einmal-Link! Delivered even when Apple drops them. + Auslieferung, selbst wenn Apple sie löscht. No comment provided by engineer. @@ -2631,6 +2683,7 @@ Das ist Ihr eigener Einmal-Link! E2E encrypted notifications. + E2E-verschlüsselte Benachrichtigungen. No comment provided by engineer. @@ -2655,6 +2708,7 @@ Das ist Ihr eigener Einmal-Link! Enable Flux + Flux aktivieren No comment provided by engineer. @@ -2864,6 +2918,7 @@ Das ist Ihr eigener Einmal-Link! Error accepting conditions + Fehler beim Akzeptieren der Nutzungsbedingungen alert title @@ -2878,6 +2933,7 @@ Das ist Ihr eigener Einmal-Link! Error adding server + Fehler beim Hinzufügen des Servers alert title @@ -3022,6 +3078,7 @@ Das ist Ihr eigener Einmal-Link! Error loading servers + Fehler beim Laden der Server alert title @@ -3081,6 +3138,7 @@ Das ist Ihr eigener Einmal-Link! Error saving servers + Fehler beim Speichern der Server alert title @@ -3155,6 +3213,7 @@ Das ist Ihr eigener Einmal-Link! Error updating server + Fehler beim Aktualisieren des Servers alert title @@ -3204,6 +3263,7 @@ Das ist Ihr eigener Einmal-Link! Errors in servers configuration. + Fehler in der Server-Konfiguration. servers error @@ -3410,6 +3470,7 @@ Das ist Ihr eigener Einmal-Link! For chat profile %@: + Für das Chat-Profil %@: servers error @@ -3419,14 +3480,17 @@ Das ist Ihr eigener Einmal-Link! For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server. + Wenn Ihr Kontakt beispielsweise Nachrichten über einen SimpleX-Chatserver empfängt, wird Ihre App diese über einen der Server von Flux versenden. No comment provided by engineer. For private routing + Für privates Routing No comment provided by engineer. For social media + Für soziale Medien No comment provided by engineer. @@ -3740,10 +3804,12 @@ Fehler: %2$@ How it affects privacy + Wie es die Privatsphäre beeinflusst No comment provided by engineer. How it helps privacy + Wie es die Privatsphäre schützt No comment provided by engineer. @@ -4565,6 +4631,7 @@ Das ist Ihr Link für die Gruppe %@! More reliable notifications + Zuverlässigere Benachrichtigungen No comment provided by engineer. @@ -4604,6 +4671,7 @@ Das ist Ihr Link für die Gruppe %@! Network decentralization + Dezentralisiertes Netzwerk No comment provided by engineer. @@ -4618,6 +4686,7 @@ Das ist Ihr Link für die Gruppe %@! Network operator + Netzwerk-Betreiber No comment provided by engineer. @@ -4677,6 +4746,7 @@ Das ist Ihr Link für die Gruppe %@! New events + Neue Ereignisse notification @@ -4706,6 +4776,7 @@ Das ist Ihr Link für die Gruppe %@! New server + Neuer Server No comment provided by engineer. @@ -4765,10 +4836,12 @@ Das ist Ihr Link für die Gruppe %@! No media & file servers. + Keine Medien- und Dateiserver. servers error No message servers. + Keine Nachrichten-Server. servers error @@ -4803,18 +4876,22 @@ Das ist Ihr Link für die Gruppe %@! No servers for private message routing. + Keine Server für privates Nachrichten-Routing. servers error No servers to receive files. + Keine Server für den Empfang von Dateien. servers error No servers to receive messages. + Keine Server für den Empfang von Nachrichten. servers error No servers to send files. + Keine Server für das Versenden von Dateien. servers error @@ -4849,6 +4926,7 @@ Das ist Ihr Link für die Gruppe %@! Notifications privacy + Datenschutz für Benachrichtigungen No comment provided by engineer. @@ -4991,6 +5069,7 @@ Dies erfordert die Aktivierung eines VPNs. Open changes + Änderungen öffnen No comment provided by engineer. @@ -5005,6 +5084,7 @@ Dies erfordert die Aktivierung eines VPNs. Open conditions + Nutzungsbedingungen öffnen No comment provided by engineer. @@ -5024,10 +5104,12 @@ Dies erfordert die Aktivierung eines VPNs. Operator + Betreiber No comment provided by engineer. Operator server + Betreiber-Server alert title @@ -5056,6 +5138,7 @@ Dies erfordert die Aktivierung eines VPNs. Or to share privately + Oder zum privaten Teilen No comment provided by engineer. @@ -5276,6 +5359,7 @@ Fehler: %@ Preset servers + Voreingestellte Server No comment provided by engineer. @@ -5452,6 +5536,7 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Push Notifications + Push-Benachrichtigungen No comment provided by engineer. @@ -5827,10 +5912,12 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Review conditions + Nutzungsbedingungen einsehen No comment provided by engineer. Review later + Später einsehen No comment provided by engineer. @@ -5880,10 +5967,12 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Same conditions will apply to operator **%@**. + Dieselben Nutzungsbedingungen gelten auch für den Betreiber **%@**. No comment provided by engineer. Same conditions will apply to operator(s): **%@**. + Dieselben Nutzungsbedingungen gelten auch für den/die Betreiber: **%@**. No comment provided by engineer. @@ -6289,6 +6378,7 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Server added to operator %@. + Der Server wurde dem Betreiber %@ hinzugefügt. alert message @@ -6308,14 +6398,17 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Server operator changed. + Der Server-Betreiber wurde geändert. alert title Server operators + Server-Betreiber No comment provided by engineer. Server protocol changed. + Das Server-Protokoll wurde geändert. alert title @@ -6446,10 +6539,12 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Share 1-time link with a friend + Den Einmal-Einladungslink mit einem Freund teilen No comment provided by engineer. Share SimpleX address on social media. + Die SimpleX-Adresse auf sozialen Medien teilen. No comment provided by engineer. @@ -6459,6 +6554,7 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Share address publicly + Die Adresse öffentlich teilen No comment provided by engineer. @@ -6583,10 +6679,12 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. SimpleX address and 1-time links are safe to share via any messenger. + Die SimpleX-Adresse und Einmal-Links können sicher über beliebige Messenger geteilt werden. No comment provided by engineer. SimpleX address or 1-time link? + SimpleX-Adresse oder Einmal-Link? No comment provided by engineer. @@ -6682,6 +6780,8 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Some servers failed the test: %@ + Einige Server haben den Test nicht bestanden: +%@ alert message @@ -6952,6 +7052,7 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro The app protects your privacy by using different operators in each conversation. + Durch Verwendung verschiedener Netzwerk-Betreiber für jede Unterhaltung schützt die App Ihre Privatsphäre. No comment provided by engineer. @@ -6971,6 +7072,7 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro The connection reached the limit of undelivered messages, your contact may be offline. + Diese Verbindung hat das Limit der nicht ausgelieferten Nachrichten erreicht. Ihr Kontakt ist möglicherweise offline. No comment provided by engineer. @@ -7035,6 +7137,7 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro The second preset operator in the app! + Der zweite voreingestellte Netzwerk-Betreiber in der App! No comment provided by engineer. @@ -7054,6 +7157,7 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro The servers for new files of your current chat profile **%@**. + Die Server Deines aktuellen Chat-Profils für neue Dateien **%@**. No comment provided by engineer. @@ -7073,6 +7177,7 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro These conditions will also apply for: **%@**. + Diese Nutzungsbedingungen gelten auch für: **%@**. No comment provided by engineer. @@ -7177,6 +7282,7 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro To protect against your link being replaced, you can compare contact security codes. + Zum Schutz vor dem Austausch Ihres Links können Sie die Sicherheitscodes Ihrer Kontakte vergleichen. No comment provided by engineer. @@ -7203,6 +7309,7 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt To receive + Für den Empfang No comment provided by engineer. @@ -7227,6 +7334,7 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt To send + Für das Senden No comment provided by engineer. @@ -7236,6 +7344,7 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt To use the servers of **%@**, accept conditions of use. + Um die Server von **%@** zu nutzen, müssen Sie dessen Nutzungsbedingungen akzeptieren. No comment provided by engineer. @@ -7330,6 +7439,7 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt Undelivered messages + Nicht ausgelieferte Nachrichten No comment provided by engineer. @@ -7491,6 +7601,7 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Use %@ + Verwende %@ No comment provided by engineer. @@ -7520,10 +7631,12 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Use for files + Für Dateien verwenden No comment provided by engineer. Use for messages + Für Nachrichten verwenden No comment provided by engineer. @@ -7568,6 +7681,7 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Use servers + Verwende Server No comment provided by engineer. @@ -7662,6 +7776,7 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s View conditions + Nutzungsbedingungen anschauen No comment provided by engineer. @@ -7671,6 +7786,7 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s View updated conditions + Aktualisierte Nutzungsbedingungen anschauen No comment provided by engineer. @@ -7785,6 +7901,7 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s When more than one operator is enabled, none of them has metadata to learn who communicates with whom. + Wenn mehrere Netzwerk-Betreiber aktiviert sind, hat keiner von ihnen Metadaten, um zu erfahren, wer mit wem kommuniziert. No comment provided by engineer. @@ -7882,6 +7999,10 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Sie sind bereits mit %@ verbunden. No comment provided by engineer. + + You are already connected with %@. + No comment provided by engineer. + You are already connecting to %@. Sie sind bereits mit %@ verbunden. @@ -7946,10 +8067,12 @@ Verbindungsanfrage wiederholen? You can configure operators in Network & servers settings. + Sie können die Betreiber in den Netzwerk- und Servereinstellungen konfigurieren. No comment provided by engineer. You can configure servers via settings. + Sie können die Server über die Einstellungen konfigurieren. No comment provided by engineer. @@ -7994,6 +8117,7 @@ Verbindungsanfrage wiederholen? You can set connection name, to remember who the link was shared with. + Sie können einen Verbindungsnamen festlegen, um sich zu merken, mit wem der Link geteilt wurde. No comment provided by engineer. @@ -8295,6 +8419,7 @@ Verbindungsanfrage wiederholen? Your servers + Ihre Server No comment provided by engineer. @@ -8719,6 +8844,7 @@ Verbindungsanfrage wiederholen? for better metadata privacy. + für einen besseren Metadatenschutz. No comment provided by engineer. @@ -9350,22 +9476,27 @@ Zuletzt empfangene Nachricht: %2$@ %d new events + %d neue Ereignisse notification body From: %@ + Von: %@ notification body New events + Neue Ereignisse notification New messages + Neue Nachrichten notification New messages in %d chats + Neue Nachrichten in %d Chats notification body diff --git a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff index 09a63ab3c4..72ad43f136 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff +++ b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff @@ -635,6 +635,11 @@ Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts. No comment provided by engineer. + + Add friends + Add friends + No comment provided by engineer. + Add profile Add profile @@ -650,6 +655,11 @@ Add servers by scanning QR codes. No comment provided by engineer. + + Add team members + Add team members + No comment provided by engineer. + Add to another device Add to another device @@ -660,6 +670,11 @@ Add welcome message No comment provided by engineer. + + Add your team members to the conversations. + Add your team members to the conversations. + No comment provided by engineer. + Added media & file servers Added media & file servers @@ -1230,6 +1245,11 @@ Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! No comment provided by engineer. + + Business address + Business address + 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). By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA). @@ -1372,6 +1392,16 @@ Change user profiles authentication reason + + Chat already exists + Chat already exists + No comment provided by engineer. + + + Chat already exists! + Chat already exists! + No comment provided by engineer. + Chat colors Chat colors @@ -7977,6 +8007,11 @@ To connect, please ask your contact to create another connection link and check You are already connected to %@. No comment provided by engineer. + + You are already connected with %@. + You are already connected with %@. + No comment provided by engineer. + You are already connecting to %@. You are already connecting to %@. diff --git a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff index 8d109187c2..cfffd783d9 100644 --- a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff +++ b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff @@ -114,10 +114,12 @@ %@ server + %@ servidor No comment provided by engineer. %@ servers + %@ servidores No comment provided by engineer. @@ -152,7 +154,7 @@ %d days - %d días + %d día(s) time interval @@ -177,37 +179,37 @@ %d hours - %d horas + %d hora(s) time interval %d messages not forwarded - %d mensajes no enviados + %d mensaje(s) no enviado(s) alert title %d min - %d minutos + %d minuto(s) time interval %d months - %d meses + %d mes(es) time interval %d sec - %d segundos + %d segundo(s) time interval %d skipped message(s) - %d mensaje(s) saltado(s + %d mensaje(s) omitido(s) integrity error chat item %d weeks - %d semanas + %d semana(s) time interval @@ -382,6 +384,7 @@ **Scan / Paste link**: to connect via a link you received. + **Escanear / Pegar enlace**: para conectar mediante un enlace recibido. No comment provided by engineer. @@ -492,10 +495,12 @@ 1-time link + Enlace de un uso No comment provided by engineer. 1-time link can be used *with one contact only* - share in person or via any messenger. + Los enlaces de un uso pueden ser usados *solamente con un contacto* - compártelos en persona o mediante cualquier aplicación de mensajería. No comment provided by engineer. @@ -586,6 +591,7 @@ Accept conditions + Aceptar condiciones No comment provided by engineer. @@ -606,6 +612,7 @@ Accepted conditions + Condiciones aceptadas No comment provided by engineer. @@ -628,6 +635,10 @@ Añade la dirección a tu perfil para que tus contactos puedan compartirla con otros. La actualización del perfil se enviará a tus contactos. No comment provided by engineer. + + Add friends + No comment provided by engineer. + Add profile Añadir perfil @@ -643,6 +654,10 @@ Añadir servidores mediante el escaneo de códigos QR. No comment provided by engineer. + + Add team members + No comment provided by engineer. + Add to another device Añadir a otro dispositivo @@ -653,12 +668,18 @@ Añadir mensaje de bienvenida No comment provided by engineer. + + Add your team members to the conversations. + No comment provided by engineer. + Added media & file servers + Servidores de archivos y multimedia añadidos No comment provided by engineer. Added message servers + Servidores de mensajes añadidos No comment provided by engineer. @@ -688,10 +709,12 @@ Address or 1-time link? + ¿Dirección o enlace de un uso? No comment provided by engineer. Address settings + Configuración de dirección No comment provided by engineer. @@ -741,6 +764,7 @@ All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages. + Todos los mensajes y archivos son enviados **cifrados de extremo a extremo** y con seguridad de cifrado postcuántico en mensajes directos. No comment provided by engineer. @@ -1218,6 +1242,10 @@ Búlgaro, Finlandés, Tailandés y Ucraniano - gracias a los usuarios y [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! No comment provided by engineer. + + Business address + 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). Mediante perfil (predeterminado) o [por conexión](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA). @@ -1357,8 +1385,17 @@ Change user profiles + Cambiar perfil de usuario authentication reason + + Chat already exists + No comment provided by engineer. + + + Chat already exists! + No comment provided by engineer. + Chat colors Colores del chat @@ -1441,10 +1478,12 @@ Check messages every 20 min. + Comprobar mensajes cada 20 min. No comment provided by engineer. Check messages when allowed. + Comprobar mensajes cuando se permita. No comment provided by engineer. @@ -1539,38 +1578,47 @@ Conditions accepted on: %@. + Condiciones aceptadas el: %@. No comment provided by engineer. Conditions are accepted for the operator(s): **%@**. + Las condiciones se han aceptado para el(los) operador(s): **%@**. No comment provided by engineer. Conditions are already accepted for following operator(s): **%@**. + Las condiciones ya se han aceptado para el/los siguiente(s) operador(s): **%@**. No comment provided by engineer. Conditions of use + Condiciones de uso No comment provided by engineer. Conditions will be accepted for enabled operators after 30 days. + Las condiciones de los operadores habilitados serán aceptadas después de 30 días. No comment provided by engineer. Conditions will be accepted for operator(s): **%@**. + Las condiciones serán aceptadas para el/los operador(es): **%@**. No comment provided by engineer. Conditions will be accepted for the operator(s): **%@**. + Las condiciones serán aceptadas para el/los operador(es): **%@**. No comment provided by engineer. Conditions will be accepted on: %@. + Las condiciones serán aceptadas el: %@. No comment provided by engineer. Conditions will be automatically accepted for enabled operators on: %@. + Las condiciones serán aceptadas automáticamente para los operadores habilitados el: %@. No comment provided by engineer. @@ -1769,6 +1817,7 @@ This is your own one-time link! Connection security + Seguridad de conexión No comment provided by engineer. @@ -1888,6 +1937,7 @@ This is your own one-time link! Create 1-time link + Crear enlace de un uso No comment provided by engineer. @@ -1977,6 +2027,7 @@ This is your own one-time link! Current conditions text couldn't be loaded, you can review conditions via this link: + El texto con las condiciones actuales no se ha podido cargar, puedes revisar las condiciones en el siguiente enlace: No comment provided by engineer. @@ -2346,6 +2397,7 @@ This is your own one-time link! Delivered even when Apple drops them. + Entregados incluso cuando Apple los descarta. No comment provided by engineer. @@ -2631,6 +2683,7 @@ This is your own one-time link! E2E encrypted notifications. + Notificaciones cifradas E2E. No comment provided by engineer. @@ -2655,6 +2708,7 @@ This is your own one-time link! Enable Flux + Habilitar Flux No comment provided by engineer. @@ -2864,6 +2918,7 @@ This is your own one-time link! Error accepting conditions + Error al aceptar las condiciones alert title @@ -2878,6 +2933,7 @@ This is your own one-time link! Error adding server + Error al añadir servidor alert title @@ -3022,6 +3078,7 @@ This is your own one-time link! Error loading servers + Error al cargar servidores alert title @@ -3081,6 +3138,7 @@ This is your own one-time link! Error saving servers + Error al guardar servidores alert title @@ -3155,6 +3213,7 @@ This is your own one-time link! Error updating server + Error al actualizar el servidor alert title @@ -3204,6 +3263,7 @@ This is your own one-time link! Errors in servers configuration. + Error en la configuración del servidor. servers error @@ -3410,6 +3470,7 @@ This is your own one-time link! For chat profile %@: + Para el perfil de chat %@: servers error @@ -3419,14 +3480,17 @@ This is your own one-time link! For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server. + Si por ejemplo tu contacto recibe los mensajes a través de un servidor de SimpleX Chat, tu aplicación los entregará a través de un servidor de Flux. No comment provided by engineer. For private routing + Para el enrutamiento privado No comment provided by engineer. For social media + Para redes sociales No comment provided by engineer. @@ -3740,10 +3804,12 @@ Error: %2$@ How it affects privacy + Cómo afecta a la privacidad No comment provided by engineer. How it helps privacy + Cómo ayuda a la privacidad No comment provided by engineer. @@ -4565,6 +4631,7 @@ This is your link for group %@! More reliable notifications + Notificaciones más fiables No comment provided by engineer. @@ -4604,6 +4671,7 @@ This is your link for group %@! Network decentralization + Descentralización de la red No comment provided by engineer. @@ -4618,6 +4686,7 @@ This is your link for group %@! Network operator + Operador de red No comment provided by engineer. @@ -4677,6 +4746,7 @@ This is your link for group %@! New events + Eventos nuevos notification @@ -4706,6 +4776,7 @@ This is your link for group %@! New server + Servidor nuevo No comment provided by engineer. @@ -4765,10 +4836,12 @@ This is your link for group %@! No media & file servers. + Ningún servidor de archivos y multimedia. servers error No message servers. + Ningún servidor de mensajes. servers error @@ -4793,6 +4866,7 @@ This is your link for group %@! No push server + Ningún servidor push No comment provided by engineer. @@ -4802,18 +4876,22 @@ This is your link for group %@! No servers for private message routing. + Ningún servidor para enrutamiento privado. servers error No servers to receive files. + Ningún servidor para recibir archivos. servers error No servers to receive messages. + Ningún servidor para recibir mensajes. servers error No servers to send files. + Ningún servidor para enviar archivos. servers error @@ -4848,6 +4926,7 @@ This is your link for group %@! Notifications privacy + Privacidad en las notificaciones No comment provided by engineer. @@ -4990,6 +5069,7 @@ Requiere activación de la VPN. Open changes + Abrir cambios No comment provided by engineer. @@ -5004,6 +5084,7 @@ Requiere activación de la VPN. Open conditions + Abrir condiciones No comment provided by engineer. @@ -5023,10 +5104,12 @@ Requiere activación de la VPN. Operator + Operador No comment provided by engineer. Operator server + Servidor del operador alert title @@ -5055,6 +5138,7 @@ Requiere activación de la VPN. Or to share privately + O para compartir en privado No comment provided by engineer. @@ -5275,6 +5359,7 @@ Error: %@ Preset servers + Servidores predefinidos No comment provided by engineer. @@ -5451,6 +5536,7 @@ Actívalo en ajustes de *Servidores y Redes*. Push Notifications + Notificaciones push No comment provided by engineer. @@ -5826,10 +5912,12 @@ Actívalo en ajustes de *Servidores y Redes*. Review conditions + Revisar condiciones No comment provided by engineer. Review later + Revisar más tarde No comment provided by engineer. @@ -5879,10 +5967,12 @@ Actívalo en ajustes de *Servidores y Redes*. Same conditions will apply to operator **%@**. + Las mismas condiciones se aplicarán al operador **%@**. No comment provided by engineer. Same conditions will apply to operator(s): **%@**. + Las mismas condiciones se aplicarán a el/los operador(es) **%@**. No comment provided by engineer. @@ -6288,6 +6378,7 @@ Actívalo en ajustes de *Servidores y Redes*. Server added to operator %@. + Servidor añadido al operador %@. alert message @@ -6307,14 +6398,17 @@ Actívalo en ajustes de *Servidores y Redes*. Server operator changed. + El operador del servidor ha cambiado. alert title Server operators + Operadores de servidores No comment provided by engineer. Server protocol changed. + El protocolo del servidor ha cambiado. alert title @@ -6445,10 +6539,12 @@ Actívalo en ajustes de *Servidores y Redes*. Share 1-time link with a friend + Compartir enlace de un uso con un amigo No comment provided by engineer. Share SimpleX address on social media. + Compartir dirección SimpleX en redes sociales. No comment provided by engineer. @@ -6458,6 +6554,7 @@ Actívalo en ajustes de *Servidores y Redes*. Share address publicly + Campartir dirección públicamente No comment provided by engineer. @@ -6582,10 +6679,12 @@ Actívalo en ajustes de *Servidores y Redes*. SimpleX address and 1-time links are safe to share via any messenger. + Compartir enlaces de un uso y direcciones SimpleX es seguro a través de cualquier medio. No comment provided by engineer. SimpleX address or 1-time link? + Dirección SimpleX o enlace de un uso? No comment provided by engineer. @@ -6681,6 +6780,8 @@ Actívalo en ajustes de *Servidores y Redes*. Some servers failed the test: %@ + Algunos servidores no han superado la prueba: +%@ alert message @@ -6951,6 +7052,7 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. The app protects your privacy by using different operators in each conversation. + La aplicación protege tu privacidad mediante el uso de diferentes operadores en cada conversación. No comment provided by engineer. @@ -6970,6 +7072,7 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. The connection reached the limit of undelivered messages, your contact may be offline. + La conexión ha alcanzado el límite de mensajes no entregados. es posible que tu contacto esté desconectado. No comment provided by engineer. @@ -7034,6 +7137,7 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. The second preset operator in the app! + El segundo operador predefinido! No comment provided by engineer. @@ -7053,6 +7157,7 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. The servers for new files of your current chat profile **%@**. + Los servidores para archivos nuevos en tu perfil actual **%@**. No comment provided by engineer. @@ -7072,6 +7177,7 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. These conditions will also apply for: **%@**. + Estas condiciones también se aplican para: **%@**. No comment provided by engineer. @@ -7176,6 +7282,7 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. To protect against your link being replaced, you can compare contact security codes. + Para protegerte contra una sustitución del enlace, puedes comparar los códigos de seguridad con tu contacto. No comment provided by engineer. @@ -7202,6 +7309,7 @@ Se te pedirá que completes la autenticación antes de activar esta función. To receive + Para recibir No comment provided by engineer. @@ -7226,6 +7334,7 @@ Se te pedirá que completes la autenticación antes de activar esta función. To send + Para enviar No comment provided by engineer. @@ -7235,6 +7344,7 @@ Se te pedirá que completes la autenticación antes de activar esta función. To use the servers of **%@**, accept conditions of use. + Para usar los servidores de **%@**, acepta las condiciones de uso. No comment provided by engineer. @@ -7329,6 +7439,7 @@ Se te pedirá que completes la autenticación antes de activar esta función. Undelivered messages + Mensajes no entregados No comment provided by engineer. @@ -7490,6 +7601,7 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión Use %@ + Usar %@ No comment provided by engineer. @@ -7519,10 +7631,12 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión Use for files + Usar para archivos No comment provided by engineer. Use for messages + Usar para mensajes No comment provided by engineer. @@ -7567,6 +7681,7 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión Use servers + Usar servidores No comment provided by engineer. @@ -7661,6 +7776,7 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión View conditions + Ver condiciones No comment provided by engineer. @@ -7670,6 +7786,7 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión View updated conditions + Ver condiciones actualizadas No comment provided by engineer. @@ -7784,6 +7901,7 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión When more than one operator is enabled, none of them has metadata to learn who communicates with whom. + Cuando está habilitado más de un operador, ninguno dispone de los metadatos para conocer quién se comunica con quién. No comment provided by engineer. @@ -7881,6 +7999,10 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión Ya estás conectado a %@. No comment provided by engineer. + + You are already connected with %@. + No comment provided by engineer. + You are already connecting to %@. Ya estás conectando con %@. @@ -7945,10 +8067,12 @@ Repeat join request? You can configure operators in Network & servers settings. + Puedes configurar los operadores desde Servidores y Redes. No comment provided by engineer. You can configure servers via settings. + Puedes configurar los servidores a través de su configuración. No comment provided by engineer. @@ -7993,6 +8117,7 @@ Repeat join request? You can set connection name, to remember who the link was shared with. + Puedes añadir un nombre a la conexión para recordar a quién corresponde. No comment provided by engineer. @@ -8294,6 +8419,7 @@ Repeat connection request? Your servers + Tus servidores No comment provided by engineer. @@ -8718,6 +8844,7 @@ Repeat connection request? for better metadata privacy. + para mayor privacidad de los metadatos. No comment provided by engineer. @@ -9349,22 +9476,27 @@ last received msg: %2$@ %d new events + %d evento(s) nuevo(s) notification body From: %@ + De: %@ notification body New events + Eventos nuevos notification New messages + Mensajes nuevos notification New messages in %d chats + Mensajes nuevos en %d chat(s) notification body diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff index 325732cb8d..763b502ddb 100644 --- a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff @@ -595,6 +595,10 @@ Lisää osoite profiiliisi, jotta kontaktisi voivat jakaa sen muiden kanssa. Profiilipäivitys lähetetään kontakteillesi. No comment provided by engineer. + + Add friends + No comment provided by engineer. + Add profile Lisää profiili @@ -610,6 +614,10 @@ Lisää palvelimia skannaamalla QR-koodeja. No comment provided by engineer. + + Add team members + No comment provided by engineer. + Add to another device Lisää toiseen laitteeseen @@ -620,6 +628,10 @@ Lisää tervetuloviesti No comment provided by engineer. + + Add your team members to the conversations. + No comment provided by engineer. + Added media & file servers No comment provided by engineer. @@ -1138,6 +1150,10 @@ Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! No comment provided by engineer. + + Business address + 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). Chat-profiilin mukaan (oletus) tai [yhteyden mukaan](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA). @@ -1270,6 +1286,14 @@ Change user profiles authentication reason + + Chat already exists + No comment provided by engineer. + + + Chat already exists! + No comment provided by engineer. + Chat colors No comment provided by engineer. @@ -7323,6 +7347,10 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Olet jo muodostanut yhteyden %@:n kanssa. No comment provided by engineer. + + You are already connected with %@. + No comment provided by engineer. + You are already connecting to %@. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff index 56c57a2237..d91ce3c106 100644 --- a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff @@ -628,6 +628,10 @@ Ajoutez une adresse à votre profil, afin que vos contacts puissent la partager avec d'autres personnes. La mise à jour du profil sera envoyée à vos contacts. No comment provided by engineer. + + Add friends + No comment provided by engineer. + Add profile Ajouter un profil @@ -643,6 +647,10 @@ Ajoutez des serveurs en scannant des codes QR. No comment provided by engineer. + + Add team members + No comment provided by engineer. + Add to another device Ajouter à un autre appareil @@ -653,6 +661,10 @@ Ajouter un message d'accueil No comment provided by engineer. + + Add your team members to the conversations. + No comment provided by engineer. + Added media & file servers No comment provided by engineer. @@ -1218,6 +1230,10 @@ Bulgare, finnois, thaïlandais et ukrainien - grâce aux utilisateurs et à [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat) ! No comment provided by engineer. + + Business address + 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). Par profil de chat (par défaut) ou [par connexion](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA). @@ -1359,6 +1375,14 @@ Change user profiles authentication reason + + Chat already exists + No comment provided by engineer. + + + Chat already exists! + No comment provided by engineer. + Chat colors Couleurs de chat @@ -7882,6 +7906,10 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Vous êtes déjà connecté·e à %@ via ce lien. No comment provided by engineer. + + You are already connected with %@. + No comment provided by engineer. + You are already connecting to %@. Vous êtes déjà en train de vous connecter à %@. diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff index b8b760b5a0..44750dfbb2 100644 --- a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff @@ -44,7 +44,7 @@ #secret# - #titkos# + #titok# No comment provided by engineer. @@ -114,10 +114,12 @@ %@ server + %@ kiszolgáló No comment provided by engineer. %@ servers + %@ kiszolgáló No comment provided by engineer. @@ -267,7 +269,7 @@ %lld new interface languages - %lld új nyelvi csomag + %lld új kezelőfelületi nyelv No comment provided by engineer. @@ -382,6 +384,7 @@ **Scan / Paste link**: to connect via a link you received. + **Hivatkozás beolvasása / beillesztése**: egy kapott hivatkozáson keresztüli kapcsolódáshoz. No comment provided by engineer. @@ -492,10 +495,12 @@ 1-time link + Egyszer használható meghívó-hivatkozás No comment provided by engineer. 1-time link can be used *with one contact only* - share in person or via any messenger. + Az egyszer használható meghívó-hivatkozás csak *egyetlen ismerőssel használható* - személyesen vagy bármilyen üzenetküldőn keresztül megosztható. No comment provided by engineer. @@ -586,6 +591,7 @@ Accept conditions + Feltételek elfogadása No comment provided by engineer. @@ -606,6 +612,7 @@ Accepted conditions + Elfogadott feltételek No comment provided by engineer. @@ -628,6 +635,10 @@ Cím hozzáadása a profilhoz, hogy az ismerősei megoszthassák másokkal. A profilfrissítés elküldésre kerül az ismerősei számára. No comment provided by engineer. + + Add friends + No comment provided by engineer. + Add profile Profil hozzáadása @@ -643,6 +654,10 @@ Kiszolgáló hozzáadása QR-kód beolvasásával. No comment provided by engineer. + + Add team members + No comment provided by engineer. + Add to another device Hozzáadás egy másik eszközhöz @@ -653,12 +668,18 @@ Üdvözlőüzenet hozzáadása No comment provided by engineer. + + Add your team members to the conversations. + No comment provided by engineer. + Added media & file servers + Hozzáadott média- és fájlkiszolgálók No comment provided by engineer. Added message servers + Hozzáadott üzenetkiszolgálók No comment provided by engineer. @@ -688,10 +709,12 @@ Address or 1-time link? + Cím vagy egyszer használható meghívó-hivatkozás? No comment provided by engineer. Address settings + Címbeállítások No comment provided by engineer. @@ -741,6 +764,7 @@ All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages. + Az összes üzenetet és fájlt **végpontok közötti titkosítással** küldi, a közvetlen üzenetekben pedig kvantumrezisztens biztonsággal. No comment provided by engineer. @@ -1218,6 +1242,10 @@ Bolgár, finn, thai és ukrán – köszönet a felhasználóknak és a [Weblate-nek](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! No comment provided by engineer. + + Business address + 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). A csevegési profillal (alapértelmezett), vagy a [kapcsolattal] (https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BÉTA). @@ -1357,8 +1385,17 @@ Change user profiles + Felhasználói profilok megváltoztatása authentication reason + + Chat already exists + No comment provided by engineer. + + + Chat already exists! + No comment provided by engineer. + Chat colors Csevegés színei @@ -1441,10 +1478,12 @@ Check messages every 20 min. + Üzenetek ellenőrzése 20 percenként. No comment provided by engineer. Check messages when allowed. + Üzenetek ellenőrzése, amikor engedélyezett. No comment provided by engineer. @@ -1539,38 +1578,47 @@ Conditions accepted on: %@. + Feltételek elfogadva ekkor: %@. No comment provided by engineer. Conditions are accepted for the operator(s): **%@**. + A következő üzemeltető(k) számára elfogadott feltételek: **%@**. No comment provided by engineer. Conditions are already accepted for following operator(s): **%@**. + A feltételek már el lettek fogadva a következő üzemeltető(k) számára: **%@**. No comment provided by engineer. Conditions of use + Használati feltételek No comment provided by engineer. Conditions will be accepted for enabled operators after 30 days. + A feltételek 30 nap elteltével lesznek elfogadva az engedélyezett üzemeltető számára. No comment provided by engineer. Conditions will be accepted for operator(s): **%@**. + A feltételek el lesznek fogadva a következő üzemeltető(k) számára: **%@**. No comment provided by engineer. Conditions will be accepted for the operator(s): **%@**. + A feltételek el lesznek fogadva a következő üzemeltető(k) számára: **%@**. No comment provided by engineer. Conditions will be accepted on: %@. + A feltételek ekkor lesznek elfogadva: %@. No comment provided by engineer. Conditions will be automatically accepted for enabled operators on: %@. + A feltételek automatikusan elfogadásra kerülnek az engedélyezett üzemeltető számára: %@. No comment provided by engineer. @@ -1669,7 +1717,7 @@ Ez az Ön SimpleX-címe! Connect to yourself? This is your own one-time link! Kapcsolódás saját magához? -Ez az Ön egyszer használható hivatkozása! +Ez az Ön egyszer használható meghívó-hivatkozása! No comment provided by engineer. @@ -1684,7 +1732,7 @@ Ez az Ön egyszer használható hivatkozása! Connect via one-time link - Kapcsolódás egyszer használható hivatkozáson keresztül + Kapcsolódás egyszer használható meghívó-hivatkozáson keresztül No comment provided by engineer. @@ -1769,6 +1817,7 @@ Ez az Ön egyszer használható hivatkozása! Connection security + Kapcsolatbiztonság No comment provided by engineer. @@ -1888,6 +1937,7 @@ Ez az Ön egyszer használható hivatkozása! Create 1-time link + Egyszer használható meghívó-hivatkozás létrehozása No comment provided by engineer. @@ -1977,6 +2027,7 @@ Ez az Ön egyszer használható hivatkozása! Current conditions text couldn't be loaded, you can review conditions via this link: + A jelenlegi feltételek szövegét nem lehetett betölteni, a feltételeket ezen a hivatkozáson keresztül vizsgálhatja felül: No comment provided by engineer. @@ -2346,6 +2397,7 @@ Ez az Ön egyszer használható hivatkozása! Delivered even when Apple drops them. + Kézbesítés akkor is, amikor az Apple eldobja őket. No comment provided by engineer. @@ -2631,6 +2683,7 @@ Ez az Ön egyszer használható hivatkozása! E2E encrypted notifications. + Végpontok közötti titkosított értesítések. No comment provided by engineer. @@ -2655,6 +2708,7 @@ Ez az Ön egyszer használható hivatkozása! Enable Flux + Flux engedélyezése No comment provided by engineer. @@ -2864,6 +2918,7 @@ Ez az Ön egyszer használható hivatkozása! Error accepting conditions + Hiba a feltételek elfogadásakor alert title @@ -2878,6 +2933,7 @@ Ez az Ön egyszer használható hivatkozása! Error adding server + Hiba a kiszolgáló hozzáadásakor alert title @@ -3022,6 +3078,7 @@ Ez az Ön egyszer használható hivatkozása! Error loading servers + Hiba a kiszolgálók betöltésekor alert title @@ -3081,6 +3138,7 @@ Ez az Ön egyszer használható hivatkozása! Error saving servers + Hiba a kiszolgálók mentésekor alert title @@ -3155,6 +3213,7 @@ Ez az Ön egyszer használható hivatkozása! Error updating server + Hiba a kiszolgáló frissítésekor alert title @@ -3204,6 +3263,7 @@ Ez az Ön egyszer használható hivatkozása! Errors in servers configuration. + Hibák a kiszolgálók konfigurációjában. servers error @@ -3410,6 +3470,7 @@ Ez az Ön egyszer használható hivatkozása! For chat profile %@: + A(z) %@ nevű csevegési profilhoz: servers error @@ -3419,14 +3480,17 @@ Ez az Ön egyszer használható hivatkozása! For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server. + Ha például az ismerőse a SimpleX Chat kiszolgálón keresztül fogadja az üzeneteket, az Ön alkalmazása a Flux egyik kiszolgálóját használja a kézbesítéshez. No comment provided by engineer. For private routing + A privát útválasztáshoz No comment provided by engineer. For social media + A közösségi médiához No comment provided by engineer. @@ -3740,10 +3804,12 @@ Hiba: %2$@ How it affects privacy + Hogyan érinti az adatvédelmet No comment provided by engineer. How it helps privacy + Hogyan segíti az adatvédelmet No comment provided by engineer. @@ -3972,7 +4038,7 @@ További fejlesztések hamarosan! Interface - Felület + Kezelőfelület No comment provided by engineer. @@ -4062,7 +4128,7 @@ További fejlesztések hamarosan! It allows having many anonymous connections without any shared data between them in a single chat profile. - Lehetővé teszi, hogy egyetlen csevegőprofilon belül több anonim kapcsolat legyen, anélkül, hogy megosztott adatok lennének közöttük. + Lehetővé teszi, hogy egyetlen csevegőprofilon belül több névtelen kapcsolat legyen, anélkül, hogy megosztott adatok lennének közöttük. No comment provided by engineer. @@ -4565,6 +4631,7 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! More reliable notifications + Megbízhatóbb értesítések No comment provided by engineer. @@ -4604,6 +4671,7 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! Network decentralization + Hálózati decentralizáció No comment provided by engineer. @@ -4618,6 +4686,7 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! Network operator + Hálózati üzemeltető No comment provided by engineer. @@ -4677,6 +4746,7 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! New events + Új események notification @@ -4706,6 +4776,7 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! New server + Új kiszolgáló No comment provided by engineer. @@ -4765,10 +4836,12 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! No media & file servers. + Nincsenek média- és fájlkiszolgálók. servers error No message servers. + Nincsenek üzenet-kiszolgálók. servers error @@ -4803,18 +4876,22 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! No servers for private message routing. + Nincsenek kiszolgálók a privát üzenet-útválasztáshoz. servers error No servers to receive files. + Nincsenek fájlfogadó-kiszolgálók. servers error No servers to receive messages. + Nincsenek üzenetfogadó-kiszolgálók. servers error No servers to send files. + Nincsenek fájlküldő-kiszolgálók. servers error @@ -4849,6 +4926,7 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! Notifications privacy + Értesítési adatvédelem No comment provided by engineer. @@ -4991,6 +5069,7 @@ VPN engedélyezése szükséges. Open changes + Változások megnyitása No comment provided by engineer. @@ -5005,6 +5084,7 @@ VPN engedélyezése szükséges. Open conditions + Feltételek megnyitása No comment provided by engineer. @@ -5024,10 +5104,12 @@ VPN engedélyezése szükséges. Operator + Üzemeltető No comment provided by engineer. Operator server + Kiszolgáló üzemeltető alert title @@ -5056,6 +5138,7 @@ VPN engedélyezése szükséges. Or to share privately + Vagy a privát megosztáshoz No comment provided by engineer. @@ -5276,6 +5359,7 @@ Hiba: %@ Preset servers + Előre beállított kiszolgálók No comment provided by engineer. @@ -5452,6 +5536,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Push Notifications + Push értesítések No comment provided by engineer. @@ -5827,10 +5912,12 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Review conditions + Feltételek felülvizsgálata No comment provided by engineer. Review later + Felülvizsgálat később No comment provided by engineer. @@ -5880,10 +5967,12 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Same conditions will apply to operator **%@**. + Ugyanezek a feltételek vonatkoznak a következő üzemeltetőre is: **%@**. No comment provided by engineer. Same conditions will apply to operator(s): **%@**. + Ugyanezek a feltételek lesznek elfogadva a következő üzemeltető(k)re is: **%@**. No comment provided by engineer. @@ -6289,6 +6378,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Server added to operator %@. + Kiszolgáló hozzáadva a következő üzemeltetőhöz: %@. alert message @@ -6308,14 +6398,17 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Server operator changed. + A kiszolgáló üzemeltetője megváltozott. alert title Server operators + Kiszolgáló-üzemeltetők No comment provided by engineer. Server protocol changed. + A kiszolgáló-protokoll megváltozott. alert title @@ -6446,10 +6539,12 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Share 1-time link with a friend + Egyszer használható meghívó-hivatkozás megosztása egy baráttal No comment provided by engineer. Share SimpleX address on social media. + SimpleX-cím megosztása a közösségi médiában. No comment provided by engineer. @@ -6459,6 +6554,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Share address publicly + Cím nyilvános megosztása No comment provided by engineer. @@ -6583,10 +6679,12 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. SimpleX address and 1-time links are safe to share via any messenger. + A SimpleX-cím és az egyszer használható meghívó-hivatkozás biztonságosan megosztható bármilyen üzenetküldőn keresztül. No comment provided by engineer. SimpleX address or 1-time link? + SimpleX-cím vagy egyszer használható meghívó-hivatkozás? No comment provided by engineer. @@ -6621,12 +6719,12 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. SimpleX one-time invitation - Egyszer használható SimpleX-meghívó + Egyszer használható SimpleX-meghívó-hivatkozás simplex link type SimpleX protocols reviewed by Trail of Bits. - A SimpleX Chat biztonsága a Trail of Bits által lett újraauditálva. + A SimpleX Chat biztonsága a Trail of Bits által lett felülvizsgálva. No comment provided by engineer. @@ -6682,6 +6780,8 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Some servers failed the test: %@ + Néhány kiszolgáló megbukott a teszten: +%@ alert message @@ -6811,7 +6911,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Switch chat profile for 1-time invitations. - Csevegési profilváltás az egyszer használható meghívókhoz. + Csevegési profilváltás az egyszer használható meghívó-hivatkozásokhoz. No comment provided by engineer. @@ -6952,6 +7052,7 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. The app protects your privacy by using different operators in each conversation. + Az alkalmazás úgy védi az adatait, hogy minden egyes beszélgetésben más-más üzemeltetőket használ. No comment provided by engineer. @@ -6971,6 +7072,7 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. The connection reached the limit of undelivered messages, your contact may be offline. + A kapcsolat elérte a kézbesítetlen üzenetek számának határát, az Ön ismerőse lehet, hogy offline állapotban van. No comment provided by engineer. @@ -7035,6 +7137,7 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. The second preset operator in the app! + A második előre beállított üzemeltető az alkalmazásban! No comment provided by engineer. @@ -7054,6 +7157,7 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. The servers for new files of your current chat profile **%@**. + Az Ön jelenlegi **%@** nevű csevegőprofiljához tartozó új fájlok kiszolgálói. No comment provided by engineer. @@ -7073,6 +7177,7 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. These conditions will also apply for: **%@**. + Ezek a feltételek lesznek elfogadva a következő számára is: **%@**. No comment provided by engineer. @@ -7137,7 +7242,7 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. This is your own one-time link! - Ez az Ön egyszer használható hivatkozása! + Ez az Ön egyszer használható meghívó-hivatkozása! No comment provided by engineer. @@ -7177,6 +7282,7 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. To protect against your link being replaced, you can compare contact security codes. + A hivatkozás cseréje elleni védelem érdekében összehasonlíthatja a biztonsági kódokat az ismerősével. No comment provided by engineer. @@ -7203,6 +7309,7 @@ A funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beáll To receive + A fogadáshoz No comment provided by engineer. @@ -7227,6 +7334,7 @@ A funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beáll To send + A küldéshez No comment provided by engineer. @@ -7236,6 +7344,7 @@ A funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beáll To use the servers of **%@**, accept conditions of use. + A(z) **%@** kiszolgálóinak használatához fogadja el a használati feltételeket. No comment provided by engineer. @@ -7330,6 +7439,7 @@ A funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beáll Undelivered messages + Kézbesítetlen üzenetek No comment provided by engineer. @@ -7491,6 +7601,7 @@ A kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapc Use %@ + %@ használata No comment provided by engineer. @@ -7520,10 +7631,12 @@ A kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapc Use for files + Használat a fájlokhoz No comment provided by engineer. Use for messages + Használat az üzenetekhez No comment provided by engineer. @@ -7538,7 +7651,7 @@ A kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapc Use iOS call interface - Az iOS hívófelület használata + Az iOS hívási felületét használata No comment provided by engineer. @@ -7568,6 +7681,7 @@ A kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapc Use servers + Kiszolgálók használata No comment provided by engineer. @@ -7662,6 +7776,7 @@ A kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapc View conditions + Feltételek megtekintése No comment provided by engineer. @@ -7671,6 +7786,7 @@ A kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapc View updated conditions + Frissített feltételek megtekintése No comment provided by engineer. @@ -7785,6 +7901,7 @@ A kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapc When more than one operator is enabled, none of them has metadata to learn who communicates with whom. + Amikor egynél több hálózati üzemeltető van engedélyezve, egyikük sem rendelkezik olyan metaadatokkal ahhoz, hogy felderítse, ki kommunikál kivel. No comment provided by engineer. @@ -7882,6 +7999,10 @@ A kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapc Ön már kapcsolódva van ehhez: %@. No comment provided by engineer. + + You are already connected with %@. + No comment provided by engineer. + You are already connecting to %@. Már folyamatban van a kapcsolódás ehhez: %@. @@ -7889,7 +8010,7 @@ A kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapc You are already connecting via this one-time link! - A kapcsolódás már folyamatban van ezen az egyszer használható hivatkozáson keresztül! + A kapcsolódás már folyamatban van ezen az egyszer használható meghívó-hivatkozáson keresztül! No comment provided by engineer. @@ -7946,10 +8067,12 @@ Csatlakozáskérés megismétlése? You can configure operators in Network & servers settings. + Az üzemeltetőket a „Hálózat és kiszolgálók” beállításaban konfigurálhatja. No comment provided by engineer. You can configure servers via settings. + A kiszolgálókat a beállításokon keresztül konfigurálhatja. No comment provided by engineer. @@ -7994,6 +8117,7 @@ Csatlakozáskérés megismétlése? You can set connection name, to remember who the link was shared with. + Beállíthatja az ismerős nevét, hogy emlékezzen arra, hogy kivel osztotta meg a hivatkozást. No comment provided by engineer. @@ -8295,6 +8419,7 @@ Kapcsolatkérés megismétlése? Your servers + Az Ön kiszolgálói No comment provided by engineer. @@ -8719,6 +8844,7 @@ Kapcsolatkérés megismétlése? for better metadata privacy. + a metaadatok jobb védelme érdekében. No comment provided by engineer. @@ -8768,7 +8894,7 @@ Kapcsolatkérés megismétlése? incognito via one-time link - inkognitó egy egyszer használható hivatkozáson keresztül + inkognitó egy egyszer használható meghívó-hivatkozáson keresztül chat list item description @@ -8903,7 +9029,7 @@ Kapcsolatkérés megismétlése? new message - Rejtett üzenet + új üzenet notification @@ -9159,7 +9285,7 @@ utoljára fogadott üzenet: %2$@ via one-time link - egyszer használható hivatkozáson keresztül + egyszer használható meghívó-hivatkozáson keresztül chat list item description @@ -9259,12 +9385,12 @@ utoljára fogadott üzenet: %2$@ you shared one-time link - egyszer használható hivatkozást osztott meg + Ön egy egyszer használható meghívó-hivatkozást osztott meg chat list item description you shared one-time link incognito - egyszer használható hivatkozást osztott meg inkognitóban + Ön egy egyszer használható meghívó-hivatkozást osztott meg inkognitóban chat list item description @@ -9350,22 +9476,27 @@ utoljára fogadott üzenet: %2$@ %d new events + %d új esemény notification body From: %@ + Tőle: %@ notification body New events + Új események notification New messages + Új üzenetek notification New messages in %d chats + Új üzenetek %d csevegésben notification body diff --git a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff index 55eee758d5..8e54ba40dd 100644 --- a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff +++ b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff @@ -114,10 +114,12 @@ %@ server + %@ server No comment provided by engineer. %@ servers + %@ server No comment provided by engineer. @@ -382,6 +384,7 @@ **Scan / Paste link**: to connect via a link you received. + **Scansiona / Incolla link**: per connetterti tramite un link che hai ricevuto. No comment provided by engineer. @@ -492,10 +495,12 @@ 1-time link + Link una tantum No comment provided by engineer. 1-time link can be used *with one contact only* - share in person or via any messenger. + Il link una tantum può essere usato *con un solo contatto* - condividilo di persona o tramite qualsiasi messenger. No comment provided by engineer. @@ -586,6 +591,7 @@ Accept conditions + Accetta le condizioni No comment provided by engineer. @@ -606,6 +612,7 @@ Accepted conditions + Condizioni accettate No comment provided by engineer. @@ -628,6 +635,10 @@ Aggiungi l'indirizzo al tuo profilo, in modo che i tuoi contatti possano condividerlo con altre persone. L'aggiornamento del profilo verrà inviato ai tuoi contatti. No comment provided by engineer. + + Add friends + No comment provided by engineer. + Add profile Aggiungi profilo @@ -643,6 +654,10 @@ Aggiungi server scansionando codici QR. No comment provided by engineer. + + Add team members + No comment provided by engineer. + Add to another device Aggiungi ad un altro dispositivo @@ -653,12 +668,18 @@ Aggiungi messaggio di benvenuto No comment provided by engineer. + + Add your team members to the conversations. + No comment provided by engineer. + Added media & file servers + Server di multimediali e file aggiunti No comment provided by engineer. Added message servers + Server dei messaggi aggiunti No comment provided by engineer. @@ -688,10 +709,12 @@ Address or 1-time link? + Indirizzo o link una tantum? No comment provided by engineer. Address settings + Impostazioni dell'indirizzo No comment provided by engineer. @@ -741,6 +764,7 @@ All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages. + Tutti i messaggi e i file vengono inviati **crittografati end-to-end**, con sicurezza resistenti alla quantistica nei messaggi diretti. No comment provided by engineer. @@ -1218,6 +1242,10 @@ Bulgaro, finlandese, tailandese e ucraino - grazie agli utenti e a [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! No comment provided by engineer. + + Business address + 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). Per profilo di chat (predefinito) o [per connessione](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA). @@ -1357,8 +1385,17 @@ Change user profiles + Modifica profili utente authentication reason + + Chat already exists + No comment provided by engineer. + + + Chat already exists! + No comment provided by engineer. + Chat colors Colori della chat @@ -1441,10 +1478,12 @@ Check messages every 20 min. + Controlla i messaggi ogni 20 min. No comment provided by engineer. Check messages when allowed. + Controlla i messaggi quando consentito. No comment provided by engineer. @@ -1539,38 +1578,47 @@ Conditions accepted on: %@. + Condizioni accettate il: %@. No comment provided by engineer. Conditions are accepted for the operator(s): **%@**. + Le condizioni sono state accettate per gli operatori: **%@**. No comment provided by engineer. Conditions are already accepted for following operator(s): **%@**. + Le condizioni sono già state accettate per i seguenti operatori: **%@**. No comment provided by engineer. Conditions of use + Condizioni d'uso No comment provided by engineer. Conditions will be accepted for enabled operators after 30 days. + Le condizioni verranno accettate per gli operatori attivati dopo 30 giorni. No comment provided by engineer. Conditions will be accepted for operator(s): **%@**. + Le condizioni verranno accettate per gli operatori: **%@**. No comment provided by engineer. Conditions will be accepted for the operator(s): **%@**. + Le condizioni verranno accettate per gli operatori: **%@**. No comment provided by engineer. Conditions will be accepted on: %@. + Le condizioni verranno accettate il: %@. No comment provided by engineer. Conditions will be automatically accepted for enabled operators on: %@. + Le condizioni verranno accettate automaticamente per gli operatori attivi il: %@. No comment provided by engineer. @@ -1769,6 +1817,7 @@ Questo è il tuo link una tantum! Connection security + Sicurezza della connessione No comment provided by engineer. @@ -1888,6 +1937,7 @@ Questo è il tuo link una tantum! Create 1-time link + Crea link una tantum No comment provided by engineer. @@ -1977,6 +2027,7 @@ Questo è il tuo link una tantum! Current conditions text couldn't be loaded, you can review conditions via this link: + Il testo delle condizioni attuali testo non è stato caricato, puoi consultare le condizioni tramite questo link: No comment provided by engineer. @@ -2346,6 +2397,7 @@ Questo è il tuo link una tantum! Delivered even when Apple drops them. + Consegnati anche quando Apple li scarta. No comment provided by engineer. @@ -2631,6 +2683,7 @@ Questo è il tuo link una tantum! E2E encrypted notifications. + Notifiche crittografate E2E. No comment provided by engineer. @@ -2655,6 +2708,7 @@ Questo è il tuo link una tantum! Enable Flux + Attiva Flux No comment provided by engineer. @@ -2864,6 +2918,7 @@ Questo è il tuo link una tantum! Error accepting conditions + Errore di accettazione delle condizioni alert title @@ -2878,6 +2933,7 @@ Questo è il tuo link una tantum! Error adding server + Errore di aggiunta del server alert title @@ -3022,6 +3078,7 @@ Questo è il tuo link una tantum! Error loading servers + Errore nel caricamento dei server alert title @@ -3081,6 +3138,7 @@ Questo è il tuo link una tantum! Error saving servers + Errore di salvataggio dei server alert title @@ -3155,6 +3213,7 @@ Questo è il tuo link una tantum! Error updating server + Errore di aggiornamento del server alert title @@ -3204,6 +3263,7 @@ Questo è il tuo link una tantum! Errors in servers configuration. + Errori nella configurazione dei server. servers error @@ -3410,6 +3470,7 @@ Questo è il tuo link una tantum! For chat profile %@: + Per il profilo di chat %@: servers error @@ -3419,14 +3480,17 @@ Questo è il tuo link una tantum! For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server. + Ad esempio, se il tuo contatto riceve messaggi tramite un server di SimpleX Chat, la tua app li consegnerà tramite un server Flux. No comment provided by engineer. For private routing + Per l'instradamento privato No comment provided by engineer. For social media + Per i social media No comment provided by engineer. @@ -3740,10 +3804,12 @@ Errore: %2$@ How it affects privacy + Come influisce sulla privacy No comment provided by engineer. How it helps privacy + Come aiuta la privacy No comment provided by engineer. @@ -4565,6 +4631,7 @@ Questo è il tuo link per il gruppo %@! More reliable notifications + Notifiche più affidabili No comment provided by engineer. @@ -4604,6 +4671,7 @@ Questo è il tuo link per il gruppo %@! Network decentralization + Decentralizzazione della rete No comment provided by engineer. @@ -4618,6 +4686,7 @@ Questo è il tuo link per il gruppo %@! Network operator + Operatore di rete No comment provided by engineer. @@ -4677,6 +4746,7 @@ Questo è il tuo link per il gruppo %@! New events + Nuovi eventi notification @@ -4706,6 +4776,7 @@ Questo è il tuo link per il gruppo %@! New server + Nuovo server No comment provided by engineer. @@ -4765,10 +4836,12 @@ Questo è il tuo link per il gruppo %@! No media & file servers. + Nessun server di multimediali e file. servers error No message servers. + Nessun server dei messaggi. servers error @@ -4803,18 +4876,22 @@ Questo è il tuo link per il gruppo %@! No servers for private message routing. + Nessun server per l'instradamento dei messaggi privati. servers error No servers to receive files. + Nessun server per ricevere file. servers error No servers to receive messages. + Nessun server per ricevere messaggi. servers error No servers to send files. + Nessun server per inviare file. servers error @@ -4849,6 +4926,7 @@ Questo è il tuo link per il gruppo %@! Notifications privacy + Privacy delle notifiche No comment provided by engineer. @@ -4991,6 +5069,7 @@ Richiede l'attivazione della VPN. Open changes + Apri le modifiche No comment provided by engineer. @@ -5005,6 +5084,7 @@ Richiede l'attivazione della VPN. Open conditions + Apri le condizioni No comment provided by engineer. @@ -5024,10 +5104,12 @@ Richiede l'attivazione della VPN. Operator + Operatore No comment provided by engineer. Operator server + Server dell'operatore alert title @@ -5056,6 +5138,7 @@ Richiede l'attivazione della VPN. Or to share privately + O per condividere in modo privato No comment provided by engineer. @@ -5276,6 +5359,7 @@ Errore: %@ Preset servers + Server preimpostati No comment provided by engineer. @@ -5452,6 +5536,7 @@ Attivalo nelle impostazioni *Rete e server*. Push Notifications + Notifiche push No comment provided by engineer. @@ -5827,10 +5912,12 @@ Attivalo nelle impostazioni *Rete e server*. Review conditions + Esamina le condizioni No comment provided by engineer. Review later + Esamina più tardi No comment provided by engineer. @@ -5880,10 +5967,12 @@ Attivalo nelle impostazioni *Rete e server*. Same conditions will apply to operator **%@**. + Le stesse condizioni si applicheranno all'operatore **%@**. No comment provided by engineer. Same conditions will apply to operator(s): **%@**. + Le stesse condizioni si applicheranno agli operatori **%@**. No comment provided by engineer. @@ -6289,6 +6378,7 @@ Attivalo nelle impostazioni *Rete e server*. Server added to operator %@. + Server aggiunto all'operatore %@. alert message @@ -6308,14 +6398,17 @@ Attivalo nelle impostazioni *Rete e server*. Server operator changed. + L'operatore del server è cambiato. alert title Server operators + Operatori server No comment provided by engineer. Server protocol changed. + Il protocollo del server è cambiato. alert title @@ -6446,10 +6539,12 @@ Attivalo nelle impostazioni *Rete e server*. Share 1-time link with a friend + Condividi link una tantum con un amico No comment provided by engineer. Share SimpleX address on social media. + Condividi indirizzo SimpleX sui social media. No comment provided by engineer. @@ -6459,6 +6554,7 @@ Attivalo nelle impostazioni *Rete e server*. Share address publicly + Condividi indirizzo pubblicamente No comment provided by engineer. @@ -6583,10 +6679,12 @@ Attivalo nelle impostazioni *Rete e server*. SimpleX address and 1-time links are safe to share via any messenger. + L'indirizzo SimpleX e i link una tantum sono sicuri da condividere tramite qualsiasi messenger. No comment provided by engineer. SimpleX address or 1-time link? + Indirizzo SimpleX o link una tantum? No comment provided by engineer. @@ -6682,6 +6780,8 @@ Attivalo nelle impostazioni *Rete e server*. Some servers failed the test: %@ + Alcuni server hanno fallito il test: +%@ alert message @@ -6952,6 +7052,7 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa. The app protects your privacy by using different operators in each conversation. + L'app protegge la tua privacy usando diversi operatori in ogni conversazione. No comment provided by engineer. @@ -6971,6 +7072,7 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa. The connection reached the limit of undelivered messages, your contact may be offline. + La connessione ha raggiunto il limite di messaggi non consegnati, il contatto potrebbe essere offline. No comment provided by engineer. @@ -7035,6 +7137,7 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa. The second preset operator in the app! + Il secondo operatore preimpostato nell'app! No comment provided by engineer. @@ -7054,6 +7157,7 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa. The servers for new files of your current chat profile **%@**. + I server per nuovi file del tuo profilo di chat attuale **%@**. No comment provided by engineer. @@ -7073,6 +7177,7 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa. These conditions will also apply for: **%@**. + Queste condizioni si applicheranno anche per: **%@**. No comment provided by engineer. @@ -7177,6 +7282,7 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa. To protect against your link being replaced, you can compare contact security codes. + Per proteggerti dalla sostituzione del tuo link, puoi confrontare i codici di sicurezza del contatto. No comment provided by engineer. @@ -7203,6 +7309,7 @@ Ti verrà chiesto di completare l'autenticazione prima di attivare questa funzio To receive + Per ricevere No comment provided by engineer. @@ -7227,6 +7334,7 @@ Ti verrà chiesto di completare l'autenticazione prima di attivare questa funzio To send + Per inviare No comment provided by engineer. @@ -7236,6 +7344,7 @@ Ti verrà chiesto di completare l'autenticazione prima di attivare questa funzio To use the servers of **%@**, accept conditions of use. + Per usare i server di **%@**, accetta le condizioni d'uso. No comment provided by engineer. @@ -7330,6 +7439,7 @@ Ti verrà chiesto di completare l'autenticazione prima di attivare questa funzio Undelivered messages + Messaggi non consegnati No comment provided by engineer. @@ -7491,6 +7601,7 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Use %@ + Usa %@ No comment provided by engineer. @@ -7520,10 +7631,12 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Use for files + Usa per i file No comment provided by engineer. Use for messages + Usa per i messaggi No comment provided by engineer. @@ -7568,6 +7681,7 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Use servers + Usa i server No comment provided by engineer. @@ -7662,6 +7776,7 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e View conditions + Vedi le condizioni No comment provided by engineer. @@ -7671,6 +7786,7 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e View updated conditions + Vedi le condizioni aggiornate No comment provided by engineer. @@ -7785,6 +7901,7 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e When more than one operator is enabled, none of them has metadata to learn who communicates with whom. + Quando più di un operatore è attivato, nessuno di essi ha metadati per scoprire chi comunica con chi. No comment provided by engineer. @@ -7882,6 +7999,10 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Sei già connesso/a a %@. No comment provided by engineer. + + You are already connected with %@. + No comment provided by engineer. + You are already connecting to %@. Ti stai già connettendo a %@. @@ -7946,10 +8067,12 @@ Ripetere la richiesta di ingresso? You can configure operators in Network & servers settings. + Puoi configurare gli operatori nelle impostazioni di rete e server. No comment provided by engineer. You can configure servers via settings. + Puoi configurare i server nelle impostazioni. No comment provided by engineer. @@ -7994,6 +8117,7 @@ Ripetere la richiesta di ingresso? You can set connection name, to remember who the link was shared with. + Puoi impostare il nome della connessione per ricordare con chi è stato condiviso il link. No comment provided by engineer. @@ -8295,6 +8419,7 @@ Ripetere la richiesta di connessione? Your servers + I tuoi server No comment provided by engineer. @@ -8719,6 +8844,7 @@ Ripetere la richiesta di connessione? for better metadata privacy. + per una migliore privacy dei metadati. No comment provided by engineer. @@ -9350,22 +9476,27 @@ ultimo msg ricevuto: %2$@ %d new events + %d nuovi eventi notification body From: %@ + Da: %@ notification body New events + Nuovi eventi notification New messages + Nuovi messaggi notification New messages in %d chats + Nuovi messaggi in %d chat notification body diff --git a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff index 6b833800c0..1a5ffdd680 100644 --- a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff +++ b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff @@ -612,6 +612,10 @@ プロフィールにアドレスを追加し、連絡先があなたのアドレスを他の人と共有できるようにします。プロフィールの更新は連絡先に送信されます。 No comment provided by engineer. + + Add friends + No comment provided by engineer. + Add profile プロフィールを追加 @@ -627,6 +631,10 @@ QRコードでサーバを追加する。 No comment provided by engineer. + + Add team members + No comment provided by engineer. + Add to another device 別の端末に追加 @@ -637,6 +645,10 @@ ウェルカムメッセージを追加 No comment provided by engineer. + + Add your team members to the conversations. + No comment provided by engineer. + Added media & file servers No comment provided by engineer. @@ -1162,6 +1174,10 @@ ブルガリア語、フィンランド語、タイ語、ウクライナ語 - ユーザーと [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)に感謝します! No comment provided by engineer. + + Business address + 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). チャット プロファイル経由 (デフォルト) または [接続経由](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA). @@ -1294,6 +1310,14 @@ Change user profiles authentication reason + + Chat already exists + No comment provided by engineer. + + + Chat already exists! + No comment provided by engineer. + Chat colors No comment provided by engineer. @@ -1360,6 +1384,7 @@ Chat theme + チャットテーマ No comment provided by engineer. @@ -1401,10 +1426,12 @@ Chunks deleted + チャンクが削除されました No comment provided by engineer. Chunks downloaded + チャンクがダウンロードされました No comment provided by engineer. @@ -1428,6 +1455,7 @@ Clear private notes? + プライベートノートを消しますか? No comment provided by engineer. @@ -1441,6 +1469,7 @@ Color mode + 色設定 No comment provided by engineer. @@ -1455,6 +1484,7 @@ Completed + 完了 No comment provided by engineer. @@ -1559,10 +1589,12 @@ Connect to desktop + デスクトップに接続 No comment provided by engineer. Connect to your friends faster. + 友達ともっと速くつながりましょう。 No comment provided by engineer. @@ -1599,22 +1631,27 @@ This is your own one-time link! Connected + 接続中 No comment provided by engineer. Connected desktop + デスクトップに接続済 No comment provided by engineer. Connected servers + 接続中のサーバ No comment provided by engineer. Connected to desktop + デスクトップに接続済 No comment provided by engineer. Connecting + 接続待ち No comment provided by engineer. @@ -1629,10 +1666,12 @@ This is your own one-time link! Connecting to contact, please wait or check later! + 連絡先に接続中です。しばらくお待ちいただくか、後で確認してください! No comment provided by engineer. Connecting to desktop + デスクトップに接続中 No comment provided by engineer. @@ -1642,6 +1681,7 @@ This is your own one-time link! Connection and servers status. + 接続とサーバーのステータス No comment provided by engineer. @@ -1669,6 +1709,7 @@ This is your own one-time link! Connection terminated + 接続停止 No comment provided by engineer. @@ -1882,6 +1923,7 @@ This is your own one-time link! Customize theme + カスタムテーマ No comment provided by engineer. @@ -1891,6 +1933,7 @@ This is your own one-time link! Dark mode colors + ダークモードカラー No comment provided by engineer. @@ -1993,6 +2036,7 @@ This is your own one-time link! Debug delivery + 配信のデバッグ No comment provided by engineer. @@ -2241,6 +2285,7 @@ This is your own one-time link! Desktop devices + デスクトップ機器 No comment provided by engineer. @@ -2270,6 +2315,7 @@ This is your own one-time link! Developer options + 開発者向けの設定 No comment provided by engineer. @@ -7343,6 +7389,10 @@ To connect, please ask your contact to create another connection link and check すでに %@ に接続されています。 No comment provided by engineer. + + You are already connected with %@. + No comment provided by engineer. + You are already connecting to %@. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff index 603db9d75a..50b3fdae3e 100644 --- a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff @@ -114,10 +114,12 @@ %@ server + %@ server No comment provided by engineer. %@ servers + %@ servers No comment provided by engineer. @@ -382,6 +384,7 @@ **Scan / Paste link**: to connect via a link you received. + **Link scannen/plakken**: om verbinding te maken via een link die u hebt ontvangen. No comment provided by engineer. @@ -492,10 +495,12 @@ 1-time link + Eenmalige link No comment provided by engineer. 1-time link can be used *with one contact only* - share in person or via any messenger. + Eenmalige link die *slechts met één contactpersoon* kan worden gebruikt - deel persoonlijk of via een messenger. No comment provided by engineer. @@ -586,6 +591,7 @@ Accept conditions + Accepteer voorwaarden No comment provided by engineer. @@ -606,6 +612,7 @@ Accepted conditions + Geaccepteerde voorwaarden No comment provided by engineer. @@ -628,6 +635,10 @@ Voeg een adres toe aan uw profiel, zodat uw contacten het met andere mensen kunnen delen. Profiel update wordt naar uw contacten verzonden. No comment provided by engineer. + + Add friends + No comment provided by engineer. + Add profile Profiel toevoegen @@ -643,6 +654,10 @@ Servers toevoegen door QR-codes te scannen. No comment provided by engineer. + + Add team members + No comment provided by engineer. + Add to another device Toevoegen aan een ander apparaat @@ -653,12 +668,18 @@ Welkom bericht toevoegen No comment provided by engineer. + + Add your team members to the conversations. + No comment provided by engineer. + Added media & file servers + Media- en bestandsservers toegevoegd No comment provided by engineer. Added message servers + Berichtservers toegevoegd No comment provided by engineer. @@ -688,10 +709,12 @@ Address or 1-time link? + Adres of eenmalige link? No comment provided by engineer. Address settings + Adres instellingen No comment provided by engineer. @@ -741,6 +764,7 @@ All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages. + Alle berichten en bestanden worden **end-to-end versleuteld** verzonden, met post-quantumbeveiliging in directe berichten. No comment provided by engineer. @@ -1218,6 +1242,10 @@ Bulgaars, Fins, Thais en Oekraïens - dankzij de gebruikers en [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! No comment provided by engineer. + + Business address + 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). Via chatprofiel (standaard) of [via verbinding](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA). @@ -1357,8 +1385,17 @@ Change user profiles + Gebruikersprofielen wijzigen authentication reason + + Chat already exists + No comment provided by engineer. + + + Chat already exists! + No comment provided by engineer. + Chat colors Chat kleuren @@ -1441,10 +1478,12 @@ Check messages every 20 min. + Controleer uw berichten elke 20 minuten. No comment provided by engineer. Check messages when allowed. + Controleer berichten indien toegestaan. No comment provided by engineer. @@ -1539,38 +1578,47 @@ Conditions accepted on: %@. + Voorwaarden geaccepteerd op: %@. No comment provided by engineer. Conditions are accepted for the operator(s): **%@**. + Voorwaarden worden geaccepteerd voor de operator(s): **%@**. No comment provided by engineer. Conditions are already accepted for following operator(s): **%@**. + Voorwaarden zijn reeds geaccepteerd voor de volgende operator(s): **%@**. No comment provided by engineer. Conditions of use + Gebruiksvoorwaarden No comment provided by engineer. Conditions will be accepted for enabled operators after 30 days. + Voor ingeschakelde operators worden de voorwaarden na 30 dagen geaccepteerd. No comment provided by engineer. Conditions will be accepted for operator(s): **%@**. + Voorwaarden worden geaccepteerd voor operator(s): **%@**. No comment provided by engineer. Conditions will be accepted for the operator(s): **%@**. + Voorwaarden worden geaccepteerd voor de operator(s): **%@**. No comment provided by engineer. Conditions will be accepted on: %@. + Voorwaarden worden geaccepteerd op: %@. No comment provided by engineer. Conditions will be automatically accepted for enabled operators on: %@. + Voorwaarden worden automatisch geaccepteerd voor ingeschakelde operators op: %@. No comment provided by engineer. @@ -1769,6 +1817,7 @@ Dit is uw eigen eenmalige link! Connection security + Beveiliging van de verbinding No comment provided by engineer. @@ -1888,6 +1937,7 @@ Dit is uw eigen eenmalige link! Create 1-time link + Eenmalige link maken No comment provided by engineer. @@ -1977,6 +2027,7 @@ Dit is uw eigen eenmalige link! Current conditions text couldn't be loaded, you can review conditions via this link: + De tekst van de huidige voorwaarden kon niet worden geladen. U kunt de voorwaarden bekijken via deze link: No comment provided by engineer. @@ -2346,6 +2397,7 @@ Dit is uw eigen eenmalige link! Delivered even when Apple drops them. + Geleverd ook als Apple ze verliest No comment provided by engineer. @@ -2631,6 +2683,7 @@ Dit is uw eigen eenmalige link! E2E encrypted notifications. + E2E versleutelde meldingen. No comment provided by engineer. @@ -2655,6 +2708,7 @@ Dit is uw eigen eenmalige link! Enable Flux + Flux inschakelen No comment provided by engineer. @@ -2864,6 +2918,7 @@ Dit is uw eigen eenmalige link! Error accepting conditions + Fout bij het accepteren van voorwaarden alert title @@ -2878,6 +2933,7 @@ Dit is uw eigen eenmalige link! Error adding server + Fout bij toevoegen server alert title @@ -3022,6 +3078,7 @@ Dit is uw eigen eenmalige link! Error loading servers + Fout bij het laden van servers alert title @@ -3081,6 +3138,7 @@ Dit is uw eigen eenmalige link! Error saving servers + Fout bij het opslaan van servers alert title @@ -3155,6 +3213,7 @@ Dit is uw eigen eenmalige link! Error updating server + Fout bij het updaten van de server alert title @@ -3204,6 +3263,7 @@ Dit is uw eigen eenmalige link! Errors in servers configuration. + Fouten in de serverconfiguratie. servers error @@ -3410,6 +3470,7 @@ Dit is uw eigen eenmalige link! For chat profile %@: + Voor chatprofiel %@: servers error @@ -3419,14 +3480,17 @@ Dit is uw eigen eenmalige link! For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server. + Als uw contactpersoon bijvoorbeeld berichten ontvangt via een SimpleX Chat-server, worden deze door uw app via een Flux-server verzonden. No comment provided by engineer. For private routing + Voor privé-routering No comment provided by engineer. For social media + Voor social media No comment provided by engineer. @@ -3740,10 +3804,12 @@ Fout: %2$@ How it affects privacy + Hoe het de privacy beïnvloedt No comment provided by engineer. How it helps privacy + Hoe het de privacy helpt No comment provided by engineer. @@ -4565,6 +4631,7 @@ Dit is jouw link voor groep %@! More reliable notifications + Betrouwbaardere meldingen No comment provided by engineer. @@ -4604,6 +4671,7 @@ Dit is jouw link voor groep %@! Network decentralization + Netwerk decentralisatie No comment provided by engineer. @@ -4618,6 +4686,7 @@ Dit is jouw link voor groep %@! Network operator + Netwerkbeheerder No comment provided by engineer. @@ -4677,6 +4746,7 @@ Dit is jouw link voor groep %@! New events + Nieuwe gebeurtenissen notification @@ -4706,6 +4776,7 @@ Dit is jouw link voor groep %@! New server + Nieuwe server No comment provided by engineer. @@ -4765,10 +4836,12 @@ Dit is jouw link voor groep %@! No media & file servers. + Geen media- en bestandsservers. servers error No message servers. + Geen berichtenservers. servers error @@ -4803,18 +4876,22 @@ Dit is jouw link voor groep %@! No servers for private message routing. + Geen servers voor het routeren van privéberichten. servers error No servers to receive files. + Geen servers om bestanden te ontvangen. servers error No servers to receive messages. + Geen servers om berichten te ontvangen. servers error No servers to send files. + Geen servers om bestanden te verzenden. servers error @@ -4849,6 +4926,7 @@ Dit is jouw link voor groep %@! Notifications privacy + Privacy van meldingen No comment provided by engineer. @@ -4991,6 +5069,7 @@ Vereist het inschakelen van VPN. Open changes + Wijzigingen openen No comment provided by engineer. @@ -5005,6 +5084,7 @@ Vereist het inschakelen van VPN. Open conditions + Open voorwaarden No comment provided by engineer. @@ -5024,10 +5104,12 @@ Vereist het inschakelen van VPN. Operator + Operator No comment provided by engineer. Operator server + Operatorserver alert title @@ -5056,6 +5138,7 @@ Vereist het inschakelen van VPN. Or to share privately + Of om privé te delen No comment provided by engineer. @@ -5276,6 +5359,7 @@ Fout: %@ Preset servers + Vooraf ingestelde servers No comment provided by engineer. @@ -5452,6 +5536,7 @@ Schakel dit in in *Netwerk en servers*-instellingen. Push Notifications + Pushmeldingen No comment provided by engineer. @@ -5827,10 +5912,12 @@ Schakel dit in in *Netwerk en servers*-instellingen. Review conditions + Voorwaarden bekijken No comment provided by engineer. Review later + Later beoordelen No comment provided by engineer. @@ -5880,10 +5967,12 @@ Schakel dit in in *Netwerk en servers*-instellingen. Same conditions will apply to operator **%@**. + Dezelfde voorwaarden gelden voor operator **%@**. No comment provided by engineer. Same conditions will apply to operator(s): **%@**. + Dezelfde voorwaarden gelden voor operator(s): **%@**. No comment provided by engineer. @@ -6289,6 +6378,7 @@ Schakel dit in in *Netwerk en servers*-instellingen. Server added to operator %@. + Server toegevoegd aan operator %@. alert message @@ -6308,14 +6398,17 @@ Schakel dit in in *Netwerk en servers*-instellingen. Server operator changed. + Serveroperator gewijzigd. alert title Server operators + Serverbeheerders No comment provided by engineer. Server protocol changed. + Serverprotocol gewijzigd. alert title @@ -6446,10 +6539,12 @@ Schakel dit in in *Netwerk en servers*-instellingen. Share 1-time link with a friend + Deel eenmalig een link met een vriend No comment provided by engineer. Share SimpleX address on social media. + Deel het SimpleX-adres op sociale media. No comment provided by engineer. @@ -6459,6 +6554,7 @@ Schakel dit in in *Netwerk en servers*-instellingen. Share address publicly + Adres openbaar delen No comment provided by engineer. @@ -6583,10 +6679,12 @@ Schakel dit in in *Netwerk en servers*-instellingen. SimpleX address and 1-time links are safe to share via any messenger. + SimpleX-adressen en eenmalige links kunnen veilig worden gedeeld via elke messenger. No comment provided by engineer. SimpleX address or 1-time link? + SimpleX adres of eenmalige link? No comment provided by engineer. @@ -6682,6 +6780,8 @@ Schakel dit in in *Netwerk en servers*-instellingen. Some servers failed the test: %@ + Sommige servers zijn niet geslaagd voor de test: +%@ alert message @@ -6952,6 +7052,7 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast. The app protects your privacy by using different operators in each conversation. + De app beschermt uw privacy door in elk gesprek andere operatoren te gebruiken. No comment provided by engineer. @@ -6971,6 +7072,7 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast. The connection reached the limit of undelivered messages, your contact may be offline. + De verbinding heeft de limiet van niet-afgeleverde berichten bereikt. Uw contactpersoon is mogelijk offline. No comment provided by engineer. @@ -7035,6 +7137,7 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast. The second preset operator in the app! + De tweede vooraf ingestelde operator in de app! No comment provided by engineer. @@ -7054,6 +7157,7 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast. The servers for new files of your current chat profile **%@**. + De servers voor nieuwe bestanden van uw huidige chatprofiel **%@**. No comment provided by engineer. @@ -7073,6 +7177,7 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast. These conditions will also apply for: **%@**. + Deze voorwaarden zijn ook van toepassing op: **%@**. No comment provided by engineer. @@ -7177,6 +7282,7 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast. To protect against your link being replaced, you can compare contact security codes. + Om te voorkomen dat uw link wordt vervangen, kunt u contactbeveiligingscodes vergelijken. No comment provided by engineer. @@ -7203,6 +7309,7 @@ U wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingesc To receive + Om te ontvangen No comment provided by engineer. @@ -7227,6 +7334,7 @@ U wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingesc To send + Om te verzenden No comment provided by engineer. @@ -7236,6 +7344,7 @@ U wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingesc To use the servers of **%@**, accept conditions of use. + Om de servers van **%@** te gebruiken, moet u de gebruiksvoorwaarden accepteren. No comment provided by engineer. @@ -7330,6 +7439,7 @@ U wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingesc Undelivered messages + Niet afgeleverde berichten No comment provided by engineer. @@ -7491,6 +7601,7 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Use %@ + Gebruik %@ No comment provided by engineer. @@ -7520,10 +7631,12 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Use for files + Gebruik voor bestanden No comment provided by engineer. Use for messages + Gebruik voor berichten No comment provided by engineer. @@ -7568,6 +7681,7 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Use servers + Gebruik servers No comment provided by engineer. @@ -7662,6 +7776,7 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak View conditions + Bekijk voorwaarden No comment provided by engineer. @@ -7671,6 +7786,7 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak View updated conditions + Bekijk de bijgewerkte voorwaarden No comment provided by engineer. @@ -7785,6 +7901,7 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak When more than one operator is enabled, none of them has metadata to learn who communicates with whom. + Wanneer er meer dan één operator is ingeschakeld, beschikt geen enkele operator over metagegevens om te achterhalen wie met wie communiceert. No comment provided by engineer. @@ -7882,6 +7999,10 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak U bent al verbonden met %@. No comment provided by engineer. + + You are already connected with %@. + No comment provided by engineer. + You are already connecting to %@. U maakt al verbinding met %@. @@ -7946,10 +8067,12 @@ Deelnameverzoek herhalen? You can configure operators in Network & servers settings. + U kunt operators configureren in Netwerk- en serverinstellingen. No comment provided by engineer. You can configure servers via settings. + U kunt servers configureren via instellingen. No comment provided by engineer. @@ -7994,6 +8117,7 @@ Deelnameverzoek herhalen? You can set connection name, to remember who the link was shared with. + U kunt een verbindingsnaam instellen, zodat u kunt onthouden met wie de link is gedeeld. No comment provided by engineer. @@ -8295,6 +8419,7 @@ Verbindingsverzoek herhalen? Your servers + Uw servers No comment provided by engineer. @@ -8719,6 +8844,7 @@ Verbindingsverzoek herhalen? for better metadata privacy. + voor betere privacy van metagegevens. No comment provided by engineer. @@ -9350,22 +9476,27 @@ laatst ontvangen bericht: %2$@ %d new events + ‐%d nieuwe gebeurtenissen notification body From: %@ + Van: %@ notification body New events + Nieuwe gebeurtenissen notification New messages + Nieuwe berichten notification New messages in %d chats + Nieuwe berichten in %d chats notification body diff --git a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff index 8a772bf470..0095b5e031 100644 --- a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff @@ -628,6 +628,10 @@ Dodaj adres do swojego profilu, aby Twoje kontakty mogły go udostępnić innym osobom. Aktualizacja profilu zostanie wysłana do Twoich kontaktów. No comment provided by engineer. + + Add friends + No comment provided by engineer. + Add profile Dodaj profil @@ -643,6 +647,10 @@ Dodaj serwery, skanując kody QR. No comment provided by engineer. + + Add team members + No comment provided by engineer. + Add to another device Dodaj do innego urządzenia @@ -653,6 +661,10 @@ Dodaj wiadomość powitalną No comment provided by engineer. + + Add your team members to the conversations. + No comment provided by engineer. + Added media & file servers No comment provided by engineer. @@ -1213,6 +1225,10 @@ Bułgarski, fiński, tajski i ukraiński – dzięki użytkownikom i [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! No comment provided by engineer. + + Business address + 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). @@ -1354,6 +1370,14 @@ Change user profiles authentication reason + + Chat already exists + No comment provided by engineer. + + + Chat already exists! + No comment provided by engineer. + Chat colors Kolory czatu @@ -7869,6 +7893,10 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Jesteś już połączony z %@. No comment provided by engineer. + + You are already connected with %@. + No comment provided by engineer. + You are already connecting to %@. Już się łączysz z %@. diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff index 9b6cbf519e..e7230dbcb2 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff @@ -628,6 +628,10 @@ Добавьте адрес в свой профиль, чтобы Ваши контакты могли поделиться им. Профиль будет отправлен Вашим контактам. No comment provided by engineer. + + Add friends + No comment provided by engineer. + Add profile Добавить профиль @@ -643,6 +647,10 @@ Добавить серверы через QR код. No comment provided by engineer. + + Add team members + No comment provided by engineer. + Add to another device Добавить на другое устройство @@ -653,6 +661,10 @@ Добавить приветственное сообщение No comment provided by engineer. + + Add your team members to the conversations. + No comment provided by engineer. + Added media & file servers No comment provided by engineer. @@ -1219,6 +1231,10 @@ Болгарский, финский, тайский и украинский - благодаря пользователям и [Weblate] (https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! No comment provided by engineer. + + Business address + 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). По профилю чата или [по соединению](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (БЕТА). @@ -1360,6 +1376,14 @@ Change user profiles authentication reason + + Chat already exists + No comment provided by engineer. + + + Chat already exists! + No comment provided by engineer. + Chat colors Цвета чата @@ -7882,6 +7906,10 @@ To connect, please ask your contact to create another connection link and check Вы уже соединены с контактом %@. No comment provided by engineer. + + You are already connected with %@. + No comment provided by engineer. + You are already connecting to %@. Вы уже соединяетесь с %@. diff --git a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff index 8066daf54d..f3097bf8f2 100644 --- a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff +++ b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff @@ -587,6 +587,10 @@ เพิ่มที่อยู่ลงในโปรไฟล์ของคุณ เพื่อให้ผู้ติดต่อของคุณสามารถแชร์กับผู้อื่นได้ การอัปเดตโปรไฟล์จะถูกส่งไปยังผู้ติดต่อของคุณ No comment provided by engineer. + + Add friends + No comment provided by engineer. + Add profile เพิ่มโปรไฟล์ @@ -602,6 +606,10 @@ เพิ่มเซิร์ฟเวอร์โดยการสแกนรหัสคิวอาร์โค้ด No comment provided by engineer. + + Add team members + No comment provided by engineer. + Add to another device เพิ่มเข้าไปในอุปกรณ์อื่น @@ -612,6 +620,10 @@ เพิ่มข้อความต้อนรับ No comment provided by engineer. + + Add your team members to the conversations. + No comment provided by engineer. + Added media & file servers No comment provided by engineer. @@ -1130,6 +1142,10 @@ Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! No comment provided by engineer. + + Business address + 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). ตามโปรไฟล์แชท (ค่าเริ่มต้น) หรือ [โดยการเชื่อมต่อ](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (เบต้า) @@ -1262,6 +1278,14 @@ Change user profiles authentication reason + + Chat already exists + No comment provided by engineer. + + + Chat already exists! + No comment provided by engineer. + Chat colors No comment provided by engineer. @@ -7293,6 +7317,10 @@ To connect, please ask your contact to create another connection link and check คุณได้เชื่อมต่อกับ %@ แล้ว No comment provided by engineer. + + You are already connected with %@. + No comment provided by engineer. + You are already connecting to %@. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff index f578a6225d..eca1c67b85 100644 --- a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff @@ -628,6 +628,10 @@ Kişilerinizin başkalarıyla paylaşabilmesi için profilinize adres ekleyin. Profil güncellemesi kişilerinize gönderilecek. No comment provided by engineer. + + Add friends + No comment provided by engineer. + Add profile Profil ekle @@ -643,6 +647,10 @@ Karekod taratarak sunucuları ekleyin. No comment provided by engineer. + + Add team members + No comment provided by engineer. + Add to another device Başka bir cihaza ekle @@ -653,6 +661,10 @@ Karşılama mesajı ekleyin No comment provided by engineer. + + Add your team members to the conversations. + No comment provided by engineer. + Added media & file servers No comment provided by engineer. @@ -1218,6 +1230,10 @@ Bulgarca, Fince, Tayca ve Ukraynaca - kullanıcılara ve [Weblate] e teşekkürler! (https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! No comment provided by engineer. + + Business address + 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). Sohbet profiline göre (varsayılan) veya [bağlantıya göre](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA). @@ -1359,6 +1375,14 @@ Change user profiles authentication reason + + Chat already exists + No comment provided by engineer. + + + Chat already exists! + No comment provided by engineer. + Chat colors Sohbet renkleri @@ -7882,6 +7906,10 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Zaten %@'a bağlısınız. No comment provided by engineer. + + You are already connected with %@. + No comment provided by engineer. + You are already connecting to %@. Zaten %@'a bağlanıyorsunuz. diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff index 136f45830b..0d05edbfbe 100644 --- a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff @@ -114,10 +114,12 @@ %@ server + %@ сервер No comment provided by engineer. %@ servers + %@ сервери No comment provided by engineer. @@ -132,6 +134,7 @@ %1$@, %2$@ + %1$@, %2$@ format for date separator in chat @@ -171,6 +174,7 @@ %d file(s) were not downloaded. + %d файл(и) не було завантажено. forward confirmation reason @@ -180,6 +184,7 @@ %d messages not forwarded + %d повідомлень не переслано alert title @@ -379,6 +384,7 @@ **Scan / Paste link**: to connect via a link you received. + **Відсканувати / Вставити посилання**: підключитися за отриманим посиланням. No comment provided by engineer. @@ -489,10 +495,12 @@ 1-time link + Одноразове посилання No comment provided by engineer. 1-time link can be used *with one contact only* - share in person or via any messenger. + Одноразове посилання можна використовувати *тільки з одним контактом* - поділіться ним особисто або через будь-який месенджер. No comment provided by engineer. @@ -583,6 +591,7 @@ Accept conditions + Прийняти умови No comment provided by engineer. @@ -603,6 +612,7 @@ Accepted conditions + Прийняті умови No comment provided by engineer. @@ -625,6 +635,10 @@ Додайте адресу до свого профілю, щоб ваші контакти могли поділитися нею з іншими людьми. Повідомлення про оновлення профілю буде надіслано вашим контактам. No comment provided by engineer. + + Add friends + No comment provided by engineer. + Add profile Додати профіль @@ -640,6 +654,10 @@ Додайте сервери, відсканувавши QR-код. No comment provided by engineer. + + Add team members + No comment provided by engineer. + Add to another device Додати до іншого пристрою @@ -650,12 +668,18 @@ Додати вітальне повідомлення No comment provided by engineer. + + Add your team members to the conversations. + No comment provided by engineer. + Added media & file servers + Додано медіа та файлові сервери No comment provided by engineer. Added message servers + Додано сервери повідомлень No comment provided by engineer. @@ -685,10 +709,12 @@ Address or 1-time link? + Адреса чи одноразове посилання? No comment provided by engineer. Address settings + Налаштування адреси No comment provided by engineer. @@ -738,6 +764,7 @@ All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages. + Всі повідомлення та файли надсилаються **наскрізним шифруванням**, з пост-квантовим захистом у прямих повідомленнях. No comment provided by engineer. @@ -957,6 +984,7 @@ App session + Сесія програми No comment provided by engineer. @@ -1066,6 +1094,7 @@ Auto-accept settings + Автоприйняття налаштувань alert title @@ -1095,6 +1124,7 @@ Better calls + Кращі дзвінки No comment provided by engineer. @@ -1104,6 +1134,7 @@ Better message dates. + Кращі дати повідомлень. No comment provided by engineer. @@ -1118,14 +1149,17 @@ Better notifications + Кращі сповіщення No comment provided by engineer. Better security ✅ + Краща безпека ✅ No comment provided by engineer. Better user experience + Покращений користувацький досвід No comment provided by engineer. @@ -1208,6 +1242,10 @@ Болгарською, фінською, тайською та українською мовами - завдяки користувачам та [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! No comment provided by engineer. + + Business address + 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). Через профіль чату (за замовчуванням) або [за з'єднанням](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA). @@ -1347,8 +1385,17 @@ Change user profiles + Зміна профілів користувачів authentication reason + + Chat already exists + No comment provided by engineer. + + + Chat already exists! + No comment provided by engineer. + Chat colors Кольори чату @@ -1411,6 +1458,7 @@ Chat preferences were changed. + Змінено налаштування чату. alert message @@ -1430,10 +1478,12 @@ Check messages every 20 min. + Перевіряйте повідомлення кожні 20 хв. No comment provided by engineer. Check messages when allowed. + Перевірте повідомлення, коли це дозволено. No comment provided by engineer. @@ -1528,38 +1578,47 @@ Conditions accepted on: %@. + Умови приймаються на: %@. No comment provided by engineer. Conditions are accepted for the operator(s): **%@**. + Для оператора(ів) приймаються умови: **%@**. No comment provided by engineer. Conditions are already accepted for following operator(s): **%@**. + Умови вже прийняті для наступних операторів: **%@**. No comment provided by engineer. Conditions of use + Умови використання No comment provided by engineer. Conditions will be accepted for enabled operators after 30 days. + Умови будуть прийняті для ввімкнених операторів через 30 днів. No comment provided by engineer. Conditions will be accepted for operator(s): **%@**. + Умови приймаються для оператора(ів): **%@**. No comment provided by engineer. Conditions will be accepted for the operator(s): **%@**. + Для оператора(ів) приймаються умови: **%@**. No comment provided by engineer. Conditions will be accepted on: %@. + Умови приймаються на: %@. No comment provided by engineer. Conditions will be automatically accepted for enabled operators on: %@. + Умови будуть автоматично прийняті для увімкнених операторів на: %@. No comment provided by engineer. @@ -1758,6 +1817,7 @@ This is your own one-time link! Connection security + Безпека з'єднання No comment provided by engineer. @@ -1862,6 +1922,7 @@ This is your own one-time link! Corner + Кут No comment provided by engineer. @@ -1876,6 +1937,7 @@ This is your own one-time link! Create 1-time link + Створити одноразове посилання No comment provided by engineer. @@ -1965,6 +2027,7 @@ This is your own one-time link! Current conditions text couldn't be loaded, you can review conditions via this link: + Текст поточних умов не вдалося завантажити, ви можете переглянути умови за цим посиланням: No comment provided by engineer. @@ -1989,6 +2052,7 @@ This is your own one-time link! Customizable message shape. + Налаштовується форма повідомлення. No comment provided by engineer. @@ -2278,6 +2342,7 @@ This is your own one-time link! Delete or moderate up to 200 messages. + Видалити або модерувати до 200 повідомлень. No comment provided by engineer. @@ -2332,6 +2397,7 @@ This is your own one-time link! Delivered even when Apple drops them. + Доставляються навіть тоді, коли Apple кидає їх. No comment provided by engineer. @@ -2536,6 +2602,7 @@ This is your own one-time link! Do not use credentials with proxy. + Не використовуйте облікові дані з проксі. No comment provided by engineer. @@ -2581,6 +2648,7 @@ This is your own one-time link! Download files + Завантажити файли alert action @@ -2615,6 +2683,7 @@ This is your own one-time link! E2E encrypted notifications. + Зашифровані сповіщення E2E. No comment provided by engineer. @@ -2639,6 +2708,7 @@ This is your own one-time link! Enable Flux + Увімкнути Flux No comment provided by engineer. @@ -2848,6 +2918,7 @@ This is your own one-time link! Error accepting conditions + Помилка прийняття умов alert title @@ -2862,6 +2933,7 @@ This is your own one-time link! Error adding server + Помилка додавання сервера alert title @@ -2871,6 +2943,7 @@ This is your own one-time link! Error changing connection profile + Помилка при зміні профілю з'єднання No comment provided by engineer. @@ -2885,6 +2958,7 @@ This is your own one-time link! Error changing to incognito! + Помилка переходу на інкогніто! No comment provided by engineer. @@ -3004,10 +3078,12 @@ This is your own one-time link! Error loading servers + Помилка завантаження серверів alert title Error migrating settings + Помилка міграції налаштувань No comment provided by engineer. @@ -3062,6 +3138,7 @@ This is your own one-time link! Error saving servers + Сервери збереження помилок alert title @@ -3111,6 +3188,7 @@ This is your own one-time link! Error switching profile + Помилка перемикання профілю No comment provided by engineer. @@ -3135,6 +3213,7 @@ This is your own one-time link! Error updating server + Помилка оновлення сервера alert title @@ -3184,6 +3263,7 @@ This is your own one-time link! Errors in servers configuration. + Помилки в конфігурації серверів. servers error @@ -3259,6 +3339,8 @@ This is your own one-time link! File errors: %@ + Помилки файлів: +%@ alert message @@ -3388,6 +3470,7 @@ This is your own one-time link! For chat profile %@: + Для профілю чату %@: servers error @@ -3397,14 +3480,17 @@ This is your own one-time link! For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server. + Наприклад, якщо ваш контакт отримує повідомлення через сервер SimpleX Chat, ваш додаток доставлятиме їх через сервер Flux. No comment provided by engineer. For private routing + Для приватної маршрутизації No comment provided by engineer. For social media + Для соціальних мереж No comment provided by engineer. @@ -3414,6 +3500,7 @@ This is your own one-time link! Forward %d message(s)? + Переслати %d повідомлення(ь)? alert title @@ -3423,14 +3510,17 @@ This is your own one-time link! Forward messages + Пересилання повідомлень alert action Forward messages without files? + Пересилати повідомлення без файлів? alert message Forward up to 20 messages at once. + Пересилайте до 20 повідомлень одночасно. No comment provided by engineer. @@ -3445,6 +3535,7 @@ This is your own one-time link! Forwarding %lld messages + Пересилання повідомлень %lld No comment provided by engineer. @@ -3713,10 +3804,12 @@ Error: %2$@ How it affects privacy + Як це впливає на конфіденційність No comment provided by engineer. How it helps privacy + Як це захищає приватність No comment provided by engineer. @@ -3746,6 +3839,7 @@ Error: %2$@ IP address + IP-адреса No comment provided by engineer. @@ -3826,6 +3920,8 @@ Error: %2$@ Improved delivery, reduced traffic usage. More improvements are coming soon! + Покращена доставка, зменшене використання трафіку. +Незабаром з'являться нові покращення! No comment provided by engineer. @@ -4380,6 +4476,7 @@ This is your link for group %@! Message shape + Форма повідомлення No comment provided by engineer. @@ -4434,6 +4531,7 @@ This is your link for group %@! Messages were deleted after you selected them. + Повідомлення були видалені після того, як ви їх вибрали. alert message @@ -4533,6 +4631,7 @@ This is your link for group %@! More reliable notifications + Більш надійні сповіщення No comment provided by engineer. @@ -4572,6 +4671,7 @@ This is your link for group %@! Network decentralization + Децентралізація мережі No comment provided by engineer. @@ -4586,6 +4686,7 @@ This is your link for group %@! Network operator + Мережевий оператор No comment provided by engineer. @@ -4605,10 +4706,12 @@ This is your link for group %@! New SOCKS credentials will be used every time you start the app. + Нові облікові дані SOCKS будуть використовуватися при кожному запуску програми. No comment provided by engineer. New SOCKS credentials will be used for each server. + Для кожного сервера будуть використовуватися нові облікові дані SOCKS. No comment provided by engineer. @@ -4643,6 +4746,7 @@ This is your link for group %@! New events + Нові події notification @@ -4672,6 +4776,7 @@ This is your link for group %@! New server + Новий сервер No comment provided by engineer. @@ -4731,10 +4836,12 @@ This is your link for group %@! No media & file servers. + Ніяких медіа та файлових серверів. servers error No message servers. + Ніяких серверів повідомлень. servers error @@ -4744,10 +4851,12 @@ This is your link for group %@! No permission to record speech + Немає дозволу на запис промови No comment provided by engineer. No permission to record video + Немає дозволу на запис відео No comment provided by engineer. @@ -4767,18 +4876,22 @@ This is your link for group %@! No servers for private message routing. + Немає серверів для маршрутизації приватних повідомлень. servers error No servers to receive files. + Немає серверів для отримання файлів. servers error No servers to receive messages. + Немає серверів для отримання повідомлень. servers error No servers to send files. + Немає серверів для надсилання файлів. servers error @@ -4798,6 +4911,7 @@ This is your link for group %@! Nothing to forward! + Нічого пересилати! alert title @@ -4812,6 +4926,7 @@ This is your link for group %@! Notifications privacy + Сповіщення про приватність No comment provided by engineer. @@ -4954,6 +5069,7 @@ Requires compatible VPN. Open changes + Відкриті зміни No comment provided by engineer. @@ -4968,6 +5084,7 @@ Requires compatible VPN. Open conditions + Відкриті умови No comment provided by engineer. @@ -4987,10 +5104,12 @@ Requires compatible VPN. Operator + Оператор No comment provided by engineer. Operator server + Сервер оператора alert title @@ -5019,6 +5138,7 @@ Requires compatible VPN. Or to share privately + Або поділитися приватно No comment provided by engineer. @@ -5029,6 +5149,8 @@ Requires compatible VPN. Other file errors: %@ + Інші помилки файлів: +%@ alert message @@ -5068,6 +5190,7 @@ Requires compatible VPN. Password + Пароль No comment provided by engineer. @@ -5216,6 +5339,7 @@ Error: %@ Port + Порт No comment provided by engineer. @@ -5235,6 +5359,7 @@ Error: %@ Preset servers + Попередньо встановлені сервери No comment provided by engineer. @@ -5406,10 +5531,12 @@ Enable in *Network & servers* settings. Proxy requires password + Проксі вимагає пароль No comment provided by engineer. Push Notifications + Push-сповіщення No comment provided by engineer. @@ -5630,6 +5757,7 @@ Enable in *Network & servers* settings. Remove archive? + Видалити архів? No comment provided by engineer. @@ -5784,10 +5912,12 @@ Enable in *Network & servers* settings. Review conditions + Умови перегляду No comment provided by engineer. Review later + Перегляньте пізніше No comment provided by engineer. @@ -5822,6 +5952,7 @@ Enable in *Network & servers* settings. SOCKS proxy + Проксі SOCKS No comment provided by engineer. @@ -5836,10 +5967,12 @@ Enable in *Network & servers* settings. Same conditions will apply to operator **%@**. + Такі ж умови діятимуть і для оператора **%@**. No comment provided by engineer. Same conditions will apply to operator(s): **%@**. + Такі ж умови будуть застосовуватися до оператора(ів): **%@**. No comment provided by engineer. @@ -5915,6 +6048,7 @@ Enable in *Network & servers* settings. Save your profile? + Зберегти свій профіль? alert title @@ -5939,6 +6073,7 @@ Enable in *Network & servers* settings. Saving %lld messages + Збереження повідомлень %lld No comment provided by engineer. @@ -6023,6 +6158,7 @@ Enable in *Network & servers* settings. Select chat profile + Виберіть профіль чату No comment provided by engineer. @@ -6237,10 +6373,12 @@ Enable in *Network & servers* settings. Server + Сервер No comment provided by engineer. Server added to operator %@. + Сервер додано до оператора %@. alert message @@ -6260,14 +6398,17 @@ Enable in *Network & servers* settings. Server operator changed. + Оператор сервера змінився. alert title Server operators + Оператори серверів No comment provided by engineer. Server protocol changed. + Протокол сервера змінено. alert title @@ -6377,6 +6518,7 @@ Enable in *Network & servers* settings. Settings were changed. + Налаштування були змінені. alert message @@ -6397,10 +6539,12 @@ Enable in *Network & servers* settings. Share 1-time link with a friend + Поділіться одноразовим посиланням з другом No comment provided by engineer. Share SimpleX address on social media. + Поділіться адресою SimpleX у соціальних мережах. No comment provided by engineer. @@ -6410,6 +6554,7 @@ Enable in *Network & servers* settings. Share address publicly + Поділіться адресою публічно No comment provided by engineer. @@ -6429,6 +6574,7 @@ Enable in *Network & servers* settings. Share profile + Поділіться профілем No comment provided by engineer. @@ -6533,10 +6679,12 @@ Enable in *Network & servers* settings. SimpleX address and 1-time links are safe to share via any messenger. + SimpleX-адреси та одноразові посилання можна безпечно ділитися через будь-який месенджер. No comment provided by engineer. SimpleX address or 1-time link? + SimpleX адреса або одноразове посилання? No comment provided by engineer. @@ -6576,6 +6724,7 @@ Enable in *Network & servers* settings. SimpleX protocols reviewed by Trail of Bits. + Протоколи SimpleX, розглянуті Trail of Bits. No comment provided by engineer. @@ -6610,6 +6759,7 @@ Enable in *Network & servers* settings. Some app settings were not migrated. + Деякі налаштування програми не були перенесені. No comment provided by engineer. @@ -6630,6 +6780,8 @@ Enable in *Network & servers* settings. Some servers failed the test: %@ + Деякі сервери не пройшли тестування: +%@ alert message @@ -6754,10 +6906,12 @@ Enable in *Network & servers* settings. Switch audio and video during the call. + Перемикайте аудіо та відео під час дзвінка. No comment provided by engineer. Switch chat profile for 1-time invitations. + Переключіть профіль чату для отримання одноразових запрошень. No comment provided by engineer. @@ -6797,6 +6951,7 @@ Enable in *Network & servers* settings. Tail + Хвіст No comment provided by engineer. @@ -6897,6 +7052,7 @@ It can happen because of some bug or when the connection is compromised. The app protects your privacy by using different operators in each conversation. + Додаток захищає вашу конфіденційність, використовуючи різних операторів у кожній розмові. No comment provided by engineer. @@ -6916,6 +7072,7 @@ It can happen because of some bug or when the connection is compromised. The connection reached the limit of undelivered messages, your contact may be offline. + З'єднання досягло ліміту недоставлених повідомлень, ваш контакт може бути офлайн. No comment provided by engineer. @@ -6980,6 +7137,7 @@ It can happen because of some bug or when the connection is compromised. The second preset operator in the app! + Другий попередньо встановлений оператор у застосунку! No comment provided by engineer. @@ -6999,6 +7157,7 @@ It can happen because of some bug or when the connection is compromised. The servers for new files of your current chat profile **%@**. + Сервери для нових файлів вашого поточного профілю чату **%@**. No comment provided by engineer. @@ -7008,6 +7167,7 @@ It can happen because of some bug or when the connection is compromised. The uploaded database archive will be permanently removed from the servers. + Завантажений архів бази даних буде назавжди видалено з серверів. No comment provided by engineer. @@ -7017,6 +7177,7 @@ It can happen because of some bug or when the connection is compromised. These conditions will also apply for: **%@**. + Ці умови також поширюються на: **%@**. No comment provided by engineer. @@ -7121,6 +7282,7 @@ It can happen because of some bug or when the connection is compromised. To protect against your link being replaced, you can compare contact security codes. + Щоб захиститися від заміни вашого посилання, ви можете порівняти коди безпеки контактів. No comment provided by engineer. @@ -7147,14 +7309,17 @@ You will be prompted to complete authentication before this feature is enabled.< To receive + Щоб отримати No comment provided by engineer. To record speech please grant permission to use Microphone. + Для запису промови, будь ласка, надайте дозвіл на використання мікрофону. No comment provided by engineer. To record video please grant permission to use Camera. + Для запису відео, будь ласка, надайте дозвіл на використання камери. No comment provided by engineer. @@ -7169,6 +7334,7 @@ You will be prompted to complete authentication before this feature is enabled.< To send + Щоб відправити No comment provided by engineer. @@ -7178,6 +7344,7 @@ You will be prompted to complete authentication before this feature is enabled.< To use the servers of **%@**, accept conditions of use. + Щоб користуватися серверами **%@**, прийміть умови використання. No comment provided by engineer. @@ -7272,6 +7439,7 @@ You will be prompted to complete authentication before this feature is enabled.< Undelivered messages + Недоставлені повідомлення No comment provided by engineer. @@ -7433,6 +7601,7 @@ To connect, please ask your contact to create another connection link and check Use %@ + Використовуйте %@ No comment provided by engineer. @@ -7442,6 +7611,7 @@ To connect, please ask your contact to create another connection link and check Use SOCKS proxy + Використовуйте SOCKS проксі No comment provided by engineer. @@ -7461,10 +7631,12 @@ To connect, please ask your contact to create another connection link and check Use for files + Використовуйте для файлів No comment provided by engineer. Use for messages + Використовуйте для повідомлень No comment provided by engineer. @@ -7509,6 +7681,7 @@ To connect, please ask your contact to create another connection link and check Use servers + Використовуйте сервери No comment provided by engineer. @@ -7528,6 +7701,7 @@ To connect, please ask your contact to create another connection link and check Username + Ім'я користувача No comment provided by engineer. @@ -7602,6 +7776,7 @@ To connect, please ask your contact to create another connection link and check View conditions + Умови перегляду No comment provided by engineer. @@ -7611,6 +7786,7 @@ To connect, please ask your contact to create another connection link and check View updated conditions + Переглянути оновлені умови No comment provided by engineer. @@ -7725,6 +7901,7 @@ To connect, please ask your contact to create another connection link and check When more than one operator is enabled, none of them has metadata to learn who communicates with whom. + Коли увімкнено більше одного оператора, жоден з них не має метаданих, щоб дізнатися, хто з ким спілкується. No comment provided by engineer. @@ -7822,6 +7999,10 @@ To connect, please ask your contact to create another connection link and check Ви вже підключені до %@. No comment provided by engineer. + + You are already connected with %@. + No comment provided by engineer. + You are already connecting to %@. Ви вже з'єднані з %@. @@ -7886,10 +8067,12 @@ Repeat join request? You can configure operators in Network & servers settings. + Ви можете налаштувати операторів у налаштуваннях Мережі та серверів. No comment provided by engineer. You can configure servers via settings. + Ви можете налаштувати сервери за допомогою налаштувань. No comment provided by engineer. @@ -7934,6 +8117,7 @@ Repeat join request? You can set connection name, to remember who the link was shared with. + Ви можете задати ім'я з'єднання, щоб запам'ятати, з ким ви поділилися посиланням. No comment provided by engineer. @@ -8145,6 +8329,7 @@ Repeat connection request? Your chat preferences + Ваші налаштування чату alert title @@ -8154,6 +8339,7 @@ Repeat connection request? Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile. + Ваше з'єднання було переміщено на %@, але під час перенаправлення на профіль сталася несподівана помилка. No comment provided by engineer. @@ -8173,6 +8359,7 @@ Repeat connection request? Your credentials may be sent unencrypted. + Ваші облікові дані можуть бути надіслані незашифрованими. No comment provided by engineer. @@ -8212,6 +8399,7 @@ Repeat connection request? Your profile was changed. If you save it, the updated profile will be sent to all your contacts. + Ваш профіль було змінено. Якщо ви збережете його, оновлений профіль буде надіслано всім вашим контактам. alert message @@ -8231,6 +8419,7 @@ Repeat connection request? Your servers + Ваші сервери No comment provided by engineer. @@ -8655,6 +8844,7 @@ Repeat connection request? for better metadata privacy. + для кращої конфіденційності метаданих. No comment provided by engineer. @@ -9286,22 +9476,27 @@ last received msg: %2$@ %d new events + %d нових подій notification body From: %@ + Від: %@ notification body New events + Нові події notification New messages + Нові повідомлення notification New messages in %d chats + Нові повідомлення в чатах %d notification body diff --git a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff index 1663530290..300a33d7b4 100644 --- a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff +++ b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff @@ -622,6 +622,10 @@ 将地址添加到您的个人资料,以便您的联系人可以与其他人共享。个人资料更新将发送给您的联系人。 No comment provided by engineer. + + Add friends + No comment provided by engineer. + Add profile 添加个人资料 @@ -637,6 +641,10 @@ 扫描二维码来添加服务器。 No comment provided by engineer. + + Add team members + No comment provided by engineer. + Add to another device 添加另一设备 @@ -647,6 +655,10 @@ 添加欢迎信息 No comment provided by engineer. + + Add your team members to the conversations. + No comment provided by engineer. + Added media & file servers No comment provided by engineer. @@ -1205,6 +1217,10 @@ 保加利亚语、芬兰语、泰语和乌克兰语——感谢用户和[Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! No comment provided by engineer. + + Business address + 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). 通过聊天资料(默认)或者[通过连接](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)。 @@ -1346,6 +1362,14 @@ Change user profiles authentication reason + + Chat already exists + No comment provided by engineer. + + + Chat already exists! + No comment provided by engineer. + Chat colors 聊天颜色 @@ -7819,6 +7843,10 @@ To connect, please ask your contact to create another connection link and check 您已经连接到 %@。 No comment provided by engineer. + + You are already connected with %@. + No comment provided by engineer. + You are already connecting to %@. 您已连接到 %@。 diff --git a/apps/ios/SimpleX NSE/de.lproj/Localizable.strings b/apps/ios/SimpleX NSE/de.lproj/Localizable.strings index 5ef592ec70..f9779c6e05 100644 --- a/apps/ios/SimpleX NSE/de.lproj/Localizable.strings +++ b/apps/ios/SimpleX NSE/de.lproj/Localizable.strings @@ -1,7 +1,15 @@ -/* - Localizable.strings - SimpleX +/* notification body */ +"%d new events" = "%d neue Ereignisse"; + +/* notification body */ +"From: %@" = "Von: %@"; + +/* notification */ +"New events" = "Neue Ereignisse"; + +/* notification */ +"New messages" = "Neue Nachrichten"; + +/* notification body */ +"New messages in %d chats" = "Neue Nachrichten in %d Chats"; - Created by EP on 30/07/2024. - Copyright © 2024 SimpleX Chat. All rights reserved. -*/ diff --git a/apps/ios/SimpleX NSE/es.lproj/Localizable.strings b/apps/ios/SimpleX NSE/es.lproj/Localizable.strings index 5ef592ec70..fb190400e1 100644 --- a/apps/ios/SimpleX NSE/es.lproj/Localizable.strings +++ b/apps/ios/SimpleX NSE/es.lproj/Localizable.strings @@ -1,7 +1,15 @@ -/* - Localizable.strings - SimpleX +/* notification body */ +"%d new events" = "%d evento(s) nuevo(s)"; + +/* notification body */ +"From: %@" = "De: %@"; + +/* notification */ +"New events" = "Eventos nuevos"; + +/* notification */ +"New messages" = "Mensajes nuevos"; + +/* notification body */ +"New messages in %d chats" = "Mensajes nuevos en %d chat(s)"; - Created by EP on 30/07/2024. - Copyright © 2024 SimpleX Chat. All rights reserved. -*/ diff --git a/apps/ios/SimpleX NSE/hu.lproj/Localizable.strings b/apps/ios/SimpleX NSE/hu.lproj/Localizable.strings index 5ef592ec70..e64c98df9e 100644 --- a/apps/ios/SimpleX NSE/hu.lproj/Localizable.strings +++ b/apps/ios/SimpleX NSE/hu.lproj/Localizable.strings @@ -1,7 +1,15 @@ -/* - Localizable.strings - SimpleX +/* notification body */ +"%d new events" = "%d új esemény"; + +/* notification body */ +"From: %@" = "Tőle: %@"; + +/* notification */ +"New events" = "Új események"; + +/* notification */ +"New messages" = "Új üzenetek"; + +/* notification body */ +"New messages in %d chats" = "Új üzenetek %d csevegésben"; - Created by EP on 30/07/2024. - Copyright © 2024 SimpleX Chat. All rights reserved. -*/ diff --git a/apps/ios/SimpleX NSE/it.lproj/Localizable.strings b/apps/ios/SimpleX NSE/it.lproj/Localizable.strings index 5ef592ec70..31f463eb5b 100644 --- a/apps/ios/SimpleX NSE/it.lproj/Localizable.strings +++ b/apps/ios/SimpleX NSE/it.lproj/Localizable.strings @@ -1,7 +1,15 @@ -/* - Localizable.strings - SimpleX +/* notification body */ +"%d new events" = "%d nuovi eventi"; + +/* notification body */ +"From: %@" = "Da: %@"; + +/* notification */ +"New events" = "Nuovi eventi"; + +/* notification */ +"New messages" = "Nuovi messaggi"; + +/* notification body */ +"New messages in %d chats" = "Nuovi messaggi in %d chat"; - Created by EP on 30/07/2024. - Copyright © 2024 SimpleX Chat. All rights reserved. -*/ diff --git a/apps/ios/SimpleX NSE/nl.lproj/Localizable.strings b/apps/ios/SimpleX NSE/nl.lproj/Localizable.strings index 5ef592ec70..4cf91689b5 100644 --- a/apps/ios/SimpleX NSE/nl.lproj/Localizable.strings +++ b/apps/ios/SimpleX NSE/nl.lproj/Localizable.strings @@ -1,7 +1,15 @@ -/* - Localizable.strings - SimpleX +/* notification body */ +"%d new events" = "‐%d nieuwe gebeurtenissen"; + +/* notification body */ +"From: %@" = "Van: %@"; + +/* notification */ +"New events" = "Nieuwe gebeurtenissen"; + +/* notification */ +"New messages" = "Nieuwe berichten"; + +/* notification body */ +"New messages in %d chats" = "Nieuwe berichten in %d chats"; - Created by EP on 30/07/2024. - Copyright © 2024 SimpleX Chat. All rights reserved. -*/ diff --git a/apps/ios/SimpleX NSE/uk.lproj/Localizable.strings b/apps/ios/SimpleX NSE/uk.lproj/Localizable.strings index 5ef592ec70..69cc53bff1 100644 --- a/apps/ios/SimpleX NSE/uk.lproj/Localizable.strings +++ b/apps/ios/SimpleX NSE/uk.lproj/Localizable.strings @@ -1,7 +1,15 @@ -/* - Localizable.strings - SimpleX +/* notification body */ +"%d new events" = "%d нових подій"; + +/* notification body */ +"From: %@" = "Від: %@"; + +/* notification */ +"New events" = "Нові події"; + +/* notification */ +"New messages" = "Нові повідомлення"; + +/* notification body */ +"New messages in %d chats" = "Нові повідомлення в чатах %d"; - Created by EP on 30/07/2024. - Copyright © 2024 SimpleX Chat. All rights reserved. -*/ diff --git a/apps/ios/de.lproj/Localizable.strings b/apps/ios/de.lproj/Localizable.strings index f680727010..1e92d094b4 100644 --- a/apps/ios/de.lproj/Localizable.strings +++ b/apps/ios/de.lproj/Localizable.strings @@ -82,6 +82,9 @@ /* No comment provided by engineer. */ "**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from." = "**Empfohlen**: Nur Ihr Geräte-Token und ihre Benachrichtigungen werden an den SimpleX-Chat-Benachrichtigungs-Server gesendet, aber weder der Nachrichteninhalt noch deren Größe oder von wem sie gesendet wurde."; +/* No comment provided by engineer. */ +"**Scan / Paste link**: to connect via a link you received." = "**Link scannen / einfügen**: Um eine Verbindung über den Link herzustellen, den Sie erhalten haben."; + /* No comment provided by engineer. */ "**Warning**: Instant push notifications require passphrase saved in Keychain." = "**Warnung**: Sofortige Push-Benachrichtigungen erfordern die Eingabe eines Passworts, welches in Ihrem Schlüsselbund gespeichert ist."; @@ -142,6 +145,12 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ wurde erfolgreich überprüft"; +/* No comment provided by engineer. */ +"%@ server" = "%@ Server"; + +/* No comment provided by engineer. */ +"%@ servers" = "%@ Server"; + /* No comment provided by engineer. */ "%@ uploaded" = "%@ hochgeladen"; @@ -295,6 +304,12 @@ /* time interval */ "1 week" = "wöchentlich"; +/* No comment provided by engineer. */ +"1-time link" = "Einmal-Link"; + +/* No comment provided by engineer. */ +"1-time link can be used *with one contact only* - share in person or via any messenger." = "Ein Einmal-Link kann *nur mit einem Kontakt* genutzt werden - teilen Sie in nur persönlich oder über einen beliebigen Messenger."; + /* No comment provided by engineer. */ "5 minutes" = "5 Minuten"; @@ -342,6 +357,9 @@ swipe action */ "Accept" = "Annehmen"; +/* No comment provided by engineer. */ +"Accept conditions" = "Nutzungsbedingungen akzeptieren"; + /* No comment provided by engineer. */ "Accept connection request?" = "Kontaktanfrage annehmen?"; @@ -355,6 +373,9 @@ /* call status */ "accepted call" = "Anruf angenommen"; +/* No comment provided by engineer. */ +"Accepted conditions" = "Akzeptierte Nutzungsbedingungen"; + /* No comment provided by engineer. */ "Acknowledged" = "Bestätigt"; @@ -382,6 +403,12 @@ /* No comment provided by engineer. */ "Add welcome message" = "Begrüßungsmeldung hinzufügen"; +/* No comment provided by engineer. */ +"Added media & file servers" = "Medien- und Dateiserver hinzugefügt"; + +/* No comment provided by engineer. */ +"Added message servers" = "Nachrichtenserver hinzugefügt"; + /* No comment provided by engineer. */ "Additional accent" = "Erste Akzentfarbe"; @@ -397,6 +424,12 @@ /* No comment provided by engineer. */ "Address change will be aborted. Old receiving address will be used." = "Der Wechsel der Empfängeradresse wird beendet. Die bisherige Adresse wird weiter verwendet."; +/* No comment provided by engineer. */ +"Address or 1-time link?" = "Adress- oder Einmal-Link?"; + +/* No comment provided by engineer. */ +"Address settings" = "Adress-Einstellungen"; + /* member role */ "admin" = "Admin"; @@ -439,6 +472,9 @@ /* feature role */ "all members" = "Alle Mitglieder"; +/* No comment provided by engineer. */ +"All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages." = "Alle Nachrichten und Dateien werden **Ende-zu-Ende verschlüsselt** versendet - in Direkt-Nachrichten mit Post-Quantum-Security."; + /* No comment provided by engineer. */ "All messages will be deleted - this cannot be undone!" = "Es werden alle Nachrichten gelöscht. Dies kann nicht rückgängig gemacht werden!"; @@ -855,6 +891,9 @@ set passcode view */ "Change self-destruct passcode" = "Selbstzerstörungs-Zugangscode ändern"; +/* authentication reason */ +"Change user profiles" = "Chat-Profile wechseln"; + /* chat item text */ "changed address for you" = "Wechselte die Empfängeradresse von Ihnen"; @@ -918,6 +957,12 @@ /* No comment provided by engineer. */ "Chats" = "Chats"; +/* No comment provided by engineer. */ +"Check messages every 20 min." = "Alle 20min Nachrichten überprüfen."; + +/* No comment provided by engineer. */ +"Check messages when allowed." = "Wenn es erlaubt ist, Nachrichten überprüfen."; + /* alert title */ "Check server address and try again." = "Überprüfen Sie die Serveradresse und versuchen Sie es nochmal."; @@ -978,6 +1023,33 @@ /* No comment provided by engineer. */ "Completed" = "Abgeschlossen"; +/* No comment provided by engineer. */ +"Conditions accepted on: %@." = "Die Nutzungsbedingungen wurden akzeptiert am: %@."; + +/* No comment provided by engineer. */ +"Conditions are accepted for the operator(s): **%@**." = "Die Nutzungsbedingungen der/des Betreiber(s) werden akzeptiert: **%@**."; + +/* No comment provided by engineer. */ +"Conditions are already accepted for following operator(s): **%@**." = "Die Nutzungsbedingungen der/des folgenden Betreiber(s) wurden schon akzeptiert: **%@**."; + +/* No comment provided by engineer. */ +"Conditions of use" = "Nutzungsbedingungen"; + +/* No comment provided by engineer. */ +"Conditions will be accepted for enabled operators after 30 days." = "Die Nutzungsbedingungen der aktivierten Betreiber werden nach 30 Tagen akzeptiert."; + +/* No comment provided by engineer. */ +"Conditions will be accepted for operator(s): **%@**." = "Die Nutzungsbedingungen der/des Betreiber(s) werden akzeptiert: **%@**."; + +/* No comment provided by engineer. */ +"Conditions will be accepted for the operator(s): **%@**." = "Die Nutzungsbedingungen der/des Betreiber(s) werden akzeptiert: **%@**."; + +/* No comment provided by engineer. */ +"Conditions will be accepted on: %@." = "Die Nutzungsbedingungen werden akzeptiert am: %@."; + +/* No comment provided by engineer. */ +"Conditions will be automatically accepted for enabled operators on: %@." = "Die Nutzungsbedingungen der aktivierten Betreiber werden automatisch akzeptiert am: %@."; + /* No comment provided by engineer. */ "Configure ICE servers" = "ICE-Server konfigurieren"; @@ -1125,6 +1197,9 @@ /* No comment provided by engineer. */ "Connection request sent!" = "Verbindungsanfrage wurde gesendet!"; +/* No comment provided by engineer. */ +"Connection security" = "Verbindungs-Sicherheit"; + /* No comment provided by engineer. */ "Connection terminated" = "Verbindung beendet"; @@ -1206,6 +1281,9 @@ /* No comment provided by engineer. */ "Create" = "Erstellen"; +/* No comment provided by engineer. */ +"Create 1-time link" = "Einmal-Link erstellen"; + /* No comment provided by engineer. */ "Create a group using a random profile." = "Erstellen Sie eine Gruppe mit einem zufälligen Profil."; @@ -1257,6 +1335,9 @@ /* No comment provided by engineer. */ "creator" = "Ersteller"; +/* No comment provided by engineer. */ +"Current conditions text couldn't be loaded, you can review conditions via this link:" = "Der Text der aktuellen Nutzungsbedingungen konnte nicht geladen werden. Sie können die Nutzungsbedingungen unter diesem Link einsehen:"; + /* No comment provided by engineer. */ "Current Passcode" = "Aktueller Zugangscode"; @@ -1505,6 +1586,9 @@ /* No comment provided by engineer. */ "Deletion errors" = "Fehler beim Löschen"; +/* No comment provided by engineer. */ +"Delivered even when Apple drops them." = "Auslieferung, selbst wenn Apple sie löscht."; + /* No comment provided by engineer. */ "Delivery" = "Zustellung"; @@ -1692,6 +1776,9 @@ /* No comment provided by engineer. */ "e2e encrypted" = "E2E-verschlüsselt"; +/* No comment provided by engineer. */ +"E2E encrypted notifications." = "E2E-verschlüsselte Benachrichtigungen."; + /* chat item action */ "Edit" = "Bearbeiten"; @@ -1710,6 +1797,9 @@ /* No comment provided by engineer. */ "Enable camera access" = "Kamera-Zugriff aktivieren"; +/* No comment provided by engineer. */ +"Enable Flux" = "Flux aktivieren"; + /* No comment provided by engineer. */ "Enable for all" = "Für Alle aktivieren"; @@ -1869,12 +1959,18 @@ /* No comment provided by engineer. */ "Error aborting address change" = "Fehler beim Beenden des Adresswechsels"; +/* alert title */ +"Error accepting conditions" = "Fehler beim Akzeptieren der Nutzungsbedingungen"; + /* No comment provided by engineer. */ "Error accepting contact request" = "Fehler beim Annehmen der Kontaktanfrage"; /* No comment provided by engineer. */ "Error adding member(s)" = "Fehler beim Hinzufügen von Mitgliedern"; +/* alert title */ +"Error adding server" = "Fehler beim Hinzufügen des Servers"; + /* No comment provided by engineer. */ "Error changing address" = "Fehler beim Wechseln der Empfängeradresse"; @@ -1959,6 +2055,9 @@ /* No comment provided by engineer. */ "Error joining group" = "Fehler beim Beitritt zur Gruppe"; +/* alert title */ +"Error loading servers" = "Fehler beim Laden der Server"; + /* No comment provided by engineer. */ "Error migrating settings" = "Fehler beim Migrieren der Einstellungen"; @@ -1992,6 +2091,9 @@ /* No comment provided by engineer. */ "Error saving passphrase to keychain" = "Fehler beim Speichern des Passworts in den Schlüsselbund"; +/* alert title */ +"Error saving servers" = "Fehler beim Speichern der Server"; + /* when migrating */ "Error saving settings" = "Fehler beim Abspeichern der Einstellungen"; @@ -2034,6 +2136,9 @@ /* No comment provided by engineer. */ "Error updating message" = "Fehler beim Aktualisieren der Nachricht"; +/* alert title */ +"Error updating server" = "Fehler beim Aktualisieren des Servers"; + /* No comment provided by engineer. */ "Error updating settings" = "Fehler beim Aktualisieren der Einstellungen"; @@ -2061,6 +2166,9 @@ /* No comment provided by engineer. */ "Errors" = "Fehler"; +/* servers error */ +"Errors in servers configuration." = "Fehler in der Server-Konfiguration."; + /* No comment provided by engineer. */ "Even when disabled in the conversation." = "Auch wenn sie im Chat deaktiviert sind."; @@ -2187,9 +2295,24 @@ /* No comment provided by engineer. */ "Fix not supported by group member" = "Reparatur wird vom Gruppenmitglied nicht unterstützt"; +/* No comment provided by engineer. */ +"for better metadata privacy." = "für einen besseren Metadatenschutz."; + +/* servers error */ +"For chat profile %@:" = "Für das Chat-Profil %@:"; + /* No comment provided by engineer. */ "For console" = "Für Konsole"; +/* No comment provided by engineer. */ +"For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server." = "Wenn Ihr Kontakt beispielsweise Nachrichten über einen SimpleX-Chatserver empfängt, wird Ihre App diese über einen der Server von Flux versenden."; + +/* No comment provided by engineer. */ +"For private routing" = "Für privates Routing"; + +/* No comment provided by engineer. */ +"For social media" = "Für soziale Medien"; + /* chat item action */ "Forward" = "Weiterleiten"; @@ -2382,6 +2505,12 @@ /* time unit */ "hours" = "Stunden"; +/* No comment provided by engineer. */ +"How it affects privacy" = "Wie es die Privatsphäre beeinflusst"; + +/* No comment provided by engineer. */ +"How it helps privacy" = "Wie es die Privatsphäre schützt"; + /* No comment provided by engineer. */ "How SimpleX works" = "Wie SimpleX funktioniert"; @@ -2958,6 +3087,9 @@ /* No comment provided by engineer. */ "More reliable network connection." = "Zuverlässigere Netzwerkverbindung."; +/* No comment provided by engineer. */ +"More reliable notifications" = "Zuverlässigere Benachrichtigungen"; + /* item status description */ "Most likely this connection is deleted." = "Wahrscheinlich ist diese Verbindung gelöscht worden."; @@ -2982,12 +3114,18 @@ /* No comment provided by engineer. */ "Network connection" = "Netzwerkverbindung"; +/* No comment provided by engineer. */ +"Network decentralization" = "Dezentralisiertes Netzwerk"; + /* snd error text */ "Network issues - message expired after many attempts to send it." = "Netzwerk-Fehler - die Nachricht ist nach vielen Sende-Versuchen abgelaufen."; /* No comment provided by engineer. */ "Network management" = "Netzwerk-Verwaltung"; +/* No comment provided by engineer. */ +"Network operator" = "Netzwerk-Betreiber"; + /* No comment provided by engineer. */ "Network settings" = "Netzwerkeinstellungen"; @@ -3015,6 +3153,9 @@ /* No comment provided by engineer. */ "New display name" = "Neuer Anzeigename"; +/* notification */ +"New events" = "Neue Ereignisse"; + /* No comment provided by engineer. */ "New in %@" = "Neu in %@"; @@ -3036,6 +3177,9 @@ /* No comment provided by engineer. */ "New passphrase…" = "Neues Passwort…"; +/* No comment provided by engineer. */ +"New server" = "Neuer Server"; + /* No comment provided by engineer. */ "New SOCKS credentials will be used every time you start the app." = "Jedes Mal wenn Sie die App starten, werden neue SOCKS-Anmeldeinformationen genutzt"; @@ -3081,6 +3225,12 @@ /* No comment provided by engineer. */ "No info, try to reload" = "Keine Information - es wird versucht neu zu laden"; +/* servers error */ +"No media & file servers." = "Keine Medien- und Dateiserver."; + +/* servers error */ +"No message servers." = "Keine Nachrichten-Server."; + /* No comment provided by engineer. */ "No network connection" = "Keine Netzwerkverbindung"; @@ -3099,6 +3249,18 @@ /* No comment provided by engineer. */ "No received or sent files" = "Keine empfangenen oder gesendeten Dateien"; +/* servers error */ +"No servers for private message routing." = "Keine Server für privates Nachrichten-Routing."; + +/* servers error */ +"No servers to receive files." = "Keine Server für den Empfang von Dateien."; + +/* servers error */ +"No servers to receive messages." = "Keine Server für den Empfang von Nachrichten."; + +/* servers error */ +"No servers to send files." = "Keine Server für das Versenden von Dateien."; + /* copied message info in history */ "no text" = "Kein Text"; @@ -3120,6 +3282,9 @@ /* No comment provided by engineer. */ "Notifications are disabled!" = "Benachrichtigungen sind deaktiviert!"; +/* No comment provided by engineer. */ +"Notifications privacy" = "Datenschutz für Benachrichtigungen"; + /* No comment provided by engineer. */ "Now admins can:\n- delete members' messages.\n- disable members (\"observer\" role)" = "Administratoren können nun\n- Nachrichten von Gruppenmitgliedern löschen\n- Gruppenmitglieder deaktivieren (\"Beobachter\"-Rolle)"; @@ -3212,12 +3377,18 @@ /* No comment provided by engineer. */ "Open" = "Öffnen"; +/* No comment provided by engineer. */ +"Open changes" = "Änderungen öffnen"; + /* No comment provided by engineer. */ "Open chat" = "Chat öffnen"; /* authentication reason */ "Open chat console" = "Chat-Konsole öffnen"; +/* No comment provided by engineer. */ +"Open conditions" = "Nutzungsbedingungen öffnen"; + /* No comment provided by engineer. */ "Open group" = "Gruppe öffnen"; @@ -3230,6 +3401,12 @@ /* No comment provided by engineer. */ "Opening app…" = "App wird geöffnet…"; +/* No comment provided by engineer. */ +"Operator" = "Betreiber"; + +/* alert title */ +"Operator server" = "Betreiber-Server"; + /* No comment provided by engineer. */ "Or paste archive link" = "Oder fügen Sie den Archiv-Link ein"; @@ -3242,6 +3419,9 @@ /* No comment provided by engineer. */ "Or show this code" = "Oder diesen QR-Code anzeigen"; +/* No comment provided by engineer. */ +"Or to share privately" = "Oder zum privaten Teilen"; + /* No comment provided by engineer. */ "other" = "Andere"; @@ -3383,6 +3563,9 @@ /* No comment provided by engineer. */ "Preset server address" = "Voreingestellte Serveradresse"; +/* No comment provided by engineer. */ +"Preset servers" = "Voreingestellte Server"; + /* No comment provided by engineer. */ "Preview" = "Vorschau"; @@ -3488,6 +3671,9 @@ /* No comment provided by engineer. */ "Push notifications" = "Push-Benachrichtigungen"; +/* No comment provided by engineer. */ +"Push Notifications" = "Push-Benachrichtigungen"; + /* No comment provided by engineer. */ "Push server" = "Push-Server"; @@ -3735,6 +3921,12 @@ /* chat item action */ "Reveal" = "Aufdecken"; +/* No comment provided by engineer. */ +"Review conditions" = "Nutzungsbedingungen einsehen"; + +/* No comment provided by engineer. */ +"Review later" = "Später einsehen"; + /* No comment provided by engineer. */ "Revoke" = "Widerrufen"; @@ -3756,6 +3948,12 @@ /* No comment provided by engineer. */ "Safer groups" = "Sicherere Gruppen"; +/* No comment provided by engineer. */ +"Same conditions will apply to operator **%@**." = "Dieselben Nutzungsbedingungen gelten auch für den Betreiber **%@**."; + +/* No comment provided by engineer. */ +"Same conditions will apply to operator(s): **%@**." = "Dieselben Nutzungsbedingungen gelten auch für den/die Betreiber: **%@**."; + /* alert button chat item action */ "Save" = "Speichern"; @@ -4021,6 +4219,9 @@ /* No comment provided by engineer. */ "Server" = "Server"; +/* alert message */ +"Server added to operator %@." = "Der Server wurde dem Betreiber %@ hinzugefügt."; + /* No comment provided by engineer. */ "Server address" = "Server-Adresse"; @@ -4030,6 +4231,15 @@ /* srv error text. */ "Server address is incompatible with network settings." = "Die Server-Adresse ist nicht mit den Netzwerkeinstellungen kompatibel."; +/* alert title */ +"Server operator changed." = "Der Server-Betreiber wurde geändert."; + +/* No comment provided by engineer. */ +"Server operators" = "Server-Betreiber"; + +/* alert title */ +"Server protocol changed." = "Das Server-Protokoll wurde geändert."; + /* queue info */ "server queue info: %@\n\nlast received msg: %@" = "Server-Warteschlangen-Information: %1$@\n\nZuletzt empfangene Nachricht: %2$@"; @@ -4115,9 +4325,15 @@ /* No comment provided by engineer. */ "Share 1-time link" = "Einmal-Link teilen"; +/* No comment provided by engineer. */ +"Share 1-time link with a friend" = "Den Einmal-Einladungslink mit einem Freund teilen"; + /* No comment provided by engineer. */ "Share address" = "Adresse teilen"; +/* No comment provided by engineer. */ +"Share address publicly" = "Die Adresse öffentlich teilen"; + /* alert title */ "Share address with contacts?" = "Die Adresse mit Kontakten teilen?"; @@ -4130,6 +4346,9 @@ /* No comment provided by engineer. */ "Share profile" = "Profil teilen"; +/* No comment provided by engineer. */ +"Share SimpleX address on social media." = "Die SimpleX-Adresse auf sozialen Medien teilen."; + /* No comment provided by engineer. */ "Share this 1-time invite link" = "Teilen Sie diesen Einmal-Einladungslink"; @@ -4175,6 +4394,12 @@ /* No comment provided by engineer. */ "SimpleX Address" = "SimpleX-Adresse"; +/* No comment provided by engineer. */ +"SimpleX address and 1-time links are safe to share via any messenger." = "Die SimpleX-Adresse und Einmal-Links können sicher über beliebige Messenger geteilt werden."; + +/* No comment provided by engineer. */ +"SimpleX address or 1-time link?" = "SimpleX-Adresse oder Einmal-Link?"; + /* No comment provided by engineer. */ "SimpleX Chat security was audited by Trail of Bits." = "Die Sicherheit von SimpleX Chat wurde von Trail of Bits überprüft."; @@ -4250,6 +4475,9 @@ /* No comment provided by engineer. */ "Some non-fatal errors occurred during import:" = "Während des Imports traten ein paar nicht schwerwiegende Fehler auf:"; +/* alert message */ +"Some servers failed the test:\n%@" = "Einige Server haben den Test nicht bestanden:\n%@"; + /* notification title */ "Somebody" = "Jemand"; @@ -4412,6 +4640,9 @@ /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "Wenn sie Nachrichten oder Kontaktanfragen empfangen, kann Sie die App benachrichtigen - Um dies zu aktivieren, öffnen Sie bitte die Einstellungen."; +/* No comment provided by engineer. */ +"The app protects your privacy by using different operators in each conversation." = "Durch Verwendung verschiedener Netzwerk-Betreiber für jede Unterhaltung schützt die App Ihre Privatsphäre."; + /* No comment provided by engineer. */ "The app will ask to confirm downloads from unknown file servers (except .onion)." = "Die App wird eine Bestätigung bei Downloads von unbekannten Datei-Servern anfordern (außer bei .onion)."; @@ -4421,6 +4652,9 @@ /* No comment provided by engineer. */ "The code you scanned is not a SimpleX link QR code." = "Der von Ihnen gescannte Code ist kein SimpleX-Link-QR-Code."; +/* No comment provided by engineer. */ +"The connection reached the limit of undelivered messages, your contact may be offline." = "Diese Verbindung hat das Limit der nicht ausgelieferten Nachrichten erreicht. Ihr Kontakt ist möglicherweise offline."; + /* No comment provided by engineer. */ "The connection you accepted will be cancelled!" = "Die von Ihnen akzeptierte Verbindung wird abgebrochen!"; @@ -4460,6 +4694,9 @@ /* No comment provided by engineer. */ "The profile is only shared with your contacts." = "Das Profil wird nur mit Ihren Kontakten geteilt."; +/* No comment provided by engineer. */ +"The second preset operator in the app!" = "Der zweite voreingestellte Netzwerk-Betreiber in der App!"; + /* No comment provided by engineer. */ "The second tick we missed! ✅" = "Wir haben das zweite Häkchen vermisst! ✅"; @@ -4469,6 +4706,9 @@ /* No comment provided by engineer. */ "The servers for new connections of your current chat profile **%@**." = "Mögliche Server für neue Verbindungen von Ihrem aktuellen Chat-Profil **%@**."; +/* No comment provided by engineer. */ +"The servers for new files of your current chat profile **%@**." = "Die Server Deines aktuellen Chat-Profils für neue Dateien **%@**."; + /* No comment provided by engineer. */ "The text you pasted is not a SimpleX link." = "Der von Ihnen eingefügte Text ist kein SimpleX-Link."; @@ -4478,6 +4718,9 @@ /* No comment provided by engineer. */ "Themes" = "Design"; +/* No comment provided by engineer. */ +"These conditions will also apply for: **%@**." = "Diese Nutzungsbedingungen gelten auch für: **%@**."; + /* No comment provided by engineer. */ "These settings are for your current profile **%@**." = "Diese Einstellungen betreffen Ihr aktuelles Profil **%@**."; @@ -4541,6 +4784,9 @@ /* No comment provided by engineer. */ "To make a new connection" = "Um eine Verbindung mit einem neuen Kontakt zu erstellen"; +/* No comment provided by engineer. */ +"To protect against your link being replaced, you can compare contact security codes." = "Zum Schutz vor dem Austausch Ihres Links können Sie die Sicherheitscodes Ihrer Kontakte vergleichen."; + /* No comment provided by engineer. */ "To protect timezone, image/voice files use UTC." = "Bild- und Sprachdateinamen enthalten UTC, um Informationen zur Zeitzone zu schützen."; @@ -4553,6 +4799,9 @@ /* No comment provided by engineer. */ "To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Zum Schutz Ihrer Privatsphäre verwendet SimpleX an Stelle von Benutzerkennungen, die von allen anderen Plattformen verwendet werden, Kennungen für Nachrichtenwarteschlangen, die für jeden Ihrer Kontakte individuell sind."; +/* No comment provided by engineer. */ +"To receive" = "Für den Empfang"; + /* No comment provided by engineer. */ "To record speech please grant permission to use Microphone." = "Bitte erteilen Sie für Sprach-Aufnahmen die Genehmigung das Mikrofon zu nutzen."; @@ -4565,9 +4814,15 @@ /* No comment provided by engineer. */ "To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." = "Geben Sie ein vollständiges Passwort in das Suchfeld auf der Seite **Ihre Chat-Profile** ein, um Ihr verborgenes Profil zu sehen."; +/* No comment provided by engineer. */ +"To send" = "Für das Senden"; + /* No comment provided by engineer. */ "To support instant push notifications the chat database has to be migrated." = "Um sofortige Push-Benachrichtigungen zu unterstützen, muss die Chat-Datenbank migriert werden."; +/* No comment provided by engineer. */ +"To use the servers of **%@**, accept conditions of use." = "Um die Server von **%@** zu nutzen, müssen Sie dessen Nutzungsbedingungen akzeptieren."; + /* No comment provided by engineer. */ "To verify end-to-end encryption with your contact compare (or scan) the code on your devices." = "Um die Ende-zu-Ende-Verschlüsselung mit Ihrem Kontakt zu überprüfen, müssen Sie den Sicherheitscode in Ihren Apps vergleichen oder scannen."; @@ -4625,6 +4880,9 @@ /* rcv group event chat item */ "unblocked %@" = "%@ wurde freigegeben"; +/* No comment provided by engineer. */ +"Undelivered messages" = "Nicht ausgelieferte Nachrichten"; + /* No comment provided by engineer. */ "Unexpected migration state" = "Unerwarteter Migrationsstatus"; @@ -4742,12 +5000,21 @@ /* No comment provided by engineer. */ "Use .onion hosts" = "Verwende .onion-Hosts"; +/* No comment provided by engineer. */ +"Use %@" = "Verwende %@"; + /* No comment provided by engineer. */ "Use chat" = "Verwenden Sie Chat"; /* No comment provided by engineer. */ "Use current profile" = "Aktuelles Profil nutzen"; +/* No comment provided by engineer. */ +"Use for files" = "Für Dateien verwenden"; + +/* No comment provided by engineer. */ +"Use for messages" = "Für Nachrichten verwenden"; + /* No comment provided by engineer. */ "Use for new connections" = "Für neue Verbindungen nutzen"; @@ -4772,6 +5039,9 @@ /* No comment provided by engineer. */ "Use server" = "Server nutzen"; +/* No comment provided by engineer. */ +"Use servers" = "Verwende Server"; + /* No comment provided by engineer. */ "Use SimpleX Chat servers?" = "Verwenden Sie SimpleX-Chat-Server?"; @@ -4856,9 +5126,15 @@ /* No comment provided by engineer. */ "Videos and files up to 1gb" = "Videos und Dateien bis zu 1GB"; +/* No comment provided by engineer. */ +"View conditions" = "Nutzungsbedingungen anschauen"; + /* No comment provided by engineer. */ "View security code" = "Schauen Sie sich den Sicherheitscode an"; +/* No comment provided by engineer. */ +"View updated conditions" = "Aktualisierte Nutzungsbedingungen anschauen"; + /* chat feature */ "Visible history" = "Sichtbarer Nachrichtenverlauf"; @@ -4940,6 +5216,9 @@ /* No comment provided by engineer. */ "when IP hidden" = "Wenn die IP-Adresse versteckt ist"; +/* No comment provided by engineer. */ +"When more than one operator is enabled, none of them has metadata to learn who communicates with whom." = "Wenn mehrere Netzwerk-Betreiber aktiviert sind, hat keiner von ihnen Metadaten, um zu erfahren, wer mit wem kommuniziert."; + /* No comment provided by engineer. */ "When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "Wenn Sie ein Inkognito-Profil mit Jemandem teilen, wird dieses Profil auch für die Gruppen verwendet, für die Sie von diesem Kontakt eingeladen werden."; @@ -5048,6 +5327,12 @@ /* No comment provided by engineer. */ "You can change it in Appearance settings." = "Kann von Ihnen in den Erscheinungsbild-Einstellungen geändert werden."; +/* No comment provided by engineer. */ +"You can configure operators in Network & servers settings." = "Sie können die Betreiber in den Netzwerk- und Servereinstellungen konfigurieren."; + +/* No comment provided by engineer. */ +"You can configure servers via settings." = "Sie können die Server über die Einstellungen konfigurieren."; + /* No comment provided by engineer. */ "You can create it later" = "Sie können dies später erstellen"; @@ -5072,6 +5357,9 @@ /* No comment provided by engineer. */ "You can send messages to %@ from Archived contacts." = "Sie können aus den archivierten Kontakten heraus Nachrichten an %@ versenden."; +/* No comment provided by engineer. */ +"You can set connection name, to remember who the link was shared with." = "Sie können einen Verbindungsnamen festlegen, um sich zu merken, mit wem der Link geteilt wurde."; + /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "Über die Geräte-Einstellungen können Sie die Benachrichtigungsvorschau im Sperrbildschirm erlauben."; @@ -5273,6 +5561,9 @@ /* No comment provided by engineer. */ "Your server address" = "Ihre Serveradresse"; +/* No comment provided by engineer. */ +"Your servers" = "Ihre Server"; + /* No comment provided by engineer. */ "Your settings" = "Einstellungen"; diff --git a/apps/ios/es.lproj/Localizable.strings b/apps/ios/es.lproj/Localizable.strings index 103065a05a..d02497515e 100644 --- a/apps/ios/es.lproj/Localizable.strings +++ b/apps/ios/es.lproj/Localizable.strings @@ -82,6 +82,9 @@ /* No comment provided by engineer. */ "**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from." = "**Recomendado**: el token del dispositivo y las notificaciones se envían al servidor de notificaciones de SimpleX Chat, pero no el contenido del mensaje, su tamaño o su procedencia."; +/* No comment provided by engineer. */ +"**Scan / Paste link**: to connect via a link you received." = "**Escanear / Pegar enlace**: para conectar mediante un enlace recibido."; + /* No comment provided by engineer. */ "**Warning**: Instant push notifications require passphrase saved in Keychain." = "**Advertencia**: Las notificaciones automáticas instantáneas requieren una contraseña guardada en Keychain."; @@ -142,6 +145,12 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ está verificado"; +/* No comment provided by engineer. */ +"%@ server" = "%@ servidor"; + +/* No comment provided by engineer. */ +"%@ servers" = "%@ servidores"; + /* No comment provided by engineer. */ "%@ uploaded" = "%@ subido"; @@ -161,7 +170,7 @@ "%@:" = "%@:"; /* time interval */ -"%d days" = "%d días"; +"%d days" = "%d día(s)"; /* forward confirmation reason */ "%d file(s) are still being downloaded." = "%d archivo(s) se está(n) descargando todavía."; @@ -176,25 +185,25 @@ "%d file(s) were not downloaded." = "%d archivo(s) no se ha(n) descargado."; /* time interval */ -"%d hours" = "%d horas"; +"%d hours" = "%d hora(s)"; /* alert title */ -"%d messages not forwarded" = "%d mensajes no enviados"; +"%d messages not forwarded" = "%d mensaje(s) no enviado(s)"; /* time interval */ -"%d min" = "%d minutos"; +"%d min" = "%d minuto(s)"; /* time interval */ -"%d months" = "%d meses"; +"%d months" = "%d mes(es)"; /* time interval */ -"%d sec" = "%d segundos"; +"%d sec" = "%d segundo(s)"; /* integrity error chat item */ -"%d skipped message(s)" = "%d mensaje(s) saltado(s"; +"%d skipped message(s)" = "%d mensaje(s) omitido(s)"; /* time interval */ -"%d weeks" = "%d semanas"; +"%d weeks" = "%d semana(s)"; /* No comment provided by engineer. */ "%lld" = "%lld"; @@ -295,6 +304,12 @@ /* time interval */ "1 week" = "una semana"; +/* No comment provided by engineer. */ +"1-time link" = "Enlace de un uso"; + +/* No comment provided by engineer. */ +"1-time link can be used *with one contact only* - share in person or via any messenger." = "Los enlaces de un uso pueden ser usados *solamente con un contacto* - compártelos en persona o mediante cualquier aplicación de mensajería."; + /* No comment provided by engineer. */ "5 minutes" = "5 minutos"; @@ -342,6 +357,9 @@ swipe action */ "Accept" = "Aceptar"; +/* No comment provided by engineer. */ +"Accept conditions" = "Aceptar condiciones"; + /* No comment provided by engineer. */ "Accept connection request?" = "¿Aceptar solicitud de conexión?"; @@ -355,6 +373,9 @@ /* call status */ "accepted call" = "llamada aceptada"; +/* No comment provided by engineer. */ +"Accepted conditions" = "Condiciones aceptadas"; + /* No comment provided by engineer. */ "Acknowledged" = "Confirmaciones"; @@ -382,6 +403,12 @@ /* No comment provided by engineer. */ "Add welcome message" = "Añadir mensaje de bienvenida"; +/* No comment provided by engineer. */ +"Added media & file servers" = "Servidores de archivos y multimedia añadidos"; + +/* No comment provided by engineer. */ +"Added message servers" = "Servidores de mensajes añadidos"; + /* No comment provided by engineer. */ "Additional accent" = "Acento adicional"; @@ -397,6 +424,12 @@ /* No comment provided by engineer. */ "Address change will be aborted. Old receiving address will be used." = "El cambio de dirección se cancelará. Se usará la antigua dirección de recepción."; +/* No comment provided by engineer. */ +"Address or 1-time link?" = "¿Dirección o enlace de un uso?"; + +/* No comment provided by engineer. */ +"Address settings" = "Configuración de dirección"; + /* member role */ "admin" = "administrador"; @@ -439,6 +472,9 @@ /* feature role */ "all members" = "todos los miembros"; +/* No comment provided by engineer. */ +"All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages." = "Todos los mensajes y archivos son enviados **cifrados de extremo a extremo** y con seguridad de cifrado postcuántico en mensajes directos."; + /* No comment provided by engineer. */ "All messages will be deleted - this cannot be undone!" = "Todos los mensajes serán borrados. ¡No podrá deshacerse!"; @@ -855,6 +891,9 @@ set passcode view */ "Change self-destruct passcode" = "Cambiar código autodestrucción"; +/* authentication reason */ +"Change user profiles" = "Cambiar perfil de usuario"; + /* chat item text */ "changed address for you" = "ha cambiado tu servidor de envío"; @@ -918,6 +957,12 @@ /* No comment provided by engineer. */ "Chats" = "Chats"; +/* No comment provided by engineer. */ +"Check messages every 20 min." = "Comprobar mensajes cada 20 min."; + +/* No comment provided by engineer. */ +"Check messages when allowed." = "Comprobar mensajes cuando se permita."; + /* alert title */ "Check server address and try again." = "Comprueba la dirección del servidor e inténtalo de nuevo."; @@ -978,6 +1023,33 @@ /* No comment provided by engineer. */ "Completed" = "Completadas"; +/* No comment provided by engineer. */ +"Conditions accepted on: %@." = "Condiciones aceptadas el: %@."; + +/* No comment provided by engineer. */ +"Conditions are accepted for the operator(s): **%@**." = "Las condiciones se han aceptado para el(los) operador(s): **%@**."; + +/* No comment provided by engineer. */ +"Conditions are already accepted for following operator(s): **%@**." = "Las condiciones ya se han aceptado para el/los siguiente(s) operador(s): **%@**."; + +/* No comment provided by engineer. */ +"Conditions of use" = "Condiciones de uso"; + +/* No comment provided by engineer. */ +"Conditions will be accepted for enabled operators after 30 days." = "Las condiciones de los operadores habilitados serán aceptadas después de 30 días."; + +/* No comment provided by engineer. */ +"Conditions will be accepted for operator(s): **%@**." = "Las condiciones serán aceptadas para el/los operador(es): **%@**."; + +/* No comment provided by engineer. */ +"Conditions will be accepted for the operator(s): **%@**." = "Las condiciones serán aceptadas para el/los operador(es): **%@**."; + +/* No comment provided by engineer. */ +"Conditions will be accepted on: %@." = "Las condiciones serán aceptadas el: %@."; + +/* No comment provided by engineer. */ +"Conditions will be automatically accepted for enabled operators on: %@." = "Las condiciones serán aceptadas automáticamente para los operadores habilitados el: %@."; + /* No comment provided by engineer. */ "Configure ICE servers" = "Configure servidores ICE"; @@ -1125,6 +1197,9 @@ /* No comment provided by engineer. */ "Connection request sent!" = "¡Solicitud de conexión enviada!"; +/* No comment provided by engineer. */ +"Connection security" = "Seguridad de conexión"; + /* No comment provided by engineer. */ "Connection terminated" = "Conexión finalizada"; @@ -1206,6 +1281,9 @@ /* No comment provided by engineer. */ "Create" = "Crear"; +/* No comment provided by engineer. */ +"Create 1-time link" = "Crear enlace de un uso"; + /* No comment provided by engineer. */ "Create a group using a random profile." = "Crear grupo usando perfil aleatorio."; @@ -1257,6 +1335,9 @@ /* No comment provided by engineer. */ "creator" = "creador"; +/* No comment provided by engineer. */ +"Current conditions text couldn't be loaded, you can review conditions via this link:" = "El texto con las condiciones actuales no se ha podido cargar, puedes revisar las condiciones en el siguiente enlace:"; + /* No comment provided by engineer. */ "Current Passcode" = "Código de Acceso"; @@ -1505,6 +1586,9 @@ /* No comment provided by engineer. */ "Deletion errors" = "Errores de eliminación"; +/* No comment provided by engineer. */ +"Delivered even when Apple drops them." = "Entregados incluso cuando Apple los descarta."; + /* No comment provided by engineer. */ "Delivery" = "Entrega"; @@ -1692,6 +1776,9 @@ /* No comment provided by engineer. */ "e2e encrypted" = "cifrado de extremo a extremo"; +/* No comment provided by engineer. */ +"E2E encrypted notifications." = "Notificaciones cifradas E2E."; + /* chat item action */ "Edit" = "Editar"; @@ -1710,6 +1797,9 @@ /* No comment provided by engineer. */ "Enable camera access" = "Permitir acceso a la cámara"; +/* No comment provided by engineer. */ +"Enable Flux" = "Habilitar Flux"; + /* No comment provided by engineer. */ "Enable for all" = "Activar para todos"; @@ -1869,12 +1959,18 @@ /* No comment provided by engineer. */ "Error aborting address change" = "Error al cancelar cambio de dirección"; +/* alert title */ +"Error accepting conditions" = "Error al aceptar las condiciones"; + /* No comment provided by engineer. */ "Error accepting contact request" = "Error al aceptar solicitud del contacto"; /* No comment provided by engineer. */ "Error adding member(s)" = "Error al añadir miembro(s)"; +/* alert title */ +"Error adding server" = "Error al añadir servidor"; + /* No comment provided by engineer. */ "Error changing address" = "Error al cambiar servidor"; @@ -1959,6 +2055,9 @@ /* No comment provided by engineer. */ "Error joining group" = "Error al unirte al grupo"; +/* alert title */ +"Error loading servers" = "Error al cargar servidores"; + /* No comment provided by engineer. */ "Error migrating settings" = "Error al migrar la configuración"; @@ -1992,6 +2091,9 @@ /* No comment provided by engineer. */ "Error saving passphrase to keychain" = "Error al guardar contraseña en Keychain"; +/* alert title */ +"Error saving servers" = "Error al guardar servidores"; + /* when migrating */ "Error saving settings" = "Error al guardar ajustes"; @@ -2034,6 +2136,9 @@ /* No comment provided by engineer. */ "Error updating message" = "Error al actualizar mensaje"; +/* alert title */ +"Error updating server" = "Error al actualizar el servidor"; + /* No comment provided by engineer. */ "Error updating settings" = "Error al actualizar configuración"; @@ -2061,6 +2166,9 @@ /* No comment provided by engineer. */ "Errors" = "Errores"; +/* servers error */ +"Errors in servers configuration." = "Error en la configuración del servidor."; + /* No comment provided by engineer. */ "Even when disabled in the conversation." = "Incluso si está desactivado para la conversación."; @@ -2187,9 +2295,24 @@ /* No comment provided by engineer. */ "Fix not supported by group member" = "Corrección no compatible con miembro del grupo"; +/* No comment provided by engineer. */ +"for better metadata privacy." = "para mayor privacidad de los metadatos."; + +/* servers error */ +"For chat profile %@:" = "Para el perfil de chat %@:"; + /* No comment provided by engineer. */ "For console" = "Para consola"; +/* No comment provided by engineer. */ +"For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server." = "Si por ejemplo tu contacto recibe los mensajes a través de un servidor de SimpleX Chat, tu aplicación los entregará a través de un servidor de Flux."; + +/* No comment provided by engineer. */ +"For private routing" = "Para el enrutamiento privado"; + +/* No comment provided by engineer. */ +"For social media" = "Para redes sociales"; + /* chat item action */ "Forward" = "Reenviar"; @@ -2382,6 +2505,12 @@ /* time unit */ "hours" = "horas"; +/* No comment provided by engineer. */ +"How it affects privacy" = "Cómo afecta a la privacidad"; + +/* No comment provided by engineer. */ +"How it helps privacy" = "Cómo ayuda a la privacidad"; + /* No comment provided by engineer. */ "How SimpleX works" = "Cómo funciona SimpleX"; @@ -2958,6 +3087,9 @@ /* No comment provided by engineer. */ "More reliable network connection." = "Conexión de red más fiable."; +/* No comment provided by engineer. */ +"More reliable notifications" = "Notificaciones más fiables"; + /* item status description */ "Most likely this connection is deleted." = "Probablemente la conexión ha sido eliminada."; @@ -2982,12 +3114,18 @@ /* No comment provided by engineer. */ "Network connection" = "Conexión de red"; +/* No comment provided by engineer. */ +"Network decentralization" = "Descentralización de la red"; + /* snd error text */ "Network issues - message expired after many attempts to send it." = "Problema en la red - el mensaje ha expirado tras muchos intentos de envío."; /* No comment provided by engineer. */ "Network management" = "Gestión de la red"; +/* No comment provided by engineer. */ +"Network operator" = "Operador de red"; + /* No comment provided by engineer. */ "Network settings" = "Configuración de red"; @@ -3015,6 +3153,9 @@ /* No comment provided by engineer. */ "New display name" = "Nuevo nombre mostrado"; +/* notification */ +"New events" = "Eventos nuevos"; + /* No comment provided by engineer. */ "New in %@" = "Nuevo en %@"; @@ -3036,6 +3177,9 @@ /* No comment provided by engineer. */ "New passphrase…" = "Contraseña nueva…"; +/* No comment provided by engineer. */ +"New server" = "Servidor nuevo"; + /* No comment provided by engineer. */ "New SOCKS credentials will be used every time you start the app." = "Se usarán credenciales SOCKS nuevas cada vez que inicies la aplicación."; @@ -3081,6 +3225,12 @@ /* No comment provided by engineer. */ "No info, try to reload" = "No hay información, intenta recargar"; +/* servers error */ +"No media & file servers." = "Ningún servidor de archivos y multimedia."; + +/* servers error */ +"No message servers." = "Ningún servidor de mensajes."; + /* No comment provided by engineer. */ "No network connection" = "Sin conexión de red"; @@ -3093,9 +3243,24 @@ /* No comment provided by engineer. */ "No permission to record voice message" = "Sin permiso para grabar mensajes de voz"; +/* No comment provided by engineer. */ +"No push server" = "Ningún servidor push"; + /* No comment provided by engineer. */ "No received or sent files" = "Sin archivos recibidos o enviados"; +/* servers error */ +"No servers for private message routing." = "Ningún servidor para enrutamiento privado."; + +/* servers error */ +"No servers to receive files." = "Ningún servidor para recibir archivos."; + +/* servers error */ +"No servers to receive messages." = "Ningún servidor para recibir mensajes."; + +/* servers error */ +"No servers to send files." = "Ningún servidor para enviar archivos."; + /* copied message info in history */ "no text" = "sin texto"; @@ -3117,6 +3282,9 @@ /* No comment provided by engineer. */ "Notifications are disabled!" = "¡Las notificaciones están desactivadas!"; +/* No comment provided by engineer. */ +"Notifications privacy" = "Privacidad en las notificaciones"; + /* No comment provided by engineer. */ "Now admins can:\n- delete members' messages.\n- disable members (\"observer\" role)" = "Ahora los administradores pueden:\n- eliminar mensajes de los miembros.\n- desactivar el rol miembro (a rol \"observador\")"; @@ -3209,12 +3377,18 @@ /* No comment provided by engineer. */ "Open" = "Abrir"; +/* No comment provided by engineer. */ +"Open changes" = "Abrir cambios"; + /* No comment provided by engineer. */ "Open chat" = "Abrir chat"; /* authentication reason */ "Open chat console" = "Abrir consola de Chat"; +/* No comment provided by engineer. */ +"Open conditions" = "Abrir condiciones"; + /* No comment provided by engineer. */ "Open group" = "Grupo abierto"; @@ -3227,6 +3401,12 @@ /* No comment provided by engineer. */ "Opening app…" = "Iniciando aplicación…"; +/* No comment provided by engineer. */ +"Operator" = "Operador"; + +/* alert title */ +"Operator server" = "Servidor del operador"; + /* No comment provided by engineer. */ "Or paste archive link" = "O pegar enlace del archivo"; @@ -3239,6 +3419,9 @@ /* No comment provided by engineer. */ "Or show this code" = "O muestra este código QR"; +/* No comment provided by engineer. */ +"Or to share privately" = "O para compartir en privado"; + /* No comment provided by engineer. */ "other" = "otros"; @@ -3380,6 +3563,9 @@ /* No comment provided by engineer. */ "Preset server address" = "Dirección del servidor predefinida"; +/* No comment provided by engineer. */ +"Preset servers" = "Servidores predefinidos"; + /* No comment provided by engineer. */ "Preview" = "Vista previa"; @@ -3485,6 +3671,9 @@ /* No comment provided by engineer. */ "Push notifications" = "Notificaciones automáticas"; +/* No comment provided by engineer. */ +"Push Notifications" = "Notificaciones push"; + /* No comment provided by engineer. */ "Push server" = "Servidor push"; @@ -3732,6 +3921,12 @@ /* chat item action */ "Reveal" = "Revelar"; +/* No comment provided by engineer. */ +"Review conditions" = "Revisar condiciones"; + +/* No comment provided by engineer. */ +"Review later" = "Revisar más tarde"; + /* No comment provided by engineer. */ "Revoke" = "Revocar"; @@ -3753,6 +3948,12 @@ /* No comment provided by engineer. */ "Safer groups" = "Grupos más seguros"; +/* No comment provided by engineer. */ +"Same conditions will apply to operator **%@**." = "Las mismas condiciones se aplicarán al operador **%@**."; + +/* No comment provided by engineer. */ +"Same conditions will apply to operator(s): **%@**." = "Las mismas condiciones se aplicarán a el/los operador(es) **%@**."; + /* alert button chat item action */ "Save" = "Guardar"; @@ -4018,6 +4219,9 @@ /* No comment provided by engineer. */ "Server" = "Servidor"; +/* alert message */ +"Server added to operator %@." = "Servidor añadido al operador %@."; + /* No comment provided by engineer. */ "Server address" = "Dirección del servidor"; @@ -4027,6 +4231,15 @@ /* srv error text. */ "Server address is incompatible with network settings." = "La dirección del servidor es incompatible con la configuración de la red."; +/* alert title */ +"Server operator changed." = "El operador del servidor ha cambiado."; + +/* No comment provided by engineer. */ +"Server operators" = "Operadores de servidores"; + +/* alert title */ +"Server protocol changed." = "El protocolo del servidor ha cambiado."; + /* queue info */ "server queue info: %@\n\nlast received msg: %@" = "información cola del servidor: %1$@\n\núltimo mensaje recibido: %2$@"; @@ -4112,9 +4325,15 @@ /* No comment provided by engineer. */ "Share 1-time link" = "Compartir enlace de un uso"; +/* No comment provided by engineer. */ +"Share 1-time link with a friend" = "Compartir enlace de un uso con un amigo"; + /* No comment provided by engineer. */ "Share address" = "Compartir dirección"; +/* No comment provided by engineer. */ +"Share address publicly" = "Campartir dirección públicamente"; + /* alert title */ "Share address with contacts?" = "¿Compartir la dirección con los contactos?"; @@ -4127,6 +4346,9 @@ /* No comment provided by engineer. */ "Share profile" = "Comparte perfil"; +/* No comment provided by engineer. */ +"Share SimpleX address on social media." = "Compartir dirección SimpleX en redes sociales."; + /* No comment provided by engineer. */ "Share this 1-time invite link" = "Comparte este enlace de un solo uso"; @@ -4172,6 +4394,12 @@ /* No comment provided by engineer. */ "SimpleX Address" = "Dirección SimpleX"; +/* No comment provided by engineer. */ +"SimpleX address and 1-time links are safe to share via any messenger." = "Compartir enlaces de un uso y direcciones SimpleX es seguro a través de cualquier medio."; + +/* No comment provided by engineer. */ +"SimpleX address or 1-time link?" = "Dirección SimpleX o enlace de un uso?"; + /* No comment provided by engineer. */ "SimpleX Chat security was audited by Trail of Bits." = "La seguridad de SimpleX Chat ha sido auditada por Trail of Bits."; @@ -4247,6 +4475,9 @@ /* No comment provided by engineer. */ "Some non-fatal errors occurred during import:" = "Han ocurrido algunos errores no críticos durante la importación:"; +/* alert message */ +"Some servers failed the test:\n%@" = "Algunos servidores no han superado la prueba:\n%@"; + /* notification title */ "Somebody" = "Alguien"; @@ -4409,6 +4640,9 @@ /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "La aplicación puede notificarte cuando recibas mensajes o solicitudes de contacto: por favor, abre la configuración para activarlo."; +/* No comment provided by engineer. */ +"The app protects your privacy by using different operators in each conversation." = "La aplicación protege tu privacidad mediante el uso de diferentes operadores en cada conversación."; + /* No comment provided by engineer. */ "The app will ask to confirm downloads from unknown file servers (except .onion)." = "La aplicación pedirá que confirmes las descargas desde servidores de archivos desconocidos (excepto si son .onion)."; @@ -4418,6 +4652,9 @@ /* No comment provided by engineer. */ "The code you scanned is not a SimpleX link QR code." = "El código QR escaneado no es un enlace SimpleX."; +/* No comment provided by engineer. */ +"The connection reached the limit of undelivered messages, your contact may be offline." = "La conexión ha alcanzado el límite de mensajes no entregados. es posible que tu contacto esté desconectado."; + /* No comment provided by engineer. */ "The connection you accepted will be cancelled!" = "¡La conexión que has aceptado se cancelará!"; @@ -4457,6 +4694,9 @@ /* No comment provided by engineer. */ "The profile is only shared with your contacts." = "El perfil sólo se comparte con tus contactos."; +/* No comment provided by engineer. */ +"The second preset operator in the app!" = "El segundo operador predefinido!"; + /* No comment provided by engineer. */ "The second tick we missed! ✅" = "¡El doble check que nos faltaba! ✅"; @@ -4466,6 +4706,9 @@ /* No comment provided by engineer. */ "The servers for new connections of your current chat profile **%@**." = "Lista de servidores para las conexiones nuevas de tu perfil actual **%@**."; +/* No comment provided by engineer. */ +"The servers for new files of your current chat profile **%@**." = "Los servidores para archivos nuevos en tu perfil actual **%@**."; + /* No comment provided by engineer. */ "The text you pasted is not a SimpleX link." = "El texto pegado no es un enlace SimpleX."; @@ -4475,6 +4718,9 @@ /* No comment provided by engineer. */ "Themes" = "Temas"; +/* No comment provided by engineer. */ +"These conditions will also apply for: **%@**." = "Estas condiciones también se aplican para: **%@**."; + /* No comment provided by engineer. */ "These settings are for your current profile **%@**." = "Esta configuración afecta a tu perfil actual **%@**."; @@ -4538,6 +4784,9 @@ /* No comment provided by engineer. */ "To make a new connection" = "Para hacer una conexión nueva"; +/* No comment provided by engineer. */ +"To protect against your link being replaced, you can compare contact security codes." = "Para protegerte contra una sustitución del enlace, puedes comparar los códigos de seguridad con tu contacto."; + /* No comment provided by engineer. */ "To protect timezone, image/voice files use UTC." = "Para proteger la zona horaria, los archivos de imagen/voz usan la hora UTC."; @@ -4550,6 +4799,9 @@ /* No comment provided by engineer. */ "To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Para proteger tu privacidad, en lugar de los identificadores de usuario que usan el resto de plataformas, SimpleX dispone de identificadores para las colas de mensajes, independientes para cada uno de tus contactos."; +/* No comment provided by engineer. */ +"To receive" = "Para recibir"; + /* No comment provided by engineer. */ "To record speech please grant permission to use Microphone." = "Para grabación de voz, por favor concede el permiso para usar el micrófono."; @@ -4562,9 +4814,15 @@ /* No comment provided by engineer. */ "To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." = "Para hacer visible tu perfil oculto, introduce la contraseña en el campo de búsqueda del menú **Mis perfiles**."; +/* No comment provided by engineer. */ +"To send" = "Para enviar"; + /* No comment provided by engineer. */ "To support instant push notifications the chat database has to be migrated." = "Para permitir las notificaciones automáticas instantáneas, la base de datos se debe migrar."; +/* No comment provided by engineer. */ +"To use the servers of **%@**, accept conditions of use." = "Para usar los servidores de **%@**, acepta las condiciones de uso."; + /* No comment provided by engineer. */ "To verify end-to-end encryption with your contact compare (or scan) the code on your devices." = "Para verificar el cifrado de extremo a extremo con tu contacto, compara (o escanea) el código en ambos dispositivos."; @@ -4622,6 +4880,9 @@ /* rcv group event chat item */ "unblocked %@" = "ha desbloqueado a %@"; +/* No comment provided by engineer. */ +"Undelivered messages" = "Mensajes no entregados"; + /* No comment provided by engineer. */ "Unexpected migration state" = "Estado de migración inesperado"; @@ -4739,12 +5000,21 @@ /* No comment provided by engineer. */ "Use .onion hosts" = "Usar hosts .onion"; +/* No comment provided by engineer. */ +"Use %@" = "Usar %@"; + /* No comment provided by engineer. */ "Use chat" = "Usar Chat"; /* No comment provided by engineer. */ "Use current profile" = "Usar perfil actual"; +/* No comment provided by engineer. */ +"Use for files" = "Usar para archivos"; + +/* No comment provided by engineer. */ +"Use for messages" = "Usar para mensajes"; + /* No comment provided by engineer. */ "Use for new connections" = "Usar para conexiones nuevas"; @@ -4769,6 +5039,9 @@ /* No comment provided by engineer. */ "Use server" = "Usar servidor"; +/* No comment provided by engineer. */ +"Use servers" = "Usar servidores"; + /* No comment provided by engineer. */ "Use SimpleX Chat servers?" = "¿Usar servidores SimpleX Chat?"; @@ -4853,9 +5126,15 @@ /* No comment provided by engineer. */ "Videos and files up to 1gb" = "Vídeos y archivos de hasta 1Gb"; +/* No comment provided by engineer. */ +"View conditions" = "Ver condiciones"; + /* No comment provided by engineer. */ "View security code" = "Mostrar código de seguridad"; +/* No comment provided by engineer. */ +"View updated conditions" = "Ver condiciones actualizadas"; + /* chat feature */ "Visible history" = "Historial visible"; @@ -4937,6 +5216,9 @@ /* No comment provided by engineer. */ "when IP hidden" = "con IP oculta"; +/* No comment provided by engineer. */ +"When more than one operator is enabled, none of them has metadata to learn who communicates with whom." = "Cuando está habilitado más de un operador, ninguno dispone de los metadatos para conocer quién se comunica con quién."; + /* No comment provided by engineer. */ "When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "Cuando compartes un perfil incógnito con alguien, este perfil también se usará para los grupos a los que te inviten."; @@ -5045,6 +5327,12 @@ /* No comment provided by engineer. */ "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 configure operators in Network & servers settings." = "Puedes configurar los operadores desde Servidores y Redes."; + +/* No comment provided by engineer. */ +"You can configure servers via settings." = "Puedes configurar los servidores a través de su configuración."; + /* No comment provided by engineer. */ "You can create it later" = "Puedes crearla más tarde"; @@ -5069,6 +5357,9 @@ /* No comment provided by engineer. */ "You can send messages to %@ from Archived contacts." = "Puedes enviar mensajes a %@ desde Contactos archivados."; +/* No comment provided by engineer. */ +"You can set connection name, to remember who the link was shared with." = "Puedes añadir un nombre a la conexión para recordar a quién corresponde."; + /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "Puedes configurar las notificaciones de la pantalla de bloqueo desde Configuración."; @@ -5270,6 +5561,9 @@ /* No comment provided by engineer. */ "Your server address" = "Dirección del servidor"; +/* No comment provided by engineer. */ +"Your servers" = "Tus servidores"; + /* No comment provided by engineer. */ "Your settings" = "Configuración"; diff --git a/apps/ios/hu.lproj/Localizable.strings b/apps/ios/hu.lproj/Localizable.strings index b64c75fd1d..9103f4baf3 100644 --- a/apps/ios/hu.lproj/Localizable.strings +++ b/apps/ios/hu.lproj/Localizable.strings @@ -82,6 +82,9 @@ /* No comment provided by engineer. */ "**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from." = "**Megjegyzés:** az eszköztoken és az értesítések elküldésre kerülnek a SimpleX Chat értesítési kiszolgálóra, kivéve az üzenet tartalma, mérete vagy az, hogy kitől származik."; +/* No comment provided by engineer. */ +"**Scan / Paste link**: to connect via a link you received." = "**Hivatkozás beolvasása / beillesztése**: egy kapott hivatkozáson keresztüli kapcsolódáshoz."; + /* No comment provided by engineer. */ "**Warning**: Instant push notifications require passphrase saved in Keychain." = "**Figyelmeztetés:** Az azonnali push-értesítésekhez a kulcstartóban tárolt jelmondat megadása szükséges."; @@ -101,7 +104,7 @@ "## In reply to" = "## Válaszul erre:"; /* No comment provided by engineer. */ -"#secret#" = "#titkos#"; +"#secret#" = "#titok#"; /* No comment provided by engineer. */ "%@" = "%@"; @@ -142,6 +145,12 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ hitelesítve"; +/* No comment provided by engineer. */ +"%@ server" = "%@ kiszolgáló"; + +/* No comment provided by engineer. */ +"%@ servers" = "%@ kiszolgáló"; + /* No comment provided by engineer. */ "%@ uploaded" = "%@ feltöltve"; @@ -230,7 +239,7 @@ "%lld minutes" = "%lld perc"; /* No comment provided by engineer. */ -"%lld new interface languages" = "%lld új nyelvi csomag"; +"%lld new interface languages" = "%lld új kezelőfelületi nyelv"; /* No comment provided by engineer. */ "%lld second(s)" = "%lld másodperc"; @@ -295,6 +304,12 @@ /* time interval */ "1 week" = "1 hét"; +/* No comment provided by engineer. */ +"1-time link" = "Egyszer használható meghívó-hivatkozás"; + +/* No comment provided by engineer. */ +"1-time link can be used *with one contact only* - share in person or via any messenger." = "Az egyszer használható meghívó-hivatkozás csak *egyetlen ismerőssel használható* - személyesen vagy bármilyen üzenetküldőn keresztül megosztható."; + /* No comment provided by engineer. */ "5 minutes" = "5 perc"; @@ -342,6 +357,9 @@ swipe action */ "Accept" = "Elfogadás"; +/* No comment provided by engineer. */ +"Accept conditions" = "Feltételek elfogadása"; + /* No comment provided by engineer. */ "Accept connection request?" = "Kapcsolatkérés elfogadása?"; @@ -355,6 +373,9 @@ /* call status */ "accepted call" = "elfogadott hívás"; +/* No comment provided by engineer. */ +"Accepted conditions" = "Elfogadott feltételek"; + /* No comment provided by engineer. */ "Acknowledged" = "Nyugtázva"; @@ -382,6 +403,12 @@ /* No comment provided by engineer. */ "Add welcome message" = "Üdvözlőüzenet hozzáadása"; +/* No comment provided by engineer. */ +"Added media & file servers" = "Hozzáadott média- és fájlkiszolgálók"; + +/* No comment provided by engineer. */ +"Added message servers" = "Hozzáadott üzenetkiszolgálók"; + /* No comment provided by engineer. */ "Additional accent" = "További kiemelés"; @@ -397,6 +424,12 @@ /* No comment provided by engineer. */ "Address change will be aborted. Old receiving address will be used." = "A cím módosítása megszakad. A régi fogadási cím kerül felhasználásra."; +/* No comment provided by engineer. */ +"Address or 1-time link?" = "Cím vagy egyszer használható meghívó-hivatkozás?"; + +/* No comment provided by engineer. */ +"Address settings" = "Címbeállítások"; + /* member role */ "admin" = "adminisztrátor"; @@ -439,6 +472,9 @@ /* feature role */ "all members" = "összes tag"; +/* No comment provided by engineer. */ +"All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages." = "Az összes üzenetet és fájlt **végpontok közötti titkosítással** küldi, a közvetlen üzenetekben pedig kvantumrezisztens biztonsággal."; + /* No comment provided by engineer. */ "All messages will be deleted - this cannot be undone!" = "Az összes üzenet törlésre kerül – ez a művelet nem vonható vissza!"; @@ -855,6 +891,9 @@ set passcode view */ "Change self-destruct passcode" = "Önmegsemmisító jelkód megváltoztatása"; +/* authentication reason */ +"Change user profiles" = "Felhasználói profilok megváltoztatása"; + /* chat item text */ "changed address for you" = "cím megváltoztatva"; @@ -918,6 +957,12 @@ /* No comment provided by engineer. */ "Chats" = "Csevegések"; +/* No comment provided by engineer. */ +"Check messages every 20 min." = "Üzenetek ellenőrzése 20 percenként."; + +/* No comment provided by engineer. */ +"Check messages when allowed." = "Üzenetek ellenőrzése, amikor engedélyezett."; + /* alert title */ "Check server address and try again." = "Kiszolgáló címének ellenőrzése és újrapróbálkozás."; @@ -978,6 +1023,33 @@ /* No comment provided by engineer. */ "Completed" = "Elkészült"; +/* No comment provided by engineer. */ +"Conditions accepted on: %@." = "Feltételek elfogadva ekkor: %@."; + +/* No comment provided by engineer. */ +"Conditions are accepted for the operator(s): **%@**." = "A következő üzemeltető(k) számára elfogadott feltételek: **%@**."; + +/* No comment provided by engineer. */ +"Conditions are already accepted for following operator(s): **%@**." = "A feltételek már el lettek fogadva a következő üzemeltető(k) számára: **%@**."; + +/* No comment provided by engineer. */ +"Conditions of use" = "Használati feltételek"; + +/* No comment provided by engineer. */ +"Conditions will be accepted for enabled operators after 30 days." = "A feltételek 30 nap elteltével lesznek elfogadva az engedélyezett üzemeltető számára."; + +/* No comment provided by engineer. */ +"Conditions will be accepted for operator(s): **%@**." = "A feltételek el lesznek fogadva a következő üzemeltető(k) számára: **%@**."; + +/* No comment provided by engineer. */ +"Conditions will be accepted for the operator(s): **%@**." = "A feltételek el lesznek fogadva a következő üzemeltető(k) számára: **%@**."; + +/* No comment provided by engineer. */ +"Conditions will be accepted on: %@." = "A feltételek ekkor lesznek elfogadva: %@."; + +/* No comment provided by engineer. */ +"Conditions will be automatically accepted for enabled operators on: %@." = "A feltételek automatikusan elfogadásra kerülnek az engedélyezett üzemeltető számára: %@."; + /* No comment provided by engineer. */ "Configure ICE servers" = "ICE-kiszolgálók beállítása"; @@ -1033,7 +1105,7 @@ "Connect to yourself?" = "Kapcsolódás saját magához?"; /* No comment provided by engineer. */ -"Connect to yourself?\nThis is your own one-time link!" = "Kapcsolódás saját magához?\nEz az Ön egyszer használható hivatkozása!"; +"Connect to yourself?\nThis is your own one-time link!" = "Kapcsolódás saját magához?\nEz az Ön egyszer használható meghívó-hivatkozása!"; /* No comment provided by engineer. */ "Connect to yourself?\nThis is your own SimpleX address!" = "Kapcsolódás saját magához?\nEz az Ön SimpleX-címe!"; @@ -1045,7 +1117,7 @@ "Connect via link" = "Kapcsolódás egy hivatkozáson keresztül"; /* No comment provided by engineer. */ -"Connect via one-time link" = "Kapcsolódás egyszer használható hivatkozáson keresztül"; +"Connect via one-time link" = "Kapcsolódás egyszer használható meghívó-hivatkozáson keresztül"; /* No comment provided by engineer. */ "Connect with %@" = "Kapcsolódás a következővel: %@"; @@ -1125,6 +1197,9 @@ /* No comment provided by engineer. */ "Connection request sent!" = "Kapcsolatkérés elküldve!"; +/* No comment provided by engineer. */ +"Connection security" = "Kapcsolatbiztonság"; + /* No comment provided by engineer. */ "Connection terminated" = "Kapcsolat megszakítva"; @@ -1206,6 +1281,9 @@ /* No comment provided by engineer. */ "Create" = "Létrehozás"; +/* No comment provided by engineer. */ +"Create 1-time link" = "Egyszer használható meghívó-hivatkozás létrehozása"; + /* No comment provided by engineer. */ "Create a group using a random profile." = "Csoport létrehozása véletlenszerűen létrehozott profillal."; @@ -1257,6 +1335,9 @@ /* No comment provided by engineer. */ "creator" = "készítő"; +/* No comment provided by engineer. */ +"Current conditions text couldn't be loaded, you can review conditions via this link:" = "A jelenlegi feltételek szövegét nem lehetett betölteni, a feltételeket ezen a hivatkozáson keresztül vizsgálhatja felül:"; + /* No comment provided by engineer. */ "Current Passcode" = "Jelenlegi jelkód"; @@ -1505,6 +1586,9 @@ /* No comment provided by engineer. */ "Deletion errors" = "Törlési hibák"; +/* No comment provided by engineer. */ +"Delivered even when Apple drops them." = "Kézbesítés akkor is, amikor az Apple eldobja őket."; + /* No comment provided by engineer. */ "Delivery" = "Kézbesítés"; @@ -1692,6 +1776,9 @@ /* No comment provided by engineer. */ "e2e encrypted" = "e2e titkosított"; +/* No comment provided by engineer. */ +"E2E encrypted notifications." = "Végpontok közötti titkosított értesítések."; + /* chat item action */ "Edit" = "Szerkesztés"; @@ -1710,6 +1797,9 @@ /* No comment provided by engineer. */ "Enable camera access" = "Kamera hozzáférés engedélyezése"; +/* No comment provided by engineer. */ +"Enable Flux" = "Flux engedélyezése"; + /* No comment provided by engineer. */ "Enable for all" = "Engedélyezés az összes tag számára"; @@ -1869,12 +1959,18 @@ /* No comment provided by engineer. */ "Error aborting address change" = "Hiba a cím megváltoztatásának megszakításakor"; +/* alert title */ +"Error accepting conditions" = "Hiba a feltételek elfogadásakor"; + /* No comment provided by engineer. */ "Error accepting contact request" = "Hiba történt a kapcsolatkérés elfogadásakor"; /* No comment provided by engineer. */ "Error adding member(s)" = "Hiba a tag(ok) hozzáadásakor"; +/* alert title */ +"Error adding server" = "Hiba a kiszolgáló hozzáadásakor"; + /* No comment provided by engineer. */ "Error changing address" = "Hiba a cím megváltoztatásakor"; @@ -1959,6 +2055,9 @@ /* No comment provided by engineer. */ "Error joining group" = "Hiba a csoporthoz való csatlakozáskor"; +/* alert title */ +"Error loading servers" = "Hiba a kiszolgálók betöltésekor"; + /* No comment provided by engineer. */ "Error migrating settings" = "Hiba a beallítások átköltöztetésekor"; @@ -1992,6 +2091,9 @@ /* No comment provided by engineer. */ "Error saving passphrase to keychain" = "Hiba a jelmondat kulcstartóba történő mentésekor"; +/* alert title */ +"Error saving servers" = "Hiba a kiszolgálók mentésekor"; + /* when migrating */ "Error saving settings" = "Hiba a beállítások mentésekor"; @@ -2034,6 +2136,9 @@ /* No comment provided by engineer. */ "Error updating message" = "Hiba az üzenet frissítésekor"; +/* alert title */ +"Error updating server" = "Hiba a kiszolgáló frissítésekor"; + /* No comment provided by engineer. */ "Error updating settings" = "Hiba történt a beállítások frissítésekor"; @@ -2061,6 +2166,9 @@ /* No comment provided by engineer. */ "Errors" = "Hibák"; +/* servers error */ +"Errors in servers configuration." = "Hibák a kiszolgálók konfigurációjában."; + /* No comment provided by engineer. */ "Even when disabled in the conversation." = "Akkor is, ha le van tiltva a beszélgetésben."; @@ -2187,9 +2295,24 @@ /* No comment provided by engineer. */ "Fix not supported by group member" = "Csoporttag általi javítás nem támogatott"; +/* No comment provided by engineer. */ +"for better metadata privacy." = "a metaadatok jobb védelme érdekében."; + +/* servers error */ +"For chat profile %@:" = "A(z) %@ nevű csevegési profilhoz:"; + /* No comment provided by engineer. */ "For console" = "Konzolhoz"; +/* No comment provided by engineer. */ +"For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server." = "Ha például az ismerőse a SimpleX Chat kiszolgálón keresztül fogadja az üzeneteket, az Ön alkalmazása a Flux egyik kiszolgálóját használja a kézbesítéshez."; + +/* No comment provided by engineer. */ +"For private routing" = "A privát útválasztáshoz"; + +/* No comment provided by engineer. */ +"For social media" = "A közösségi médiához"; + /* chat item action */ "Forward" = "Továbbítás"; @@ -2382,6 +2505,12 @@ /* time unit */ "hours" = "óra"; +/* No comment provided by engineer. */ +"How it affects privacy" = "Hogyan érinti az adatvédelmet"; + +/* No comment provided by engineer. */ +"How it helps privacy" = "Hogyan segíti az adatvédelmet"; + /* No comment provided by engineer. */ "How SimpleX works" = "Hogyan működik a SimpleX"; @@ -2488,7 +2617,7 @@ "incognito via group link" = "inkognitó a csoporthivatkozáson keresztül"; /* chat list item description */ -"incognito via one-time link" = "inkognitó egy egyszer használható hivatkozáson keresztül"; +"incognito via one-time link" = "inkognitó egy egyszer használható meghívó-hivatkozáson keresztül"; /* notification */ "Incoming audio call" = "Bejövő hanghívás"; @@ -2530,7 +2659,7 @@ "Instant push notifications will be hidden!\n" = "Az azonnali push-értesítések elrejtésre kerülnek!\n"; /* No comment provided by engineer. */ -"Interface" = "Felület"; +"Interface" = "Kezelőfelület"; /* No comment provided by engineer. */ "Interface colors" = "Kezelőfelület színei"; @@ -2620,7 +2749,7 @@ "Irreversible message deletion is prohibited in this group." = "Az üzenetek végleges törlése le van tiltva ebben a csoportban."; /* No comment provided by engineer. */ -"It allows having many anonymous connections without any shared data between them in a single chat profile." = "Lehetővé teszi, hogy egyetlen csevegőprofilon belül több anonim kapcsolat legyen, anélkül, hogy megosztott adatok lennének közöttük."; +"It allows having many anonymous connections without any shared data between them in a single chat profile." = "Lehetővé teszi, hogy egyetlen csevegőprofilon belül több névtelen kapcsolat legyen, anélkül, hogy megosztott adatok lennének közöttük."; /* No comment provided by engineer. */ "It can happen when you or your connection used the old database backup." = "Ez akkor fordulhat elő, ha Ön vagy az ismerőse régi adatbázis biztonsági mentést használt."; @@ -2958,6 +3087,9 @@ /* No comment provided by engineer. */ "More reliable network connection." = "Megbízhatóbb hálózati kapcsolat."; +/* No comment provided by engineer. */ +"More reliable notifications" = "Megbízhatóbb értesítések"; + /* item status description */ "Most likely this connection is deleted." = "Valószínűleg ez a kapcsolat törlésre került."; @@ -2982,12 +3114,18 @@ /* No comment provided by engineer. */ "Network connection" = "Internetkapcsolat"; +/* No comment provided by engineer. */ +"Network decentralization" = "Hálózati decentralizáció"; + /* snd error text */ "Network issues - message expired after many attempts to send it." = "Hálózati problémák - az üzenet többszöri elküldési kísérlet után lejárt."; /* No comment provided by engineer. */ "Network management" = "Hálózatkezelés"; +/* No comment provided by engineer. */ +"Network operator" = "Hálózati üzemeltető"; + /* No comment provided by engineer. */ "Network settings" = "Hálózati beállítások"; @@ -3015,6 +3153,9 @@ /* No comment provided by engineer. */ "New display name" = "Új megjelenítési név"; +/* notification */ +"New events" = "Új események"; + /* No comment provided by engineer. */ "New in %@" = "Újdonságok a(z) %@ verzióban"; @@ -3025,7 +3166,7 @@ "New member role" = "Új tag szerepköre"; /* notification */ -"new message" = "Rejtett üzenet"; +"new message" = "új üzenet"; /* notification */ "New message" = "Új üzenet"; @@ -3036,6 +3177,9 @@ /* No comment provided by engineer. */ "New passphrase…" = "Új jelmondat…"; +/* No comment provided by engineer. */ +"New server" = "Új kiszolgáló"; + /* No comment provided by engineer. */ "New SOCKS credentials will be used every time you start the app." = "Minden alkalommal, amikor elindítja az alkalmazást, új SOCKS-hitelesítő-adatokat fog használni."; @@ -3081,6 +3225,12 @@ /* No comment provided by engineer. */ "No info, try to reload" = "Nincs információ, próbálja meg újratölteni"; +/* servers error */ +"No media & file servers." = "Nincsenek média- és fájlkiszolgálók."; + +/* servers error */ +"No message servers." = "Nincsenek üzenet-kiszolgálók."; + /* No comment provided by engineer. */ "No network connection" = "Nincs hálózati kapcsolat"; @@ -3099,6 +3249,18 @@ /* No comment provided by engineer. */ "No received or sent files" = "Nincsenek fogadott vagy küldött fájlok"; +/* servers error */ +"No servers for private message routing." = "Nincsenek kiszolgálók a privát üzenet-útválasztáshoz."; + +/* servers error */ +"No servers to receive files." = "Nincsenek fájlfogadó-kiszolgálók."; + +/* servers error */ +"No servers to receive messages." = "Nincsenek üzenetfogadó-kiszolgálók."; + +/* servers error */ +"No servers to send files." = "Nincsenek fájlküldő-kiszolgálók."; + /* copied message info in history */ "no text" = "nincs szöveg"; @@ -3120,6 +3282,9 @@ /* No comment provided by engineer. */ "Notifications are disabled!" = "Az értesítések le vannak tiltva!"; +/* No comment provided by engineer. */ +"Notifications privacy" = "Értesítési adatvédelem"; + /* No comment provided by engineer. */ "Now admins can:\n- delete members' messages.\n- disable members (\"observer\" role)" = "Most már az adminisztrátorok is:\n- törölhetik a tagok üzeneteit.\n- letilthatnak tagokat („megfigyelő” szerepkör)"; @@ -3212,12 +3377,18 @@ /* No comment provided by engineer. */ "Open" = "Megnyitás"; +/* No comment provided by engineer. */ +"Open changes" = "Változások megnyitása"; + /* No comment provided by engineer. */ "Open chat" = "Csevegés megnyitása"; /* authentication reason */ "Open chat console" = "Csevegés konzol megnyitása"; +/* No comment provided by engineer. */ +"Open conditions" = "Feltételek megnyitása"; + /* No comment provided by engineer. */ "Open group" = "Csoport megnyitása"; @@ -3230,6 +3401,12 @@ /* No comment provided by engineer. */ "Opening app…" = "Az alkalmazás megnyitása…"; +/* No comment provided by engineer. */ +"Operator" = "Üzemeltető"; + +/* alert title */ +"Operator server" = "Kiszolgáló üzemeltető"; + /* No comment provided by engineer. */ "Or paste archive link" = "Vagy az archívum hivatkozásának beillesztése"; @@ -3242,6 +3419,9 @@ /* No comment provided by engineer. */ "Or show this code" = "Vagy mutassa meg ezt a kódot"; +/* No comment provided by engineer. */ +"Or to share privately" = "Vagy a privát megosztáshoz"; + /* No comment provided by engineer. */ "other" = "egyéb"; @@ -3383,6 +3563,9 @@ /* No comment provided by engineer. */ "Preset server address" = "Előre beállított kiszolgáló címe"; +/* No comment provided by engineer. */ +"Preset servers" = "Előre beállított kiszolgálók"; + /* No comment provided by engineer. */ "Preview" = "Előnézet"; @@ -3488,6 +3671,9 @@ /* No comment provided by engineer. */ "Push notifications" = "Push-értesítések"; +/* No comment provided by engineer. */ +"Push Notifications" = "Push értesítések"; + /* No comment provided by engineer. */ "Push server" = "Push-kiszolgáló"; @@ -3735,6 +3921,12 @@ /* chat item action */ "Reveal" = "Felfedés"; +/* No comment provided by engineer. */ +"Review conditions" = "Feltételek felülvizsgálata"; + +/* No comment provided by engineer. */ +"Review later" = "Felülvizsgálat később"; + /* No comment provided by engineer. */ "Revoke" = "Visszavonás"; @@ -3756,6 +3948,12 @@ /* No comment provided by engineer. */ "Safer groups" = "Biztonságosabb csoportok"; +/* No comment provided by engineer. */ +"Same conditions will apply to operator **%@**." = "Ugyanezek a feltételek vonatkoznak a következő üzemeltetőre is: **%@**."; + +/* No comment provided by engineer. */ +"Same conditions will apply to operator(s): **%@**." = "Ugyanezek a feltételek lesznek elfogadva a következő üzemeltető(k)re is: **%@**."; + /* alert button chat item action */ "Save" = "Mentés"; @@ -4021,6 +4219,9 @@ /* No comment provided by engineer. */ "Server" = "Kiszolgáló"; +/* alert message */ +"Server added to operator %@." = "Kiszolgáló hozzáadva a következő üzemeltetőhöz: %@."; + /* No comment provided by engineer. */ "Server address" = "Kiszolgáló címe"; @@ -4030,6 +4231,15 @@ /* srv error text. */ "Server address is incompatible with network settings." = "A kiszolgáló címe nem kompatibilis a hálózati beállításokkal."; +/* alert title */ +"Server operator changed." = "A kiszolgáló üzemeltetője megváltozott."; + +/* No comment provided by engineer. */ +"Server operators" = "Kiszolgáló-üzemeltetők"; + +/* alert title */ +"Server protocol changed." = "A kiszolgáló-protokoll megváltozott."; + /* queue info */ "server queue info: %@\n\nlast received msg: %@" = "a kiszolgáló üzenet-sorbaállítási információi: %1$@\n\nutoljára fogadott üzenet: %2$@"; @@ -4115,9 +4325,15 @@ /* No comment provided by engineer. */ "Share 1-time link" = "Egyszer használható hivatkozás megosztása"; +/* No comment provided by engineer. */ +"Share 1-time link with a friend" = "Egyszer használható meghívó-hivatkozás megosztása egy baráttal"; + /* No comment provided by engineer. */ "Share address" = "Cím megosztása"; +/* No comment provided by engineer. */ +"Share address publicly" = "Cím nyilvános megosztása"; + /* alert title */ "Share address with contacts?" = "Megosztja a címet az ismerőseivel?"; @@ -4130,6 +4346,9 @@ /* No comment provided by engineer. */ "Share profile" = "Profil megosztása"; +/* No comment provided by engineer. */ +"Share SimpleX address on social media." = "SimpleX-cím megosztása a közösségi médiában."; + /* No comment provided by engineer. */ "Share this 1-time invite link" = "Egyszer használható meghívó-hivatkozás megosztása"; @@ -4175,6 +4394,12 @@ /* No comment provided by engineer. */ "SimpleX Address" = "SimpleX-cím"; +/* No comment provided by engineer. */ +"SimpleX address and 1-time links are safe to share via any messenger." = "A SimpleX-cím és az egyszer használható meghívó-hivatkozás biztonságosan megosztható bármilyen üzenetküldőn keresztül."; + +/* No comment provided by engineer. */ +"SimpleX address or 1-time link?" = "SimpleX-cím vagy egyszer használható meghívó-hivatkozás?"; + /* No comment provided by engineer. */ "SimpleX Chat security was audited by Trail of Bits." = "A SimpleX Chat biztonsága a Trail of Bits által lett auditálva."; @@ -4209,10 +4434,10 @@ "SimpleX Lock turned on" = "SimpleX-zár bekapcsolva"; /* simplex link type */ -"SimpleX one-time invitation" = "Egyszer használható SimpleX-meghívó"; +"SimpleX one-time invitation" = "Egyszer használható SimpleX-meghívó-hivatkozás"; /* No comment provided by engineer. */ -"SimpleX protocols reviewed by Trail of Bits." = "A SimpleX Chat biztonsága a Trail of Bits által lett újraauditálva."; +"SimpleX protocols reviewed by Trail of Bits." = "A SimpleX Chat biztonsága a Trail of Bits által lett felülvizsgálva."; /* No comment provided by engineer. */ "Simplified incognito mode" = "Egyszerűsített inkognitómód"; @@ -4250,6 +4475,9 @@ /* No comment provided by engineer. */ "Some non-fatal errors occurred during import:" = "Néhány nem végzetes hiba történt az importáláskor:"; +/* alert message */ +"Some servers failed the test:\n%@" = "Néhány kiszolgáló megbukott a teszten:\n%@"; + /* notification title */ "Somebody" = "Valaki"; @@ -4335,7 +4563,7 @@ "Switch audio and video during the call." = "Hang/Videó váltása hívás közben."; /* No comment provided by engineer. */ -"Switch chat profile for 1-time invitations." = "Csevegési profilváltás az egyszer használható meghívókhoz."; +"Switch chat profile for 1-time invitations." = "Csevegési profilváltás az egyszer használható meghívó-hivatkozásokhoz."; /* No comment provided by engineer. */ "System" = "Rendszer"; @@ -4412,6 +4640,9 @@ /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "Az alkalmazás értesíteni fogja, amikor üzeneteket vagy kapcsolatkéréseket kap – beállítások megnyitása az engedélyezéshez."; +/* No comment provided by engineer. */ +"The app protects your privacy by using different operators in each conversation." = "Az alkalmazás úgy védi az adatait, hogy minden egyes beszélgetésben más-más üzemeltetőket használ."; + /* No comment provided by engineer. */ "The app will ask to confirm downloads from unknown file servers (except .onion)." = "Az alkalmazás kérni fogja az ismeretlen fájlkiszolgálókról (kivéve .onion) történő letöltések megerősítését."; @@ -4421,6 +4652,9 @@ /* No comment provided by engineer. */ "The code you scanned is not a SimpleX link QR code." = "A beolvasott QR-kód nem egy SimpleX-QR-kód-hivatkozás."; +/* No comment provided by engineer. */ +"The connection reached the limit of undelivered messages, your contact may be offline." = "A kapcsolat elérte a kézbesítetlen üzenetek számának határát, az Ön ismerőse lehet, hogy offline állapotban van."; + /* No comment provided by engineer. */ "The connection you accepted will be cancelled!" = "Az Ön által elfogadott kérelem vissza lesz vonva!"; @@ -4460,6 +4694,9 @@ /* No comment provided by engineer. */ "The profile is only shared with your contacts." = "A profilja csak az ismerőseivel kerül megosztásra."; +/* No comment provided by engineer. */ +"The second preset operator in the app!" = "A második előre beállított üzemeltető az alkalmazásban!"; + /* No comment provided by engineer. */ "The second tick we missed! ✅" = "A második jelölés, amit kihagytunk! ✅"; @@ -4469,6 +4706,9 @@ /* No comment provided by engineer. */ "The servers for new connections of your current chat profile **%@**." = "A jelenlegi csevegési profilhoz tartozó új kapcsolatok kiszolgálói **%@**."; +/* No comment provided by engineer. */ +"The servers for new files of your current chat profile **%@**." = "Az Ön jelenlegi **%@** nevű csevegőprofiljához tartozó új fájlok kiszolgálói."; + /* No comment provided by engineer. */ "The text you pasted is not a SimpleX link." = "A beillesztett szöveg nem egy SimpleX-hivatkozás."; @@ -4478,6 +4718,9 @@ /* No comment provided by engineer. */ "Themes" = "Témák"; +/* No comment provided by engineer. */ +"These conditions will also apply for: **%@**." = "Ezek a feltételek lesznek elfogadva a következő számára is: **%@**."; + /* No comment provided by engineer. */ "These settings are for your current profile **%@**." = "Ezek a beállítások csak a jelenlegi (**%@**) profiljára vonatkoznak."; @@ -4515,7 +4758,7 @@ "This group no longer exists." = "Ez a csoport már nem létezik."; /* No comment provided by engineer. */ -"This is your own one-time link!" = "Ez az Ön egyszer használható hivatkozása!"; +"This is your own one-time link!" = "Ez az Ön egyszer használható meghívó-hivatkozása!"; /* No comment provided by engineer. */ "This is your own SimpleX address!" = "Ez az Ön SimpleX-címe!"; @@ -4541,6 +4784,9 @@ /* No comment provided by engineer. */ "To make a new connection" = "Új kapcsolat létrehozásához"; +/* No comment provided by engineer. */ +"To protect against your link being replaced, you can compare contact security codes." = "A hivatkozás cseréje elleni védelem érdekében összehasonlíthatja a biztonsági kódokat az ismerősével."; + /* No comment provided by engineer. */ "To protect timezone, image/voice files use UTC." = "Az időzóna védelmének érdekében a kép-/hangfájlok UTC-t használnak."; @@ -4553,6 +4799,9 @@ /* No comment provided by engineer. */ "To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Az adatvédelem érdekében (a más csevegési platformokon megszokott felhasználó-azonosítók helyett) a SimpleX csak az üzenetek sorbaállításához használ azonosítókat, az összes ismerőséhez különbözőt."; +/* No comment provided by engineer. */ +"To receive" = "A fogadáshoz"; + /* No comment provided by engineer. */ "To record speech please grant permission to use Microphone." = "A beszéd rögzítéséhez adjon engedélyt a Mikrofon használatára."; @@ -4565,9 +4814,15 @@ /* No comment provided by engineer. */ "To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." = "Rejtett profilja megjelenítéséhez írja be a teljes jelszavát a keresőmezőbe a **Csevegési profilok** menüben."; +/* No comment provided by engineer. */ +"To send" = "A küldéshez"; + /* No comment provided by engineer. */ "To support instant push notifications the chat database has to be migrated." = "Az azonnali push-értesítések támogatásához a csevegési adatbázis átköltöztetése szükséges."; +/* No comment provided by engineer. */ +"To use the servers of **%@**, accept conditions of use." = "A(z) **%@** kiszolgálóinak használatához fogadja el a használati feltételeket."; + /* No comment provided by engineer. */ "To verify end-to-end encryption with your contact compare (or scan) the code on your devices." = "A végpontok közötti titkosítás hitelesítéséhez hasonlítsa össze (vagy olvassa be a QR-kódot) az ismerőse eszközén lévő kóddal."; @@ -4625,6 +4880,9 @@ /* rcv group event chat item */ "unblocked %@" = "feloldotta %@ letiltását"; +/* No comment provided by engineer. */ +"Undelivered messages" = "Kézbesítetlen üzenetek"; + /* No comment provided by engineer. */ "Unexpected migration state" = "Váratlan átköltöztetési állapot"; @@ -4742,12 +5000,21 @@ /* No comment provided by engineer. */ "Use .onion hosts" = "Onion-kiszolgálók használata"; +/* No comment provided by engineer. */ +"Use %@" = "%@ használata"; + /* No comment provided by engineer. */ "Use chat" = "Csevegés használata"; /* No comment provided by engineer. */ "Use current profile" = "Jelenlegi profil használata"; +/* No comment provided by engineer. */ +"Use for files" = "Használat a fájlokhoz"; + +/* No comment provided by engineer. */ +"Use for messages" = "Használat az üzenetekhez"; + /* No comment provided by engineer. */ "Use for new connections" = "Alkalmazás új kapcsolatokhoz"; @@ -4755,7 +5022,7 @@ "Use from desktop" = "Társítás számítógéppel"; /* No comment provided by engineer. */ -"Use iOS call interface" = "Az iOS hívófelület használata"; +"Use iOS call interface" = "Az iOS hívási felületét használata"; /* No comment provided by engineer. */ "Use new incognito profile" = "Új inkognitóprofil használata"; @@ -4772,6 +5039,9 @@ /* No comment provided by engineer. */ "Use server" = "Kiszolgáló használata"; +/* No comment provided by engineer. */ +"Use servers" = "Kiszolgálók használata"; + /* No comment provided by engineer. */ "Use SimpleX Chat servers?" = "SimpleX Chat-kiszolgálók használata?"; @@ -4830,7 +5100,7 @@ "via group link" = "a csoporthivatkozáson keresztül"; /* chat list item description */ -"via one-time link" = "egyszer használható hivatkozáson keresztül"; +"via one-time link" = "egyszer használható meghívó-hivatkozáson keresztül"; /* No comment provided by engineer. */ "via relay" = "közvetítő-kiszolgálón keresztül"; @@ -4856,9 +5126,15 @@ /* No comment provided by engineer. */ "Videos and files up to 1gb" = "Videók és fájlok 1Gb méretig"; +/* No comment provided by engineer. */ +"View conditions" = "Feltételek megtekintése"; + /* No comment provided by engineer. */ "View security code" = "Biztonsági kód megtekintése"; +/* No comment provided by engineer. */ +"View updated conditions" = "Frissített feltételek megtekintése"; + /* chat feature */ "Visible history" = "Látható előzmények"; @@ -4940,6 +5216,9 @@ /* No comment provided by engineer. */ "when IP hidden" = "ha az IP-cím rejtett"; +/* No comment provided by engineer. */ +"When more than one operator is enabled, none of them has metadata to learn who communicates with whom." = "Amikor egynél több hálózati üzemeltető van engedélyezve, egyikük sem rendelkezik olyan metaadatokkal ahhoz, hogy felderítse, ki kommunikál kivel."; + /* No comment provided by engineer. */ "When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "Inkognitóprofil megosztása esetén a rendszer azt a profilt fogja használni azokhoz a csoportokhoz, amelyekbe meghívást kapott."; @@ -5007,7 +5286,7 @@ "You are already connecting to %@." = "Már folyamatban van a kapcsolódás ehhez: %@."; /* No comment provided by engineer. */ -"You are already connecting via this one-time link!" = "A kapcsolódás már folyamatban van ezen az egyszer használható hivatkozáson keresztül!"; +"You are already connecting via this one-time link!" = "A kapcsolódás már folyamatban van ezen az egyszer használható meghívó-hivatkozáson keresztül!"; /* No comment provided by engineer. */ "You are already in group %@." = "Ön már a(z) %@ nevű csoport tagja."; @@ -5048,6 +5327,12 @@ /* No comment provided by engineer. */ "You can change it in Appearance settings." = "Ezt a „Megjelenés” menüben módosíthatja."; +/* No comment provided by engineer. */ +"You can configure operators in Network & servers settings." = "Az üzemeltetőket a „Hálózat és kiszolgálók” beállításaban konfigurálhatja."; + +/* No comment provided by engineer. */ +"You can configure servers via settings." = "A kiszolgálókat a beállításokon keresztül konfigurálhatja."; + /* No comment provided by engineer. */ "You can create it later" = "Létrehozás később"; @@ -5072,6 +5357,9 @@ /* No comment provided by engineer. */ "You can send messages to %@ from Archived contacts." = "Az „Archivált ismerősökből” továbbra is küldhet üzeneteket neki: %@."; +/* No comment provided by engineer. */ +"You can set connection name, to remember who the link was shared with." = "Beállíthatja az ismerős nevét, hogy emlékezzen arra, hogy kivel osztotta meg a hivatkozást."; + /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "A beállításokon keresztül beállíthatja a lezárási képernyő értesítési előnézetét."; @@ -5163,10 +5451,10 @@ "You sent group invitation" = "Csoportmeghívó elküldve"; /* chat list item description */ -"you shared one-time link" = "egyszer használható hivatkozást osztott meg"; +"you shared one-time link" = "Ön egy egyszer használható meghívó-hivatkozást osztott meg"; /* chat list item description */ -"you shared one-time link incognito" = "egyszer használható hivatkozást osztott meg inkognitóban"; +"you shared one-time link incognito" = "Ön egy egyszer használható meghívó-hivatkozást osztott meg inkognitóban"; /* snd group event chat item */ "you unblocked %@" = "Ön feloldotta %@ letiltását"; @@ -5273,6 +5561,9 @@ /* No comment provided by engineer. */ "Your server address" = "Saját SMP-kiszolgálójának címe"; +/* No comment provided by engineer. */ +"Your servers" = "Az Ön kiszolgálói"; + /* No comment provided by engineer. */ "Your settings" = "Beállítások"; diff --git a/apps/ios/it.lproj/Localizable.strings b/apps/ios/it.lproj/Localizable.strings index 06d78256c7..9abd660f0c 100644 --- a/apps/ios/it.lproj/Localizable.strings +++ b/apps/ios/it.lproj/Localizable.strings @@ -82,6 +82,9 @@ /* No comment provided by engineer. */ "**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from." = "**Consigliato**: vengono inviati il token del dispositivo e le notifiche al server di notifica di SimpleX Chat, ma non il contenuto del messaggio,la sua dimensione o il suo mittente."; +/* No comment provided by engineer. */ +"**Scan / Paste link**: to connect via a link you received." = "**Scansiona / Incolla link**: per connetterti tramite un link che hai ricevuto."; + /* No comment provided by engineer. */ "**Warning**: Instant push notifications require passphrase saved in Keychain." = "**Attenzione**: le notifiche push istantanee richiedono una password salvata nel portachiavi."; @@ -142,6 +145,12 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ è verificato/a"; +/* No comment provided by engineer. */ +"%@ server" = "%@ server"; + +/* No comment provided by engineer. */ +"%@ servers" = "%@ server"; + /* No comment provided by engineer. */ "%@ uploaded" = "%@ caricati"; @@ -295,6 +304,12 @@ /* time interval */ "1 week" = "1 settimana"; +/* No comment provided by engineer. */ +"1-time link" = "Link una tantum"; + +/* No comment provided by engineer. */ +"1-time link can be used *with one contact only* - share in person or via any messenger." = "Il link una tantum può essere usato *con un solo contatto* - condividilo di persona o tramite qualsiasi messenger."; + /* No comment provided by engineer. */ "5 minutes" = "5 minuti"; @@ -342,6 +357,9 @@ swipe action */ "Accept" = "Accetta"; +/* No comment provided by engineer. */ +"Accept conditions" = "Accetta le condizioni"; + /* No comment provided by engineer. */ "Accept connection request?" = "Accettare la richiesta di connessione?"; @@ -355,6 +373,9 @@ /* call status */ "accepted call" = "chiamata accettata"; +/* No comment provided by engineer. */ +"Accepted conditions" = "Condizioni accettate"; + /* No comment provided by engineer. */ "Acknowledged" = "Riconosciuto"; @@ -382,6 +403,12 @@ /* No comment provided by engineer. */ "Add welcome message" = "Aggiungi messaggio di benvenuto"; +/* No comment provided by engineer. */ +"Added media & file servers" = "Server di multimediali e file aggiunti"; + +/* No comment provided by engineer. */ +"Added message servers" = "Server dei messaggi aggiunti"; + /* No comment provided by engineer. */ "Additional accent" = "Principale aggiuntivo"; @@ -397,6 +424,12 @@ /* No comment provided by engineer. */ "Address change will be aborted. Old receiving address will be used." = "Il cambio di indirizzo verrà interrotto. Verrà usato il vecchio indirizzo di ricezione."; +/* No comment provided by engineer. */ +"Address or 1-time link?" = "Indirizzo o link una tantum?"; + +/* No comment provided by engineer. */ +"Address settings" = "Impostazioni dell'indirizzo"; + /* member role */ "admin" = "amministratore"; @@ -439,6 +472,9 @@ /* feature role */ "all members" = "tutti i membri"; +/* No comment provided by engineer. */ +"All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages." = "Tutti i messaggi e i file vengono inviati **crittografati end-to-end**, con sicurezza resistenti alla quantistica nei messaggi diretti."; + /* No comment provided by engineer. */ "All messages will be deleted - this cannot be undone!" = "Tutti i messaggi verranno eliminati, non è reversibile!"; @@ -855,6 +891,9 @@ set passcode view */ "Change self-destruct passcode" = "Cambia codice di autodistruzione"; +/* authentication reason */ +"Change user profiles" = "Modifica profili utente"; + /* chat item text */ "changed address for you" = "indirizzo cambiato per te"; @@ -918,6 +957,12 @@ /* No comment provided by engineer. */ "Chats" = "Chat"; +/* No comment provided by engineer. */ +"Check messages every 20 min." = "Controlla i messaggi ogni 20 min."; + +/* No comment provided by engineer. */ +"Check messages when allowed." = "Controlla i messaggi quando consentito."; + /* alert title */ "Check server address and try again." = "Controlla l'indirizzo del server e riprova."; @@ -978,6 +1023,33 @@ /* No comment provided by engineer. */ "Completed" = "Completato"; +/* No comment provided by engineer. */ +"Conditions accepted on: %@." = "Condizioni accettate il: %@."; + +/* No comment provided by engineer. */ +"Conditions are accepted for the operator(s): **%@**." = "Le condizioni sono state accettate per gli operatori: **%@**."; + +/* No comment provided by engineer. */ +"Conditions are already accepted for following operator(s): **%@**." = "Le condizioni sono già state accettate per i seguenti operatori: **%@**."; + +/* No comment provided by engineer. */ +"Conditions of use" = "Condizioni d'uso"; + +/* No comment provided by engineer. */ +"Conditions will be accepted for enabled operators after 30 days." = "Le condizioni verranno accettate per gli operatori attivati dopo 30 giorni."; + +/* No comment provided by engineer. */ +"Conditions will be accepted for operator(s): **%@**." = "Le condizioni verranno accettate per gli operatori: **%@**."; + +/* No comment provided by engineer. */ +"Conditions will be accepted for the operator(s): **%@**." = "Le condizioni verranno accettate per gli operatori: **%@**."; + +/* No comment provided by engineer. */ +"Conditions will be accepted on: %@." = "Le condizioni verranno accettate il: %@."; + +/* No comment provided by engineer. */ +"Conditions will be automatically accepted for enabled operators on: %@." = "Le condizioni verranno accettate automaticamente per gli operatori attivi il: %@."; + /* No comment provided by engineer. */ "Configure ICE servers" = "Configura server ICE"; @@ -1125,6 +1197,9 @@ /* No comment provided by engineer. */ "Connection request sent!" = "Richiesta di connessione inviata!"; +/* No comment provided by engineer. */ +"Connection security" = "Sicurezza della connessione"; + /* No comment provided by engineer. */ "Connection terminated" = "Connessione terminata"; @@ -1206,6 +1281,9 @@ /* No comment provided by engineer. */ "Create" = "Crea"; +/* No comment provided by engineer. */ +"Create 1-time link" = "Crea link una tantum"; + /* No comment provided by engineer. */ "Create a group using a random profile." = "Crea un gruppo usando un profilo casuale."; @@ -1257,6 +1335,9 @@ /* No comment provided by engineer. */ "creator" = "creatore"; +/* No comment provided by engineer. */ +"Current conditions text couldn't be loaded, you can review conditions via this link:" = "Il testo delle condizioni attuali testo non è stato caricato, puoi consultare le condizioni tramite questo link:"; + /* No comment provided by engineer. */ "Current Passcode" = "Codice di accesso attuale"; @@ -1505,6 +1586,9 @@ /* No comment provided by engineer. */ "Deletion errors" = "Errori di eliminazione"; +/* No comment provided by engineer. */ +"Delivered even when Apple drops them." = "Consegnati anche quando Apple li scarta."; + /* No comment provided by engineer. */ "Delivery" = "Consegna"; @@ -1692,6 +1776,9 @@ /* No comment provided by engineer. */ "e2e encrypted" = "crittografato e2e"; +/* No comment provided by engineer. */ +"E2E encrypted notifications." = "Notifiche crittografate E2E."; + /* chat item action */ "Edit" = "Modifica"; @@ -1710,6 +1797,9 @@ /* No comment provided by engineer. */ "Enable camera access" = "Attiva l'accesso alla fotocamera"; +/* No comment provided by engineer. */ +"Enable Flux" = "Attiva Flux"; + /* No comment provided by engineer. */ "Enable for all" = "Attiva per tutti"; @@ -1869,12 +1959,18 @@ /* No comment provided by engineer. */ "Error aborting address change" = "Errore nell'interruzione del cambio di indirizzo"; +/* alert title */ +"Error accepting conditions" = "Errore di accettazione delle condizioni"; + /* No comment provided by engineer. */ "Error accepting contact request" = "Errore nell'accettazione della richiesta di contatto"; /* No comment provided by engineer. */ "Error adding member(s)" = "Errore di aggiunta membro/i"; +/* alert title */ +"Error adding server" = "Errore di aggiunta del server"; + /* No comment provided by engineer. */ "Error changing address" = "Errore nella modifica dell'indirizzo"; @@ -1959,6 +2055,9 @@ /* No comment provided by engineer. */ "Error joining group" = "Errore di ingresso nel gruppo"; +/* alert title */ +"Error loading servers" = "Errore nel caricamento dei server"; + /* No comment provided by engineer. */ "Error migrating settings" = "Errore nella migrazione delle impostazioni"; @@ -1992,6 +2091,9 @@ /* No comment provided by engineer. */ "Error saving passphrase to keychain" = "Errore nel salvataggio della password nel portachiavi"; +/* alert title */ +"Error saving servers" = "Errore di salvataggio dei server"; + /* when migrating */ "Error saving settings" = "Errore di salvataggio delle impostazioni"; @@ -2034,6 +2136,9 @@ /* No comment provided by engineer. */ "Error updating message" = "Errore nell'aggiornamento del messaggio"; +/* alert title */ +"Error updating server" = "Errore di aggiornamento del server"; + /* No comment provided by engineer. */ "Error updating settings" = "Errore nell'aggiornamento delle impostazioni"; @@ -2061,6 +2166,9 @@ /* No comment provided by engineer. */ "Errors" = "Errori"; +/* servers error */ +"Errors in servers configuration." = "Errori nella configurazione dei server."; + /* No comment provided by engineer. */ "Even when disabled in the conversation." = "Anche quando disattivato nella conversazione."; @@ -2187,9 +2295,24 @@ /* No comment provided by engineer. */ "Fix not supported by group member" = "Correzione non supportata dal membro del gruppo"; +/* No comment provided by engineer. */ +"for better metadata privacy." = "per una migliore privacy dei metadati."; + +/* servers error */ +"For chat profile %@:" = "Per il profilo di chat %@:"; + /* No comment provided by engineer. */ "For console" = "Per console"; +/* No comment provided by engineer. */ +"For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server." = "Ad esempio, se il tuo contatto riceve messaggi tramite un server di SimpleX Chat, la tua app li consegnerà tramite un server Flux."; + +/* No comment provided by engineer. */ +"For private routing" = "Per l'instradamento privato"; + +/* No comment provided by engineer. */ +"For social media" = "Per i social media"; + /* chat item action */ "Forward" = "Inoltra"; @@ -2382,6 +2505,12 @@ /* time unit */ "hours" = "ore"; +/* No comment provided by engineer. */ +"How it affects privacy" = "Come influisce sulla privacy"; + +/* No comment provided by engineer. */ +"How it helps privacy" = "Come aiuta la privacy"; + /* No comment provided by engineer. */ "How SimpleX works" = "Come funziona SimpleX"; @@ -2958,6 +3087,9 @@ /* No comment provided by engineer. */ "More reliable network connection." = "Connessione di rete più affidabile."; +/* No comment provided by engineer. */ +"More reliable notifications" = "Notifiche più affidabili"; + /* item status description */ "Most likely this connection is deleted." = "Probabilmente questa connessione è stata eliminata."; @@ -2982,12 +3114,18 @@ /* No comment provided by engineer. */ "Network connection" = "Connessione di rete"; +/* No comment provided by engineer. */ +"Network decentralization" = "Decentralizzazione della rete"; + /* snd error text */ "Network issues - message expired after many attempts to send it." = "Problemi di rete - messaggio scaduto dopo molti tentativi di inviarlo."; /* No comment provided by engineer. */ "Network management" = "Gestione della rete"; +/* No comment provided by engineer. */ +"Network operator" = "Operatore di rete"; + /* No comment provided by engineer. */ "Network settings" = "Impostazioni di rete"; @@ -3015,6 +3153,9 @@ /* No comment provided by engineer. */ "New display name" = "Nuovo nome da mostrare"; +/* notification */ +"New events" = "Nuovi eventi"; + /* No comment provided by engineer. */ "New in %@" = "Novità nella %@"; @@ -3036,6 +3177,9 @@ /* No comment provided by engineer. */ "New passphrase…" = "Nuova password…"; +/* No comment provided by engineer. */ +"New server" = "Nuovo server"; + /* No comment provided by engineer. */ "New SOCKS credentials will be used every time you start the app." = "Le nuove credenziali SOCKS verranno usate ogni volta che avvii l'app."; @@ -3081,6 +3225,12 @@ /* No comment provided by engineer. */ "No info, try to reload" = "Nessuna informazione, prova a ricaricare"; +/* servers error */ +"No media & file servers." = "Nessun server di multimediali e file."; + +/* servers error */ +"No message servers." = "Nessun server dei messaggi."; + /* No comment provided by engineer. */ "No network connection" = "Nessuna connessione di rete"; @@ -3099,6 +3249,18 @@ /* No comment provided by engineer. */ "No received or sent files" = "Nessun file ricevuto o inviato"; +/* servers error */ +"No servers for private message routing." = "Nessun server per l'instradamento dei messaggi privati."; + +/* servers error */ +"No servers to receive files." = "Nessun server per ricevere file."; + +/* servers error */ +"No servers to receive messages." = "Nessun server per ricevere messaggi."; + +/* servers error */ +"No servers to send files." = "Nessun server per inviare file."; + /* copied message info in history */ "no text" = "nessun testo"; @@ -3120,6 +3282,9 @@ /* No comment provided by engineer. */ "Notifications are disabled!" = "Le notifiche sono disattivate!"; +/* No comment provided by engineer. */ +"Notifications privacy" = "Privacy delle notifiche"; + /* No comment provided by engineer. */ "Now admins can:\n- delete members' messages.\n- disable members (\"observer\" role)" = "Ora gli amministratori possono:\n- eliminare i messaggi dei membri.\n- disattivare i membri (ruolo \"osservatore\")"; @@ -3212,12 +3377,18 @@ /* No comment provided by engineer. */ "Open" = "Apri"; +/* No comment provided by engineer. */ +"Open changes" = "Apri le modifiche"; + /* No comment provided by engineer. */ "Open chat" = "Apri chat"; /* authentication reason */ "Open chat console" = "Apri la console della chat"; +/* No comment provided by engineer. */ +"Open conditions" = "Apri le condizioni"; + /* No comment provided by engineer. */ "Open group" = "Apri gruppo"; @@ -3230,6 +3401,12 @@ /* No comment provided by engineer. */ "Opening app…" = "Apertura dell'app…"; +/* No comment provided by engineer. */ +"Operator" = "Operatore"; + +/* alert title */ +"Operator server" = "Server dell'operatore"; + /* No comment provided by engineer. */ "Or paste archive link" = "O incolla il link dell'archivio"; @@ -3242,6 +3419,9 @@ /* No comment provided by engineer. */ "Or show this code" = "O mostra questo codice"; +/* No comment provided by engineer. */ +"Or to share privately" = "O per condividere in modo privato"; + /* No comment provided by engineer. */ "other" = "altro"; @@ -3383,6 +3563,9 @@ /* No comment provided by engineer. */ "Preset server address" = "Indirizzo server preimpostato"; +/* No comment provided by engineer. */ +"Preset servers" = "Server preimpostati"; + /* No comment provided by engineer. */ "Preview" = "Anteprima"; @@ -3488,6 +3671,9 @@ /* No comment provided by engineer. */ "Push notifications" = "Notifiche push"; +/* No comment provided by engineer. */ +"Push Notifications" = "Notifiche push"; + /* No comment provided by engineer. */ "Push server" = "Server push"; @@ -3735,6 +3921,12 @@ /* chat item action */ "Reveal" = "Rivela"; +/* No comment provided by engineer. */ +"Review conditions" = "Esamina le condizioni"; + +/* No comment provided by engineer. */ +"Review later" = "Esamina più tardi"; + /* No comment provided by engineer. */ "Revoke" = "Revoca"; @@ -3756,6 +3948,12 @@ /* No comment provided by engineer. */ "Safer groups" = "Gruppi più sicuri"; +/* No comment provided by engineer. */ +"Same conditions will apply to operator **%@**." = "Le stesse condizioni si applicheranno all'operatore **%@**."; + +/* No comment provided by engineer. */ +"Same conditions will apply to operator(s): **%@**." = "Le stesse condizioni si applicheranno agli operatori **%@**."; + /* alert button chat item action */ "Save" = "Salva"; @@ -4021,6 +4219,9 @@ /* No comment provided by engineer. */ "Server" = "Server"; +/* alert message */ +"Server added to operator %@." = "Server aggiunto all'operatore %@."; + /* No comment provided by engineer. */ "Server address" = "Indirizzo server"; @@ -4030,6 +4231,15 @@ /* srv error text. */ "Server address is incompatible with network settings." = "L'indirizzo del server non è compatibile con le impostazioni di rete."; +/* alert title */ +"Server operator changed." = "L'operatore del server è cambiato."; + +/* No comment provided by engineer. */ +"Server operators" = "Operatori server"; + +/* alert title */ +"Server protocol changed." = "Il protocollo del server è cambiato."; + /* queue info */ "server queue info: %@\n\nlast received msg: %@" = "info coda server: %1$@\n\nultimo msg ricevuto: %2$@"; @@ -4115,9 +4325,15 @@ /* No comment provided by engineer. */ "Share 1-time link" = "Condividi link una tantum"; +/* No comment provided by engineer. */ +"Share 1-time link with a friend" = "Condividi link una tantum con un amico"; + /* No comment provided by engineer. */ "Share address" = "Condividi indirizzo"; +/* No comment provided by engineer. */ +"Share address publicly" = "Condividi indirizzo pubblicamente"; + /* alert title */ "Share address with contacts?" = "Condividere l'indirizzo con i contatti?"; @@ -4130,6 +4346,9 @@ /* No comment provided by engineer. */ "Share profile" = "Condividi il profilo"; +/* No comment provided by engineer. */ +"Share SimpleX address on social media." = "Condividi indirizzo SimpleX sui social media."; + /* No comment provided by engineer. */ "Share this 1-time invite link" = "Condividi questo link di invito una tantum"; @@ -4175,6 +4394,12 @@ /* No comment provided by engineer. */ "SimpleX Address" = "Indirizzo SimpleX"; +/* No comment provided by engineer. */ +"SimpleX address and 1-time links are safe to share via any messenger." = "L'indirizzo SimpleX e i link una tantum sono sicuri da condividere tramite qualsiasi messenger."; + +/* No comment provided by engineer. */ +"SimpleX address or 1-time link?" = "Indirizzo SimpleX o link una tantum?"; + /* No comment provided by engineer. */ "SimpleX Chat security was audited by Trail of Bits." = "La sicurezza di SimpleX Chat è stata verificata da Trail of Bits."; @@ -4250,6 +4475,9 @@ /* No comment provided by engineer. */ "Some non-fatal errors occurred during import:" = "Si sono verificati alcuni errori non fatali durante l'importazione:"; +/* alert message */ +"Some servers failed the test:\n%@" = "Alcuni server hanno fallito il test:\n%@"; + /* notification title */ "Somebody" = "Qualcuno"; @@ -4412,6 +4640,9 @@ /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "L'app può avvisarti quando ricevi messaggi o richieste di contatto: apri le impostazioni per attivare."; +/* No comment provided by engineer. */ +"The app protects your privacy by using different operators in each conversation." = "L'app protegge la tua privacy usando diversi operatori in ogni conversazione."; + /* No comment provided by engineer. */ "The app will ask to confirm downloads from unknown file servers (except .onion)." = "L'app chiederà di confermare i download da server di file sconosciuti (eccetto .onion)."; @@ -4421,6 +4652,9 @@ /* No comment provided by engineer. */ "The code you scanned is not a SimpleX link QR code." = "Il codice che hai scansionato non è un codice QR di link SimpleX."; +/* No comment provided by engineer. */ +"The connection reached the limit of undelivered messages, your contact may be offline." = "La connessione ha raggiunto il limite di messaggi non consegnati, il contatto potrebbe essere offline."; + /* No comment provided by engineer. */ "The connection you accepted will be cancelled!" = "La connessione che hai accettato verrà annullata!"; @@ -4460,6 +4694,9 @@ /* No comment provided by engineer. */ "The profile is only shared with your contacts." = "Il profilo è condiviso solo con i tuoi contatti."; +/* No comment provided by engineer. */ +"The second preset operator in the app!" = "Il secondo operatore preimpostato nell'app!"; + /* No comment provided by engineer. */ "The second tick we missed! ✅" = "Il secondo segno di spunta che ci mancava! ✅"; @@ -4469,6 +4706,9 @@ /* No comment provided by engineer. */ "The servers for new connections of your current chat profile **%@**." = "I server per le nuove connessioni del profilo di chat attuale **%@**."; +/* No comment provided by engineer. */ +"The servers for new files of your current chat profile **%@**." = "I server per nuovi file del tuo profilo di chat attuale **%@**."; + /* No comment provided by engineer. */ "The text you pasted is not a SimpleX link." = "Il testo che hai incollato non è un link SimpleX."; @@ -4478,6 +4718,9 @@ /* No comment provided by engineer. */ "Themes" = "Temi"; +/* No comment provided by engineer. */ +"These conditions will also apply for: **%@**." = "Queste condizioni si applicheranno anche per: **%@**."; + /* No comment provided by engineer. */ "These settings are for your current profile **%@**." = "Queste impostazioni sono per il tuo profilo attuale **%@**."; @@ -4541,6 +4784,9 @@ /* No comment provided by engineer. */ "To make a new connection" = "Per creare una nuova connessione"; +/* No comment provided by engineer. */ +"To protect against your link being replaced, you can compare contact security codes." = "Per proteggerti dalla sostituzione del tuo link, puoi confrontare i codici di sicurezza del contatto."; + /* No comment provided by engineer. */ "To protect timezone, image/voice files use UTC." = "Per proteggere il fuso orario, i file immagine/vocali usano UTC."; @@ -4553,6 +4799,9 @@ /* No comment provided by engineer. */ "To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Per proteggere la privacy, invece degli ID utente utilizzati da tutte le altre piattaforme, SimpleX ha identificatori per le code di messaggi, separati per ciascuno dei tuoi contatti."; +/* No comment provided by engineer. */ +"To receive" = "Per ricevere"; + /* No comment provided by engineer. */ "To record speech please grant permission to use Microphone." = "Per registrare l'audio, concedi l'autorizzazione di usare il microfono."; @@ -4565,9 +4814,15 @@ /* No comment provided by engineer. */ "To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." = "Per rivelare il tuo profilo nascosto, inserisci una password completa in un campo di ricerca nella pagina **I tuoi profili di chat**."; +/* No comment provided by engineer. */ +"To send" = "Per inviare"; + /* No comment provided by engineer. */ "To support instant push notifications the chat database has to be migrated." = "Per supportare le notifiche push istantanee, il database della chat deve essere migrato."; +/* No comment provided by engineer. */ +"To use the servers of **%@**, accept conditions of use." = "Per usare i server di **%@**, accetta le condizioni d'uso."; + /* No comment provided by engineer. */ "To verify end-to-end encryption with your contact compare (or scan) the code on your devices." = "Per verificare la crittografia end-to-end con il tuo contatto, confrontate (o scansionate) il codice sui vostri dispositivi."; @@ -4625,6 +4880,9 @@ /* rcv group event chat item */ "unblocked %@" = "ha sbloccato %@"; +/* No comment provided by engineer. */ +"Undelivered messages" = "Messaggi non consegnati"; + /* No comment provided by engineer. */ "Unexpected migration state" = "Stato di migrazione imprevisto"; @@ -4742,12 +5000,21 @@ /* No comment provided by engineer. */ "Use .onion hosts" = "Usa gli host .onion"; +/* No comment provided by engineer. */ +"Use %@" = "Usa %@"; + /* No comment provided by engineer. */ "Use chat" = "Usa la chat"; /* No comment provided by engineer. */ "Use current profile" = "Usa il profilo attuale"; +/* No comment provided by engineer. */ +"Use for files" = "Usa per i file"; + +/* No comment provided by engineer. */ +"Use for messages" = "Usa per i messaggi"; + /* No comment provided by engineer. */ "Use for new connections" = "Usa per connessioni nuove"; @@ -4772,6 +5039,9 @@ /* No comment provided by engineer. */ "Use server" = "Usa il server"; +/* No comment provided by engineer. */ +"Use servers" = "Usa i server"; + /* No comment provided by engineer. */ "Use SimpleX Chat servers?" = "Usare i server di SimpleX Chat?"; @@ -4856,9 +5126,15 @@ /* No comment provided by engineer. */ "Videos and files up to 1gb" = "Video e file fino a 1 GB"; +/* No comment provided by engineer. */ +"View conditions" = "Vedi le condizioni"; + /* No comment provided by engineer. */ "View security code" = "Vedi codice di sicurezza"; +/* No comment provided by engineer. */ +"View updated conditions" = "Vedi le condizioni aggiornate"; + /* chat feature */ "Visible history" = "Cronologia visibile"; @@ -4940,6 +5216,9 @@ /* No comment provided by engineer. */ "when IP hidden" = "quando l'IP è nascosto"; +/* No comment provided by engineer. */ +"When more than one operator is enabled, none of them has metadata to learn who communicates with whom." = "Quando più di un operatore è attivato, nessuno di essi ha metadati per scoprire chi comunica con chi."; + /* No comment provided by engineer. */ "When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "Quando condividi un profilo in incognito con qualcuno, questo profilo verrà utilizzato per i gruppi a cui ti invitano."; @@ -5048,6 +5327,12 @@ /* No comment provided by engineer. */ "You can change it in Appearance settings." = "Puoi cambiarlo nelle impostazioni dell'aspetto."; +/* No comment provided by engineer. */ +"You can configure operators in Network & servers settings." = "Puoi configurare gli operatori nelle impostazioni di rete e server."; + +/* No comment provided by engineer. */ +"You can configure servers via settings." = "Puoi configurare i server nelle impostazioni."; + /* No comment provided by engineer. */ "You can create it later" = "Puoi crearlo più tardi"; @@ -5072,6 +5357,9 @@ /* No comment provided by engineer. */ "You can send messages to %@ from Archived contacts." = "Puoi inviare messaggi a %@ dai contatti archiviati."; +/* No comment provided by engineer. */ +"You can set connection name, to remember who the link was shared with." = "Puoi impostare il nome della connessione per ricordare con chi è stato condiviso il link."; + /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "Puoi impostare l'anteprima della notifica nella schermata di blocco tramite le impostazioni."; @@ -5273,6 +5561,9 @@ /* No comment provided by engineer. */ "Your server address" = "L'indirizzo del tuo server"; +/* No comment provided by engineer. */ +"Your servers" = "I tuoi server"; + /* No comment provided by engineer. */ "Your settings" = "Le tue impostazioni"; diff --git a/apps/ios/ja.lproj/Localizable.strings b/apps/ios/ja.lproj/Localizable.strings index 7e1d7f0527..5bcd706702 100644 --- a/apps/ios/ja.lproj/Localizable.strings +++ b/apps/ios/ja.lproj/Localizable.strings @@ -684,6 +684,9 @@ /* No comment provided by engineer. */ "Chat profile" = "ユーザープロフィール"; +/* No comment provided by engineer. */ +"Chat theme" = "チャットテーマ"; + /* No comment provided by engineer. */ "Chats" = "チャット"; @@ -699,6 +702,12 @@ /* No comment provided by engineer. */ "Choose from library" = "ライブラリから選択"; +/* No comment provided by engineer. */ +"Chunks deleted" = "チャンクが削除されました"; + +/* No comment provided by engineer. */ +"Chunks downloaded" = "チャンクがダウンロードされました"; + /* swipe action */ "Clear" = "消す"; @@ -708,9 +717,15 @@ /* No comment provided by engineer. */ "Clear conversation?" = "ダイアログのクリアしますか?"; +/* No comment provided by engineer. */ +"Clear private notes?" = "プライベートノートを消しますか?"; + /* No comment provided by engineer. */ "Clear verification" = "検証を消す"; +/* No comment provided by engineer. */ +"Color mode" = "色設定"; + /* No comment provided by engineer. */ "colored" = "色付き"; @@ -723,6 +738,9 @@ /* No comment provided by engineer. */ "complete" = "完了"; +/* No comment provided by engineer. */ +"Completed" = "完了"; + /* No comment provided by engineer. */ "Configure ICE servers" = "ICEサーバを設定"; @@ -747,9 +765,15 @@ /* No comment provided by engineer. */ "Connect incognito" = "シークレットモードで接続"; +/* No comment provided by engineer. */ +"Connect to desktop" = "デスクトップに接続"; + /* No comment provided by engineer. */ "connect to SimpleX Chat developers." = "SimpleX Chat 開発者に接続します。"; +/* No comment provided by engineer. */ +"Connect to your friends faster." = "友達ともっと速くつながりましょう。"; + /* No comment provided by engineer. */ "Connect via link" = "リンク経由で接続"; @@ -759,9 +783,24 @@ /* No comment provided by engineer. */ "connected" = "接続中"; +/* No comment provided by engineer. */ +"Connected" = "接続中"; + +/* No comment provided by engineer. */ +"Connected desktop" = "デスクトップに接続済"; + +/* No comment provided by engineer. */ +"Connected servers" = "接続中のサーバ"; + +/* No comment provided by engineer. */ +"Connected to desktop" = "デスクトップに接続済"; + /* No comment provided by engineer. */ "connecting" = "接続待ち"; +/* No comment provided by engineer. */ +"Connecting" = "接続待ち"; + /* No comment provided by engineer. */ "connecting (accepted)" = "接続待ち (承諾済み)"; @@ -783,12 +822,21 @@ /* No comment provided by engineer. */ "Connecting server… (error: %@)" = "サーバーに接続中… (エラー: %@)"; +/* No comment provided by engineer. */ +"Connecting to contact, please wait or check later!" = "連絡先に接続中です。しばらくお待ちいただくか、後で確認してください!"; + +/* No comment provided by engineer. */ +"Connecting to desktop" = "デスクトップに接続中"; + /* chat list item title */ "connecting…" = "接続待ち…"; /* No comment provided by engineer. */ "Connection" = "接続"; +/* No comment provided by engineer. */ +"Connection and servers status." = "接続とサーバーのステータス"; + /* No comment provided by engineer. */ "Connection error" = "接続エラー"; @@ -801,6 +849,9 @@ /* No comment provided by engineer. */ "Connection request sent!" = "接続リクエストを送信しました!"; +/* No comment provided by engineer. */ +"Connection terminated" = "接続停止"; + /* No comment provided by engineer. */ "Connection timeout" = "接続タイムアウト"; @@ -891,9 +942,15 @@ /* No comment provided by engineer. */ "Custom time" = "カスタム時間"; +/* No comment provided by engineer. */ +"Customize theme" = "カスタムテーマ"; + /* No comment provided by engineer. */ "Dark" = "ダークモード"; +/* No comment provided by engineer. */ +"Dark mode colors" = "ダークモードカラー"; + /* No comment provided by engineer. */ "Database downgrade" = "データーベースのダウングレード"; @@ -954,6 +1011,9 @@ /* time unit */ "days" = "日"; +/* No comment provided by engineer. */ +"Debug delivery" = "配信のデバッグ"; + /* No comment provided by engineer. */ "Decentralized" = "分散型"; @@ -1085,9 +1145,15 @@ /* No comment provided by engineer. */ "Description" = "説明"; +/* No comment provided by engineer. */ +"Desktop devices" = "デスクトップ機器"; + /* No comment provided by engineer. */ "Develop" = "開発"; +/* No comment provided by engineer. */ +"Developer options" = "開発者向けの設定"; + /* No comment provided by engineer. */ "Developer tools" = "開発ツール"; diff --git a/apps/ios/nl.lproj/Localizable.strings b/apps/ios/nl.lproj/Localizable.strings index aa324a2ee0..fadec1b09b 100644 --- a/apps/ios/nl.lproj/Localizable.strings +++ b/apps/ios/nl.lproj/Localizable.strings @@ -82,6 +82,9 @@ /* No comment provided by engineer. */ "**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from." = "**Aanbevolen**: apparaattoken en meldingen worden naar de SimpleX Chat-meldingsserver gestuurd, maar niet de berichtinhoud, -grootte of van wie het afkomstig is."; +/* No comment provided by engineer. */ +"**Scan / Paste link**: to connect via a link you received." = "**Link scannen/plakken**: om verbinding te maken via een link die u hebt ontvangen."; + /* No comment provided by engineer. */ "**Warning**: Instant push notifications require passphrase saved in Keychain." = "**Waarschuwing**: voor directe push meldingen is een wachtwoord vereist dat is opgeslagen in de Keychain."; @@ -142,6 +145,12 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ is geverifieerd"; +/* No comment provided by engineer. */ +"%@ server" = "%@ server"; + +/* No comment provided by engineer. */ +"%@ servers" = "%@ servers"; + /* No comment provided by engineer. */ "%@ uploaded" = "%@ geüpload"; @@ -295,6 +304,12 @@ /* time interval */ "1 week" = "1 week"; +/* No comment provided by engineer. */ +"1-time link" = "Eenmalige link"; + +/* No comment provided by engineer. */ +"1-time link can be used *with one contact only* - share in person or via any messenger." = "Eenmalige link die *slechts met één contactpersoon* kan worden gebruikt - deel persoonlijk of via een messenger."; + /* No comment provided by engineer. */ "5 minutes" = "5 minuten"; @@ -342,6 +357,9 @@ swipe action */ "Accept" = "Accepteer"; +/* No comment provided by engineer. */ +"Accept conditions" = "Accepteer voorwaarden"; + /* No comment provided by engineer. */ "Accept connection request?" = "Accepteer contact"; @@ -355,6 +373,9 @@ /* call status */ "accepted call" = "geaccepteerde oproep"; +/* No comment provided by engineer. */ +"Accepted conditions" = "Geaccepteerde voorwaarden"; + /* No comment provided by engineer. */ "Acknowledged" = "Erkend"; @@ -382,6 +403,12 @@ /* No comment provided by engineer. */ "Add welcome message" = "Welkom bericht toevoegen"; +/* No comment provided by engineer. */ +"Added media & file servers" = "Media- en bestandsservers toegevoegd"; + +/* No comment provided by engineer. */ +"Added message servers" = "Berichtservers toegevoegd"; + /* No comment provided by engineer. */ "Additional accent" = "Extra accent"; @@ -397,6 +424,12 @@ /* No comment provided by engineer. */ "Address change will be aborted. Old receiving address will be used." = "Adres wijziging wordt afgebroken. Het oude ontvangstadres wordt gebruikt."; +/* No comment provided by engineer. */ +"Address or 1-time link?" = "Adres of eenmalige link?"; + +/* No comment provided by engineer. */ +"Address settings" = "Adres instellingen"; + /* member role */ "admin" = "Beheerder"; @@ -439,6 +472,9 @@ /* feature role */ "all members" = "alle leden"; +/* No comment provided by engineer. */ +"All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages." = "Alle berichten en bestanden worden **end-to-end versleuteld** verzonden, met post-quantumbeveiliging in directe berichten."; + /* No comment provided by engineer. */ "All messages will be deleted - this cannot be undone!" = "Alle berichten worden verwijderd. Dit kan niet ongedaan worden gemaakt!"; @@ -855,6 +891,9 @@ set passcode view */ "Change self-destruct passcode" = "Zelfvernietigings code wijzigen"; +/* authentication reason */ +"Change user profiles" = "Gebruikersprofielen wijzigen"; + /* chat item text */ "changed address for you" = "adres voor u gewijzigd"; @@ -918,6 +957,12 @@ /* No comment provided by engineer. */ "Chats" = "Chats"; +/* No comment provided by engineer. */ +"Check messages every 20 min." = "Controleer uw berichten elke 20 minuten."; + +/* No comment provided by engineer. */ +"Check messages when allowed." = "Controleer berichten indien toegestaan."; + /* alert title */ "Check server address and try again." = "Controleer het server adres en probeer het opnieuw."; @@ -978,6 +1023,33 @@ /* No comment provided by engineer. */ "Completed" = "Voltooid"; +/* No comment provided by engineer. */ +"Conditions accepted on: %@." = "Voorwaarden geaccepteerd op: %@."; + +/* No comment provided by engineer. */ +"Conditions are accepted for the operator(s): **%@**." = "Voorwaarden worden geaccepteerd voor de operator(s): **%@**."; + +/* No comment provided by engineer. */ +"Conditions are already accepted for following operator(s): **%@**." = "Voorwaarden zijn reeds geaccepteerd voor de volgende operator(s): **%@**."; + +/* No comment provided by engineer. */ +"Conditions of use" = "Gebruiksvoorwaarden"; + +/* No comment provided by engineer. */ +"Conditions will be accepted for enabled operators after 30 days." = "Voor ingeschakelde operators worden de voorwaarden na 30 dagen geaccepteerd."; + +/* No comment provided by engineer. */ +"Conditions will be accepted for operator(s): **%@**." = "Voorwaarden worden geaccepteerd voor operator(s): **%@**."; + +/* No comment provided by engineer. */ +"Conditions will be accepted for the operator(s): **%@**." = "Voorwaarden worden geaccepteerd voor de operator(s): **%@**."; + +/* No comment provided by engineer. */ +"Conditions will be accepted on: %@." = "Voorwaarden worden geaccepteerd op: %@."; + +/* No comment provided by engineer. */ +"Conditions will be automatically accepted for enabled operators on: %@." = "Voorwaarden worden automatisch geaccepteerd voor ingeschakelde operators op: %@."; + /* No comment provided by engineer. */ "Configure ICE servers" = "ICE servers configureren"; @@ -1125,6 +1197,9 @@ /* No comment provided by engineer. */ "Connection request sent!" = "Verbindingsverzoek verzonden!"; +/* No comment provided by engineer. */ +"Connection security" = "Beveiliging van de verbinding"; + /* No comment provided by engineer. */ "Connection terminated" = "Verbinding beëindigd"; @@ -1206,6 +1281,9 @@ /* No comment provided by engineer. */ "Create" = "Maak"; +/* No comment provided by engineer. */ +"Create 1-time link" = "Eenmalige link maken"; + /* No comment provided by engineer. */ "Create a group using a random profile." = "Maak een groep met een willekeurig profiel."; @@ -1257,6 +1335,9 @@ /* No comment provided by engineer. */ "creator" = "creator"; +/* No comment provided by engineer. */ +"Current conditions text couldn't be loaded, you can review conditions via this link:" = "De tekst van de huidige voorwaarden kon niet worden geladen. U kunt de voorwaarden bekijken via deze link:"; + /* No comment provided by engineer. */ "Current Passcode" = "Huidige toegangscode"; @@ -1505,6 +1586,9 @@ /* No comment provided by engineer. */ "Deletion errors" = "Verwijderingsfouten"; +/* No comment provided by engineer. */ +"Delivered even when Apple drops them." = "Geleverd ook als Apple ze verliest"; + /* No comment provided by engineer. */ "Delivery" = "Bezorging"; @@ -1692,6 +1776,9 @@ /* No comment provided by engineer. */ "e2e encrypted" = "e2e versleuteld"; +/* No comment provided by engineer. */ +"E2E encrypted notifications." = "E2E versleutelde meldingen."; + /* chat item action */ "Edit" = "Bewerk"; @@ -1710,6 +1797,9 @@ /* No comment provided by engineer. */ "Enable camera access" = "Schakel cameratoegang in"; +/* No comment provided by engineer. */ +"Enable Flux" = "Flux inschakelen"; + /* No comment provided by engineer. */ "Enable for all" = "Inschakelen voor iedereen"; @@ -1869,12 +1959,18 @@ /* No comment provided by engineer. */ "Error aborting address change" = "Fout bij het afbreken van adres wijziging"; +/* alert title */ +"Error accepting conditions" = "Fout bij het accepteren van voorwaarden"; + /* No comment provided by engineer. */ "Error accepting contact request" = "Fout bij het accepteren van een contactverzoek"; /* No comment provided by engineer. */ "Error adding member(s)" = "Fout bij het toevoegen van leden"; +/* alert title */ +"Error adding server" = "Fout bij toevoegen server"; + /* No comment provided by engineer. */ "Error changing address" = "Fout bij wijzigen van adres"; @@ -1959,6 +2055,9 @@ /* No comment provided by engineer. */ "Error joining group" = "Fout bij lid worden van groep"; +/* alert title */ +"Error loading servers" = "Fout bij het laden van servers"; + /* No comment provided by engineer. */ "Error migrating settings" = "Fout bij migreren van instellingen"; @@ -1992,6 +2091,9 @@ /* No comment provided by engineer. */ "Error saving passphrase to keychain" = "Fout bij opslaan van wachtwoord in de keychain"; +/* alert title */ +"Error saving servers" = "Fout bij het opslaan van servers"; + /* when migrating */ "Error saving settings" = "Fout bij opslaan van instellingen"; @@ -2034,6 +2136,9 @@ /* No comment provided by engineer. */ "Error updating message" = "Fout bij updaten van bericht"; +/* alert title */ +"Error updating server" = "Fout bij het updaten van de server"; + /* No comment provided by engineer. */ "Error updating settings" = "Fout bij bijwerken van instellingen"; @@ -2061,6 +2166,9 @@ /* No comment provided by engineer. */ "Errors" = "Fouten"; +/* servers error */ +"Errors in servers configuration." = "Fouten in de serverconfiguratie."; + /* No comment provided by engineer. */ "Even when disabled in the conversation." = "Zelfs wanneer uitgeschakeld in het gesprek."; @@ -2187,9 +2295,24 @@ /* No comment provided by engineer. */ "Fix not supported by group member" = "Herstel wordt niet ondersteund door groepslid"; +/* No comment provided by engineer. */ +"for better metadata privacy." = "voor betere privacy van metagegevens."; + +/* servers error */ +"For chat profile %@:" = "Voor chatprofiel %@:"; + /* No comment provided by engineer. */ "For console" = "Voor console"; +/* No comment provided by engineer. */ +"For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server." = "Als uw contactpersoon bijvoorbeeld berichten ontvangt via een SimpleX Chat-server, worden deze door uw app via een Flux-server verzonden."; + +/* No comment provided by engineer. */ +"For private routing" = "Voor privé-routering"; + +/* No comment provided by engineer. */ +"For social media" = "Voor social media"; + /* chat item action */ "Forward" = "Doorsturen"; @@ -2382,6 +2505,12 @@ /* time unit */ "hours" = "uren"; +/* No comment provided by engineer. */ +"How it affects privacy" = "Hoe het de privacy beïnvloedt"; + +/* No comment provided by engineer. */ +"How it helps privacy" = "Hoe het de privacy helpt"; + /* No comment provided by engineer. */ "How SimpleX works" = "Hoe SimpleX werkt"; @@ -2958,6 +3087,9 @@ /* No comment provided by engineer. */ "More reliable network connection." = "Betrouwbaardere netwerkverbinding."; +/* No comment provided by engineer. */ +"More reliable notifications" = "Betrouwbaardere meldingen"; + /* item status description */ "Most likely this connection is deleted." = "Hoogstwaarschijnlijk is deze verbinding verwijderd."; @@ -2982,12 +3114,18 @@ /* No comment provided by engineer. */ "Network connection" = "Netwerkverbinding"; +/* No comment provided by engineer. */ +"Network decentralization" = "Netwerk decentralisatie"; + /* snd error text */ "Network issues - message expired after many attempts to send it." = "Netwerkproblemen - bericht is verlopen na vele pogingen om het te verzenden."; /* No comment provided by engineer. */ "Network management" = "Netwerkbeheer"; +/* No comment provided by engineer. */ +"Network operator" = "Netwerkbeheerder"; + /* No comment provided by engineer. */ "Network settings" = "Netwerk instellingen"; @@ -3015,6 +3153,9 @@ /* No comment provided by engineer. */ "New display name" = "Nieuwe weergavenaam"; +/* notification */ +"New events" = "Nieuwe gebeurtenissen"; + /* No comment provided by engineer. */ "New in %@" = "Nieuw in %@"; @@ -3036,6 +3177,9 @@ /* No comment provided by engineer. */ "New passphrase…" = "Nieuw wachtwoord…"; +/* No comment provided by engineer. */ +"New server" = "Nieuwe server"; + /* No comment provided by engineer. */ "New SOCKS credentials will be used every time you start the app." = "Elke keer dat u de app start, worden er nieuwe SOCKS-inloggegevens gebruikt."; @@ -3081,6 +3225,12 @@ /* No comment provided by engineer. */ "No info, try to reload" = "Geen info, probeer opnieuw te laden"; +/* servers error */ +"No media & file servers." = "Geen media- en bestandsservers."; + +/* servers error */ +"No message servers." = "Geen berichtenservers."; + /* No comment provided by engineer. */ "No network connection" = "Geen netwerkverbinding"; @@ -3099,6 +3249,18 @@ /* No comment provided by engineer. */ "No received or sent files" = "Geen ontvangen of verzonden bestanden"; +/* servers error */ +"No servers for private message routing." = "Geen servers voor het routeren van privéberichten."; + +/* servers error */ +"No servers to receive files." = "Geen servers om bestanden te ontvangen."; + +/* servers error */ +"No servers to receive messages." = "Geen servers om berichten te ontvangen."; + +/* servers error */ +"No servers to send files." = "Geen servers om bestanden te verzenden."; + /* copied message info in history */ "no text" = "geen tekst"; @@ -3120,6 +3282,9 @@ /* No comment provided by engineer. */ "Notifications are disabled!" = "Meldingen zijn uitgeschakeld!"; +/* No comment provided by engineer. */ +"Notifications privacy" = "Privacy van meldingen"; + /* No comment provided by engineer. */ "Now admins can:\n- delete members' messages.\n- disable members (\"observer\" role)" = "Nu kunnen beheerders: \n- berichten van leden verwijderen.\n- schakel leden uit (\"waarnemer\" rol)"; @@ -3212,12 +3377,18 @@ /* No comment provided by engineer. */ "Open" = "Open"; +/* No comment provided by engineer. */ +"Open changes" = "Wijzigingen openen"; + /* No comment provided by engineer. */ "Open chat" = "Chat openen"; /* authentication reason */ "Open chat console" = "Chat console openen"; +/* No comment provided by engineer. */ +"Open conditions" = "Open voorwaarden"; + /* No comment provided by engineer. */ "Open group" = "Open groep"; @@ -3230,6 +3401,12 @@ /* No comment provided by engineer. */ "Opening app…" = "App openen…"; +/* No comment provided by engineer. */ +"Operator" = "Operator"; + +/* alert title */ +"Operator server" = "Operatorserver"; + /* No comment provided by engineer. */ "Or paste archive link" = "Of plak de archief link"; @@ -3242,6 +3419,9 @@ /* No comment provided by engineer. */ "Or show this code" = "Of laat deze code zien"; +/* No comment provided by engineer. */ +"Or to share privately" = "Of om privé te delen"; + /* No comment provided by engineer. */ "other" = "overig"; @@ -3383,6 +3563,9 @@ /* No comment provided by engineer. */ "Preset server address" = "Vooraf ingesteld server adres"; +/* No comment provided by engineer. */ +"Preset servers" = "Vooraf ingestelde servers"; + /* No comment provided by engineer. */ "Preview" = "Voorbeeld"; @@ -3488,6 +3671,9 @@ /* No comment provided by engineer. */ "Push notifications" = "Push meldingen"; +/* No comment provided by engineer. */ +"Push Notifications" = "Pushmeldingen"; + /* No comment provided by engineer. */ "Push server" = "Push server"; @@ -3735,6 +3921,12 @@ /* chat item action */ "Reveal" = "Onthullen"; +/* No comment provided by engineer. */ +"Review conditions" = "Voorwaarden bekijken"; + +/* No comment provided by engineer. */ +"Review later" = "Later beoordelen"; + /* No comment provided by engineer. */ "Revoke" = "Intrekken"; @@ -3756,6 +3948,12 @@ /* No comment provided by engineer. */ "Safer groups" = "Veiligere groepen"; +/* No comment provided by engineer. */ +"Same conditions will apply to operator **%@**." = "Dezelfde voorwaarden gelden voor operator **%@**."; + +/* No comment provided by engineer. */ +"Same conditions will apply to operator(s): **%@**." = "Dezelfde voorwaarden gelden voor operator(s): **%@**."; + /* alert button chat item action */ "Save" = "Opslaan"; @@ -4021,6 +4219,9 @@ /* No comment provided by engineer. */ "Server" = "Server"; +/* alert message */ +"Server added to operator %@." = "Server toegevoegd aan operator %@."; + /* No comment provided by engineer. */ "Server address" = "Server adres"; @@ -4030,6 +4231,15 @@ /* srv error text. */ "Server address is incompatible with network settings." = "Serveradres is niet compatibel met netwerkinstellingen."; +/* alert title */ +"Server operator changed." = "Serveroperator gewijzigd."; + +/* No comment provided by engineer. */ +"Server operators" = "Serverbeheerders"; + +/* alert title */ +"Server protocol changed." = "Serverprotocol gewijzigd."; + /* queue info */ "server queue info: %@\n\nlast received msg: %@" = "informatie over serverwachtrij: %1$@\n\nlaatst ontvangen bericht: %2$@"; @@ -4115,9 +4325,15 @@ /* No comment provided by engineer. */ "Share 1-time link" = "Eenmalige link delen"; +/* No comment provided by engineer. */ +"Share 1-time link with a friend" = "Deel eenmalig een link met een vriend"; + /* No comment provided by engineer. */ "Share address" = "Adres delen"; +/* No comment provided by engineer. */ +"Share address publicly" = "Adres openbaar delen"; + /* alert title */ "Share address with contacts?" = "Adres delen met contacten?"; @@ -4130,6 +4346,9 @@ /* No comment provided by engineer. */ "Share profile" = "Profiel delen"; +/* No comment provided by engineer. */ +"Share SimpleX address on social media." = "Deel het SimpleX-adres op sociale media."; + /* No comment provided by engineer. */ "Share this 1-time invite link" = "Deel deze eenmalige uitnodigingslink"; @@ -4175,6 +4394,12 @@ /* No comment provided by engineer. */ "SimpleX Address" = "SimpleX adres"; +/* No comment provided by engineer. */ +"SimpleX address and 1-time links are safe to share via any messenger." = "SimpleX-adressen en eenmalige links kunnen veilig worden gedeeld via elke messenger."; + +/* No comment provided by engineer. */ +"SimpleX address or 1-time link?" = "SimpleX adres of eenmalige link?"; + /* No comment provided by engineer. */ "SimpleX Chat security was audited by Trail of Bits." = "De beveiliging van SimpleX Chat is gecontroleerd door Trail of Bits."; @@ -4250,6 +4475,9 @@ /* No comment provided by engineer. */ "Some non-fatal errors occurred during import:" = "Er zijn enkele niet-fatale fouten opgetreden tijdens het importeren:"; +/* alert message */ +"Some servers failed the test:\n%@" = "Sommige servers zijn niet geslaagd voor de test:\n%@"; + /* notification title */ "Somebody" = "Iemand"; @@ -4412,6 +4640,9 @@ /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "De app kan u op de hoogte stellen wanneer u berichten of contact verzoeken ontvangt - open de instellingen om dit in te schakelen."; +/* No comment provided by engineer. */ +"The app protects your privacy by using different operators in each conversation." = "De app beschermt uw privacy door in elk gesprek andere operatoren te gebruiken."; + /* No comment provided by engineer. */ "The app will ask to confirm downloads from unknown file servers (except .onion)." = "De app vraagt om downloads van onbekende bestandsservers (behalve .onion) te bevestigen."; @@ -4421,6 +4652,9 @@ /* No comment provided by engineer. */ "The code you scanned is not a SimpleX link QR code." = "De code die u heeft gescand is geen SimpleX link QR-code."; +/* No comment provided by engineer. */ +"The connection reached the limit of undelivered messages, your contact may be offline." = "De verbinding heeft de limiet van niet-afgeleverde berichten bereikt. Uw contactpersoon is mogelijk offline."; + /* No comment provided by engineer. */ "The connection you accepted will be cancelled!" = "De door u geaccepteerde verbinding wordt geannuleerd!"; @@ -4460,6 +4694,9 @@ /* No comment provided by engineer. */ "The profile is only shared with your contacts." = "Het profiel wordt alleen gedeeld met uw contacten."; +/* No comment provided by engineer. */ +"The second preset operator in the app!" = "De tweede vooraf ingestelde operator in de app!"; + /* No comment provided by engineer. */ "The second tick we missed! ✅" = "De tweede vink die we gemist hebben! ✅"; @@ -4469,6 +4706,9 @@ /* No comment provided by engineer. */ "The servers for new connections of your current chat profile **%@**." = "De servers voor nieuwe verbindingen van uw huidige chatprofiel **%@**."; +/* No comment provided by engineer. */ +"The servers for new files of your current chat profile **%@**." = "De servers voor nieuwe bestanden van uw huidige chatprofiel **%@**."; + /* No comment provided by engineer. */ "The text you pasted is not a SimpleX link." = "De tekst die u hebt geplakt is geen SimpleX link."; @@ -4478,6 +4718,9 @@ /* No comment provided by engineer. */ "Themes" = "Thema's"; +/* No comment provided by engineer. */ +"These conditions will also apply for: **%@**." = "Deze voorwaarden zijn ook van toepassing op: **%@**."; + /* No comment provided by engineer. */ "These settings are for your current profile **%@**." = "Deze instellingen zijn voor uw huidige profiel **%@**."; @@ -4541,6 +4784,9 @@ /* No comment provided by engineer. */ "To make a new connection" = "Om een nieuwe verbinding te maken"; +/* No comment provided by engineer. */ +"To protect against your link being replaced, you can compare contact security codes." = "Om te voorkomen dat uw link wordt vervangen, kunt u contactbeveiligingscodes vergelijken."; + /* No comment provided by engineer. */ "To protect timezone, image/voice files use UTC." = "Om de tijdzone te beschermen, gebruiken afbeeldings-/spraakbestanden UTC."; @@ -4553,6 +4799,9 @@ /* No comment provided by engineer. */ "To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Om de privacy te beschermen, heeft SimpleX in plaats van gebruikers-ID's die door alle andere platforms worden gebruikt, ID's voor berichten wachtrijen, afzonderlijk voor elk van uw contacten."; +/* No comment provided by engineer. */ +"To receive" = "Om te ontvangen"; + /* No comment provided by engineer. */ "To record speech please grant permission to use Microphone." = "Geef toestemming om de microfoon te gebruiken om spraak op te nemen."; @@ -4565,9 +4814,15 @@ /* No comment provided by engineer. */ "To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." = "Om uw verborgen profiel te onthullen, voert u een volledig wachtwoord in een zoek veld in op de pagina **Uw chatprofielen**."; +/* No comment provided by engineer. */ +"To send" = "Om te verzenden"; + /* No comment provided by engineer. */ "To support instant push notifications the chat database has to be migrated." = "Om directe push meldingen te ondersteunen, moet de chat database worden gemigreerd."; +/* No comment provided by engineer. */ +"To use the servers of **%@**, accept conditions of use." = "Om de servers van **%@** te gebruiken, moet u de gebruiksvoorwaarden accepteren."; + /* No comment provided by engineer. */ "To verify end-to-end encryption with your contact compare (or scan) the code on your devices." = "Vergelijk (of scan) de code op uw apparaten om end-to-end-codering met uw contact te verifiëren."; @@ -4625,6 +4880,9 @@ /* rcv group event chat item */ "unblocked %@" = "gedeblokkeerd %@"; +/* No comment provided by engineer. */ +"Undelivered messages" = "Niet afgeleverde berichten"; + /* No comment provided by engineer. */ "Unexpected migration state" = "Onverwachte migratiestatus"; @@ -4742,12 +5000,21 @@ /* No comment provided by engineer. */ "Use .onion hosts" = "Gebruik .onion-hosts"; +/* No comment provided by engineer. */ +"Use %@" = "Gebruik %@"; + /* No comment provided by engineer. */ "Use chat" = "Gebruik chat"; /* No comment provided by engineer. */ "Use current profile" = "Gebruik het huidige profiel"; +/* No comment provided by engineer. */ +"Use for files" = "Gebruik voor bestanden"; + +/* No comment provided by engineer. */ +"Use for messages" = "Gebruik voor berichten"; + /* No comment provided by engineer. */ "Use for new connections" = "Gebruik voor nieuwe verbindingen"; @@ -4772,6 +5039,9 @@ /* No comment provided by engineer. */ "Use server" = "Gebruik server"; +/* No comment provided by engineer. */ +"Use servers" = "Gebruik servers"; + /* No comment provided by engineer. */ "Use SimpleX Chat servers?" = "SimpleX Chat servers gebruiken?"; @@ -4856,9 +5126,15 @@ /* No comment provided by engineer. */ "Videos and files up to 1gb" = "Video's en bestanden tot 1 GB"; +/* No comment provided by engineer. */ +"View conditions" = "Bekijk voorwaarden"; + /* No comment provided by engineer. */ "View security code" = "Beveiligingscode bekijken"; +/* No comment provided by engineer. */ +"View updated conditions" = "Bekijk de bijgewerkte voorwaarden"; + /* chat feature */ "Visible history" = "Zichtbare geschiedenis"; @@ -4940,6 +5216,9 @@ /* No comment provided by engineer. */ "when IP hidden" = "wanneer IP verborgen is"; +/* No comment provided by engineer. */ +"When more than one operator is enabled, none of them has metadata to learn who communicates with whom." = "Wanneer er meer dan één operator is ingeschakeld, beschikt geen enkele operator over metagegevens om te achterhalen wie met wie communiceert."; + /* No comment provided by engineer. */ "When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "Wanneer je een incognito profiel met iemand deelt, wordt dit profiel gebruikt voor de groepen waarvoor ze je uitnodigen."; @@ -5048,6 +5327,12 @@ /* No comment provided by engineer. */ "You can change it in Appearance settings." = "U kunt dit wijzigen in de instellingen onder uiterlijk."; +/* No comment provided by engineer. */ +"You can configure operators in Network & servers settings." = "U kunt operators configureren in Netwerk- en serverinstellingen."; + +/* No comment provided by engineer. */ +"You can configure servers via settings." = "U kunt servers configureren via instellingen."; + /* No comment provided by engineer. */ "You can create it later" = "U kan het later maken"; @@ -5072,6 +5357,9 @@ /* No comment provided by engineer. */ "You can send messages to %@ from Archived contacts." = "U kunt berichten naar %@ sturen vanuit gearchiveerde contacten."; +/* No comment provided by engineer. */ +"You can set connection name, to remember who the link was shared with." = "U kunt een verbindingsnaam instellen, zodat u kunt onthouden met wie de link is gedeeld."; + /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "U kunt een voorbeeld van een melding op het vergrendeld scherm instellen via instellingen."; @@ -5273,6 +5561,9 @@ /* No comment provided by engineer. */ "Your server address" = "Uw server adres"; +/* No comment provided by engineer. */ +"Your servers" = "Uw servers"; + /* No comment provided by engineer. */ "Your settings" = "Uw instellingen"; diff --git a/apps/ios/uk.lproj/Localizable.strings b/apps/ios/uk.lproj/Localizable.strings index 9af4581140..7b66aa5efb 100644 --- a/apps/ios/uk.lproj/Localizable.strings +++ b/apps/ios/uk.lproj/Localizable.strings @@ -82,6 +82,9 @@ /* No comment provided by engineer. */ "**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from." = "**Рекомендується**: токен пристрою та сповіщення надсилаються на сервер сповіщень SimpleX Chat, але не вміст повідомлення, його розмір або від кого воно надійшло."; +/* No comment provided by engineer. */ +"**Scan / Paste link**: to connect via a link you received." = "**Відсканувати / Вставити посилання**: підключитися за отриманим посиланням."; + /* No comment provided by engineer. */ "**Warning**: Instant push notifications require passphrase saved in Keychain." = "**Попередження**: Для отримання миттєвих пуш-сповіщень потрібна парольна фраза, збережена у брелоку."; @@ -142,12 +145,21 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ перевірено"; +/* No comment provided by engineer. */ +"%@ server" = "%@ сервер"; + +/* No comment provided by engineer. */ +"%@ servers" = "%@ сервери"; + /* No comment provided by engineer. */ "%@ uploaded" = "%@ завантажено"; /* notification title */ "%@ wants to connect!" = "%@ хоче підключитися!"; +/* format for date separator in chat */ +"%@, %@" = "%1$@, %2$@"; + /* No comment provided by engineer. */ "%@, %@ and %lld members" = "%@, %@ та %lld учасників"; @@ -169,9 +181,15 @@ /* forward confirmation reason */ "%d file(s) were deleted." = "%их файл(ів) було видалено."; +/* forward confirmation reason */ +"%d file(s) were not downloaded." = "%d файл(и) не було завантажено."; + /* time interval */ "%d hours" = "%d годин"; +/* alert title */ +"%d messages not forwarded" = "%d повідомлень не переслано"; + /* time interval */ "%d min" = "%d хв"; @@ -286,6 +304,12 @@ /* time interval */ "1 week" = "1 тиждень"; +/* No comment provided by engineer. */ +"1-time link" = "Одноразове посилання"; + +/* No comment provided by engineer. */ +"1-time link can be used *with one contact only* - share in person or via any messenger." = "Одноразове посилання можна використовувати *тільки з одним контактом* - поділіться ним особисто або через будь-який месенджер."; + /* No comment provided by engineer. */ "5 minutes" = "5 хвилин"; @@ -333,6 +357,9 @@ swipe action */ "Accept" = "Прийняти"; +/* No comment provided by engineer. */ +"Accept conditions" = "Прийняти умови"; + /* No comment provided by engineer. */ "Accept connection request?" = "Прийняти запит на підключення?"; @@ -346,6 +373,9 @@ /* call status */ "accepted call" = "прийнято виклик"; +/* No comment provided by engineer. */ +"Accepted conditions" = "Прийняті умови"; + /* No comment provided by engineer. */ "Acknowledged" = "Визнано"; @@ -373,6 +403,12 @@ /* No comment provided by engineer. */ "Add welcome message" = "Додати вітальне повідомлення"; +/* No comment provided by engineer. */ +"Added media & file servers" = "Додано медіа та файлові сервери"; + +/* No comment provided by engineer. */ +"Added message servers" = "Додано сервери повідомлень"; + /* No comment provided by engineer. */ "Additional accent" = "Додатковий акцент"; @@ -388,6 +424,12 @@ /* No comment provided by engineer. */ "Address change will be aborted. Old receiving address will be used." = "Зміна адреси буде скасована. Буде використано стару адресу отримання."; +/* No comment provided by engineer. */ +"Address or 1-time link?" = "Адреса чи одноразове посилання?"; + +/* No comment provided by engineer. */ +"Address settings" = "Налаштування адреси"; + /* member role */ "admin" = "адмін"; @@ -430,6 +472,9 @@ /* feature role */ "all members" = "всі учасники"; +/* No comment provided by engineer. */ +"All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages." = "Всі повідомлення та файли надсилаються **наскрізним шифруванням**, з пост-квантовим захистом у прямих повідомленнях."; + /* No comment provided by engineer. */ "All messages will be deleted - this cannot be undone!" = "Усі повідомлення будуть видалені - цю дію не можна скасувати!"; @@ -565,6 +610,9 @@ /* No comment provided by engineer. */ "App passcode is replaced with self-destruct passcode." = "Пароль програми замінено на пароль самознищення."; +/* No comment provided by engineer. */ +"App session" = "Сесія програми"; + /* No comment provided by engineer. */ "App version" = "Версія програми"; @@ -637,6 +685,9 @@ /* No comment provided by engineer. */ "Auto-accept images" = "Автоматичне прийняття зображень"; +/* alert title */ +"Auto-accept settings" = "Автоприйняття налаштувань"; + /* No comment provided by engineer. */ "Back" = "Назад"; @@ -658,15 +709,30 @@ /* No comment provided by engineer. */ "Bad message ID" = "Неправильний ідентифікатор повідомлення"; +/* No comment provided by engineer. */ +"Better calls" = "Кращі дзвінки"; + /* No comment provided by engineer. */ "Better groups" = "Кращі групи"; +/* No comment provided by engineer. */ +"Better message dates." = "Кращі дати повідомлень."; + /* No comment provided by engineer. */ "Better messages" = "Кращі повідомлення"; /* No comment provided by engineer. */ "Better networking" = "Краща мережа"; +/* No comment provided by engineer. */ +"Better notifications" = "Кращі сповіщення"; + +/* No comment provided by engineer. */ +"Better security ✅" = "Краща безпека ✅"; + +/* No comment provided by engineer. */ +"Better user experience" = "Покращений користувацький досвід"; + /* No comment provided by engineer. */ "Black" = "Чорний"; @@ -825,6 +891,9 @@ set passcode view */ "Change self-destruct passcode" = "Змінити пароль самознищення"; +/* authentication reason */ +"Change user profiles" = "Зміна профілів користувачів"; + /* chat item text */ "changed address for you" = "змінили для вас адресу"; @@ -876,6 +945,9 @@ /* No comment provided by engineer. */ "Chat preferences" = "Налаштування чату"; +/* alert message */ +"Chat preferences were changed." = "Змінено налаштування чату."; + /* No comment provided by engineer. */ "Chat profile" = "Профіль користувача"; @@ -885,6 +957,12 @@ /* No comment provided by engineer. */ "Chats" = "Чати"; +/* No comment provided by engineer. */ +"Check messages every 20 min." = "Перевіряйте повідомлення кожні 20 хв."; + +/* No comment provided by engineer. */ +"Check messages when allowed." = "Перевірте повідомлення, коли це дозволено."; + /* alert title */ "Check server address and try again." = "Перевірте адресу сервера та спробуйте ще раз."; @@ -945,6 +1023,33 @@ /* No comment provided by engineer. */ "Completed" = "Завершено"; +/* No comment provided by engineer. */ +"Conditions accepted on: %@." = "Умови приймаються на: %@."; + +/* No comment provided by engineer. */ +"Conditions are accepted for the operator(s): **%@**." = "Для оператора(ів) приймаються умови: **%@**."; + +/* No comment provided by engineer. */ +"Conditions are already accepted for following operator(s): **%@**." = "Умови вже прийняті для наступних операторів: **%@**."; + +/* No comment provided by engineer. */ +"Conditions of use" = "Умови використання"; + +/* No comment provided by engineer. */ +"Conditions will be accepted for enabled operators after 30 days." = "Умови будуть прийняті для ввімкнених операторів через 30 днів."; + +/* No comment provided by engineer. */ +"Conditions will be accepted for operator(s): **%@**." = "Умови приймаються для оператора(ів): **%@**."; + +/* No comment provided by engineer. */ +"Conditions will be accepted for the operator(s): **%@**." = "Для оператора(ів) приймаються умови: **%@**."; + +/* No comment provided by engineer. */ +"Conditions will be accepted on: %@." = "Умови приймаються на: %@."; + +/* No comment provided by engineer. */ +"Conditions will be automatically accepted for enabled operators on: %@." = "Умови будуть автоматично прийняті для увімкнених операторів на: %@."; + /* No comment provided by engineer. */ "Configure ICE servers" = "Налаштування серверів ICE"; @@ -1092,6 +1197,9 @@ /* No comment provided by engineer. */ "Connection request sent!" = "Запит на підключення відправлено!"; +/* No comment provided by engineer. */ +"Connection security" = "Безпека з'єднання"; + /* No comment provided by engineer. */ "Connection terminated" = "З'єднання розірвано"; @@ -1164,12 +1272,18 @@ /* No comment provided by engineer. */ "Core version: v%@" = "Основна версія: v%@"; +/* No comment provided by engineer. */ +"Corner" = "Кут"; + /* No comment provided by engineer. */ "Correct name to %@?" = "Виправити ім'я на %@?"; /* No comment provided by engineer. */ "Create" = "Створити"; +/* No comment provided by engineer. */ +"Create 1-time link" = "Створити одноразове посилання"; + /* No comment provided by engineer. */ "Create a group using a random profile." = "Створіть групу, використовуючи випадковий профіль."; @@ -1221,6 +1335,9 @@ /* No comment provided by engineer. */ "creator" = "творець"; +/* No comment provided by engineer. */ +"Current conditions text couldn't be loaded, you can review conditions via this link:" = "Текст поточних умов не вдалося завантажити, ви можете переглянути умови за цим посиланням:"; + /* No comment provided by engineer. */ "Current Passcode" = "Поточний пароль"; @@ -1239,6 +1356,9 @@ /* No comment provided by engineer. */ "Custom time" = "Індивідуальний час"; +/* No comment provided by engineer. */ +"Customizable message shape." = "Налаштовується форма повідомлення."; + /* No comment provided by engineer. */ "Customize theme" = "Налаштувати тему"; @@ -1424,6 +1544,9 @@ /* No comment provided by engineer. */ "Delete old database?" = "Видалити стару базу даних?"; +/* No comment provided by engineer. */ +"Delete or moderate up to 200 messages." = "Видалити або модерувати до 200 повідомлень."; + /* No comment provided by engineer. */ "Delete pending connection?" = "Видалити очікуване з'єднання?"; @@ -1463,6 +1586,9 @@ /* No comment provided by engineer. */ "Deletion errors" = "Помилки видалення"; +/* No comment provided by engineer. */ +"Delivered even when Apple drops them." = "Доставляються навіть тоді, коли Apple кидає їх."; + /* No comment provided by engineer. */ "Delivery" = "Доставка"; @@ -1586,6 +1712,9 @@ /* No comment provided by engineer. */ "Do NOT send messages directly, even if your or destination server does not support private routing." = "НЕ надсилайте повідомлення напряму, навіть якщо ваш сервер або сервер призначення не підтримує приватну маршрутизацію."; +/* No comment provided by engineer. */ +"Do not use credentials with proxy." = "Не використовуйте облікові дані з проксі."; + /* No comment provided by engineer. */ "Do NOT use private routing." = "НЕ використовуйте приватну маршрутизацію."; @@ -1617,6 +1746,9 @@ /* server test step */ "Download file" = "Завантажити файл"; +/* alert action */ +"Download files" = "Завантажити файли"; + /* No comment provided by engineer. */ "Downloaded" = "Завантажено"; @@ -1644,6 +1776,9 @@ /* No comment provided by engineer. */ "e2e encrypted" = "e2e зашифрований"; +/* No comment provided by engineer. */ +"E2E encrypted notifications." = "Зашифровані сповіщення E2E."; + /* chat item action */ "Edit" = "Редагувати"; @@ -1662,6 +1797,9 @@ /* No comment provided by engineer. */ "Enable camera access" = "Увімкніть доступ до камери"; +/* No comment provided by engineer. */ +"Enable Flux" = "Увімкнути Flux"; + /* No comment provided by engineer. */ "Enable for all" = "Увімкнути для всіх"; @@ -1821,21 +1959,33 @@ /* No comment provided by engineer. */ "Error aborting address change" = "Помилка скасування зміни адреси"; +/* alert title */ +"Error accepting conditions" = "Помилка прийняття умов"; + /* No comment provided by engineer. */ "Error accepting contact request" = "Помилка при прийнятті запиту на контакт"; /* No comment provided by engineer. */ "Error adding member(s)" = "Помилка додавання користувача(ів)"; +/* alert title */ +"Error adding server" = "Помилка додавання сервера"; + /* No comment provided by engineer. */ "Error changing address" = "Помилка зміни адреси"; +/* No comment provided by engineer. */ +"Error changing connection profile" = "Помилка при зміні профілю з'єднання"; + /* No comment provided by engineer. */ "Error changing role" = "Помилка зміни ролі"; /* No comment provided by engineer. */ "Error changing setting" = "Помилка зміни налаштування"; +/* No comment provided by engineer. */ +"Error changing to incognito!" = "Помилка переходу на інкогніто!"; + /* No comment provided by engineer. */ "Error connecting to forwarding server %@. Please try later." = "Помилка підключення до сервера переадресації %@. Спробуйте пізніше."; @@ -1905,6 +2055,12 @@ /* No comment provided by engineer. */ "Error joining group" = "Помилка приєднання до групи"; +/* alert title */ +"Error loading servers" = "Помилка завантаження серверів"; + +/* No comment provided by engineer. */ +"Error migrating settings" = "Помилка міграції налаштувань"; + /* No comment provided by engineer. */ "Error opening chat" = "Помилка відкриття чату"; @@ -1935,6 +2091,9 @@ /* No comment provided by engineer. */ "Error saving passphrase to keychain" = "Помилка збереження пароля на keychain"; +/* alert title */ +"Error saving servers" = "Сервери збереження помилок"; + /* when migrating */ "Error saving settings" = "Налаштування збереження помилок"; @@ -1962,6 +2121,9 @@ /* No comment provided by engineer. */ "Error stopping chat" = "Помилка зупинки чату"; +/* No comment provided by engineer. */ +"Error switching profile" = "Помилка перемикання профілю"; + /* alertTitle */ "Error switching profile!" = "Помилка перемикання профілю!"; @@ -1974,6 +2136,9 @@ /* No comment provided by engineer. */ "Error updating message" = "Повідомлення про помилку оновлення"; +/* alert title */ +"Error updating server" = "Помилка оновлення сервера"; + /* No comment provided by engineer. */ "Error updating settings" = "Помилка оновлення налаштувань"; @@ -2001,6 +2166,9 @@ /* No comment provided by engineer. */ "Errors" = "Помилки"; +/* servers error */ +"Errors in servers configuration." = "Помилки в конфігурації серверів."; + /* No comment provided by engineer. */ "Even when disabled in the conversation." = "Навіть коли вимкнений у розмові."; @@ -2049,6 +2217,9 @@ /* No comment provided by engineer. */ "File error" = "Помилка файлу"; +/* alert message */ +"File errors:\n%@" = "Помилки файлів:\n%@"; + /* file error text */ "File not found - most likely file was deleted or cancelled." = "Файл не знайдено - найімовірніше, файл було видалено або скасовано."; @@ -2124,15 +2295,42 @@ /* No comment provided by engineer. */ "Fix not supported by group member" = "Виправлення не підтримується учасником групи"; +/* No comment provided by engineer. */ +"for better metadata privacy." = "для кращої конфіденційності метаданих."; + +/* servers error */ +"For chat profile %@:" = "Для профілю чату %@:"; + /* No comment provided by engineer. */ "For console" = "Для консолі"; +/* No comment provided by engineer. */ +"For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server." = "Наприклад, якщо ваш контакт отримує повідомлення через сервер SimpleX Chat, ваш додаток доставлятиме їх через сервер Flux."; + +/* No comment provided by engineer. */ +"For private routing" = "Для приватної маршрутизації"; + +/* No comment provided by engineer. */ +"For social media" = "Для соціальних мереж"; + /* chat item action */ "Forward" = "Пересилання"; +/* alert title */ +"Forward %d message(s)?" = "Переслати %d повідомлення(ь)?"; + /* No comment provided by engineer. */ "Forward and save messages" = "Пересилання та збереження повідомлень"; +/* alert action */ +"Forward messages" = "Пересилання повідомлень"; + +/* alert message */ +"Forward messages without files?" = "Пересилати повідомлення без файлів?"; + +/* No comment provided by engineer. */ +"Forward up to 20 messages at once." = "Пересилайте до 20 повідомлень одночасно."; + /* No comment provided by engineer. */ "forwarded" = "переслано"; @@ -2142,6 +2340,9 @@ /* No comment provided by engineer. */ "Forwarded from" = "Переслано з"; +/* No comment provided by engineer. */ +"Forwarding %lld messages" = "Пересилання повідомлень %lld"; + /* No comment provided by engineer. */ "Forwarding server %@ failed to connect to destination server %@. Please try later." = "Серверу переадресації %@ не вдалося з'єднатися з сервером призначення %@. Спробуйте пізніше."; @@ -2304,6 +2505,12 @@ /* time unit */ "hours" = "години"; +/* No comment provided by engineer. */ +"How it affects privacy" = "Як це впливає на конфіденційність"; + +/* No comment provided by engineer. */ +"How it helps privacy" = "Як це захищає приватність"; + /* No comment provided by engineer. */ "How SimpleX works" = "Як працює SimpleX"; @@ -2367,6 +2574,9 @@ /* No comment provided by engineer. */ "Importing archive" = "Імпорт архіву"; +/* No comment provided by engineer. */ +"Improved delivery, reduced traffic usage.\nMore improvements are coming soon!" = "Покращена доставка, зменшене використання трафіку.\nНезабаром з'являться нові покращення!"; + /* No comment provided by engineer. */ "Improved message delivery" = "Покращена доставка повідомлень"; @@ -2526,6 +2736,9 @@ /* No comment provided by engineer. */ "iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications." = "Пароль бази даних буде безпечно збережено в iOS Keychain після запуску чату або зміни пароля - це дасть змогу отримувати миттєві повідомлення."; +/* No comment provided by engineer. */ +"IP address" = "IP-адреса"; + /* No comment provided by engineer. */ "Irreversible message deletion" = "Безповоротне видалення повідомлення"; @@ -2766,6 +2979,9 @@ /* No comment provided by engineer. */ "Message servers" = "Сервери повідомлень"; +/* No comment provided by engineer. */ +"Message shape" = "Форма повідомлення"; + /* No comment provided by engineer. */ "Message source remains private." = "Джерело повідомлення залишається приватним."; @@ -2796,6 +3012,9 @@ /* No comment provided by engineer. */ "Messages sent" = "Надіслані повідомлення"; +/* alert message */ +"Messages were deleted after you selected them." = "Повідомлення були видалені після того, як ви їх вибрали."; + /* No comment provided by engineer. */ "Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery." = "Повідомлення, файли та дзвінки захищені **наскрізним шифруванням** з ідеальною секретністю переадресації, відмовою та відновленням після злому."; @@ -2868,6 +3087,9 @@ /* No comment provided by engineer. */ "More reliable network connection." = "Більш надійне з'єднання з мережею."; +/* No comment provided by engineer. */ +"More reliable notifications" = "Більш надійні сповіщення"; + /* item status description */ "Most likely this connection is deleted." = "Швидше за все, це з'єднання видалено."; @@ -2892,12 +3114,18 @@ /* No comment provided by engineer. */ "Network connection" = "Підключення до мережі"; +/* No comment provided by engineer. */ +"Network decentralization" = "Децентралізація мережі"; + /* snd error text */ "Network issues - message expired after many attempts to send it." = "Проблеми з мережею - термін дії повідомлення закінчився після багатьох спроб надіслати його."; /* No comment provided by engineer. */ "Network management" = "Керування мережею"; +/* No comment provided by engineer. */ +"Network operator" = "Мережевий оператор"; + /* No comment provided by engineer. */ "Network settings" = "Налаштування мережі"; @@ -2925,6 +3153,9 @@ /* No comment provided by engineer. */ "New display name" = "Нове ім'я відображення"; +/* notification */ +"New events" = "Нові події"; + /* No comment provided by engineer. */ "New in %@" = "Нове в %@"; @@ -2946,6 +3177,15 @@ /* No comment provided by engineer. */ "New passphrase…" = "Новий пароль…"; +/* No comment provided by engineer. */ +"New server" = "Новий сервер"; + +/* No comment provided by engineer. */ +"New SOCKS credentials will be used every time you start the app." = "Нові облікові дані SOCKS будуть використовуватися при кожному запуску програми."; + +/* No comment provided by engineer. */ +"New SOCKS credentials will be used for each server." = "Для кожного сервера будуть використовуватися нові облікові дані SOCKS."; + /* pref value */ "no" = "ні"; @@ -2985,9 +3225,21 @@ /* No comment provided by engineer. */ "No info, try to reload" = "Немає інформації, спробуйте перезавантажити"; +/* servers error */ +"No media & file servers." = "Ніяких медіа та файлових серверів."; + +/* servers error */ +"No message servers." = "Ніяких серверів повідомлень."; + /* No comment provided by engineer. */ "No network connection" = "Немає підключення до мережі"; +/* No comment provided by engineer. */ +"No permission to record speech" = "Немає дозволу на запис промови"; + +/* No comment provided by engineer. */ +"No permission to record video" = "Немає дозволу на запис відео"; + /* No comment provided by engineer. */ "No permission to record voice message" = "Немає дозволу на запис голосового повідомлення"; @@ -2997,6 +3249,18 @@ /* No comment provided by engineer. */ "No received or sent files" = "Немає отриманих або відправлених файлів"; +/* servers error */ +"No servers for private message routing." = "Немає серверів для маршрутизації приватних повідомлень."; + +/* servers error */ +"No servers to receive files." = "Немає серверів для отримання файлів."; + +/* servers error */ +"No servers to receive messages." = "Немає серверів для отримання повідомлень."; + +/* servers error */ +"No servers to send files." = "Немає серверів для надсилання файлів."; + /* copied message info in history */ "no text" = "без тексту"; @@ -3009,12 +3273,18 @@ /* No comment provided by engineer. */ "Nothing selected" = "Нічого не вибрано"; +/* alert title */ +"Nothing to forward!" = "Нічого пересилати!"; + /* No comment provided by engineer. */ "Notifications" = "Сповіщення"; /* No comment provided by engineer. */ "Notifications are disabled!" = "Сповіщення вимкнено!"; +/* No comment provided by engineer. */ +"Notifications privacy" = "Сповіщення про приватність"; + /* No comment provided by engineer. */ "Now admins can:\n- delete members' messages.\n- disable members (\"observer\" role)" = "Тепер адміністратори можуть\n- видаляти повідомлення користувачів.\n- відключати користувачів (роль \"спостерігач\")"; @@ -3107,12 +3377,18 @@ /* No comment provided by engineer. */ "Open" = "Відкрито"; +/* No comment provided by engineer. */ +"Open changes" = "Відкриті зміни"; + /* No comment provided by engineer. */ "Open chat" = "Відкритий чат"; /* authentication reason */ "Open chat console" = "Відкрийте консоль чату"; +/* No comment provided by engineer. */ +"Open conditions" = "Відкриті умови"; + /* No comment provided by engineer. */ "Open group" = "Відкрита група"; @@ -3125,6 +3401,12 @@ /* No comment provided by engineer. */ "Opening app…" = "Відкриваємо програму…"; +/* No comment provided by engineer. */ +"Operator" = "Оператор"; + +/* alert title */ +"Operator server" = "Сервер оператора"; + /* No comment provided by engineer. */ "Or paste archive link" = "Або вставте посилання на архів"; @@ -3137,6 +3419,9 @@ /* No comment provided by engineer. */ "Or show this code" = "Або покажіть цей код"; +/* No comment provided by engineer. */ +"Or to share privately" = "Або поділитися приватно"; + /* No comment provided by engineer. */ "other" = "інший"; @@ -3146,6 +3431,9 @@ /* No comment provided by engineer. */ "other errors" = "інші помилки"; +/* alert message */ +"Other file errors:\n%@" = "Інші помилки файлів:\n%@"; + /* member role */ "owner" = "власник"; @@ -3167,6 +3455,9 @@ /* No comment provided by engineer. */ "Passcode set!" = "Пароль встановлено!"; +/* No comment provided by engineer. */ +"Password" = "Пароль"; + /* No comment provided by engineer. */ "Password to show" = "Показати пароль"; @@ -3260,6 +3551,9 @@ /* No comment provided by engineer. */ "Polish interface" = "Польський інтерфейс"; +/* No comment provided by engineer. */ +"Port" = "Порт"; + /* server test error */ "Possibly, certificate fingerprint in server address is incorrect" = "Можливо, в адресі сервера неправильно вказано відбиток сертифіката"; @@ -3269,6 +3563,9 @@ /* No comment provided by engineer. */ "Preset server address" = "Попередньо встановлена адреса сервера"; +/* No comment provided by engineer. */ +"Preset servers" = "Попередньо встановлені сервери"; + /* No comment provided by engineer. */ "Preview" = "Попередній перегляд"; @@ -3368,9 +3665,15 @@ /* No comment provided by engineer. */ "Proxied servers" = "Проксі-сервери"; +/* No comment provided by engineer. */ +"Proxy requires password" = "Проксі вимагає пароль"; + /* No comment provided by engineer. */ "Push notifications" = "Push-повідомлення"; +/* No comment provided by engineer. */ +"Push Notifications" = "Push-сповіщення"; + /* No comment provided by engineer. */ "Push server" = "Push-сервер"; @@ -3510,6 +3813,9 @@ /* No comment provided by engineer. */ "Remove" = "Видалити"; +/* No comment provided by engineer. */ +"Remove archive?" = "Видалити архів?"; + /* No comment provided by engineer. */ "Remove image" = "Видалити зображення"; @@ -3615,6 +3921,12 @@ /* chat item action */ "Reveal" = "Показувати"; +/* No comment provided by engineer. */ +"Review conditions" = "Умови перегляду"; + +/* No comment provided by engineer. */ +"Review later" = "Перегляньте пізніше"; + /* No comment provided by engineer. */ "Revoke" = "Відкликати"; @@ -3636,6 +3948,12 @@ /* No comment provided by engineer. */ "Safer groups" = "Безпечніші групи"; +/* No comment provided by engineer. */ +"Same conditions will apply to operator **%@**." = "Такі ж умови діятимуть і для оператора **%@**."; + +/* No comment provided by engineer. */ +"Same conditions will apply to operator(s): **%@**." = "Такі ж умови будуть застосовуватися до оператора(ів): **%@**."; + /* alert button chat item action */ "Save" = "Зберегти"; @@ -3679,6 +3997,9 @@ /* No comment provided by engineer. */ "Save welcome message?" = "Зберегти вітальне повідомлення?"; +/* alert title */ +"Save your profile?" = "Зберегти свій профіль?"; + /* No comment provided by engineer. */ "saved" = "збережено"; @@ -3697,6 +4018,9 @@ /* No comment provided by engineer. */ "Saved WebRTC ICE servers will be removed" = "Збережені сервери WebRTC ICE буде видалено"; +/* No comment provided by engineer. */ +"Saving %lld messages" = "Збереження повідомлень %lld"; + /* No comment provided by engineer. */ "Scale" = "Масштаб"; @@ -3760,6 +4084,9 @@ /* chat item action */ "Select" = "Виберіть"; +/* No comment provided by engineer. */ +"Select chat profile" = "Виберіть профіль чату"; + /* No comment provided by engineer. */ "Selected %lld" = "Вибрано %lld"; @@ -3889,6 +4216,12 @@ /* No comment provided by engineer. */ "Sent via proxy" = "Відправлено через проксі"; +/* No comment provided by engineer. */ +"Server" = "Сервер"; + +/* alert message */ +"Server added to operator %@." = "Сервер додано до оператора %@."; + /* No comment provided by engineer. */ "Server address" = "Адреса сервера"; @@ -3898,6 +4231,15 @@ /* srv error text. */ "Server address is incompatible with network settings." = "Адреса сервера несумісна з налаштуваннями мережі."; +/* alert title */ +"Server operator changed." = "Оператор сервера змінився."; + +/* No comment provided by engineer. */ +"Server operators" = "Оператори серверів"; + +/* alert title */ +"Server protocol changed." = "Протокол сервера змінено."; + /* queue info */ "server queue info: %@\n\nlast received msg: %@" = "інформація про чергу на сервері: %1$@\n\nостаннє отримане повідомлення: %2$@"; @@ -3970,6 +4312,9 @@ /* No comment provided by engineer. */ "Settings" = "Налаштування"; +/* alert message */ +"Settings were changed." = "Налаштування були змінені."; + /* No comment provided by engineer. */ "Shape profile images" = "Сформуйте зображення профілю"; @@ -3980,9 +4325,15 @@ /* No comment provided by engineer. */ "Share 1-time link" = "Поділитися 1-разовим посиланням"; +/* No comment provided by engineer. */ +"Share 1-time link with a friend" = "Поділіться одноразовим посиланням з другом"; + /* No comment provided by engineer. */ "Share address" = "Поділитися адресою"; +/* No comment provided by engineer. */ +"Share address publicly" = "Поділіться адресою публічно"; + /* alert title */ "Share address with contacts?" = "Поділіться адресою з контактами?"; @@ -3992,6 +4343,12 @@ /* No comment provided by engineer. */ "Share link" = "Поділіться посиланням"; +/* No comment provided by engineer. */ +"Share profile" = "Поділіться профілем"; + +/* No comment provided by engineer. */ +"Share SimpleX address on social media." = "Поділіться адресою SimpleX у соціальних мережах."; + /* No comment provided by engineer. */ "Share this 1-time invite link" = "Поділіться цим одноразовим посиланням-запрошенням"; @@ -4037,6 +4394,12 @@ /* No comment provided by engineer. */ "SimpleX Address" = "Адреса SimpleX"; +/* No comment provided by engineer. */ +"SimpleX address and 1-time links are safe to share via any messenger." = "SimpleX-адреси та одноразові посилання можна безпечно ділитися через будь-який месенджер."; + +/* No comment provided by engineer. */ +"SimpleX address or 1-time link?" = "SimpleX адреса або одноразове посилання?"; + /* No comment provided by engineer. */ "SimpleX Chat security was audited by Trail of Bits." = "Безпека SimpleX Chat була перевірена компанією Trail of Bits."; @@ -4073,6 +4436,9 @@ /* simplex link type */ "SimpleX one-time invitation" = "Одноразове запрошення SimpleX"; +/* No comment provided by engineer. */ +"SimpleX protocols reviewed by Trail of Bits." = "Протоколи SimpleX, розглянуті Trail of Bits."; + /* No comment provided by engineer. */ "Simplified incognito mode" = "Спрощений режим інкогніто"; @@ -4091,9 +4457,15 @@ /* No comment provided by engineer. */ "SMP server" = "Сервер SMP"; +/* No comment provided by engineer. */ +"SOCKS proxy" = "Проксі SOCKS"; + /* blur media */ "Soft" = "М'який"; +/* No comment provided by engineer. */ +"Some app settings were not migrated." = "Деякі налаштування програми не були перенесені."; + /* No comment provided by engineer. */ "Some file(s) were not exported:" = "Деякі файли не було експортовано:"; @@ -4103,6 +4475,9 @@ /* No comment provided by engineer. */ "Some non-fatal errors occurred during import:" = "Під час імпорту виникли деякі несмертельні помилки:"; +/* alert message */ +"Some servers failed the test:\n%@" = "Деякі сервери не пройшли тестування:\n%@"; + /* notification title */ "Somebody" = "Хтось"; @@ -4184,12 +4559,21 @@ /* No comment provided by engineer. */ "Support SimpleX Chat" = "Підтримка чату SimpleX"; +/* No comment provided by engineer. */ +"Switch audio and video during the call." = "Перемикайте аудіо та відео під час дзвінка."; + +/* No comment provided by engineer. */ +"Switch chat profile for 1-time invitations." = "Переключіть профіль чату для отримання одноразових запрошень."; + /* No comment provided by engineer. */ "System" = "Система"; /* No comment provided by engineer. */ "System authentication" = "Автентифікація системи"; +/* No comment provided by engineer. */ +"Tail" = "Хвіст"; + /* No comment provided by engineer. */ "Take picture" = "Сфотографуйте"; @@ -4256,6 +4640,9 @@ /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "Додаток може сповіщати вас, коли ви отримуєте повідомлення або запити на контакт - будь ласка, відкрийте налаштування, щоб увімкнути цю функцію."; +/* No comment provided by engineer. */ +"The app protects your privacy by using different operators in each conversation." = "Додаток захищає вашу конфіденційність, використовуючи різних операторів у кожній розмові."; + /* No comment provided by engineer. */ "The app will ask to confirm downloads from unknown file servers (except .onion)." = "Програма попросить підтвердити завантаження з невідомих файлових серверів (крім .onion)."; @@ -4265,6 +4652,9 @@ /* No comment provided by engineer. */ "The code you scanned is not a SimpleX link QR code." = "Відсканований вами код не є QR-кодом посилання SimpleX."; +/* No comment provided by engineer. */ +"The connection reached the limit of undelivered messages, your contact may be offline." = "З'єднання досягло ліміту недоставлених повідомлень, ваш контакт може бути офлайн."; + /* No comment provided by engineer. */ "The connection you accepted will be cancelled!" = "Прийняте вами з'єднання буде скасовано!"; @@ -4304,6 +4694,9 @@ /* No comment provided by engineer. */ "The profile is only shared with your contacts." = "Профіль доступний лише вашим контактам."; +/* No comment provided by engineer. */ +"The second preset operator in the app!" = "Другий попередньо встановлений оператор у застосунку!"; + /* No comment provided by engineer. */ "The second tick we missed! ✅" = "Другу галочку ми пропустили! ✅"; @@ -4313,12 +4706,21 @@ /* No comment provided by engineer. */ "The servers for new connections of your current chat profile **%@**." = "Сервери для нових підключень вашого поточного профілю чату **%@**."; +/* No comment provided by engineer. */ +"The servers for new files of your current chat profile **%@**." = "Сервери для нових файлів вашого поточного профілю чату **%@**."; + /* No comment provided by engineer. */ "The text you pasted is not a SimpleX link." = "Текст, який ви вставили, не є посиланням SimpleX."; +/* No comment provided by engineer. */ +"The uploaded database archive will be permanently removed from the servers." = "Завантажений архів бази даних буде назавжди видалено з серверів."; + /* No comment provided by engineer. */ "Themes" = "Теми"; +/* No comment provided by engineer. */ +"These conditions will also apply for: **%@**." = "Ці умови також поширюються на: **%@**."; + /* No comment provided by engineer. */ "These settings are for your current profile **%@**." = "Ці налаштування стосуються вашого поточного профілю **%@**."; @@ -4382,6 +4784,9 @@ /* No comment provided by engineer. */ "To make a new connection" = "Щоб створити нове з'єднання"; +/* No comment provided by engineer. */ +"To protect against your link being replaced, you can compare contact security codes." = "Щоб захиститися від заміни вашого посилання, ви можете порівняти коди безпеки контактів."; + /* No comment provided by engineer. */ "To protect timezone, image/voice files use UTC." = "Для захисту часового поясу у файлах зображень/голосу використовується UTC."; @@ -4394,15 +4799,30 @@ /* No comment provided by engineer. */ "To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Щоб захистити конфіденційність, замість ідентифікаторів користувачів, які використовуються на всіх інших платформах, SimpleX має ідентифікатори для черг повідомлень, окремі для кожного з ваших контактів."; +/* No comment provided by engineer. */ +"To receive" = "Щоб отримати"; + +/* No comment provided by engineer. */ +"To record speech please grant permission to use Microphone." = "Для запису промови, будь ласка, надайте дозвіл на використання мікрофону."; + +/* No comment provided by engineer. */ +"To record video please grant permission to use Camera." = "Для запису відео, будь ласка, надайте дозвіл на використання камери."; + /* No comment provided by engineer. */ "To record voice message please grant permission to use Microphone." = "Щоб записати голосове повідомлення, будь ласка, надайте дозвіл на використання мікрофону."; /* No comment provided by engineer. */ "To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." = "Щоб відкрити свій прихований профіль, введіть повний пароль у поле пошуку на сторінці **Ваші профілі чату**."; +/* No comment provided by engineer. */ +"To send" = "Щоб відправити"; + /* No comment provided by engineer. */ "To support instant push notifications the chat database has to be migrated." = "Для підтримки миттєвих push-повідомлень необхідно перенести базу даних чату."; +/* No comment provided by engineer. */ +"To use the servers of **%@**, accept conditions of use." = "Щоб користуватися серверами **%@**, прийміть умови використання."; + /* No comment provided by engineer. */ "To verify end-to-end encryption with your contact compare (or scan) the code on your devices." = "Щоб перевірити наскрізне шифрування з вашим контактом, порівняйте (або відскануйте) код на ваших пристроях."; @@ -4460,6 +4880,9 @@ /* rcv group event chat item */ "unblocked %@" = "розблоковано %@"; +/* No comment provided by engineer. */ +"Undelivered messages" = "Недоставлені повідомлення"; + /* No comment provided by engineer. */ "Unexpected migration state" = "Неочікуваний стан міграції"; @@ -4577,12 +5000,21 @@ /* No comment provided by engineer. */ "Use .onion hosts" = "Використовуйте хости .onion"; +/* No comment provided by engineer. */ +"Use %@" = "Використовуйте %@"; + /* No comment provided by engineer. */ "Use chat" = "Використовуйте чат"; /* No comment provided by engineer. */ "Use current profile" = "Використовувати поточний профіль"; +/* No comment provided by engineer. */ +"Use for files" = "Використовуйте для файлів"; + +/* No comment provided by engineer. */ +"Use for messages" = "Використовуйте для повідомлень"; + /* No comment provided by engineer. */ "Use for new connections" = "Використовуйте для нових з'єднань"; @@ -4607,9 +5039,15 @@ /* No comment provided by engineer. */ "Use server" = "Використовувати сервер"; +/* No comment provided by engineer. */ +"Use servers" = "Використовуйте сервери"; + /* No comment provided by engineer. */ "Use SimpleX Chat servers?" = "Використовувати сервери SimpleX Chat?"; +/* No comment provided by engineer. */ +"Use SOCKS proxy" = "Використовуйте SOCKS проксі"; + /* No comment provided by engineer. */ "Use the app while in the call." = "Використовуйте додаток під час розмови."; @@ -4619,6 +5057,9 @@ /* No comment provided by engineer. */ "User selection" = "Вибір користувача"; +/* No comment provided by engineer. */ +"Username" = "Ім'я користувача"; + /* No comment provided by engineer. */ "Using SimpleX Chat servers." = "Використання серверів SimpleX Chat."; @@ -4685,9 +5126,15 @@ /* No comment provided by engineer. */ "Videos and files up to 1gb" = "Відео та файли до 1 Гб"; +/* No comment provided by engineer. */ +"View conditions" = "Умови перегляду"; + /* No comment provided by engineer. */ "View security code" = "Переглянути код безпеки"; +/* No comment provided by engineer. */ +"View updated conditions" = "Переглянути оновлені умови"; + /* chat feature */ "Visible history" = "Видима історія"; @@ -4769,6 +5216,9 @@ /* No comment provided by engineer. */ "when IP hidden" = "коли IP приховано"; +/* No comment provided by engineer. */ +"When more than one operator is enabled, none of them has metadata to learn who communicates with whom." = "Коли увімкнено більше одного оператора, жоден з них не має метаданих, щоб дізнатися, хто з ким спілкується."; + /* No comment provided by engineer. */ "When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "Коли ви ділитеся з кимось своїм профілем інкогніто, цей профіль буде використовуватися для груп, до яких вас запрошують."; @@ -4877,6 +5327,12 @@ /* No comment provided by engineer. */ "You can change it in Appearance settings." = "Ви можете змінити його в налаштуваннях зовнішнього вигляду."; +/* No comment provided by engineer. */ +"You can configure operators in Network & servers settings." = "Ви можете налаштувати операторів у налаштуваннях Мережі та серверів."; + +/* No comment provided by engineer. */ +"You can configure servers via settings." = "Ви можете налаштувати сервери за допомогою налаштувань."; + /* No comment provided by engineer. */ "You can create it later" = "Ви можете створити його пізніше"; @@ -4901,6 +5357,9 @@ /* No comment provided by engineer. */ "You can send messages to %@ from Archived contacts." = "Ви можете надсилати повідомлення на %@ з архівних контактів."; +/* No comment provided by engineer. */ +"You can set connection name, to remember who the link was shared with." = "Ви можете задати ім'я з'єднання, щоб запам'ятати, з ким ви поділилися посиланням."; + /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "Ви можете налаштувати попередній перегляд сповіщень на екрані блокування за допомогою налаштувань."; @@ -5045,9 +5504,15 @@ /* No comment provided by engineer. */ "Your chat database is not encrypted - set passphrase to encrypt it." = "Ваша база даних чату не зашифрована - встановіть ключову фразу, щоб зашифрувати її."; +/* alert title */ +"Your chat preferences" = "Ваші налаштування чату"; + /* No comment provided by engineer. */ "Your chat profiles" = "Ваші профілі чату"; +/* No comment provided by engineer. */ +"Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." = "Ваше з'єднання було переміщено на %@, але під час перенаправлення на профіль сталася несподівана помилка."; + /* No comment provided by engineer. */ "Your contact sent a file that is larger than currently supported maximum size (%@)." = "Ваш контакт надіслав файл, розмір якого перевищує підтримуваний на цей момент максимальний розмір (%@)."; @@ -5057,6 +5522,9 @@ /* No comment provided by engineer. */ "Your contacts will remain connected." = "Ваші контакти залишаться на зв'язку."; +/* No comment provided by engineer. */ +"Your credentials may be sent unencrypted." = "Ваші облікові дані можуть бути надіслані незашифрованими."; + /* No comment provided by engineer. */ "Your current chat database will be DELETED and REPLACED with the imported one." = "Ваша поточна база даних чату буде ВИДАЛЕНА і ЗАМІНЕНА імпортованою."; @@ -5081,6 +5549,9 @@ /* No comment provided by engineer. */ "Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Ваш профіль зберігається на вашому пристрої і доступний лише вашим контактам. Сервери SimpleX не бачать ваш профіль."; +/* alert message */ +"Your profile was changed. If you save it, the updated profile will be sent to all your contacts." = "Ваш профіль було змінено. Якщо ви збережете його, оновлений профіль буде надіслано всім вашим контактам."; + /* No comment provided by engineer. */ "Your profile, contacts and delivered messages are stored on your device." = "Ваш профіль, контакти та доставлені повідомлення зберігаються на вашому пристрої."; @@ -5090,6 +5561,9 @@ /* No comment provided by engineer. */ "Your server address" = "Адреса вашого сервера"; +/* No comment provided by engineer. */ +"Your servers" = "Ваші сервери"; + /* No comment provided by engineer. */ "Your settings" = "Ваші налаштування"; diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml index 2f3f245f54..c8d995cfd9 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml @@ -28,7 +28,7 @@ سيتم تغيير عنوان الاستلام إلى خادم مختلف. سيتم إكمال تغيير العنوان بعد اتصال المرسل بالإنترنت. هذا الرابط ليس رابط اتصال صالح! يسمح - أضِف خوادم محدّدة مسبقًا + أضِف خوادم مُعدة مسبقًا أضِف إلى جهاز آخر سيتم حذف جميع الدردشات والرسائل - لا يمكن التراجع عن هذا! الوصول إلى الخوادم عبر وكيل SOCKS على المنفذ %d؟ يجب بدء تشغيل الوكيل قبل تمكين هذا الخيار. @@ -124,8 +124,7 @@ 30 ثانية إلغاء الرسالة المباشرة إلغاء - سيتم استخدام اتصال TCP منفصل (وبيانات اعتماد SOCKS) لكل جهة اتصال وعضو في المجموعة. -\n الرجاء ملاحظة: إذا كان لديك العديد من التوصيلات ، فقد يكون استهلاك البطارية وحركة المرور أعلى بكثير وقد تفشل بعض الاتصالات. + لكل جهة اتصال وعضو في المجموعة\n. الرجاء ملاحظة: إذا كان لديك العديد من الاتصالات، فقد يكون استهلاك البطارية وحركة المرور أعلى بكثير وقد تفشل بعض الاتصالات.]]> جارٍ الاتصال… مكالمة صوتية المكالمات على شاشة القفل: @@ -199,7 +198,7 @@ خطأ في إحباط تغيير العنوان تفعيل قفل SimpleX تأكد من بيانات الاعتماد الخاصة بك - إنشاء عنوان SimpleX + أنشئ عنوان SimpleX متابعة تحدث مع المطورين سياق الأيقونة @@ -477,7 +476,7 @@ كيفية الاستخدام كيف يعمل SimpleX التخفي عبر رابط عنوان جهة الاتصال - رمز الحماية غير صحيحة! + رمز الأمان غير صحيحة! الإشعارات الفورية مُعطَّلة إشعارات فورية! إخفاء جهة الاتصال والرسالة @@ -824,7 +823,7 @@ تعمية ثنائية الطبقات من بين الطريفين.]]> صفّر الألوان حفظ - عنوان الخادم المحدد مسبقًا + عنوان الخادم المُعد مسبقًا حفظ وإشعار أعضاء المجموعة دوري أعد تشغيل التطبيق لاستخدام قاعدة بيانات الدردشة المستوردة. @@ -836,7 +835,7 @@ يرجى تخزين عبارة المرور بشكل آمن، فلن تتمكن من الوصول إلى الدردشة إذا فقدتها. يُرجى تحديث التطبيق والتواصل مع المطورين. دليل المستخدم.]]> - افتح ملفات تعريف الدردشة + غيّر ملفات تعريف الدردشة اسحب الوصول كشف سيتم إيقاف استلام الملف. @@ -930,7 +929,7 @@ رمز QR صفّر المنفذ %d - خادم محدد مسبقًا + خادم مُعد مسبقًا يتم استخدام خادم الترحيل فقط إذا لزم الأمر. يمكن لطرف آخر مراقبة عنوان IP الخاص بك. حفظ وإشعار جهة الاتصال إعادة التشغيل @@ -1163,7 +1162,7 @@ أنت متصل بالفعل بـ%1$s. في انتظار الفيديو سيتم استلام الفيديو عند اكتمال تحميل جهة اتصالك. - تحقق من رمز الحماية + تحقق من رمز الأمان رسائل صوتية عندما يطلب الأشخاص الاتصال، يمكنك قبوله أو رفضه. سوف تكون متصلاً بالمجموعة عندما يكون جهاز مضيف المجموعة متصلاً بالإنترنت، يرجى الانتظار أو التحقق لاحقًا! @@ -1796,7 +1795,7 @@ أظهِر قائمة الدردشة في نافذة جديدة ألوان الدردشة سمة الدردشة - تلقى الرد + تلقيت رد أزِل الصورة تكرار صفّر اللون @@ -2116,4 +2115,97 @@ أمان أفضل ✅ بروتوكولات SimpleX تمت مراجعتها بواسطة Trail of Bits. تبديل ملف تعريف الدردشة لدعوات لمرة واحدة. + أخطاء في تضبيط الخوادم. + لملف تعريف الدردشة %s: + لا يوجد وسائط أو خوادم ملفات. + لا يوجد خوادم لإرسال الملفات. + لقد وصل الاتصال إلى الحد الأقصى من الرسائل غير المُسلمة، قد يكون جهة الاتصال الخاصة بك غير متصلة بالإنترنت. + الرسائل غير المُسلَّمة + شارك رابطًا لمرة واحدة مع صديق + أمان الاتصال + لحماية الرابط الخاص بك من الاستبدال، يمكنك مقارنة رموز أمان جهات الاتصال. + خادم جديد + لوسائل التواصل الاجتماعي + أو للمشاركة بشكل خاص + إعدادات العنوان + أنشئ رابط لمرة واحدة + عنوان SimpleX أو رابط لمرة واحدة؟ + مُشغلي الشبكة + يمكنك تضبيط الخوادم عبر الإعدادات. + حدد مشغلي الشبكة الذين تريد استخدامهم. + يمكنك تضبيط المُشغلين في إعدادات الشبكة والخوادم. + حدّث + تابع + قُبل الشروط + راجع الشروط + الخوادم المُعدة مسبقًا + سيتم قبول الشروط تلقائيًا للمُشغلين المفعّلين في: %s. + خوادمك + %s.]]> + %s.]]> + مُشغل الشبكة + المُشغل + %s خوادم + الموقع الإلكتروني + سيتم قبول الشروط في: %s. + قُبل الشروط في: %s. + استخدم %s + استخدم الخوادم + %s.]]> + شروط الاستخدام + للتوجيه الخاص + لتلقي + استخدم للملفات + اعرض الشروط + %s.]]> + %s، يجب قبول شروط الاستخدام.]]> + %s.]]> + أُضيفت خوادم الوسائط والملفات + الشروط المفتوحة + الخوادم الخاصة بالملفات الجديدة لملف الدردشة الحالي الخاص بك + لإرسال + خطأ في إضافة الخادم + خطأ في تحديث الخادم + التغييرات المفتوحة + خادم المُشغل + أُضيف الخادم إلى المُشغل %s. + تغيّر مُشغل الخادم. + أشرطة أدوات التطبيق + تمويه + الشفافية + فعّل flux + اللامركزية الشبكية + المُشغل المُعد مسبقًا الثاني في التطبيق! + لتحسين خصوصية البيانات الوصفية. + تحسين التنقل في الدردشة + اعرض الشروط المُحدثة + اقبل الشروط + أُضيفت خوادم الرسائل + عنوان أو رابط لمرة واحدة؟ + مع جهة اتصال واحدة فقط - المشاركة شخصيًا أو عبر أي مُراسل.]]> + سيتم قبول الشروط للمُشغلين المفعّلين بعد 30 يومًا. + اختر المُشغلين + لا يمكن تحميل نص الشروط الحالية، يمكنك مراجعة الشروط عبر هذا الرابط: + خطأ في قبول الشروط + خطأ في حفظ الخوادم + على سبيل المثال، إذا تلقيت رسائل عبر خادم SimpleX Chat، فسيستخدم التطبيق أحد خوادم Flux للتوجيه الخاص. + لا يوجد خوادم لتوجيه الرسائل الخاصة. + لا يوجد خوادم رسائل. + لا يوجد خوادم لاستقبال الملفات. + لا توجد رسالة + لا يوجد خوادم لاستقبال الرسائل. + - فتح الدردشة عند أول رسالة غير مقروءة.\n- الانتقال إلى الرسائل المقتبسة. + يمكنك تعيين اسم الاتصال، لتذكر الأشخاص الذين تمت مشاركة الرابط معهم. + راجع لاحقًا + تغيّر بروتوكول الخادم. + شارك العنوان علناً + شارك عنوان SimpleX على وسائل التواصل الاجتماعي. + عنوان SimpleX والروابط لمرة واحدة آمنة للمشاركة عبر أي برنامج مُراسلة. + انقر فوق أنشئ عنوان SimpleX في القائمة لإنشائه لاحقًا. + حُذفت هذه الرسالة أو لم يتم استلامها بعد. + استخدم للرسائل + عندما تفعّل أكثر من مُشغل شبكة واحد، سيستخدم التطبيق خوادم مُشغلين مختلفين لكل مُحادثة. + %s.]]> + %s.]]> + %s.]]> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 55b7e2b0e7..53df6d4818 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -1733,7 +1733,6 @@ %s.]]> %s.]]> %s.]]> - %s.]]> %s.]]> %s.]]> %s.]]> diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/bg/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/bg/strings.xml index 748c264918..d59867574d 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/bg/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/bg/strings.xml @@ -89,8 +89,7 @@ за всеки чат профил, който имате в приложението.]]> аудио разговор Най-добро за батерията. Ще получавате известия само когато приложението работи (БЕЗ фонова услуга).]]> - Ще се използва отделна TCP връзка (и идентификационни данни за SOCKS) за всеки контакт и член на група. -\nМоля, обърнете внимание: ако имате много връзки, консумацията на батерията и трафика може да бъде значително по-висока и някои връзки може да се провалят. + за всеки контакт и член на група. \nМоля, обърнете внимание: ако имате много връзки, консумацията на батерията и трафика може да бъде значително по-висока и някои връзки може да се провалят.]]> Помолен да получи изображението Аудио и видео разговори аудио разговор (не е e2e криптиран) diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml index 548de53a21..b107ea1df7 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml @@ -631,8 +631,7 @@ Onion hostitelé nebudou použiti. Izolace přenosu for each chat profile you have in the app.]]> - Oddělit TCP připojení (a SOCKS pověření) bude použito pro všechny kontakty a členy skupin. -\nUpozornění: Pokud máte mnoho připojení, může být spotřeba baterie a provoz podstatně vyšší a některá připojení mohou selhat. + pro všechny kontakty a členy skupin. \nUpozornění: Pokud máte mnoho připojení, může být spotřeba baterie a provoz podstatně vyšší a některá připojení mohou selhat.]]> Vzhled Verze aplikace Verze aplikace: v%s @@ -2041,4 +2040,6 @@ Jiné FXTP servery Pozvat Pošlete zprávu pro povolení volání. + Přijmout podmínky + Přijaté podmínky \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml index 05ed6366a1..29812d0a3e 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml @@ -264,7 +264,7 @@ Danke, dass Sie SimpleX Chat installiert haben! mit den SimpleX-Chat-Entwicklern verbinden, um Fragen zu stellen und aktuelle Informationen zu erhalten.]]> Um einen neuen Chat zu starten - Schaltfläche antippen + Schaltfläche tippen Danach die gewünschte Aktion auswählen: Über Link verbinden Wenn Sie einen SimpleX-Chat-Einladungslink erhalten haben, können Sie ihn in Ihrem Browser öffnen: @@ -989,9 +989,7 @@ Verbindung Chat-Profil Dateien für alle Chat-Profile löschen - Für jeden Kontakt und jedes Gruppenmitglied wird eine separate TCP-Verbindung (und SOCKS-Berechtigung) genutzt. -\n -\nBitte beachten Sie: Wenn Sie viele Verbindungen haben, können Akkuverbrauch und Datennutzung wesentlich höher ausfallen und einige Verbindungen scheitern. + Für jeden Kontakt und jedes Gruppenmitglied wird eine separate TCP-Verbindung (und SOCKS-Berechtigung) genutzt.\nBitte beachten Sie: Wenn Sie viele Verbindungen haben, können Akkuverbrauch und Datennutzung wesentlich höher ausfallen und einige Verbindungen scheitern.]]> Für jedes von Ihnen in der App genutzte Chat-Profil wird eine separate TCP-Verbindung (und SOCKS-Berechtigung) genutzt.]]> Nur lokale Profildaten Profil und Serververbindungen @@ -1235,7 +1233,7 @@ Falls Sie sich nicht persönlich treffen können, zeigen Sie den QR-Code in einem Videoanruf oder teilen Sie den Link. Benutzeranleitung.]]> Stellen Sie sicher, dass die Datei die korrekte YAML-Syntax hat. Exportieren Sie das Design, um ein Beispiel für die Dateistruktur des Designs zu erhalten. - Offene Chat-Profile + Chat-Profile wechseln Sie können Ihre Adresse als Link oder QR-Code teilen – jede Person kann sich mit Ihnen verbinden. Werden die App-Daten komplett gelöscht. Es wurde ein leeres Chat-Profil mit dem eingegebenen Namen erstellt und die App öffnet wie gewohnt. @@ -2200,4 +2198,99 @@ Verbesserte Sicherheit ✅ Verbesserte Nachrichten-Datumsinformation Verbesserte Nutzer-Erfahrung + Fehler beim Speichern der Server + Keine Nachrichten-Server. + Keine Server für den Empfang von Nachrichten. + Fehler in der Server-Konfiguration. + Für das Chat-Profil %s: + Keine Medien- und Dateiserver. + Keine Server für den Empfang von Dateien. + Keine Server für das Versenden von Dateien. + Nicht ausgelieferte Nachrichten + Die SimpleX-Adresse auf sozialen Medien teilen. + Verbindungs-Sicherheit + Den Einmal-Einladungslink mit einem Freund teilen + Die SimpleX-Adresse und Einmal-Links können sicher über beliebige Messenger geteilt werden. + Sie können einen Verbindungsnamen festlegen, um sich zu merken, mit wem der Link geteilt wurde. + Adress-Einstellungen + Einmal-Link erstellen + Für soziale Medien + Oder zum privaten Teilen + SimpleX-Adresse oder Einmal-Link? + Betreiber auswählen + Netzwerk-Betreiber + Wenn mehr als ein Netzwerk-Betreiber aktiviert ist, verwendet die App für jede Unterhaltung Server der verschiedenen Betreiber. + Die Nutzungsbedingungen der aktivierten Betreiber werden nach 30 Tagen akzeptiert. + Wenn Sie beispielsweise Nachrichten über einen SimpleX-Chatserver empfangen, verwendet die App einen der Server von Flux für die private Weiterleitung. + Später einsehen + Wählen sie die zu nutzenden Netzwerk-Betreiber aus. + Sie können die Betreiber in den Netzwerk- und Servereinstellungen konfigurieren. + Sie können die Server über die Einstellungen konfigurieren. + Weiter + Aktualisieren + Voreingestellte Server + Nutzungsbedingungen einsehen + Die Nutzungsbedingungen der aktivierten Betreiber werden automatisch akzeptiert am: %s. + Ihre Server + Betreiber + %s Server + Netzwerk-Betreiber + Verwende Server + Webseite + Verwende %s + %s.]]> + %s.]]> + Nutzungsbedingungen + Nutzungsbedingungen anschauen + Nutzungsbedingungen akzeptieren + %s.]]> + Für den Empfang + Für Nachrichten verwenden + Nachrichtenserver hinzugefügt + Für privates Routing + Die Server Deines aktuellen Chat-Profils für neue Dateien + Für das Senden + Für Dateien verwenden + Fehler beim Hinzufügen des Servers + Änderungen öffnen + Nutzungsbedingungen öffnen + Betreiber-Server + Der Server wurde dem Betreiber %s hinzugefügt. + Der Server-Betreiber wurde geändert. + Das Server-Protokoll wurde geändert. + Transparenz + Flux aktivieren + Dezentralisiertes Netzwerk + Der zweite voreingestellte Netzwerk-Betreiber in der App! + Verbesserte Chat-Navigation + - Den Chat bei der ersten ungelesenen Nachricht öffnen.\n- Zu zitierten Nachrichten springen. + Aktualisierte Nutzungsbedingungen anschauen + Akzeptierte Nutzungsbedingungen + Medien- und Dateiserver hinzugefügt + Adress- oder Einmal-Link? + App-Symbolleiste + Verpixeln + nur mit einem Kontakt genutzt werden - teilen Sie in nur persönlich oder über einen beliebigen Messenger.]]> + %s.]]> + %s.]]> + Die Nutzungsbedingungen werden akzeptiert am: %s. + %s.]]> + %s zu nutzen, müssen Sie dessen Nutzungsbedingungen akzeptieren.]]> + Fehler beim Akzeptieren der Nutzungsbedingungen + Fehler beim Aktualisieren des Servers + für einen besseren Metadatenschutz. + Neuer Server + Keine Nachricht + Keine Server für privates Nachrichten-Routing. + Die Adresse öffentlich teilen + Tippen Sie im Menü auf SimpleX-Adresse erstellen, um sie später zu erstellen. + Diese Verbindung hat das Limit der nicht ausgelieferten Nachrichten erreicht. Ihr Kontakt ist möglicherweise offline. + Diese Nachricht wurde gelöscht oder bisher noch nicht empfangen. + Zum Schutz vor dem Austausch Ihres Links können Sie die Sicherheitscodes Ihrer Kontakte vergleichen. + %s.]]> + %s.]]> + Die Nutzungsbedingungen wurden akzeptiert am: %s. + Der Text der aktuellen Nutzungsbedingungen konnte nicht geladen werden. Sie können die Nutzungsbedingungen unter diesem Link einsehen: + Ferngesteuerte Mobiltelefone + Oder importieren Sie eine Archiv-Datei \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml index 0f31210a9a..3d22d1fcc5 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml @@ -1101,7 +1101,7 @@ ¡Nuestro agradecimiento a todos los colaboradores! Puedes contribuir a través de Weblate. Vídeos y archivos de hasta 1Gb ¡Rápido y sin necesidad de esperar a que el remitente esté en línea! - Abrir perfiles + Cambiar perfil Más información Si no puedes reunirte en persona, muestra el código QR por videollamada o comparte el enlace. Para conectarse, tu contacto puede escanear el código QR o usar el enlace en la aplicación. @@ -1111,7 +1111,7 @@ Abriendo base de datos… Error al introducir dirección Guía de Usuario.]]> - Enlace un uso + Enlace de un uso Dirección SimpleX Cuando alguien solicite conectarse podrás aceptar o rechazar su solicitud. Compartir dirección @@ -1927,7 +1927,7 @@ Abrir ubicación del archivo Por favor, reinicia la aplicación. Recordar más tarde - Saltar esta versión + Omitir esta versión Estable inactivo Error @@ -2058,7 +2058,7 @@ Nuevas opciones multimedia Puedes cambiar la posición de la barra desde el menú Apariencia. Descarga nuevas versiones desde GitHub. - Nuevo mensaje + Mensaje nuevo Enlace no válido Por favor, comprueba que el enlace SimpleX es correcto. %1$d archivo(s) se está(n) descargando todavía. @@ -2119,4 +2119,99 @@ Seguridad mejorada ✅ Borra o modera hasta 200 mensajes a la vez. Cambia el perfil de chat para invitaciones de un solo uso. + Error al guardar servidores + Error en la configuración del servidor. + Para el perfil de chat %s: + Ningún servidor de mensajes. + Ningún servidor para recibir archivos. + Ningún servidor para enviar archivos. + Seguridad de conexión + Compartir enlace de un uso con un amigo + Compartir dirección SimpleX en redes sociales. + Configuración de dirección + Crear enlace de un uso + Para redes sociales + Dirección SimpleX o enlace de un uso? + Selecciona operadores + Operadores de red + Las condiciones de los operadores habilitados serán aceptadas después de 30 días. + Revisar más tarde + Condiciones aceptadas el: %s. + Operador de red + Operador + Servidores predefinidos + Revisar condiciones + %s servidores + Las condiciones serán aceptadas el: %s. + Condiciones de uso + Para el enrutamiento privado + Error al añadir servidor + Abrir cambios + Abrir condiciones + Servidor añadido al operador %s. + El operador del servidor ha cambiado. + El protocolo del servidor ha cambiado. + Barras de herramientas + Difuminar + Navegación en el chat mejorada + Descentralización de la red + - El chat abre en el primer mensaje no leído.\n- Desplazamiento hasta los mensajes citados. + Aceptar condiciones + Condiciones aceptadas + Servidores de archivos y multimedia añadidos + Servidores de mensajes añadidos + ¿Dirección o enlace de un uso? + Las condiciones serán aceptadas automáticamente para los operadores habilitados el: %s. + Continuar + El texto con las condiciones actuales no se ha podido cargar, puedes revisar las condiciones en el siguiente enlace: + Habilitar Flux + Error al aceptar las condiciones + Error al actualizar el servidor + para mayor privacidad de los metadatos. + Ningún mensaje + Servidor nuevo + Ningún servidor de archivos y multimedia. + Ningún servidor para enrutamiento privado. + Ningún servidor para recibir mensajes. + Servidor del operador + O para compartir en privado + Selecciona los operadores de red a utilizar + Campartir dirección públicamente + Compartir enlaces de un uso y direcciones SimpleX es seguro a través de cualquier medio. + Actualizar + Sitio web + Tus servidores + Usar %s + Usar servidores + Usar para mensajes + Ver condiciones + Para recibir + Para enviar + Usar para archivos + Transparencia + Ver condiciones actualizadas + Mensajes no entregados + solamente con un contacto - comparte en persona o mediante cualquier aplicación de mensajería.]]> + Puedes añadir un nombre a la conexión para recordar a quién corresponde. + Cuando está habilitado más de un operador de red, la aplicación usa servidores de diferentes operadores para cada conversación. + Puedes configurar los operadores desde Servidores y Redes. + %s.]]> + %s.]]> + %s.]]> + %s.]]> + %s.]]> + %s.]]> + %s, acepta las condiciones de uso.]]> + Los servidores para archivos nuevos en tu perfil actual + El segundo operador predefinido! + Puedes configurar los servidores a través de su configuración. + Para protegerte contra una sustitución del enlace, puedes comparar los códigos de seguridad con tu contacto. + %s.]]> + %s.]]> + Si por ejemplo recibes los mensajes a través de un servidor de SimpleX Chat, la aplicación usará uno de Flux para el enrutamiento privado. + Pulsa Crear dirección SimpleX en el menú para crearla más tarde. + La conexión ha alcanzado el límite de mensajes no entregados. es posible que tu contacto esté desconectado. + El mensaje ha sido borrado o aún no se ha recibido. + Móvil remoto + O importa desde un archivo \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml index 23e2392fdc..8850e33a3e 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml @@ -636,8 +636,7 @@ تمام مخاطبانتان متصل باقی خواهند ماند. به‌روزرسانی نمایه به مخاطبانتان ارسال خواهد شد. اگر تایید کنید، سرورهای پیام‌رسانی خواهند توانست نشانی‌ IP، و فراهم‌کننده شما را ببینند - و این که به چه سرورهایی متصل می‌شوید. مطمئن شوید قالب نشانی‌های سرور WebRTC ICE صحیح است، در خط‌های جدا نوشته شده و تکرار نشده‌اند. - یک اتصال جدای TCP (و اطلاعات ورود SOCKS) برای هر مخاطب و عضو گروه استفاده خواهد شد. -\nلطفا توجه داشته باشید: اگر اتصال‌های زیادی داشته باشید، مصرف باتری و ترافیک شما می‌تواند به شکل قابل توجه بالاتر باشد و بعضی اتصال‌ها ممکن است با موفقیت انجام نشوند. + برای هر مخاطب و عضو گروه استفاده خواهد شد. \nلطفا توجه داشته باشید: اگر اتصال‌های زیادی داشته باشید، مصرف باتری و ترافیک شما می‌تواند به شکل قابل توجه بالاتر باشد و بعضی اتصال‌ها ممکن است با موفقیت انجام نشوند.]]> حالت انزوای ترابری به روز شود؟ ویرایش تصویر ایجاد نشانی‌ SimpleX diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/fi/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/fi/strings.xml index 28b29c59af..f933b2d6cc 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/fi/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/fi/strings.xml @@ -251,8 +251,7 @@ Poista kontakti Yllä, sitten: Keskustelujen profiili - Jokaiselle kontaktille ja ryhmän jäsenelle käytetään erillistä TCP-yhteyttä (ja SOCKS-tunnistetietoja). -\nHuomaa: jos sinulla on useita yhteyksiä, akun ja data-liikenteen määrä voi olla huomattavasti korkeampi ja jotkin yhteydet voivat epäonnistua. + Jokaiselle kontaktille ja ryhmän jäsenelle käytetään erillistä TCP-yhteyttä (ja SOCKS-tunnistetietoja). \nHuomaa: jos sinulla on useita yhteyksiä, akun ja data-liikenteen määrä voi olla huomattavasti korkeampi ja jotkin yhteydet voivat epäonnistua.]]> Äänipuhelu Paras akulle. Saat ilmoituksia vain, kun sovellus on käynnissä (EI taustapalvelua).]]> %d tuntia diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml index 07b99ebe1d..6caf5b61ef 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml @@ -907,8 +907,7 @@ Profil et connexions au serveur Transport isolé Mettre à jour le mode d\'isolement du transport \? - Une connexion TCP distincte (et identifiant SOCKS) sera utilisée pour chaque contact et membre de groupe. -\nVeuillez noter : si vous avez de nombreuses connexions, votre consommation de batterie et de réseau peut être nettement plus élevée et certaines liaisons peuvent échouer. + pour chaque contact et membre de groupe. \nVeuillez noter : si vous avez de nombreuses connexions, votre consommation de batterie et de réseau peut être nettement plus élevée et certaines liaisons peuvent échouer.]]> Profil de chat Ajouter un profil Données de profil local uniquement diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml index 216b666100..d8ce80884e 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml @@ -13,7 +13,7 @@ Címváltoztatás megszakítása? Megszakítás 30 másodperc - Egyszer használható hivatkozás + Egyszer használható meghívó-hivatkozás %1$s szeretne kapcsolatba lépni Önnel ezen keresztül: A SimpleX Chatről 1 nap @@ -102,7 +102,7 @@ Több akkumulátort használ! Az alkalmazás mindig fut a háttérben - az értesítések azonnal megjelennek.]]> Letiltás adminisztrátor - Fénykép előnézet visszavonása + Képelőnézet visszavonása A jelkód megadása után az összes adat törlésre kerül. Felkérték a videó fogadására Letiltás @@ -200,7 +200,7 @@ Csoport létrehozása véletlenszerű profillal. Az ismerős és az összes üzenet törlésre kerül - ez a művelet nem vonható vissza! Az ismerősei törlésre jelölhetnek üzeneteket; Ön majd meg tudja nézni azokat. - Kapcsolódás egyszer használható hivatkozással? + Kapcsolódás egyszer használható meghívó-hivatkozással? Kapcsolódás egy hivatkozáson vagy QR-kódon keresztül Kapcsolódási hiba (AUTH) Csak név @@ -234,9 +234,7 @@ Ismerősök Kapcsolódási hiba Az ismerős még nem kapcsolódott! - - kapcsolódás könyvtár szolgáltatáshoz (BÉTA)! -\n- kézbesítési jelentések (20 tagig). -\n- gyorsabb és stabilabb. + - kapcsolódás könyvtár szolgáltatáshoz (BÉTA)!\n- kézbesítési jelentések (20 tagig).\n- gyorsabb és stabilabb. Hozzájárulás kapcsolódás (bemutatkozó meghívó) SimpleX-cím létrehozása @@ -477,8 +475,7 @@ Hiba a hálózat konfigurációjának frissítésekor TCP életben tartása Kamera váltás - Üdvözlöm! -\nCsatlakozzon hozzám a SimpleX Chaten keresztül: %s + Üdvözlöm!\nCsatlakozzon hozzám a SimpleX Chaten keresztül: %s A megjelenített név nem tartalmazhat szóközöket. Csoport Üdvözlőüzenet megadása… (nem kötelező) @@ -692,7 +689,7 @@ Világos Az üzenet törlésre kerül - ez a művelet nem vonható vissza! Markdown súgó - Rejtett üzenet + új üzenet Régi adatbázis-archívum Speciális beállítások Nincs kézbesítési információ @@ -771,7 +768,7 @@ Nincsenek háttérhívások Üzenetek Társított hordozható eszköz - Lehetővé teszi, hogy egyetlen csevegőprofilon belül több anonim kapcsolat legyen, anélkül, hogy megosztott adatok lennének közöttük. + Lehetővé teszi, hogy egyetlen csevegőprofilon belül több névtelen kapcsolat legyen, anélkül, hogy megosztott adatok lennének közöttük. Az üzenet törlésre lesz jelölve. A címzett(ek) képes(ek) lesz(nek) felfedni ezt az üzenetet. Elhagyás Rendben @@ -885,7 +882,7 @@ Bárki üzemeltethet kiszolgálókat. Megnyitás Protokoll időtúllépése - titkos + titok Értesítés előnézete várakozás a visszaigazolásra… Fájl megállítása @@ -910,7 +907,7 @@ Csak Ön tud hívásokat indítani. Biztonságos sorbaállítás Értékelje az alkalmazást - Egyszer használható hivatkozás megosztása + Egyszer használható meghívó-hivatkozás megosztása Hiba az adatbázis visszaállításakor %s és %s Ön engedélyezi @@ -931,7 +928,7 @@ (beolvasás, vagy beillesztés a vágólapról) Várakozás a videóra Válasz - Ez az Ön egyszer használható hivatkozása! + Ez az Ön egyszer használható meghívó-hivatkozása! SimpleX Chat hívások Új inkognitóprofil használata Frissítse az alkalmazást, és lépjen kapcsolatba a fejlesztőkkel. @@ -963,7 +960,7 @@ Adatbázismentés visszaállítása Visszavonás Kérje meg az ismerősét, hogy engedélyezze a hangüzenetek küldését. - egyszer használható hivatkozást osztott meg + Ön egy egyszer használható meghívó-hivatkozást osztott meg A hivatkozás megnyitása a böngészőben gyengítheti az adatvédelmet és a biztonságot. A megbízhatatlan SimpleX-hivatkozások pirossal vannak kiemelve. Saját ICE-kiszolgálók Kapcsolat létrehozása @@ -1075,7 +1072,7 @@ tulajdonos Bekapcsolás %s, %s és %s kapcsolódott - Egyszer használható SimpleX-meghívó + Egyszer használható SimpleX-meghívó-hivatkozás Hívások nem sikerült elküldeni KEZELŐFELÜLET SZÍNEI @@ -1106,7 +1103,7 @@ Saját SMP-kiszolgálók A kézbesítési jelentések le vannak tiltva Adatbázismappa megnyitása - egyszer használható hivatkozáson keresztül + egyszer használható meghívó-hivatkozáson keresztül Csoportbeállítások megadása ezen keresztül: %1$s igen @@ -1119,7 +1116,7 @@ A kiszolgáló QR-kódjának beolvasása Megállítás Címmegosztás megállítása? - Csevegés profilok megnyitása + Csevegési profilok megváltoztatása Csatlakozáskérés megismétlése? Várakozás a képre Hangüzenetek @@ -1230,9 +1227,7 @@ Némítás megszüntetése SimpleX Chat megnyitása a hívás fogadásához Fájlfogadás megállítása? - - értesíti az ismerősöket a törlésről (nem kötelező) -\n- profil nevek szóközökkel -\n- és még sok más! + - értesíti az ismerősöket a törlésről (nem kötelező)\n- profil nevek szóközökkel\n- és még sok más! Lengyel kezelőfelület Kiszolgáló használata Fogadva ekkor: %s @@ -1272,14 +1267,12 @@ TCP kapcsolat időtúllépése A(z) %1$s nevű profiljának SimpleX-címe megosztásra fog kerülni. Ön már kapcsolódott a következőhöz: %1$s. - Jelenlegi csevegési adatbázis TÖRLÉSRE és FELCSERÉLÉSRE kerül az importált által! -\nEz a művelet nem vonható vissza - profiljai, ismerősei, csevegési üzenetei és fájljai véglegesen törölve lesznek. + Jelenlegi csevegési adatbázis TÖRLÉSRE és FELCSERÉLÉSRE kerül az importált által!\nEz a művelet nem vonható vissza - profiljai, ismerősei, csevegési üzenetei és fájljai véglegesen törölve lesznek. Ötletek és javaslatok Figyelmeztetés: néhány adat elveszhet! Koppintson ide az új csevegés indításához Várakozás a számítógépre… - A privát üzenetküldés -\nkövetkező generációja + A privát üzenetküldés\nkövetkező generációja Hálózati beállítások megváltoztatása? Várakozás a hordozható eszköz társítására: Biztonságos kapcsolat hitelesítése @@ -1288,16 +1281,14 @@ fájlok fogadása egyelőre még nem támogatott Csoportprofil mentése Visszaállítás alapértelmezettre - Hacsak az ismerőse nem törölte a kapcsolatot, vagy ez a hivatkozás már használatban volt egyszer, lehet hogy ez egy hiba – jelentse a problémát. -\nA kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapcsolattartási hivatkozást, és ellenőrizze, hogy a hálózati kapcsolat stabil-e. + Hacsak az ismerőse nem törölte a kapcsolatot, vagy ez a hivatkozás már használatban volt egyszer, lehet hogy ez egy hiba – jelentse a problémát.\nA kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapcsolattartási hivatkozást, és ellenőrizze, hogy a hálózati kapcsolat stabil-e. videóhívás (nem e2e titkosított) Alkalmazás új kapcsolatokhoz Az új üzenetek rendszeresen letöltésre kerülnek az alkalmazás által – naponta néhány százalékot használ az akkumulátorból. Az alkalmazás nem használ push-értesítéseket – az eszközről származó adatok nem kerülnek elküldésre a kiszolgálóknak. Számítógép címének beillesztése kapcsolattartási cím-hivatkozáson keresztül SimpleX-háttérszolgáltatást használja - az akkumulátornak csak néhány százalékát használja naponta.]]> - Az ismerősének online kell lennie ahhoz, hogy a kapcsolat létrejöjjön. -\nVisszavonhatja ezt az ismerőskérelmet és eltávolíthatja az ismerőst (ezt később ismét megpróbálhatja egy új hivatkozással). + Az ismerősének online kell lennie ahhoz, hogy a kapcsolat létrejöjjön.\nVisszavonhatja ezt az ismerőskérelmet és eltávolíthatja az ismerőst (ezt később ismét megpróbálhatja egy új hivatkozással). A jelszó nem található a Keystore-ban, ezért kézzel szükséges megadni. Ez akkor történhetett meg, ha visszaállította az alkalmazás adatait egy biztonságimentési eszközzel. Ha nem így történt, akkor lépjen kapcsolatba a fejlesztőkkel. Az ismerősei továbbra is kapcsolódva maradnak. A kiszolgálónak engedélyre van szüksége a várólisták létrehozásához, ellenőrizze jelszavát @@ -1319,8 +1310,7 @@ A kiszolgálónak engedélyre van szüksége a sorbaállítás létrehozásához, ellenőrizze jelszavát Kapcsolódni fog a csoport összes tagjához. Lehetséges, hogy a kiszolgáló címében szereplő tanúsítvány-ujjlenyomat helytelen - A biztonsága érdekében kapcsolja be a SimpleX-zár funkciót. -\nA funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beállítására az eszközén. + A biztonsága érdekében kapcsolja be a SimpleX-zár funkciót.\nA funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beállítására az eszközén. A videó akkor érkezik meg, amikor a küldője elérhető lesz, várjon, vagy ellenőrizze később! Ellenőrizze a hálózati kapcsolatát a következővel: %1$s, és próbálja újra. A SimpleX-zár az „Adatvédelem és biztonság” menüben kapcsolható be. @@ -1333,7 +1323,7 @@ A fájl fogadása le fog állni. Ne felejtse el, vagy tárolja biztonságosan – az elveszett jelszót nem lehet visszaállítani! A videó akkor érkezik meg, amikor a küldője befejezte annak feltöltését. - egyszer használható hivatkozást osztott meg inkognitóban + Ön egy egyszer használható meghívó-hivatkozást osztott meg inkognitóban Ön már kapcsolódott ahhoz a kiszolgálóhoz, amely az adott ismerősétől érkező üzenetek fogadására szolgál. Később engedélyezheti a „Beállításokban” Akkor lesz kapcsolódva a csoporthoz, amikor a csoport tulajdonosának eszköze online lesz, várjon, vagy ellenőrizze később! @@ -1352,8 +1342,7 @@ Az ismerősei és az üzenetek (kézbesítés után) nem kerülnek tárolásra a SimpleX-kiszolgálókon. Üzenetek formázása a szövegbe szúrt speciális karakterekkel: Megnyitás az alkalmazásban gombra.]]> - A csevegési profilja elküldésre kerül -\naz ismerőse számára + A csevegési profilja elküldésre kerül\naz ismerőse számára Egy olyan ismerősét próbálja meghívni, akivel inkognitó-profilt osztott meg abban a csoportban, amelyben a saját fő profilja van használatban %1$s nevű csoporthoz.]]> Amikor az alkalmazás fut @@ -1363,9 +1352,7 @@ A hangüzenetek küldése le van tiltva ebben a csoportban. Alkalmazás akkumulátor-használata / Korlátlan módot az alkalmazás beállításaiban.]]> Biztonságos kvantumrezisztens-protokollon keresztül. - - 5 perc hosszúságú hangüzenetek. -\n- egyéni üzenet-eltűnési időkorlát. -\n- előzmények szerkesztése. + - 5 perc hosszúságú hangüzenetek.\n- egyéni üzenet-eltűnési időkorlát.\n- előzmények szerkesztése. Társítás számítógéppel menüt a hordozható eszköz alkalmazásban és olvassa be a QR-kódot.]]> %s ekkor: %s Akkor lesz kapcsolódva, amikor az ismerősének eszköze online lesz, várjon, vagy ellenőrizze később! @@ -1399,7 +1386,7 @@ Alkalmazás akkumulátor-használata / Korlátlan módot az alkalmazás beállításaiban.]]> Ez a karakterlánc nem egy meghívó-hivatkozás! Új csevegés kezdése - A kapcsolódás már folyamatban van ezen az egyszer használható hivatkozáson keresztül! + A kapcsolódás már folyamatban van ezen az egyszer használható meghívó-hivatkozáson keresztül! Nem veszíti el az ismerőseit, ha később törli a címét. A beállítások frissítése a kiszolgálókhoz való újra kapcsolódással jár. kapcsolatba akar lépni Önnel! @@ -1444,8 +1431,7 @@ Tagok meghívásának kihagyása Ezek felülbírálhatók az ismerős- és csoportbeállításokban. Az ismerőse, akivel megosztotta ezt a hivatkozást, NEM fog tudni kapcsolódni! - A véletlenszerű jelmondat egyszerű szövegként van tárolva a beállításokban. -\nEz később megváltoztatható. + A véletlenszerű jelmondat egyszerű szövegként van tárolva a beállításokban.\nEz később megváltoztatható. Koppintson ide az inkognitóban való kapcsolódáshoz Jelmondat beállítása az exportáláshoz A kézbesítési jelentések le vannak tiltva %d csoportban @@ -1475,8 +1461,7 @@ Az Ön által elfogadott kérelem vissza lesz vonva! Élő üzenet küldése - a címzett(ek) számára frissül, ahogy beírja A KÉZBESÍTÉSI JELENTÉSEKET A KÖVETKEZŐ CÍMRE KELL KÜLDENI - 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ő. + 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ő. Az eszköz neve megosztásra kerül a társított hordozható eszközön használt alkalmazással. A címzettek a beírás közben látják a szövegváltozásokat. Tárolja el biztonságosan jelmondatát, mert ha elveszíti azt, NEM tudja megváltoztatni. @@ -1520,7 +1505,7 @@ Vagy mutassa meg ezt a kódot Kamera hozzáférés engedélyezése Fel nem használt meghívó megtartása? - Egyszer használható meghívó-hivatkozás megosztása + Ennek az egyszer használható meghívó-hivatkozásnak a megosztása Új csevegés Csevegések betöltése… Hivatkozás létrehozása… @@ -1541,10 +1526,7 @@ A kapcsolat megszakadt A kapcsolat megszakadt Számítógép kapcsolata rossz állapotban van - Jelentse a fejlesztőknek: -\n%s -\n -\nAz alkalmazás újraindítása javasolt. + Jelentse a fejlesztőknek:\n%s\n\nAz alkalmazás újraindítása javasolt. Jelentse a fejlesztőknek: \n%s %s hordozható eszköz által használt alkalmazás verziója nem támogatott. Győződjön meg arról, hogy mindkét eszközön ugyanazt a verziót használja]]> @@ -1587,7 +1569,7 @@ A keresősáv elfogadja a meghívó-hivatkozásokat. Titkosított fájlokkal és médiatartalmakkal. Csökkentett akkumulátor-használattal. - Magyar és török felhasználói felület + Magyar és török kezelőfelület A közelmúlt eseményei és továbbfejlesztett könyvtárbot. feloldotta %s letiltását Ön feloldotta %s letiltását @@ -1604,8 +1586,7 @@ Hiba a tag az összes csoporttag számára való letiltásakor Az üzenet túl nagy Az üdvözlőüzenet túl hosszú - Az adatbázis átköltöztetése folyamatban van. -\nEz eltarthat néhány percig. + Az adatbázis átköltöztetése folyamatban van.\nEz eltarthat néhány percig. Hanghívás A hívás befejeződött Videóhívás @@ -1742,13 +1723,11 @@ Profilkép alakzat Négyzet, kör vagy bármi a kettő között. Célkiszolgáló-hiba: %1$s - Továbbító kiszolgáló: %1$s -\nHiba: %2$s + Továbbító kiszolgáló: %1$s\nHiba: %2$s Hálózati problémák - az üzenet többszöri elküldési kísérlet után lejárt. A kiszolgáló verziója nem kompatibilis a hálózati beállításokkal. Hibás kulcs vagy ismeretlen kapcsolat - valószínűleg ez a kapcsolat törlődött. - Továbbító-kiszolgáló: %1$s -\nCélkiszolgáló hiba: %2$s + Továbbító-kiszolgáló: %1$s\nCélkiszolgáló hiba: %2$s Hiba: %1$s Kapacitás túllépés - a címzett nem kapta meg a korábban elküldött üzeneteket. Üzenetkézbesítési figyelmeztetés @@ -1779,8 +1758,7 @@ IP-cím védelem Az alkalmazás kérni fogja az ismeretlen fájlkiszolgálókról történő letöltések megerősítését (kivéve, ha az .onion vagy a SOCKS proxy engedélyezve van). Ismeretlen kiszolgálók! - Tor vagy VPN nélkül az IP-címe látható lesz az XFTP-közvetítő-kiszolgálók számára: -\n%1$s. + Tor vagy VPN nélkül az IP-címe látható lesz az XFTP-közvetítő-kiszolgálók számára:\n%1$s. Összes színmód Fekete Színmód @@ -1812,8 +1790,7 @@ További kiemelés 2 Alkalmazás téma Perzsa kezelőfelület - Védje IP-címét az ismerősei által kiválasztott üzenet-közvetítő-kiszolgálókkal szemben. -\nEngedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. + Védje IP-címét az ismerősei által kiválasztott üzenet-közvetítő-kiszolgálókkal szemben.\nEngedélyezze a „Beállítások / Hálózat és kiszolgálók” menüben. Ismeretlen kiszolgálókról származó fájlok megerősítése. Javított üzenetkézbesítés Alkalmazás témájának visszaállítása @@ -1822,14 +1799,12 @@ Privát üzenet-útválasztás 🚀 Fájlok biztonságos fogadása Csökkentett akkumulátor-használattal. - Hiba a WebView előkészítésekor. Frissítse rendszerét az új verzióra. Lépjen kapcsolatba a fejlesztőkkel. -\nHiba: %s + Hiba a WebView előkészítésekor. Frissítse rendszerét az új verzióra. Lépjen kapcsolatba a fejlesztőkkel.\nHiba: %s Felhasználó által létrehozott téma visszaállítása Üzenet-sorbaállítási információ nincs Kézbesítési hibák felderítése - a kiszolgáló sorbaállítási információi: %1$s -\nutoljára kézbesített üzenet: %2$s + a kiszolgáló sorbaállítási információi: %1$s\n\nutoljára kézbesített üzenet: %2$s Hibás kulcs vagy ismeretlen fájltöredék cím - valószínűleg a fájl törlődött. Ideiglenesfájl-hiba Üzenetállapot @@ -1841,8 +1816,7 @@ Fájlállapot: %s Másolási hiba Ezt a hivatkozást egy másik hordozható eszközön már használták, hozzon létre egy új hivatkozást a számítógépén. - Ellenőrizze, hogy a hordozható eszköz és a számítógép ugyanahhoz a helyi hálózathoz csatlakozik-e, valamint a számítógép tűzfalában engedélyezve van-e a kapcsolat. -\nMinden további problémát osszon meg a fejlesztőkkel. + Ellenőrizze, hogy a hordozható eszköz és a számítógép ugyanahhoz a helyi hálózathoz csatlakozik-e, valamint a számítógép tűzfalában engedélyezve van-e a kapcsolat.\nMinden további problémát osszon meg a fejlesztőkkel. Nem lehet üzenetet küldeni A kiválasztott csevegési beállítások tiltják ezt az üzenetet. Próbálja meg később. @@ -2072,8 +2046,7 @@ %1$d fájl törölve lett. %1$s üzenet továbbítása Üzenetek továbbítása… - %1$d fájlhiba: -\n%2$s + %1$d fájlhiba:\n%2$s %1$s üzenet nem lett továbbítva %1$s üzenet továbbítása? Üzenetek továbbítása fájlok nélkül? @@ -2081,8 +2054,7 @@ %1$s üzenet mentése Hiba az üzenetek továbbításakor Hang elnémítva - Hiba a WebView előkészítésekor. Győződjön meg arról, hogy a WebView telepítve van-e, és támogatja-e az arm64 architektúrát. -\nHiba: %s + Hiba a WebView előkészítésekor. Győződjön meg arról, hogy a WebView telepítve van-e, és támogatja-e az arm64 architektúrát.\nHiba: %s Sarok Üzenetbuborék alakja Farok @@ -2100,7 +2072,102 @@ Legfeljebb 200 üzenet egyszerre való törlése, vagy moderálása. Legfeljebb 20 üzenet egyszerre való továbbítása. Hang/Videó váltása hívás közben. - Csevegési profilváltás az egyszer használható meghívókhoz. + Csevegési profilváltás az egyszer használható meghívó-hivatkozásokhoz. Továbbfejlesztett biztonság ✅ - A SimpleX Chat biztonsága a Trail of Bits által lett újraauditálva. + A SimpleX Chat biztonsága a Trail of Bits által lett felülvizsgálva. + Hiba a kiszolgálók mentésekor + Nincsenek üzenet-kiszolgálók. + Nincsenek üzenetfogadó-kiszolgálók. + Nincsenek média- és fájlkiszolgálók. + A(z) %s nevű csevegési profilhoz: + Cím vagy egyszer használható meghívó-hivatkozás? + Új kiszolgáló + Címbeállítások + Előre beállított kiszolgálók + Üzemeltető + Feltételek megtekintése + Nincsenek kiszolgálók a privát üzenet-útválasztáshoz. + Nincsenek fájlküldő-kiszolgálók. + Nincsenek fájlfogadó-kiszolgálók. + Hibák a kiszolgálók konfigurációjában. + Hiba a feltételek elfogadásakor + Kézbesítetlen üzenetek + A kapcsolat elérte a kézbesítetlen üzenetek számának határát, az Ön ismerőse lehet, hogy offline állapotban van. + Nincs üzenet + Ez az üzenet törlésre került vagy még nem érkezett meg. + Koppintson a SimpleX-cím létrehozása menüpontra a későbbi létrehozáshoz. + Cím nyilvános megosztása + SimpleX-cím megosztása a közösségi médiában. + Egyszer használható meghívó-hivatkozás megosztása egy baráttal + egyetlen ismerőssel használható - személyesen vagy bármilyen üzenetküldőn keresztül megosztható.]]> + Beállíthatja az ismerős nevét, hogy emlékezzen arra, hogy kivel osztotta meg a hivatkozást. + Kapcsolatbiztonság + A SimpleX-cím és az egyszer használható meghívó-hivatkozás biztonságosan megosztható bármilyen üzenetküldőn keresztül. + A hivatkozás cseréje elleni védelem érdekében összehasonlíthatja a biztonsági kódokat az ismerősével. + A közösségi médiához + Vagy a privát megosztáshoz + SimpleX-cím vagy egyszer használható meghívó-hivatkozás? + Egyszer használható meghívó-hivatkozás létrehozása + Üzemeltetők kiválasztása + Hálózati üzemeltetők + Amikor egynél több hálózati üzemeltető van engedélyezve, akkor az alkalmazás minden egyes beszélgetéshez a különböző üzemeltetők kiszolgálóit használja. + Ha például a SimpleX Chat kiszolgálón keresztül fogadja az üzeneteket, az alkalmazás a Flux egyik kiszolgálóját használja a privát útválasztáshoz. + Válassza ki a használni kívánt hálózati üzemeltetőket. + Felülvizsgálat később + A kiszolgálókat a beállításokon keresztül konfigurálhatja. + A feltételek 30 nap elteltével lesznek elfogadva az engedélyezett üzemeltetők számára. + Az üzemeltetőket a „Hálózat és kiszolgálók” beállításaban konfigurálhatja. + Frissítés + Folytatás + Feltételek felülvizsgálata + Elfogadott feltételek + A feltételek automatikusan elfogadásra kerülnek az engedélyezett üzemeltetők számára: %s. + Az Ön kiszolgálói + %s.]]> + %s.]]> + %s kiszolgáló + Hálózati üzemeltető + Weboldal + Feltételek elfogadva ekkor: %s. + A feltételek ekkor lesznek elfogadva: %s. + Kiszolgálók használata + %s használata + A jelenlegi feltételek szövegét nem lehetett betölteni, a feltételeket ezen a hivatkozáson keresztül vizsgálhatja felül: + %s.]]> + %s.]]> + %s.]]> + %s.]]> + %s.]]> + %s.]]> + Feltételek elfogadása + Használati feltételek + %s kiszolgálóinak használatához fogadja el a használati feltételeket.]]> + Használat az üzenetekhez + A fogadáshoz + A privát útválasztáshoz + Hozzáadott üzenetkiszolgálók + Használat a fájlokhoz + A küldéshez + Hozzáadott média- és fájlkiszolgálók + Feltételek megnyitása + Változások megnyitása + Hiba a kiszolgáló frissítésekor + A kiszolgáló-protokoll megváltozott. + A kiszolgáló üzemeltetője megváltozott. + Kiszolgáló-üzemeltető + Kiszolgáló hozzáadva a következő üzemeltetőhöz: %s. + Hiba a kiszolgáló hozzáadásakor + Átlátszóság + Elhomályosítás + Hálózati decentralizáció + A második előre beállított üzemeltető az alkalmazásban! + Flux engedélyezése + Alkalmazás-eszköztárak + a metaadatok jobb védelme érdekében. + Javított csevegési navigáció + - Csevegés megnyitása az első olvasatlan üzenetnél.\n- Ugrás az idézett üzenetekre. + Frissített feltételek megtekintése + Az Ön jelenlegi csevegőprofiljához tartozó új fájlok kiszolgálói + Vagy archívumfájl importálása + Távoli hordozható eszközök \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml index caeec02deb..a081eb37bf 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml @@ -1277,4 +1277,21 @@ Hapus Dibuat pada: %s diblokir + Ganti + Buka blokir + Buka blokir anggota untuk semua? + Buka untuk semua + Diblokir oleh admin + ANGGOTA + Hapus anggota + Status pesan: %s + Status berkas: %s + Grup + dimatikan + Buka blokir anggota? + Buka blokir anggota + tidak aktif + Hapus anggota? + Hapus anggota + Pesan tersimpan \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml index 74c2397edd..0b827df4d7 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml @@ -904,8 +904,7 @@ Aggiornare la modalità di isolamento del trasporto\? Tutte le chat e i messaggi verranno eliminati. Non è reversibile! Profilo di chat - Verrà usata una connessione TCP separata (e le credenziali SOCKS) per ogni contatto e membro del gruppo . -\n Nota: : se hai molte connessioni, il consumo di batteria e traffico può essere notevolmente superiore e alcune connessioni potrebbero fallire. + per ogni contatto e membro del gruppo .\n Nota: : se hai molte connessioni, il consumo di batteria e traffico può essere notevolmente superiore e alcune connessioni potrebbero fallire.]]> Connessione Questa impostazione si applica ai messaggi del profilo di chat attuale Isolamento del trasporto @@ -1101,7 +1100,7 @@ Grazie agli utenti – contribuite via Weblate! Video e file fino a 1 GB Accetta automaticamente - Apri i profili di chat + Cambia i profili di chat Maggiori informazioni Per connettervi, il tuo contatto può scansionare il codice QR o usare il link nell\'app. Quando le persone chiedono di connettersi, puoi accettare o rifiutare. @@ -2119,4 +2118,99 @@ Cambia profilo di chat per inviti una tantum. Elimina o modera fino a 200 messaggi. Inoltra fino a 20 messaggi alla volta. + Nessun server dei messaggi. + Nessun server per ricevere messaggi. + Errori nella configurazione dei server. + Per il profilo di chat %s: + Messaggi non consegnati + Nessun messaggio + Sicurezza della connessione + L\'indirizzo SimpleX e i link una tantum sono sicuri da condividere tramite qualsiasi messenger. + Per proteggerti dalla sostituzione del tuo link, puoi confrontare i codici di sicurezza del contatto. + Puoi impostare il nome della connessione per ricordare con chi è stato condiviso il link. + Condividi link una tantum con un amico + Crea link una tantum + Per i social media + O per condividere in modo privato + Operatori di rete + Quando più di un operatore di rete è attivato, l\'app userà i server di diversi operatori per ogni conversazione. + Puoi configurare gli operatori nelle impostazioni di rete e server. + Scegli gli operatori + Seleziona gli operatori di rete da usare. + Continua + Aggiorna + Esamina più tardi + Server preimpostati + Condizioni accettate + Le condizioni verranno accettate automaticamente per gli operatori attivati il: %s. + I tuoi server + Esamina le condizioni + %s.]]> + %s.]]> + Il testo delle condizioni attuali testo non è stato caricato, puoi consultare le condizioni tramite questo link: + Operatore di rete + Server di %s + Usa %s + Sito web + %s.]]> + Condizioni accettate il: %s. + Operatore + %s.]]> + %s.]]> + %s.]]> + %s.]]> + Accetta le condizioni + Errore di aggiornamento del server + Per l\'instradamento privato + I server per nuovi file del tuo profilo di chat attuale + Per ricevere + Per inviare + Usa per i messaggi + Vedi le condizioni + %s.]]> + %s, accetta le condizioni d'uso.]]> + Condizioni d\'uso + Apri le modifiche + Apri le condizioni + Il protocollo del server è cambiato. + Errore di aggiunta del server + Server dell\'operatore + Server aggiunto all\'operatore %s. + L\'operatore del server è cambiato. + Barre degli strumenti + Trasparenza + Decentralizzazione della rete + Il secondo operatore preimpostato nell\'app! + Attiva Flux + Vedi le condizioni aggiornate + Sfocatura + Server dei messaggi aggiunti + Server di multimediali e file aggiunti + Indirizzo o link una tantum? + Impostazioni dell\'indirizzo + con un solo contatto - condividilo di persona o tramite un messenger.]]> + Le condizioni verranno accettate per gli operatori attivati dopo 30 giorni. + Le condizioni verranno accettate il: %s. + Errore di accettazione delle condizioni + Errore di salvataggio dei server + per una migliore privacy dei metadati. + Ad esempio, se ricevi messaggi tramite il server di SimpleX Chat, l\'app userà uno dei server Flux per l\'instradamento privato. + Navigazione della chat migliorata + Nuovo server + Usa per i file + Indirizzo SimpleX o link una tantum? + Questo messaggio è stato eliminato o non ancora ricevuto. + Tocca \"Crea indirizzo SimpleX\" nel menu per crearlo più tardi. + La connessione ha raggiunto il limite di messaggi non consegnati, il contatto potrebbe essere offline. + Usa i server + Puoi configurare i server nelle impostazioni. + Nessun server di multimediali e file. + Nessun server per l\'instradamento dei messaggi privati. + Nessun server per ricevere file. + Nessun server per inviare file. + - Apri la chat sul primo messaggio non letto.\n- Salta ai messaggi citati. + Condividi indirizzo pubblicamente + Condividi indirizzo SimpleX sui social media. + O importa file archivio + Telefoni remoti \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/iw/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/iw/strings.xml index c163458097..01c19a20f0 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/iw/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/iw/strings.xml @@ -60,8 +60,7 @@ קוד גישה לאפליקציה גרסת האפליקציה לכל פרופיל צ׳אט שיש ברשותך באפליקציה.]]> - חיבור TCP נפרד (ואישור SOCKS) ייווצר לכל איש קשר וחבר קבוצה. -\nשימו לב: אם ברשותכם חיבורים רבים, צריכת הסוללה ותעבורת האינטרנט עשויה להיות גבוהה משמעותית וחלק מהחיבורים עלולים להיכשל. + לכל איש קשר וחבר קבוצה. \nשימו לב: אם ברשותכם חיבורים רבים, צריכת הסוללה ותעבורת האינטרנט עשויה להיות גבוהה משמעותית וחלק מהחיבורים עלולים להיכשל.]]> הנמען התבקש לקבל את הסרטון הנמען התבקש לקבל את התמונה צרף diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml index edd22933ec..83b29a0b4c 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml @@ -41,8 +41,7 @@ アプリのバージョン アプリのバージョン: v%s アプリ内の各チャットプロフィールに、.連絡先毎にそれぞれのTCP接続(とSOCKS資格情報)が使われます。]]> - 各連絡先とグループに、それぞれのTCP接続(とSOCKS資格情報)が使われます。 -\n※注意※ 接続が多かったら、電池とデータの使用量が増えて、切断する可能性もあります。 + 各連絡先とグループに、それぞれのTCP接続(とSOCKS資格情報)が使われます。 \n※注意※ 接続が多かったら、電池とデータの使用量が増えて、切断する可能性もあります。]]> 太文字 音声通話 音声とビデオ通話 diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ko/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ko/strings.xml index 9d129847e4..8395f5ea48 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ko/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ko/strings.xml @@ -351,12 +351,7 @@ 설정에서 잠금 화면에서 바로 전화를 받을 수 있도록 설정할 수 있어요. 연결을 완료하려면 대화 상대가 온라인 상태여야 해요. \n연결 요청을 취소하고 대화 상대를 삭제할 수 있어요 (그리고 새 링크로 재시도). - 다음과 같은 경우에 발생할 수 있어요. -\n1. 대화 상대가 메시지를 보낸 지 30일 지나서 서버에서 삭제된 경우 -\n2. 메시지를 수신하는 데 사용된 서버가 업데이트되고 재부팅된 경우 -\n3. 침해된 연결의 경우 -\n서버 업데이트를 받으려면 설정에서 개발자에게 연락해 주세요. -\n저희 개발팀은 메시지 손실을 방지하기 위해 중복된 서버를 추가할 예정이에요. + 다음과 같은 경우에 발생할 수 있습니다. \n1. 대화 상대가 메시지를 보낸 지 30일 지나서 서버에서 삭제된 경우 \n2. 메시지를 수신하는 데 사용된 서버가 업데이트되고 재부팅된 경우 \n3. 침해된 연결의 경우 SimpleX 잠금 켜짐 응답됨… 확인 받음… @@ -1288,7 +1283,7 @@ 보낸 날짜 전송 디버그 파일 및 미디어는 이 그룹에서 금지됩니다. - 이 장치의 이름을 입력하십시오… + 이 기기의 이름을 입력하십시오… 데스크톱을 찾음 전달 서버: %1$s\n대상 서버 오류: %2$s 전달 서버: %1$s\n오류: %2$s @@ -1420,4 +1415,47 @@ 친구 초대 메시지 영구 삭제 초대 + 운영자 선택 + %s.]]> + 채팅 프로필 변경 + SOCKS 프록시가 지원하지 않는 경우 .onion 호스트를 No로 사용합니다.]]> + %s 이 찾을 수 없음]]> + 앱 설정에서 앱 배터리 사용량 / 제한 없음 을 선택하세요.]]> + %s 이 연결 끊김]]> + %1$s!]]> + 사용해서는 안 됩니다.]]> + 사용자 가이드에서 확인하세요.]]> + 모바일 앱에서 열기 버튼을 클릭합니다.]]> + 운영자 + %s.]]> + 약관 수락 날짜: %s. + %s.]]> + %s.]]> + %s.]]> + 추가된 미디어 및 파일 서버 + 앱 툴바 + 흐리기 + 약관 수락 + 약관을 수락함 + 추가된 메시지 서버 + 주소 또는 일회용 링크? + 주소 설정 + 한 명의 연락처에만 사용할 수 있으며 - 직접 또는 메신저를 통해 공유하십시오.]]> + %s.]]> + %s.]]> + %s 이 현재 사용 중]]> + %s.]]> + SimpleX 백그라운드 서비스를 제공합니다. - 이 기능은 하루에 몇 퍼센트의 배터리를 소모합니다.]]> + %s 의 서버를 사용하려면 사용 약관에 동의하십시오.]]> + %1$s 에 연결 중입니다.]]> + SimpleX의 백그라운드에서 실행되도록 허용하십시오. 그렇지 않으면 알림을 사용할 수 없습니다.]]> + 앱 설정에서 앱 배터리 사용량 / 제한 없음 을 선택하십시오.]]> + %s 이 현재 비활성화됨]]> + SimpleX Chat 개발자에게 연결하여 질문하고 업데이트를 받을 수 있습니다.]]> + %s 이 연결 끊김]]> + 에서 데스크톱에서 사용을 열고 QR 코드를 스캔합니다.]]> + %s]]> + %1$s 그룹에 가입하는 중 입니다.]]> + %s 버전이 지원되지 않습니다. 두 기기에서 동일한 버전을 사용하는지 확인하십시오.]]> + %1$s 그룹에 속해 있습니다.]]> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml index 2b71c3243b..3d3aac1957 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml @@ -47,8 +47,7 @@ Toestaan 1 dag Accepteer - Er wordt een afzonderlijke TCP-verbinding (en SOCKS-referentie) gebruikt voor elk contact en groepslid . -\nLet op: als u veel verbindingen heeft, kan uw batterij- en verkeersverbruik aanzienlijk hoger zijn en kunnen sommige verbindingen uitvallen. + voor elk contact en groepslid.\nLet op: als u veel verbindingen hebt, kan het batterij- en verkeersverbruik aanzienlijk hoger zijn en kunnen sommige verbindingen mislukken.]]> audio oproep Geluid aan Audio en video gesprekken @@ -1098,7 +1097,7 @@ Poolse interface Dank aan de gebruikers – draag bij via Weblate! Video\'s en bestanden tot 1 GB - Open chatprofielen + Chatprofielen wijzigen Over SimpleX adres Kom meer te weten Om verbinding te maken, kan uw contact de QR-code scannen of de link in de app gebruiken. @@ -2116,4 +2115,98 @@ Maximaal 200 berichten verwijderen of modereren. Stuur maximaal 20 berichten tegelijk door. Betere gesprekken + Accepteer voorwaarden + Berichtservers toegevoegd + Media- en bestandsservers toegevoegd + Geaccepteerde voorwaarden + Adres of eenmalige link? + %s.]]> + Fout bij het opslaan van servers + Geen berichtenservers. + Geen media- en bestandsservers. + Geen servers om bestanden te ontvangen. + Geen servers om berichten te ontvangen. + Fouten in de serverconfiguratie. + Voor chatprofiel %s: + Niet afgeleverde berichten + De verbinding heeft de limiet van niet-afgeleverde berichten bereikt. Uw contactpersoon is mogelijk offline. + Dit bericht is verwijderd of nog niet ontvangen. + Tik op SimpleX-adres maken in het menu om het later te maken. + Adres openbaar delen + Deel eenmalig een link met een vriend + Deel het SimpleX-adres op sociale media. + slechts met één contactpersoon worden gebruikt - deel persoonlijk of via een messenger.]]> + Beveiliging van de verbinding + U kunt een verbindingsnaam instellen, zodat u kunt onthouden met wie de link is gedeeld. + SimpleX-adressen en eenmalige links kunnen veilig worden gedeeld via elke messenger. + Nieuwe server + Voor social media + Of om privé te delen + Adres instellingen + Eenmalige link maken + Operators kiezen + Voor ingeschakelde operators worden de voorwaarden na 30 dagen geaccepteerd. + Netwerkbeheerders + Later beoordelen + Selecteer welke netwerkoperators u wilt gebruiken. + Update + Wanneer er meer dan één netwerkoperator is ingeschakeld, gebruikt de app voor elk gesprek de servers van verschillende operators. + U kunt operators configureren in Netwerk- en serverinstellingen. + Doorgaan + Voorwaarden bekijken + Uw servers + %s.]]> + %s.]]> + %s.]]> + Voorwaarden geaccepteerd op: %s. + Voorwaarden worden geaccepteerd op: %s. + De tekst van de huidige voorwaarden kon niet worden geladen. U kunt de voorwaarden bekijken via deze link: + Netwerkbeheerder + Operator + %s servers + Gebruik %s + Gebruik servers + Website + %s.]]> + %s.]]> + %s te gebruiken, moet u de gebruiksvoorwaarden accepteren.]]> + Gebruiksvoorwaarden + Voor privé-routering + Wijzigingen openen + De servers voor nieuwe bestanden van uw huidige chatprofiel + Om te ontvangen + Gebruik voor bestanden + Gebruik voor berichten + Bekijk voorwaarden + Serverprotocol gewijzigd. + Operatorserver + Server toegevoegd aan operator %s. + Transparantie + voor betere privacy van metagegevens. + Verbeterde chatnavigatie + Netwerk decentralisatie + De tweede vooraf ingestelde operator in de app! + Als u bijvoorbeeld berichten ontvangt via de SimpleX Chat-server, gebruikt de app een van de Flux-servers voor privéroutering. + Flux inschakelen + Geen bericht + App-werkbalken + Vervagen + %s.]]> + %s.]]> + Voorwaarden worden automatisch geaccepteerd voor ingeschakelde operators op: %s. + Fout bij het updaten van de server + Fout bij het accepteren van voorwaarden + Fout bij toevoegen server + Geen servers voor het routeren van privéberichten. + Serveroperator gewijzigd. + Geen servers om bestanden te verzenden. + Open voorwaarden + - Open chat op het eerste ongelezen bericht.\n- Ga naar geciteerde berichten. + Vooraf ingestelde servers + Bekijk de bijgewerkte voorwaarden + SimpleX adres of eenmalige link? + Om te voorkomen dat uw link wordt vervangen, kunt u contactbeveiligingscodes vergelijken. + Om te verzenden + U kunt servers configureren via instellingen. + Of importeer archiefbestand \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml index c679651e7d..10f4f79d47 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml @@ -335,8 +335,7 @@ Wersja aplikacji Wersja aplikacji: v%s dla każdego profilu czatu, który masz w aplikacji.]]> - Oddzielne połączenie TCP (i poświadczenia SOCKS) będą używane dla każdego kontaktu i członka grupy. -\nUwaga: jeśli masz wiele połączeń, zużycie baterii i ruchu może być znacznie wyższe, a niektóre połączenia mogą się nie udać. + dla każdego kontaktu i członka grupy. \nUwaga: jeśli masz wiele połączeń, zużycie baterii i ruchu może być znacznie wyższe, a niektóre połączenia mogą się nie udać.]]> Profil czatu Połączenie Wersja rdzenia: v%s diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/pt/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/pt/strings.xml index 18c20eba50..02e5547e04 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/pt/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/pt/strings.xml @@ -194,8 +194,7 @@ Preservar o último rascunho da mensagem, com anexos. Solicitada a recepção do vídeo para cada perfil de conversa que você tiver na aplicação .]]> - Uma conexão TCP separada (e credencial SOCKS) será usada para cada contato e membro do grupo. -\n Por favor note: se você tiver muitas conexões, o seu consumo de bateria e consumo de tráfego pode ser substancialmente maior e algumas conexões podem falhar. + para cada contato e membro do grupo. \n Por favor note: se você tiver muitas conexões, o seu consumo de bateria e consumo de tráfego pode ser substancialmente maior e algumas conexões podem falhar.]]> Solicitada a recepção da imagem Autenticação indisponível negrito diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml index 7f8e0e2743..c02e17f568 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml @@ -549,8 +549,7 @@ Versiunea aplicației: %s pentru fiecare profil de conversație pe care le aveți în aplicație]]> Permiteți downgrade-ul - O conexiune separată TCP (și SOCKS credential) va fi folosită pentru fiecare contact și membru de grup -\nVa rugăm considerați că: dacă aveți prea multe conexiuni, consumul dumneavoastră de baterie și trafic de internet pot fi considerabil mai mari, iar unele conexiuni pot eșua. + pentru fiecare contact și membru de grup \nVa rugăm considerați că: dacă aveți prea multe conexiuni, consumul dumneavoastră de baterie și trafic de internet pot fi considerabil mai mari, iar unele conexiuni pot eșua.]]> Conexiune terminată Se conectează la desktop Rugat să primească imaginea diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml index 3d1e92f83f..59b355fd2b 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml @@ -987,8 +987,7 @@ Все чаты и сообщения будут удалены - это нельзя отменить! Сборка приложения: %s Версия приложения: v%s - Отдельное TCP-соединение (и авторизация SOCKS) будет использоваться для каждого контакта и члена группы. -\nОбратите внимание: если у Вас много контактов, потребление батареи и трафика может быть значительно выше, и некоторые соединения могут не работать. + для каждого контакта и члена группы. \nОбратите внимание: если у Вас много контактов, потребление батареи и трафика может быть значительно выше, и некоторые соединения могут не работать.]]> для каждого профиля чата, который Вы имеете в приложении.]]> Версия ядра: v%s Удалить профиль чата\? diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml index 48e26132c8..8df7457e0f 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml @@ -71,8 +71,7 @@ İzin ver Uygulama erişim kodu Uygulamadaki her konuşma profliniz için ayrı bir TCP bağlantısı (ve SOCKS kimliği) kullanılacaktır.]]> - Konuştuğun kişilerin ve grup üyelerinin tamamı için ayrı bir TCP bağlantısı (ve SOCKS kimliği) kullanılacaktır. -\nBilgin olsun: Çok sayıda bağlantın varsa pilin ve veri kullanımın önemli ölçüde artabilir ve bazı bağlantılar başarısız olabilir. + Konuştuğun kişilerin ve grup üyelerinin tamamı için ayrı bir TCP bağlantısı (ve SOCKS kimliği) kullanılacaktır.\nBilgin olsun: Çok sayıda bağlantın varsa pilin ve veri kullanımın önemli ölçüde artabilir ve bazı bağlantılar başarısız olabilir.]]> Kaydet ve grup üyelerini bilgilendir Uygulama açıkken çalışır Ara @@ -2115,4 +2114,15 @@ 200\'e kadar mesajı silin veya düzenleyin. Daha iyi güvenlik ✅ SimpleX protokolleri Trail of Bits tarafından incelenmiştir. + Adres mi yoksa tek seferlik bağlantı mı? + Mesaj sunucuları eklendi + Koşulları kabul edin + Kabul edilen koşullar + Medya ve dosya sunucuları eklendi + Uygulama araç çubukları + Adres ayarları + Bulanıklık + sadece bir kişiyle kullanılabilir - yüz yüze veya herhangi bir mesajlaşma programı aracılığıyla paylaşın]]> + %s.]]> + %s.]]> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml index a37d13bd51..fcae74db1b 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml @@ -75,8 +75,7 @@ для кожного профілю чату, який у вас є в додатку.]]> Вигляд Версія додатку: v%s - Окреме TCP-підключення (і обліковий запис SOCKS) буде використовуватися для кожного контакту та учасника групи. -\nЗверніть увагу: якщо у вас багато підключень, споживання заряду батареї та трафіку може значно збільшитися, і деякі підключення можуть бути невдалими. + для кожного контакту та члена групи.\nЗверніть увагу: якщо у вас багато з’єднань, заряд акумулятора та споживання трафіку можуть бути значно вищими, а деякі з’єднання можуть бути невдалими.]]> Активована оптимізація батареї, вимикається фоновий сервіс і періодичні запити нових повідомлень. Ви можете знову увімкнути їх у налаштуваннях. Назад жирний @@ -104,7 +103,7 @@ поганий хеш повідомлення поганий ідентифікатор повідомлення Фон - Це можна вимкнути у налаштуваннях – сповіщення все одно будуть відображатися, коли програма працює. + Це можна вимкнути в налаштуваннях – сповіщення все одно відображатимуться під час роботи програми.]]> Служба фонового режиму завжди активна – сповіщення відображатимуться, як тільки повідомлення будуть доступні. Запит на отримання зображення Прикріпити @@ -244,7 +243,7 @@ Використовувати для нових підключень Видалити сервер Оцінити на GitHub - Як використовувати ваші сервери + Як використовувати власні сервери Збережені сервери WebRTC ICE будуть видалені. Налаштувати сервери ICE Мережа та сервери @@ -621,14 +620,14 @@ Цей рядок не є з\'єднувальним посиланням! Код безпеки Позначити, що перевірено - Ваші профілі чату + Ваші профілі Пароль бази даних та експорт Markdown у повідомленнях Надсилайте питання та ідеї - Ввести адресу сервера вручну + Ввести сервер вручну Попередньо встановлений сервер Адреса вашого сервера - Сервери для нових підключень вашого поточного профілю чату + Сервери для нових підключень до вашого поточного профілю Використовувати сервери SimpleX Chat? Сервери ICE (один на рядок) Помилка збереження серверів ICE @@ -832,7 +831,7 @@ Відео Прийняте вами з\'єднання буде скасоване! Контакт ще не підключений! - Тестові сервери + Тестування серверів Зберегти сервери Ваш сервер Тест сервера не вдався! @@ -994,7 +993,7 @@ Помилка встановлення адреси Підключити Будь ласка, запам\'ятайте або збережіть його надійно - немає можливості відновлення втраченого пароля! - Відкрити профілі чату + Змінити профілі чату Очікування на відео Очікування на файл Голосове повідомлення (%1$s) @@ -1134,7 +1133,7 @@ Файл не знайдено Підключитися через посилання Підключитися - Щоб показати ваш схований профіль, введіть повний пароль у поле пошуку на сторінці Ваші профілі чату. + Щоб показати ваш схований профіль, введіть повний пароль у поле пошуку на сторінці Ваші профілі. Підтвердити пароль %dd Захистіть свої чат-профілі паролем! @@ -1424,7 +1423,7 @@ %d подій в групі Невірне ім\'я! Перемикайте інкогніто під час підключення. - Це ваше посилання для групи %1$s! + %1$s!]]> Розблокувати Неправильний шлях до файлу - підключайтесь до служби каталогів (BETA)! @@ -1470,7 +1469,7 @@ Пристрої робочого столу Не сумісно! Зв\'язати з мобільним - Використовувати зі стаціонарного комп\'ютера + Використовувати з комп\'ютера Підключений мобільний Код сеансу Підключення завершено @@ -1487,7 +1486,7 @@ Некоректна адреса робочого столу Вставити адресу робочого столу Перевірити код з робочим столом - Сканувати QR-код з робочого столу + Сканувати QR-код з комп\'ютера Пристрої Виявлено через локальну мережу - за бажанням повідомляйте про видалених контактів. @@ -1912,7 +1911,7 @@ Будь ласка, попросіть вашого контакту увімкнути дзвінки. Зберегти і перепідключитися Видалити до 20 повідомлень за один раз. - Доступна панель інструментів чату + Доступна панель чату Користуватися застосунком однією рукою. З\'єднуйтеся з друзями швидше. Керуйте своєю мережею @@ -2014,7 +2013,7 @@ Завантажити %s (%s) Відкрити розташування файлу Пропустити цю версію - Доступна панель інструментів чату + Доступна панель чату Не можна зателефонувати контакту Підключення до контакту, будь ласка, зачекайте або перевірте пізніше! Дзвінки заборонені! @@ -2097,7 +2096,7 @@ Звук вимкнено Помилка ініціалізації WebView. Переконайтеся, що WebView встановлено, і його підтримувана архітектура — arm64. \nПомилка: %s Хвіст - Куточок + Кут Форма повідомлення Сесія додатку Нові облікові дані SOCKS будуть використовуватись щоразу, коли ви запускаєте додаток. @@ -2116,4 +2115,99 @@ Видалити або модерувати до 200 повідомлень. Переслати до 20 повідомлень одночасно. Переключити профіль чату для одноразових запрошень. + Помилка прийняття умов + Сервери збереження помилок + Для профілю чату %s: + Ніяких медіа та файлових серверів. + Немає серверів повідомлень. + Немає серверів для маршрутизації приватних повідомлень. + Недоставлені повідомлення + Немає повідомлення + Це повідомлення було видалено або ще не отримано. + Поділіться одноразовим посиланням з другом + Поділіться адресою публічно + Ви можете задати ім\'я з\'єднання, щоб запам\'ятати, з ким ви поділилися посиланням. + Безпека з\'єднання + Щоб захиститися від заміни вашого посилання, ви можете порівняти коди безпеки контактів. + Для соціальних мереж + Або поділитися приватно + Обирайте операторів + Мережеві оператори + Умови будуть прийняті для ввімкнених операторів через 30 днів. + Наприклад, якщо ви отримуєте повідомлення через сервер SimpleX Chat, програма використовуватиме один із серверів Flux для приватної маршрутизації. + Виберіть мережевих операторів для використання. + Ви можете налаштувати сервери за допомогою налаштувань. + Перегляньте пізніше + Оновлення + Прийняті умови + Умови будуть автоматично прийняті для увімкнених операторів: %s. + Оператор мережі + %s серверів + Вебсайт + Ваші сервери + Використовуйте %s + Використовуйте сервери + %s.]]> + %s.]]> + Прийняти умови + Умови перегляду + Додано сервери повідомлень + Для приватної маршрутизації + Щоб отримати + Використовуйте для файлів + Додано медіа та файлові сервери + Відкриті зміни + Відкриті умови + Сервери для нових файлів вашого поточного профілю чату + Щоб відправити + Помилка додавання сервера + Сервер оператора + Сервер додано до оператора %s. + Оператор сервера змінився. + Панелі інструментів додатків + Розмиття + Прозорість + Покращена навігація в чаті + - Відкрити чат на першому непрочитаному повідомленні.\n- Перейти до цитованих повідомлень. + Другий попередньо встановлений оператор у застосунку! + Переглянути оновлені умови + Налаштування адреси + %s.]]> + Адреса або одноразове посилання? + Умови використання + лише з одним контактом – поділіться особисто чи через будь-який месенджер.]]> + %s.]]> + %s.]]> + %s.]]> + %s.]]> + %s.]]> + %s, прийміть умови використання.]]> + Текст поточних умов не вдалося завантажити, ви можете переглянути умови за цим посиланням: + Помилки в конфігурації серверів. + Умови приймаються з: %s. + Увімкнути flux + Умови приймаються до: %s. + Продовжити + Створити одноразове посилання + Помилка оновлення сервера + для кращої конфіденційності метаданих. + Децентралізація мережі + Оператор + Немає серверів для отримання повідомлень. + SimpleX адреса або одноразове посилання? + Новий сервер + Немає серверів для отримання файлів. + Умови перегляду + Немає серверів для надсилання файлів. + Попередньо встановлені сервери + Протокол сервера змінено. + Поділіться адресою SimpleX у соціальних мережах. + SimpleX-адреси та одноразові посилання можна безпечно ділитися через будь-який месенджер. + З\'єднання досягло ліміту недоставлених повідомлень, ваш контакт може бути офлайн. + Натисніть Створити адресу SimpleX у меню, щоб створити її пізніше. + Якщо увімкнено більше одного оператора, програма використовуватиме сервери різних операторів для кожної розмови. + Використовуйте для повідомлень + Ви можете налаштувати операторів у налаштуваннях Мережі та серверів. + Або імпортуйте архівний файл + Віддалені мобільні \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml index cefaa982a5..f02f7abc15 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml @@ -115,8 +115,7 @@ Tắt âm Yêu cầu để nhận hình ảnh cho mỗi hồ sơ trò chuyện bạn có trong ứng dụng.]]> - Một kết nối TCP riêng biệt (và thông tin xác thực SOCKS) sẽ được sử dụng cho từng liên hệ và thành viên nhóm. -\nXin lưu ý: nếu bạn có nhiều kết nối, mức tiêu thụ pin và lưu lượng truy cập của bạn có thể cao hơn đáng kể và một số kết nối có thể không thành công. + cho từng liên hệ và thành viên nhóm. \nXin lưu ý: nếu bạn có nhiều kết nối, mức tiêu thụ pin và lưu lượng truy cập của bạn có thể cao hơn đáng kể và một số kết nối có thể không thành công.]]> cuộc gọi thoại Các cuộc gọi thoại và video Các cuộc gọi thoại và video @@ -1351,4 +1350,53 @@ Cấm gửi tin nhắn tự xóa. Cấm gửi đường dẫn SimpleX Được proxy + Địa chỉ hay đường dẫn dùng một lần? + với chỉ một liên hệ - chia sẻ trực tiếp hoặc thông qua bất kỳ ứng dụng tin nhắn nào.]]> + Cài đặt máy chủ .onion thành Không nếu proxy SOCKS không hỗ trợ chúng.]]> + %s.]]> + %s.]]> + %s.]]> + Đã thêm các máy chủ truyền tin nhắn + Thanh công cụ ứng dụng + Làm mờ + Chấp nhận điều kiện + Đã thêm các máy chủ truyền tệp & phương tiện + Đã chấp nhận điều kiện + Cài đặt địa chỉ + %s.]]> + %s.]]> + Kho lưu trữ GitHub của chúng tôi.]]> + %s.]]> + %s.]]> + %s.]]> + Hướng dẫn người dùng.]]> + %1$s rồi.]]> + Mức sử dụng pin ứng dụng / Không hạn chế trong phần cài đặt ứng dụng.]]> + %s]]> + cho phép SimpleX chạy trong nền trong hộp thoại tiếp theo. Nếu không, thông báo sẽ bị vô hiệu hóa.]]> + %s, vui lòng chấp nhận điều kiện sử dụng.]]> + Dịch vụ nền SimpleX – nó tiêu tốn một vài phần trăm pin mỗi ngày.]]> + Mức sử dụng pin ứng dụng / Không hạn chế trong phần cài đặt ứng dụng.]]> + %1$s rồi.]]> + %1$s!]]> + %1$s rồi.]]> + Mở trong ứng dụng di động.]]> + Chọn nhà cung cấp + kết nối với các nhà phát triển SimpleX Chat để hỏi bất kỳ câu hỏi nào và nhận thông tin cập nhật.]]> + không được sử dụng cùng một cơ sở dữ liệu trên hai thiết .]]> + Lỗi chấp nhận điều kiện + Lỗi lưu máy chủ + Lỗi trong cấu hình máy chủ. + Bảo mật kết nối + Tạo đường dẫn dùng một lần + Các điều kiện sẽ được chấp nhận với các nhà cung cấp được cho phép sau 30 ngày. + Tiếp tục + Không thể tải văn bản về các điều kiện hiện tại, bạn có thể xem xét các điều kiện thông qua đường dẫn này: + Các điều kiện sử dụng + Cho phép flux + Các điều kiện sẽ được chấp nhận vào: %s. + Lỗi thêm máy chủ + Lỗi cập nhật máy chủ + Các điều kiện đã được chấp nhận vào: %s. + Các điều kiện sẽ được tự động chấp nhận với các nhà cung cấp được cho phép vào: %s. \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml index 4c1c60e247..4c0b66d216 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml @@ -113,8 +113,7 @@ 开启音频 已要求接收图片 ,用于您在应用程序中的每个聊天资料 。]]> - 每个联系人和群组成员 将使用单独的 TCP 连接(和 SOCKS 凭证)。 -\n请注意:如果您有很多连接,您的电池和流量消耗可能会大大增加,并且某些连接可能会失败。 + 每个联系人和群成员。\n请注意:如果您有很多连接,您的电池和流量消耗可能会大大增加,并且某些连接可能会失败。]]> 返回 最长续航 。您只会在应用程序运行时收到通知(无后台服务)。]]> 较长续航 。应用每 10 分钟检查一次消息。您可能会错过来电或者紧急信息。]]> @@ -1172,7 +1171,7 @@ 收到的信息 只有您的联系人可以添加消息回应。 打开数据库中…… - 打开聊天资料 + 更改聊天资料 您的联系人可以扫描二维码或使用应用程序中的链接来建立连接。 您可以将您的地址作为链接或二维码共享——任何人都可以连接到您。 如果您不能亲自见面,可以在视频通话中展示二维码,或分享链接。 @@ -2117,4 +2116,99 @@ 对一次性邀请切换聊天配置文件。 更佳的通话 允许自行删除或管理员移除最多200条消息。 + 保存服务器出错 + 服务器配置有错误。 + 用于聊天资料 %s: + 无消息服务器 + 无私密消息路由服务器。 + 无消息接收服务器。 + 无文件发送服务器。 + 未送达的消息 + 无消息 + 连接安全性 + 和一位好友分享一次性链接 + 公开分享地址 + 在社媒上分享 SimpleX 地址。 + 你可以设置连接名称,用来记住和谁分享了这个链接。 + 可以通过任何消息应用安全分享 SimpleX 地址和一次性链接。 + 创建一次性链接 + 用于社交媒体 + 或者私下分享 + 选择运营者 + 网络运营者 + 30 天后将接受已启用的运营者的条款。 + 继续 + 稍后审阅 + 选择要使用的网络运营者。 + 更新 + 你可以通过设置配置服务器。 + %s.]]> + 将于下列日期自动接受已启用的运营者的条款:%s。 + 预设服务器 + 你的服务器 + 接受条款的将来日期为:%s。 + 网络运营者 + 运营者 + %s 台服务器 + 网站 + 无法加载当前条款文本,你可以通过此链接审阅条款: + 使用 %s + 使用服务器 + %s.]]> + %s.]]> + 查看条款 + 使用条款 + 用于私密路由 + 消息接收 + 发送 + 用于文件 + 用于消息 + 打开更改 + 打开条款 + 运营者服务器 + 已添加服务器到运营者 %s + 服务器运营者已更改。 + 服务器协议已更改。 + 透明度 + 网络去中心化 + 应用中的第二个预设运营者! + 改进了聊天导航 + 查看更新后的条款 + 比如,如果你通过 SimpleX 服务器收到消息,应用会使用 Flux 服务器中的一台进行私密路由。 + 启用了多于一个网络运营者时,应用会为每个对话使用不同运营者的服务器。 + 接受条款 + 模糊 + 地址或一次性链接? + 已添加消息服务器 + 已添加媒体和文件服务器 + 地址设置 + 已接受条款 + 应用工具栏 + 仅用于一名联系人 - 面对面或通过任何消息应用分享.]]> + %s.]]> + %s.]]> + %s.]]> + %s.]]> + %s的服务器,请接受使用条款。]]> + %s.]]> + 开启 flux + 接受条款出错 + 为了更好的元数据隐私。 + 添加服务器出错 + 无媒体和文件服务器。 + 更新服务器出错 + 新服务器 + 无文件接收服务器。 + - 在第一条未读消息上打开聊天.\n- 跳转到引用的消息. + 审阅条款 + SimpleX 地址或一次性链接? + 要稍后创建 SimpleX 地址,请在菜单中轻按“创建 SimpleX 地址” + 当前聊天资料的新文件服务器 + 此消息被删除或尚未收到。 + 连接达到了未送达消息上限,你的联系人可能处于离线状态。 + 为了防止链接被替换,你可以比较联系人安全代码。 + 你可以在“网络和服务器”设置中配置运营者。 + 接受运营者条款的日期:%s + 远程移动设备 + 或者导入压缩文件 \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml index 65c16db612..22d226829e 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml @@ -143,8 +143,7 @@ 取消檔案預覽 無法接收檔案 重複的顯示名稱! - 一個單獨的 TCP 連接(和 SOCKS 憑證)將用於每個聯絡人和群組內的成員。 -\n請注意:如果你有很多連接,你的電話電量和數據流量的消耗率會大大增加,一些連接有機會會連接失敗。 + 每個聯絡人和群組內的成員。 \n請注意:如果你有很多連接,你的電話電量和數據流量的消耗率會大大增加,一些連接有機會會連接失敗。]]> 每個聊天室的設定。]]> 返回 省電模式運行中,關閉了背景通知服務和定期更新接收訊息。你可以在通知設定內重新啟用。 From a1e25620f7091433ce3888aa660a95eb6ade31ae Mon Sep 17 00:00:00 2001 From: Evgeny Date: Tue, 3 Dec 2024 15:23:23 +0000 Subject: [PATCH 114/167] website: translations (#5306) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Translated using Weblate (German) Currently translated at 100.0% (257 of 257 strings) Translation: SimpleX Chat/SimpleX Chat website Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/de/ * Translated using Weblate (Dutch) Currently translated at 100.0% (257 of 257 strings) Translation: SimpleX Chat/SimpleX Chat website Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/nl/ * Translated using Weblate (Arabic) Currently translated at 100.0% (257 of 257 strings) Translation: SimpleX Chat/SimpleX Chat website Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/ar/ * Translated using Weblate (Italian) Currently translated at 100.0% (257 of 257 strings) Translation: SimpleX Chat/SimpleX Chat website Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/it/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (257 of 257 strings) Translation: SimpleX Chat/SimpleX Chat website Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/uk/ * Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (257 of 257 strings) Translation: SimpleX Chat/SimpleX Chat website Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/zh_Hans/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (257 of 257 strings) Translation: SimpleX Chat/SimpleX Chat website Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/hu/ * Translated using Weblate (German) Currently translated at 100.0% (257 of 257 strings) Translation: SimpleX Chat/SimpleX Chat website Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/de/ * Translated using Weblate (Dutch) Currently translated at 100.0% (257 of 257 strings) Translation: SimpleX Chat/SimpleX Chat website Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/nl/ * Translated using Weblate (Arabic) Currently translated at 100.0% (257 of 257 strings) Translation: SimpleX Chat/SimpleX Chat website Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/ar/ * Translated using Weblate (Italian) Currently translated at 100.0% (257 of 257 strings) Translation: SimpleX Chat/SimpleX Chat website Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/it/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (257 of 257 strings) Translation: SimpleX Chat/SimpleX Chat website Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/uk/ * Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (257 of 257 strings) Translation: SimpleX Chat/SimpleX Chat website Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/zh_Hans/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (257 of 257 strings) Translation: SimpleX Chat/SimpleX Chat website Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/hu/ --------- Co-authored-by: mlanp Co-authored-by: M1K4 Co-authored-by: jonnysemon Co-authored-by: Random Co-authored-by: Max Co-authored-by: 大王叫我来巡山 Co-authored-by: summoner001 --- website/langs/ar.json | 7 ++++--- website/langs/de.json | 3 ++- website/langs/hu.json | 7 ++++--- website/langs/it.json | 7 ++++--- website/langs/nl.json | 3 ++- website/langs/uk.json | 3 ++- website/langs/zh_Hans.json | 3 ++- 7 files changed, 20 insertions(+), 13 deletions(-) diff --git a/website/langs/ar.json b/website/langs/ar.json index 927dcb0c49..9274e71850 100644 --- a/website/langs/ar.json +++ b/website/langs/ar.json @@ -244,15 +244,16 @@ "f-droid-page-simplex-chat-repo-section-text": "لإضافته إلى عميل F-Droid، امسح رمز QR أو استخدم عنوان URL هذا:", "f-droid-page-f-droid-org-repo-section-text": "مستودعات SimpleX Chat و F-Droid.org مبنية على مفاتيح مختلفة. للتبديل، يُرجى تصدير قاعدة بيانات الدردشة وإعادة تثبيت التطبيق.", "comparison-section-list-point-4a": "مُرحلات SimpleX لا يمكنها أن تتنازل عن تعمية بين الطرفين. تحقق من رمز الأمان للتخفيف من الهجوم على القناة خارج النطاق", - "hero-overlay-3-title": "التقييم الأمني", + "hero-overlay-3-title": "التقييمات الأمنية", "hero-overlay-card-3-p-2": "قامت Trail of Bits بمراجعة مكونات التشفير والشبكات الخاصة بمنصة SimpleX في نوفمبر 2022. اقرأ المزيد في الإعلان.", "jobs": "انضم للفريق", - "hero-overlay-3-textlink": "التقييم الأمني", + "hero-overlay-3-textlink": "التقييمات الأمنية", "hero-overlay-card-3-p-1": "Trail of Bits هي شركة رائدة في مجال الاستشارات الأمنية والتكنولوجية، ومن بين عملائها شركات التكنولوجيا الكبرى والوكالات الحكومية ومشاريع blockchain الكبرى.", "docs-dropdown-9": "التنزيلات", "please-enable-javascript": "الرجاء تفعيل جافا سكريبت (JavaScript) لرؤية رمز QR.", "please-use-link-in-mobile-app": "يُرجى استخدام الرابط في تطبيق الجوال", "docs-dropdown-10": "الشفافية", "docs-dropdown-11": "الأسئلة الأكثر شيوعًا", - "docs-dropdown-12": "الأمان" + "docs-dropdown-12": "الأمان", + "hero-overlay-card-3-p-3": "قامت Trail of Bits بمراجعة التصميم التعموي لبروتوكولات شبكة SimpleX في يوليو 2024. اقرأ المزيد." } diff --git a/website/langs/de.json b/website/langs/de.json index 1a5c42d980..c5d1fceefa 100644 --- a/website/langs/de.json +++ b/website/langs/de.json @@ -254,5 +254,6 @@ "please-use-link-in-mobile-app": "Bitte nutzen Sie den Link in der Mobiltelefon-App", "docs-dropdown-10": "Transparent", "docs-dropdown-11": "FAQ", - "docs-dropdown-12": "Sicherheit" + "docs-dropdown-12": "Sicherheit", + "hero-overlay-card-3-p-3": "Trail of Bits hat das kryptografische Design des Netzwerk-Protokolls von SimpleX im Juli 2024 überprüft. Hier finden Sie weitere Informationen dazu." } diff --git a/website/langs/hu.json b/website/langs/hu.json index f7cf1c558b..b0b76714bc 100644 --- a/website/langs/hu.json +++ b/website/langs/hu.json @@ -29,12 +29,12 @@ "hero-p-1": "Más alkalmazások felhasználói azonosítókkal rendelkeznek: Signal, Matrix, Session, Briar, Jami, Cwtch, stb.
A SimpleX nem, még véletlenszerű számokkal sem.
Ez radikálisan javítja az adatvédelmet.", "hero-overlay-1-textlink": "Miért ártanak a felhasználói azonosítók az adatvédelemnek?", "hero-overlay-2-textlink": "Hogyan működik a SimpleX?", - "hero-overlay-3-textlink": "A biztonság értékelése", + "hero-overlay-3-textlink": "Biztonsági felmérések", "hero-2-header": "Privát kapcsolat létrehozása", "hero-2-header-desc": "A videó bemutatja, hogyan kapcsolódhat az ismerőséhez egy egyszer használható QR-kód segítségével, személyesen vagy videokapcsolaton keresztül. Ugyanakkor egy meghívó-hivatkozás megosztásával is kapcsolódhat.", "hero-overlay-1-title": "Hogyan működik a SimpleX?", "hero-overlay-2-title": "Miért ártanak a felhasználói azonosítók az adatvédelemnek?", - "hero-overlay-3-title": "A biztonság értékelése", + "hero-overlay-3-title": "Biztonsági felmérések", "feature-1-title": "E2E-titkosított üzenetek markdown formázással és szerkesztéssel", "feature-2-title": "E2E-titkosított
képek, videók és fájlok", "feature-3-title": "E2E-titkosított decentralizált csoportok — csak a felhasználók tudják, hogy ezek léteznek", @@ -254,5 +254,6 @@ "glossary": "Fogalomtár", "simplex-chat-via-f-droid": "SimpleX Chat az F-Droidon keresztül", "simplex-chat-repo": "SimpleX Chat tároló", - "stable-and-beta-versions-built-by-developers": "A fejlesztők által készített stabil és béta verziók" + "stable-and-beta-versions-built-by-developers": "A fejlesztők által készített stabil és béta verziók", + "hero-overlay-card-3-p-3": "A Trail of Bits 2024 júliusában felülvizsgálta a SimpleX hálózati protokollok kriptográfiai felépítését. Tudjon meg többet." } diff --git a/website/langs/it.json b/website/langs/it.json index 431354c068..bdc4c38d45 100644 --- a/website/langs/it.json +++ b/website/langs/it.json @@ -244,15 +244,16 @@ "stable-and-beta-versions-built-by-developers": "Versioni stabili e beta compilate dagli sviluppatori", "f-droid-page-simplex-chat-repo-section-text": "Per aggiungerlo al tuo client F-Droid scansiona il codice QR o usa questo URL:", "comparison-section-list-point-4a": "I relay di SimpleX non possono compromettere la crittografia e2e. Verifica il codice di sicurezza per mitigare gli attacchi sul canale fuori banda", - "hero-overlay-3-title": "Valutazione della sicurezza", + "hero-overlay-3-title": "Valutazioni della sicurezza", "hero-overlay-card-3-p-2": "Trail of Bits ha revisionato i componenti di crittografia e di rete della piattaforma SimpleX nel novembre 2022. Maggiori informazioni.", "jobs": "Unisciti al team", - "hero-overlay-3-textlink": "Valutazione della sicurezza", + "hero-overlay-3-textlink": "Valutazioni della sicurezza", "hero-overlay-card-3-p-1": "Trail of Bits è leader nella consulenza di sicurezza e tecnologia, i cui clienti includono grandi aziende, agenzie governative e importanti progetti di blockchain.", "docs-dropdown-9": "Download", "please-enable-javascript": "Attiva JavaScript per vedere il codice QR.", "please-use-link-in-mobile-app": "Usa il link nell'app mobile", "docs-dropdown-10": "Trasparenza", "docs-dropdown-12": "Sicurezza", - "docs-dropdown-11": "Domande frequenti" + "docs-dropdown-11": "Domande frequenti", + "hero-overlay-card-3-p-3": "Trail of Bits ha analizzato la progettazione crittografica dei protocolli della rete SimpleX nel luglio 2024. Leggi di più." } diff --git a/website/langs/nl.json b/website/langs/nl.json index 18edf45369..c725cc3d3e 100644 --- a/website/langs/nl.json +++ b/website/langs/nl.json @@ -254,5 +254,6 @@ "please-use-link-in-mobile-app": "Gebruik de link in de mobiele app", "docs-dropdown-10": "Transparantie", "docs-dropdown-11": "FAQ", - "docs-dropdown-12": "Beveiliging" + "docs-dropdown-12": "Beveiliging", + "hero-overlay-card-3-p-3": "Trail of Bits heeft in juli 2024 het cryptografische ontwerp van SimpleX-netwerkprotocollen beoordeeld. Lees meer." } diff --git a/website/langs/uk.json b/website/langs/uk.json index 5fa2f02b99..d055aa68a2 100644 --- a/website/langs/uk.json +++ b/website/langs/uk.json @@ -254,5 +254,6 @@ "please-use-link-in-mobile-app": "Будь ласка, скористайтеся посиланням у мобільному додатку", "docs-dropdown-11": "ПОШИРЕНІ ЗАПИТАННЯ", "docs-dropdown-10": "Прозорість", - "docs-dropdown-12": "Безпека" + "docs-dropdown-12": "Безпека", + "hero-overlay-card-3-p-3": "Trail of Bits переглянув криптографічний дизайн мережевих протоколів SimpleX в липні 2024 року. Детальніше." } diff --git a/website/langs/zh_Hans.json b/website/langs/zh_Hans.json index 87a8cc3c78..e482056565 100644 --- a/website/langs/zh_Hans.json +++ b/website/langs/zh_Hans.json @@ -254,5 +254,6 @@ "please-enable-javascript": "请启用 JavaScript 以查看二维码。", "docs-dropdown-10": "透明度", "docs-dropdown-11": "常问问题", - "docs-dropdown-12": "安全性" + "docs-dropdown-12": "安全性", + "hero-overlay-card-3-p-3": "Trail of Bits 于 2024 年 7 月审核了 SimpleX 网络协议的加密设计。了解更多信息。" } From 6593de89c200051cff66a1412d2ec10e984ac7c0 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Tue, 3 Dec 2024 19:25:15 +0400 Subject: [PATCH 115/167] ios: make more texts different for groups and business chats (#5307) --- .../Chat/Group/AddGroupMembersView.swift | 5 +- .../Views/Chat/Group/GroupChatInfoView.swift | 55 +++++++++++++------ .../Chat/Group/GroupMemberInfoView.swift | 31 +++++++++-- .../Views/ChatList/ChatListNavLink.swift | 21 ++++--- 4 files changed, 79 insertions(+), 33 deletions(-) diff --git a/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift b/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift index 691bda39a6..925f4120bc 100644 --- a/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift +++ b/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift @@ -140,12 +140,13 @@ struct AddGroupMembersViewCommon: View { return dummy }() - private func inviteMembersButton() -> some View { + @ViewBuilder private func inviteMembersButton() -> some View { + let label: LocalizedStringKey = groupInfo.businessChat == nil ? "Invite to group" : "Invite to chat" Button { inviteMembers() } label: { HStack { - Text("Invite to group") + Text(label) Image(systemName: "checkmark") } } diff --git a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift index 89f0fcbedf..27aa0edb5b 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift @@ -101,7 +101,12 @@ struct GroupChatInfoView: View { } header: { Text("") } footer: { - Text("Only group owners can change group preferences.") + let label: LocalizedStringKey = ( + groupInfo.businessChat == nil + ? "Only group owners can change group preferences." + : "Only chat owners can change preferences." + ) + Text(label) .foregroundColor(theme.colors.secondary) } @@ -494,11 +499,12 @@ struct GroupChatInfoView: View { } } - private func deleteGroupButton() -> some View { + @ViewBuilder private func deleteGroupButton() -> some View { + let label: LocalizedStringKey = groupInfo.businessChat == nil ? "Delete group" : "Delete chat" Button(role: .destructive) { alert = .deleteGroupAlert } label: { - Label("Delete group", systemImage: "trash") + Label(label, systemImage: "trash") .foregroundColor(Color.red) } } @@ -512,20 +518,22 @@ struct GroupChatInfoView: View { } } - private func leaveGroupButton() -> some View { + @ViewBuilder private func leaveGroupButton() -> some View { + let label: LocalizedStringKey = groupInfo.businessChat == nil ? "Leave group" : "Leave chat" Button(role: .destructive) { alert = .leaveGroupAlert } label: { - Label("Leave group", systemImage: "rectangle.portrait.and.arrow.right") + Label(label, systemImage: "rectangle.portrait.and.arrow.right") .foregroundColor(Color.red) } } // TODO reuse this and clearChatAlert with ChatInfoView private func deleteGroupAlert() -> Alert { + let label: LocalizedStringKey = groupInfo.businessChat == nil ? "Delete group?" : "Delete chat?" return Alert( - title: Text("Delete group?"), - message: deleteGroupAlertMessage(), + title: Text(label), + message: deleteGroupAlertMessage(groupInfo), primaryButton: .destructive(Text("Delete")) { Task { do { @@ -544,10 +552,6 @@ struct GroupChatInfoView: View { ) } - private func deleteGroupAlertMessage() -> Text { - groupInfo.membership.memberCurrent ? Text("Group will be deleted for all members - this cannot be undone!") : Text("Group will be deleted for you - this cannot be undone!") - } - private func clearChatAlert() -> Alert { Alert( title: Text("Clear conversation?"), @@ -563,9 +567,15 @@ struct GroupChatInfoView: View { } private func leaveGroupAlert() -> Alert { - Alert( - title: Text("Leave group?"), - message: Text("You will stop receiving messages from this group. Chat history will be preserved."), + let titleLabel: LocalizedStringKey = groupInfo.businessChat == nil ? "Leave group?" : "Leave chat?" + let messageLabel: LocalizedStringKey = ( + groupInfo.businessChat == nil + ? "You will stop receiving messages from this group. Chat history will be preserved." + : "You will stop receiving messages from this chat. Chat history will be preserved." + ) + return Alert( + title: Text(titleLabel), + message: Text(messageLabel), primaryButton: .destructive(Text("Leave")) { Task { await leaveGroup(chat.chatInfo.apiId) @@ -609,9 +619,14 @@ struct GroupChatInfoView: View { } private func removeMemberAlert(_ mem: GroupMember) -> Alert { - Alert( + let messageLabel: LocalizedStringKey = ( + groupInfo.businessChat == nil + ? "Member will be removed from group - this cannot be undone!" + : "Member will be removed from chat - this cannot be undone!" + ) + return Alert( title: Text("Remove member?"), - message: Text("Member will be removed from group - this cannot be undone!"), + message: Text(messageLabel), primaryButton: .destructive(Text("Remove")) { Task { do { @@ -631,6 +646,14 @@ struct GroupChatInfoView: View { } } +func deleteGroupAlertMessage(_ groupInfo: GroupInfo) -> Text { + groupInfo.businessChat == nil ? ( + groupInfo.membership.memberCurrent ? Text("Group will be deleted for all members - this cannot be undone!") : Text("Group will be deleted for you - this cannot be undone!") + ) : ( + groupInfo.membership.memberCurrent ? Text("Chat will be deleted for all members - this cannot be undone!") : Text("Chat will be deleted for you - this cannot be undone!") + ) +} + func groupPreferencesButton(_ groupInfo: Binding, _ creatingGroup: Bool = false) -> some View { let label: LocalizedStringKey = groupInfo.wrappedValue.businessChat == nil ? "Group preferences" : "Chat preferences" return NavigationLink { diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift index ed40c0592b..9f3de7ac59 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift @@ -135,7 +135,8 @@ struct GroupMemberInfoView: View { } Section(header: Text("Member").foregroundColor(theme.colors.secondary)) { - infoRow("Group", groupInfo.displayName) + let label: LocalizedStringKey = groupInfo.businessChat == nil ? "Group" : "Chat" + infoRow(label, groupInfo.displayName) if let roles = member.canChangeRoleTo(groupInfo: groupInfo) { Picker("Change role", selection: $newRole) { @@ -305,10 +306,15 @@ struct GroupMemberInfoView: View { } func showDirectMessagesProhibitedAlert(_ title: LocalizedStringKey) { + let messageLabel: LocalizedStringKey = ( + groupInfo.businessChat == nil + ? "Direct messages between members are prohibited in this group." + : "Direct messages between members are prohibited in this chat." + ) alert = .someAlert(alert: SomeAlert( alert: mkAlert( title: title, - message: "Direct messages between members are prohibited in this group." + message: messageLabel ), id: "can't message member, direct messages prohibited" )) @@ -537,9 +543,14 @@ struct GroupMemberInfoView: View { } private func removeMemberAlert(_ mem: GroupMember) -> Alert { - Alert( + let label: LocalizedStringKey = ( + groupInfo.businessChat == nil + ? "Member will be removed from group - this cannot be undone!" + : "Member will be removed from chat - this cannot be undone!" + ) + return Alert( title: Text("Remove member?"), - message: Text("Member will be removed from group - this cannot be undone!"), + message: Text(label), primaryButton: .destructive(Text("Remove")) { Task { do { @@ -562,7 +573,15 @@ struct GroupMemberInfoView: View { private func changeMemberRoleAlert(_ mem: GroupMember) -> Alert { Alert( title: Text("Change member role?"), - message: mem.memberCurrent ? Text("Member role will be changed to \"\(newRole.text)\". All group members will be notified.") : Text("Member role will be changed to \"\(newRole.text)\". The member will receive a new invitation."), + message: ( + mem.memberCurrent + ? ( + groupInfo.businessChat == nil + ? Text("Member role will be changed to \"\(newRole.text)\". All group members will be notified.") + : Text("Member role will be changed to \"\(newRole.text)\". All chat members will be notified.") + ) + : Text("Member role will be changed to \"\(newRole.text)\". The member will receive a new invitation.") + ), primaryButton: .default(Text("Change")) { Task { do { @@ -570,7 +589,7 @@ struct GroupMemberInfoView: View { await MainActor.run { _ = chatModel.upsertGroupMember(groupInfo, updatedMember) } - + } catch let error { newRole = mem.memberRole logger.error("apiMemberRole error: \(responseError(error))") diff --git a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift index d2a93b9bd1..6c5dad1f74 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift @@ -404,8 +404,9 @@ struct ChatListNavLink: View { } private func deleteGroupAlert(_ groupInfo: GroupInfo) -> Alert { - Alert( - title: Text("Delete group?"), + let label: LocalizedStringKey = groupInfo.businessChat == nil ? "Delete group?" : "Delete chat?" + return Alert( + title: Text(label), message: deleteGroupAlertMessage(groupInfo), primaryButton: .destructive(Text("Delete")) { Task { await deleteChat(chat) } @@ -414,10 +415,6 @@ struct ChatListNavLink: View { ) } - private func deleteGroupAlertMessage(_ groupInfo: GroupInfo) -> Text { - groupInfo.membership.memberCurrent ? Text("Group will be deleted for all members - this cannot be undone!") : Text("Group will be deleted for you - this cannot be undone!") - } - private func clearChatAlert() -> Alert { Alert( title: Text("Clear conversation?"), @@ -441,9 +438,15 @@ struct ChatListNavLink: View { } private func leaveGroupAlert(_ groupInfo: GroupInfo) -> Alert { - Alert( - title: Text("Leave group?"), - message: Text("You will stop receiving messages from this group. Chat history will be preserved."), + let titleLabel: LocalizedStringKey = groupInfo.businessChat == nil ? "Leave group?" : "Leave chat?" + let messageLabel: LocalizedStringKey = ( + groupInfo.businessChat == nil + ? "You will stop receiving messages from this group. Chat history will be preserved." + : "You will stop receiving messages from this chat. Chat history will be preserved." + ) + return Alert( + title: Text(titleLabel), + message: Text(messageLabel), primaryButton: .destructive(Text("Leave")) { Task { await leaveGroup(groupInfo.groupId) } }, From 247d12fa4015ee07210352a766eb74f62e9b8e97 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Tue, 3 Dec 2024 21:44:06 +0400 Subject: [PATCH 116/167] android, desktop: make more texts different for groups and business chats; ios: preferences texts (#5308) --- .../Chat/Group/GroupMemberInfoView.swift | 2 +- .../ar.xcloc/Localized Contents/ar.xliff | 28 +++---- .../bg.xcloc/Localized Contents/bg.xliff | 56 ++++++------- .../bn.xcloc/Localized Contents/bn.xliff | 36 ++++---- .../cs.xcloc/Localized Contents/cs.xliff | 56 ++++++------- .../de.xcloc/Localized Contents/de.xliff | 56 ++++++------- .../el.xcloc/Localized Contents/el.xliff | 28 +++---- .../en.xcloc/Localized Contents/en.xliff | 84 +++++++++---------- .../es.xcloc/Localized Contents/es.xliff | 56 ++++++------- .../fi.xcloc/Localized Contents/fi.xliff | 56 ++++++------- .../fr.xcloc/Localized Contents/fr.xliff | 56 ++++++------- .../he.xcloc/Localized Contents/he.xliff | 44 +++++----- .../hr.xcloc/Localized Contents/hr.xliff | 28 +++---- .../hu.xcloc/Localized Contents/hu.xliff | 56 ++++++------- .../it.xcloc/Localized Contents/it.xliff | 56 ++++++------- .../ja.xcloc/Localized Contents/ja.xliff | 56 ++++++------- .../ko.xcloc/Localized Contents/ko.xliff | 28 +++---- .../lt.xcloc/Localized Contents/lt.xliff | 28 +++---- .../nl.xcloc/Localized Contents/nl.xliff | 56 ++++++------- .../pl.xcloc/Localized Contents/pl.xliff | 56 ++++++------- .../Localized Contents/pt-BR.xliff | 28 +++---- .../pt.xcloc/Localized Contents/pt.xliff | 28 +++---- .../ru.xcloc/Localized Contents/ru.xliff | 56 ++++++------- .../th.xcloc/Localized Contents/th.xliff | 56 ++++++------- .../tr.xcloc/Localized Contents/tr.xliff | 56 ++++++------- .../uk.xcloc/Localized Contents/uk.xliff | 56 ++++++------- .../Localized Contents/zh-Hans.xliff | 56 ++++++------- .../Localized Contents/zh-Hant.xliff | 36 ++++---- apps/ios/SimpleXChat/ChatTypes.swift | 28 +++---- apps/ios/bg.lproj/Localizable.strings | 28 +++---- apps/ios/cs.lproj/Localizable.strings | 24 +++--- apps/ios/de.lproj/Localizable.strings | 28 +++---- apps/ios/es.lproj/Localizable.strings | 28 +++---- apps/ios/fi.lproj/Localizable.strings | 24 +++--- apps/ios/fr.lproj/Localizable.strings | 28 +++---- apps/ios/hu.lproj/Localizable.strings | 28 +++---- apps/ios/it.lproj/Localizable.strings | 28 +++---- apps/ios/ja.lproj/Localizable.strings | 24 +++--- apps/ios/nl.lproj/Localizable.strings | 28 +++---- apps/ios/pl.lproj/Localizable.strings | 28 +++---- apps/ios/ru.lproj/Localizable.strings | 28 +++---- apps/ios/th.lproj/Localizable.strings | 24 +++--- apps/ios/tr.lproj/Localizable.strings | 28 +++---- apps/ios/uk.lproj/Localizable.strings | 28 +++---- apps/ios/zh-Hans.lproj/Localizable.strings | 28 +++---- .../chat/simplex/common/model/SimpleXAPI.kt | 2 +- .../views/chat/group/AddGroupMembersView.kt | 11 ++- .../views/chat/group/GroupChatInfoView.kt | 48 +++++++---- .../views/chat/group/GroupMemberInfoView.kt | 33 +++++--- .../AdvancedNetworkSettings.kt | 4 +- .../commonMain/resources/MR/ar/strings.xml | 2 +- .../commonMain/resources/MR/base/strings.xml | 43 ++++++---- .../commonMain/resources/MR/bg/strings.xml | 2 +- .../commonMain/resources/MR/cs/strings.xml | 2 +- .../commonMain/resources/MR/de/strings.xml | 2 +- .../commonMain/resources/MR/es/strings.xml | 2 +- .../commonMain/resources/MR/fa/strings.xml | 2 +- .../commonMain/resources/MR/fi/strings.xml | 2 +- .../commonMain/resources/MR/fr/strings.xml | 2 +- .../commonMain/resources/MR/hu/strings.xml | 2 +- .../commonMain/resources/MR/in/strings.xml | 2 +- .../commonMain/resources/MR/it/strings.xml | 8 +- .../commonMain/resources/MR/iw/strings.xml | 2 +- .../commonMain/resources/MR/ja/strings.xml | 2 +- .../commonMain/resources/MR/ko/strings.xml | 2 +- .../commonMain/resources/MR/lt/strings.xml | 2 +- .../commonMain/resources/MR/nl/strings.xml | 2 +- .../commonMain/resources/MR/pl/strings.xml | 2 +- .../resources/MR/pt-rBR/strings.xml | 2 +- .../commonMain/resources/MR/pt/strings.xml | 2 +- .../commonMain/resources/MR/ru/strings.xml | 2 +- .../commonMain/resources/MR/th/strings.xml | 2 +- .../commonMain/resources/MR/tr/strings.xml | 2 +- .../commonMain/resources/MR/uk/strings.xml | 2 +- .../commonMain/resources/MR/vi/strings.xml | 2 +- .../resources/MR/zh-rCN/strings.xml | 2 +- .../resources/MR/zh-rTW/strings.xml | 2 +- 77 files changed, 1000 insertions(+), 953 deletions(-) diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift index 9f3de7ac59..90d6829d93 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift @@ -308,7 +308,7 @@ struct GroupMemberInfoView: View { func showDirectMessagesProhibitedAlert(_ title: LocalizedStringKey) { let messageLabel: LocalizedStringKey = ( groupInfo.businessChat == nil - ? "Direct messages between members are prohibited in this group." + ? "Direct messages between members are prohibited." : "Direct messages between members are prohibited in this chat." ) alert = .someAlert(alert: SomeAlert( diff --git a/apps/ios/SimpleX Localizations/ar.xcloc/Localized Contents/ar.xliff b/apps/ios/SimpleX Localizations/ar.xcloc/Localized Contents/ar.xliff index 53707d108f..ef91bb30fd 100644 --- a/apps/ios/SimpleX Localizations/ar.xcloc/Localized Contents/ar.xliff +++ b/apps/ios/SimpleX Localizations/ar.xcloc/Localized Contents/ar.xliff @@ -1043,8 +1043,8 @@ Direct messages chat feature
- - Direct messages between members are prohibited in this group. + + Direct messages between members are prohibited. No comment provided by engineer. @@ -1059,8 +1059,8 @@ Disappearing messages are prohibited in this chat. No comment provided by engineer. - - Disappearing messages are prohibited in this group. + + Disappearing messages are prohibited. No comment provided by engineer. @@ -1423,16 +1423,16 @@ Group members can irreversibly delete sent messages. No comment provided by engineer. - - Group members can send direct messages. + + Members can send direct messages. No comment provided by engineer. - - Group members can send disappearing messages. + + Members can send disappearing messages. No comment provided by engineer. - - Group members can send voice messages. + + Members can send voice messages. No comment provided by engineer. @@ -1620,8 +1620,8 @@ Irreversible message deletion is prohibited in this chat. No comment provided by engineer. - - Irreversible message deletion is prohibited in this group. + + Irreversible message deletion is prohibited. No comment provided by engineer. @@ -2855,8 +2855,8 @@ To connect, please ask your contact to create another connection link and check Voice messages are prohibited in this chat. No comment provided by engineer. - - Voice messages are prohibited in this group. + + Voice messages are prohibited. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff index f1e9ee0f39..e483406fe5 100644 --- a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff @@ -2384,8 +2384,8 @@ This is your own one-time link! Лични съобщения chat feature - - Direct messages between members are prohibited in this group. + + Direct messages between members are prohibited. Личните съобщения между членовете са забранени в тази група. No comment provided by engineer. @@ -2423,8 +2423,8 @@ This is your own one-time link! Изчезващите съобщения са забранени в този чат. No comment provided by engineer. - - Disappearing messages are prohibited in this group. + + Disappearing messages are prohibited. Изчезващите съобщения са забранени в тази група. No comment provided by engineer. @@ -3246,8 +3246,8 @@ This is your own one-time link! Файлове и медия chat feature - - Files and media are prohibited in this group. + + Files and media are prohibited. Файловете и медията са забранени в тази група. No comment provided by engineer. @@ -3502,38 +3502,38 @@ Error: %2$@ Групови линкове No comment provided by engineer. - - Group members can add message reactions. + + Members can add message reactions. Членовете на групата могат да добавят реакции към съобщенията. No comment provided by engineer. - - Group members can irreversibly delete sent messages. (24 hours) + + Members can irreversibly delete sent messages. (24 hours) Членовете на групата могат необратимо да изтриват изпратените съобщения. (24 часа) No comment provided by engineer. - - Group members can send SimpleX links. + + Members can send SimpleX links. Членовете на групата могат да изпращат SimpleX линкове. No comment provided by engineer. - - Group members can send direct messages. + + Members can send direct messages. Членовете на групата могат да изпращат лични съобщения. No comment provided by engineer. - - Group members can send disappearing messages. + + Members can send disappearing messages. Членовете на групата могат да изпращат изчезващи съобщения. No comment provided by engineer. - - Group members can send files and media. + + Members can send files and media. Членовете на групата могат да изпращат файлове и медия. No comment provided by engineer. - - Group members can send voice messages. + + Members can send voice messages. Членовете на групата могат да изпращат гласови съобщения. No comment provided by engineer. @@ -3944,8 +3944,8 @@ More improvements are coming soon! Необратимото изтриване на съобщения е забранено в този чат. No comment provided by engineer. - - Irreversible message deletion is prohibited in this group. + + Irreversible message deletion is prohibited. Необратимото изтриване на съобщения е забранено в тази група. No comment provided by engineer. @@ -4272,8 +4272,8 @@ This is your link for group %@! Реакциите на съобщения са забранени в този чат. No comment provided by engineer. - - Message reactions are prohibited in this group. + + Message reactions are prohibited. Реакциите на съобщения са забранени в тази група. No comment provided by engineer. @@ -6390,8 +6390,8 @@ Enable in *Network & servers* settings. SimpleX линкове chat feature - - SimpleX links are prohibited in this group. + + SimpleX links are prohibited. SimpleX линкове са забранени в тази група. No comment provided by engineer. @@ -7427,8 +7427,8 @@ To connect, please ask your contact to create another connection link and check Гласовите съобщения са забранени в този чат. No comment provided by engineer. - - Voice messages are prohibited in this group. + + Voice messages are prohibited. Гласовите съобщения са забранени в тази група. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/bn.xcloc/Localized Contents/bn.xliff b/apps/ios/SimpleX Localizations/bn.xcloc/Localized Contents/bn.xliff index f7630b9e1f..7002f790df 100644 --- a/apps/ios/SimpleX Localizations/bn.xcloc/Localized Contents/bn.xliff +++ b/apps/ios/SimpleX Localizations/bn.xcloc/Localized Contents/bn.xliff @@ -1247,8 +1247,8 @@ Direct messages chat feature - - Direct messages between members are prohibited in this group. + + Direct messages between members are prohibited. No comment provided by engineer. @@ -1267,8 +1267,8 @@ Disappearing messages are prohibited in this chat. No comment provided by engineer. - - Disappearing messages are prohibited in this group. + + Disappearing messages are prohibited. No comment provided by engineer. @@ -1747,24 +1747,24 @@ Group links No comment provided by engineer. - - Group members can add message reactions. + + Members can add message reactions. No comment provided by engineer. Group members can irreversibly delete sent messages. No comment provided by engineer. - - Group members can send direct messages. + + Members can send direct messages. No comment provided by engineer. - - Group members can send disappearing messages. + + Members can send disappearing messages. No comment provided by engineer. - - Group members can send voice messages. + + Members can send voice messages. No comment provided by engineer. @@ -2016,8 +2016,8 @@ Irreversible message deletion is prohibited in this chat. No comment provided by engineer. - - Irreversible message deletion is prohibited in this group. + + Irreversible message deletion is prohibited. No comment provided by engineer. @@ -2203,8 +2203,8 @@ Message reactions are prohibited in this chat. No comment provided by engineer. - - Message reactions are prohibited in this group. + + Message reactions are prohibited. No comment provided by engineer. @@ -3720,8 +3720,8 @@ To connect, please ask your contact to create another connection link and check Voice messages are prohibited in this chat. No comment provided by engineer. - - Voice messages are prohibited in this group. + + Voice messages are prohibited. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff index a4bff0f321..7efd941d11 100644 --- a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff @@ -2309,8 +2309,8 @@ This is your own one-time link! Přímé zprávy chat feature - - Direct messages between members are prohibited in this group. + + Direct messages between members are prohibited. Přímé zprávy mezi členy jsou v této skupině zakázány. No comment provided by engineer. @@ -2348,8 +2348,8 @@ This is your own one-time link! Mizící zprávy jsou v tomto chatu zakázány. No comment provided by engineer. - - Disappearing messages are prohibited in this group. + + Disappearing messages are prohibited. Mizící zprávy jsou v této skupině zakázány. No comment provided by engineer. @@ -3144,8 +3144,8 @@ This is your own one-time link! Soubory a média chat feature - - Files and media are prohibited in this group. + + Files and media are prohibited. Soubory a média jsou zakázány v této skupině. No comment provided by engineer. @@ -3389,37 +3389,37 @@ Error: %2$@ Odkazy na skupiny No comment provided by engineer. - - Group members can add message reactions. + + Members can add message reactions. Členové skupin mohou přidávat reakce na zprávy. No comment provided by engineer. - - Group members can irreversibly delete sent messages. (24 hours) + + Members can irreversibly delete sent messages. (24 hours) Členové skupiny mohou nevratně mazat odeslané zprávy. (24 hodin) No comment provided by engineer. - - Group members can send SimpleX links. + + Members can send SimpleX links. No comment provided by engineer. - - Group members can send direct messages. + + Members can send direct messages. Členové skupiny mohou posílat přímé zprávy. No comment provided by engineer. - - Group members can send disappearing messages. + + Members can send disappearing messages. Členové skupiny mohou posílat mizící zprávy. No comment provided by engineer. - - Group members can send files and media. + + Members can send files and media. Členové skupiny mohou posílat soubory a média. No comment provided by engineer. - - Group members can send voice messages. + + Members can send voice messages. Členové skupiny mohou posílat hlasové zprávy. No comment provided by engineer. @@ -3815,8 +3815,8 @@ More improvements are coming soon! Nevratné mazání zpráv je v tomto chatu zakázáno. No comment provided by engineer. - - Irreversible message deletion is prohibited in this group. + + Irreversible message deletion is prohibited. Nevratné mazání zpráv je v této skupině zakázáno. No comment provided by engineer. @@ -4132,8 +4132,8 @@ This is your link for group %@! Reakce na zprávy jsou v tomto chatu zakázány. No comment provided by engineer. - - Message reactions are prohibited in this group. + + Message reactions are prohibited. Reakce na zprávy jsou v této skupině zakázány. No comment provided by engineer. @@ -6188,8 +6188,8 @@ Enable in *Network & servers* settings. Odkazy na SimpleX chat feature - - SimpleX links are prohibited in this group. + + SimpleX links are prohibited. No comment provided by engineer. @@ -7186,8 +7186,8 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Hlasové zprávy jsou v tomto chatu zakázány. No comment provided by engineer. - - Voice messages are prohibited in this group. + + Voice messages are prohibited. Hlasové zprávy jsou v této skupině zakázány. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff index 3770207e39..afce946eea 100644 --- a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff +++ b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff @@ -2500,8 +2500,8 @@ Das ist Ihr eigener Einmal-Link! Direkte Nachrichten chat feature - - Direct messages between members are prohibited in this group. + + Direct messages between members are prohibited. In dieser Gruppe sind Direktnachrichten zwischen Mitgliedern nicht erlaubt. No comment provided by engineer. @@ -2540,8 +2540,8 @@ Das ist Ihr eigener Einmal-Link! In diesem Chat sind verschwindende Nachrichten nicht erlaubt. No comment provided by engineer. - - Disappearing messages are prohibited in this group. + + Disappearing messages are prohibited. In dieser Gruppe sind verschwindende Nachrichten nicht erlaubt. No comment provided by engineer. @@ -3398,8 +3398,8 @@ Das ist Ihr eigener Einmal-Link! Dateien und Medien chat feature - - Files and media are prohibited in this group. + + Files and media are prohibited. In dieser Gruppe sind Dateien und Medien nicht erlaubt. No comment provided by engineer. @@ -3672,38 +3672,38 @@ Fehler: %2$@ Gruppen-Links No comment provided by engineer. - - Group members can add message reactions. + + Members can add message reactions. Gruppenmitglieder können eine Reaktion auf Nachrichten geben. No comment provided by engineer. - - Group members can irreversibly delete sent messages. (24 hours) + + Members can irreversibly delete sent messages. (24 hours) Gruppenmitglieder können gesendete Nachrichten unwiederbringlich löschen. (24 Stunden) No comment provided by engineer. - - Group members can send SimpleX links. + + Members can send SimpleX links. Gruppenmitglieder können SimpleX-Links senden. No comment provided by engineer. - - Group members can send direct messages. + + Members can send direct messages. Gruppenmitglieder können Direktnachrichten versenden. No comment provided by engineer. - - Group members can send disappearing messages. + + Members can send disappearing messages. Gruppenmitglieder können verschwindende Nachrichten senden. No comment provided by engineer. - - Group members can send files and media. + + Members can send files and media. Gruppenmitglieder können Dateien und Medien senden. No comment provided by engineer. - - Group members can send voice messages. + + Members can send voice messages. Gruppenmitglieder können Sprachnachrichten versenden. No comment provided by engineer. @@ -4121,8 +4121,8 @@ Weitere Verbesserungen sind bald verfügbar! In diesem Chat ist das unwiederbringliche Löschen von Nachrichten nicht erlaubt. No comment provided by engineer. - - Irreversible message deletion is prohibited in this group. + + Irreversible message deletion is prohibited. In dieser Gruppe ist das unwiederbringliche Löschen von Nachrichten nicht erlaubt. No comment provided by engineer. @@ -4459,8 +4459,8 @@ Das ist Ihr Link für die Gruppe %@! In diesem Chat sind Reaktionen auf Nachrichten nicht erlaubt. No comment provided by engineer. - - Message reactions are prohibited in this group. + + Message reactions are prohibited. In dieser Gruppe sind Reaktionen auf Nachrichten nicht erlaubt. No comment provided by engineer. @@ -6707,8 +6707,8 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. SimpleX-Links chat feature - - SimpleX links are prohibited in this group. + + SimpleX links are prohibited. In dieser Gruppe sind SimpleX-Links nicht erlaubt. No comment provided by engineer. @@ -7804,8 +7804,8 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s In diesem Chat sind Sprachnachrichten nicht erlaubt. No comment provided by engineer. - - Voice messages are prohibited in this group. + + Voice messages are prohibited. In dieser Gruppe sind Sprachnachrichten nicht erlaubt. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/el.xcloc/Localized Contents/el.xliff b/apps/ios/SimpleX Localizations/el.xcloc/Localized Contents/el.xliff index 9a112d12fa..b601d1fa74 100644 --- a/apps/ios/SimpleX Localizations/el.xcloc/Localized Contents/el.xliff +++ b/apps/ios/SimpleX Localizations/el.xcloc/Localized Contents/el.xliff @@ -1124,8 +1124,8 @@ Available in v5.1 Direct messages chat feature - - Direct messages between members are prohibited in this group. + + Direct messages between members are prohibited. No comment provided by engineer. @@ -1140,8 +1140,8 @@ Available in v5.1 Disappearing messages are prohibited in this chat. No comment provided by engineer. - - Disappearing messages are prohibited in this group. + + Disappearing messages are prohibited. No comment provided by engineer. @@ -1576,16 +1576,16 @@ Available in v5.1 Group members can irreversibly delete sent messages. No comment provided by engineer. - - Group members can send direct messages. + + Members can send direct messages. No comment provided by engineer. - - Group members can send disappearing messages. + + Members can send disappearing messages. No comment provided by engineer. - - Group members can send voice messages. + + Members can send voice messages. No comment provided by engineer. @@ -1817,8 +1817,8 @@ Available in v5.1 Irreversible message deletion is prohibited in this chat. No comment provided by engineer. - - Irreversible message deletion is prohibited in this group. + + Irreversible message deletion is prohibited. No comment provided by engineer. @@ -3333,8 +3333,8 @@ To connect, please ask your contact to create another connection link and check Voice messages are prohibited in this chat. No comment provided by engineer. - - Voice messages are prohibited in this group. + + Voice messages are prohibited. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff index 72ad43f136..2301f671a4 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff +++ b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff @@ -2506,9 +2506,9 @@ This is your own one-time link! Direct messages chat feature - - Direct messages between members are prohibited in this group. - Direct messages between members are prohibited in this group. + + Direct messages between members are prohibited. + Direct messages between members are prohibited. No comment provided by engineer. @@ -2546,9 +2546,9 @@ This is your own one-time link! Disappearing messages are prohibited in this chat. No comment provided by engineer. - - Disappearing messages are prohibited in this group. - Disappearing messages are prohibited in this group. + + Disappearing messages are prohibited. + Disappearing messages are prohibited. No comment provided by engineer. @@ -3404,9 +3404,9 @@ This is your own one-time link! Files and media chat feature - - Files and media are prohibited in this group. - Files and media are prohibited in this group. + + Files and media are prohibited. + Files and media are prohibited. No comment provided by engineer. @@ -3678,39 +3678,39 @@ Error: %2$@ Group links No comment provided by engineer. - - Group members can add message reactions. - Group members can add message reactions. + + Members can add message reactions. + Members can add message reactions. No comment provided by engineer. - - Group members can irreversibly delete sent messages. (24 hours) - Group members can irreversibly delete sent messages. (24 hours) + + Members can irreversibly delete sent messages. (24 hours) + Members can irreversibly delete sent messages. (24 hours) No comment provided by engineer. - - Group members can send SimpleX links. - Group members can send SimpleX links. + + Members can send SimpleX links. + Members can send SimpleX links. No comment provided by engineer. - - Group members can send direct messages. - Group members can send direct messages. + + Members can send direct messages. + Members can send direct messages. No comment provided by engineer. - - Group members can send disappearing messages. - Group members can send disappearing messages. + + Members can send disappearing messages. + Members can send disappearing messages. No comment provided by engineer. - - Group members can send files and media. - Group members can send files and media. + + Members can send files and media. + Members can send files and media. No comment provided by engineer. - - Group members can send voice messages. - Group members can send voice messages. + + Members can send voice messages. + Members can send voice messages. No comment provided by engineer. @@ -4127,9 +4127,9 @@ More improvements are coming soon! Irreversible message deletion is prohibited in this chat. No comment provided by engineer. - - Irreversible message deletion is prohibited in this group. - Irreversible message deletion is prohibited in this group. + + Irreversible message deletion is prohibited. + Irreversible message deletion is prohibited. No comment provided by engineer. @@ -4465,9 +4465,9 @@ This is your link for group %@! Message reactions are prohibited in this chat. No comment provided by engineer. - - Message reactions are prohibited in this group. - Message reactions are prohibited in this group. + + Message reactions are prohibited. + Message reactions are prohibited. No comment provided by engineer. @@ -6714,9 +6714,9 @@ Enable in *Network & servers* settings. SimpleX links chat feature - - SimpleX links are prohibited in this group. - SimpleX links are prohibited in this group. + + SimpleX links are prohibited. + SimpleX links are prohibited. No comment provided by engineer. @@ -7812,9 +7812,9 @@ To connect, please ask your contact to create another connection link and check Voice messages are prohibited in this chat. No comment provided by engineer. - - Voice messages are prohibited in this group. - Voice messages are prohibited in this group. + + Voice messages are prohibited. + Voice messages are prohibited. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff index cfffd783d9..647d650698 100644 --- a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff +++ b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff @@ -2500,8 +2500,8 @@ This is your own one-time link! Mensajes directos chat feature - - Direct messages between members are prohibited in this group. + + Direct messages between members are prohibited. Los mensajes directos entre miembros del grupo no están permitidos. No comment provided by engineer. @@ -2540,8 +2540,8 @@ This is your own one-time link! Los mensajes temporales no están permitidos en este chat. No comment provided by engineer. - - Disappearing messages are prohibited in this group. + + Disappearing messages are prohibited. Los mensajes temporales no están permitidos en este grupo. No comment provided by engineer. @@ -3398,8 +3398,8 @@ This is your own one-time link! Archivos y multimedia chat feature - - Files and media are prohibited in this group. + + Files and media are prohibited. Los archivos y multimedia no están permitidos en este grupo. No comment provided by engineer. @@ -3672,38 +3672,38 @@ Error: %2$@ Enlaces de grupo No comment provided by engineer. - - Group members can add message reactions. + + Members can add message reactions. Los miembros pueden añadir reacciones a los mensajes. No comment provided by engineer. - - Group members can irreversibly delete sent messages. (24 hours) + + Members can irreversibly delete sent messages. (24 hours) Los miembros del grupo pueden eliminar mensajes de forma irreversible. (24 horas) No comment provided by engineer. - - Group members can send SimpleX links. + + Members can send SimpleX links. Los miembros del grupo pueden enviar enlaces SimpleX. No comment provided by engineer. - - Group members can send direct messages. + + Members can send direct messages. Los miembros del grupo pueden enviar mensajes directos. No comment provided by engineer. - - Group members can send disappearing messages. + + Members can send disappearing messages. Los miembros del grupo pueden enviar mensajes temporales. No comment provided by engineer. - - Group members can send files and media. + + Members can send files and media. Los miembros del grupo pueden enviar archivos y multimedia. No comment provided by engineer. - - Group members can send voice messages. + + Members can send voice messages. Los miembros del grupo pueden enviar mensajes de voz. No comment provided by engineer. @@ -4121,8 +4121,8 @@ More improvements are coming soon! La eliminación irreversible de mensajes no está permitida en este chat. No comment provided by engineer. - - Irreversible message deletion is prohibited in this group. + + Irreversible message deletion is prohibited. La eliminación irreversible de mensajes no está permitida en este grupo. No comment provided by engineer. @@ -4459,8 +4459,8 @@ This is your link for group %@! Las reacciones a los mensajes no están permitidas en este chat. No comment provided by engineer. - - Message reactions are prohibited in this group. + + Message reactions are prohibited. Las reacciones a los mensajes no están permitidas en este grupo. No comment provided by engineer. @@ -6707,8 +6707,8 @@ Actívalo en ajustes de *Servidores y Redes*. Enlaces SimpleX chat feature - - SimpleX links are prohibited in this group. + + SimpleX links are prohibited. Los enlaces SimpleX no se permiten en este grupo. No comment provided by engineer. @@ -7804,8 +7804,8 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión Los mensajes de voz no están permitidos en este chat. No comment provided by engineer. - - Voice messages are prohibited in this group. + + Voice messages are prohibited. Los mensajes de voz no están permitidos en este grupo. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff index 763b502ddb..00996b4a4f 100644 --- a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff @@ -2302,8 +2302,8 @@ This is your own one-time link! Yksityisviestit chat feature - - Direct messages between members are prohibited in this group. + + Direct messages between members are prohibited. Yksityisviestit jäsenten välillä ovat kiellettyjä tässä ryhmässä. No comment provided by engineer. @@ -2341,8 +2341,8 @@ This is your own one-time link! Katoavat viestit ovat kiellettyjä tässä keskustelussa. No comment provided by engineer. - - Disappearing messages are prohibited in this group. + + Disappearing messages are prohibited. Katoavat viestit ovat kiellettyjä tässä ryhmässä. No comment provided by engineer. @@ -3134,8 +3134,8 @@ This is your own one-time link! Tiedostot ja media chat feature - - Files and media are prohibited in this group. + + Files and media are prohibited. Tiedostot ja media ovat tässä ryhmässä kiellettyjä. No comment provided by engineer. @@ -3379,37 +3379,37 @@ Error: %2$@ Ryhmälinkit No comment provided by engineer. - - Group members can add message reactions. + + Members can add message reactions. Ryhmän jäsenet voivat lisätä viestireaktioita. No comment provided by engineer. - - Group members can irreversibly delete sent messages. (24 hours) + + Members can irreversibly delete sent messages. (24 hours) Ryhmän jäsenet voivat poistaa lähetetyt viestit peruuttamattomasti. (24 tuntia) No comment provided by engineer. - - Group members can send SimpleX links. + + Members can send SimpleX links. No comment provided by engineer. - - Group members can send direct messages. + + Members can send direct messages. Ryhmän jäsenet voivat lähettää suoraviestejä. No comment provided by engineer. - - Group members can send disappearing messages. + + Members can send disappearing messages. Ryhmän jäsenet voivat lähettää katoavia viestejä. No comment provided by engineer. - - Group members can send files and media. + + Members can send files and media. Ryhmän jäsenet voivat lähettää tiedostoja ja mediaa. No comment provided by engineer. - - Group members can send voice messages. + + Members can send voice messages. Ryhmän jäsenet voivat lähettää ääniviestejä. No comment provided by engineer. @@ -3805,8 +3805,8 @@ More improvements are coming soon! Viestien peruuttamaton poisto on kielletty tässä keskustelussa. No comment provided by engineer. - - Irreversible message deletion is prohibited in this group. + + Irreversible message deletion is prohibited. Viestien peruuttamaton poisto on kielletty tässä ryhmässä. No comment provided by engineer. @@ -4122,8 +4122,8 @@ This is your link for group %@! Viestireaktiot ovat kiellettyjä tässä keskustelussa. No comment provided by engineer. - - Message reactions are prohibited in this group. + + Message reactions are prohibited. Viestireaktiot ovat kiellettyjä tässä ryhmässä. No comment provided by engineer. @@ -6175,8 +6175,8 @@ Enable in *Network & servers* settings. SimpleX-linkit chat feature - - SimpleX links are prohibited in this group. + + SimpleX links are prohibited. No comment provided by engineer. @@ -7171,8 +7171,8 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Ääniviestit ovat kiellettyjä tässä keskustelussa. No comment provided by engineer. - - Voice messages are prohibited in this group. + + Voice messages are prohibited. Ääniviestit ovat kiellettyjä tässä ryhmässä. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff index d91ce3c106..9498c255c8 100644 --- a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff @@ -2472,8 +2472,8 @@ Il s'agit de votre propre lien unique ! Messages directs chat feature - - Direct messages between members are prohibited in this group. + + Direct messages between members are prohibited. Les messages directs entre membres sont interdits dans ce groupe. No comment provided by engineer. @@ -2512,8 +2512,8 @@ Il s'agit de votre propre lien unique ! Les messages éphémères sont interdits dans cette discussion. No comment provided by engineer. - - Disappearing messages are prohibited in this group. + + Disappearing messages are prohibited. Les messages éphémères sont interdits dans ce groupe. No comment provided by engineer. @@ -3362,8 +3362,8 @@ Il s'agit de votre propre lien unique ! Fichiers et médias chat feature - - Files and media are prohibited in this group. + + Files and media are prohibited. Les fichiers et les médias sont interdits dans ce groupe. No comment provided by engineer. @@ -3632,38 +3632,38 @@ Erreur : %2$@ Liens de groupe No comment provided by engineer. - - Group members can add message reactions. + + Members can add message reactions. Les membres du groupe peuvent ajouter des réactions aux messages. No comment provided by engineer. - - Group members can irreversibly delete sent messages. (24 hours) + + Members can irreversibly delete sent messages. (24 hours) Les membres du groupe peuvent supprimer de manière irréversible les messages envoyés. (24 heures) No comment provided by engineer. - - Group members can send SimpleX links. + + Members can send SimpleX links. Les membres du groupe peuvent envoyer des liens SimpleX. No comment provided by engineer. - - Group members can send direct messages. + + Members can send direct messages. Les membres du groupe peuvent envoyer des messages directs. No comment provided by engineer. - - Group members can send disappearing messages. + + Members can send disappearing messages. Les membres du groupes peuvent envoyer des messages éphémères. No comment provided by engineer. - - Group members can send files and media. + + Members can send files and media. Les membres du groupe peuvent envoyer des fichiers et des médias. No comment provided by engineer. - - Group members can send voice messages. + + Members can send voice messages. Les membres du groupe peuvent envoyer des messages vocaux. No comment provided by engineer. @@ -4079,8 +4079,8 @@ D'autres améliorations sont à venir ! La suppression irréversible de message est interdite dans ce chat. No comment provided by engineer. - - Irreversible message deletion is prohibited in this group. + + Irreversible message deletion is prohibited. La suppression irréversible de messages est interdite dans ce groupe. No comment provided by engineer. @@ -4417,8 +4417,8 @@ Voici votre lien pour le groupe %@ ! Les réactions aux messages sont interdites dans ce chat. No comment provided by engineer. - - Message reactions are prohibited in this group. + + Message reactions are prohibited. Les réactions aux messages sont interdites dans ce groupe. No comment provided by engineer. @@ -6633,8 +6633,8 @@ Activez-le dans les paramètres *Réseau et serveurs*. Liens SimpleX chat feature - - SimpleX links are prohibited in this group. + + SimpleX links are prohibited. Les liens SimpleX sont interdits dans ce groupe. No comment provided by engineer. @@ -7712,8 +7712,8 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Les messages vocaux sont interdits dans ce chat. No comment provided by engineer. - - Voice messages are prohibited in this group. + + Voice messages are prohibited. Les messages vocaux sont interdits dans ce groupe. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/he.xcloc/Localized Contents/he.xliff b/apps/ios/SimpleX Localizations/he.xcloc/Localized Contents/he.xliff index 813eebc01a..08f46bb056 100644 --- a/apps/ios/SimpleX Localizations/he.xcloc/Localized Contents/he.xliff +++ b/apps/ios/SimpleX Localizations/he.xcloc/Localized Contents/he.xliff @@ -1386,8 +1386,8 @@ Available in v5.1 הודעות ישירות chat feature - - Direct messages between members are prohibited in this group. + + Direct messages between members are prohibited. הודעות ישירות בין חברי קבוצה אסורות בקבוצה זו. No comment provided by engineer. @@ -1406,8 +1406,8 @@ Available in v5.1 הודעות נעלמות אסורות בצ׳אט זה. No comment provided by engineer. - - Disappearing messages are prohibited in this group. + + Disappearing messages are prohibited. הודעות נעלמות אסורות בקבוצה זו. No comment provided by engineer. @@ -1951,18 +1951,18 @@ Available in v5.1 חברי הקבוצה יכולים למחוק באופן בלתי הפיך הודעות שנשלחו. No comment provided by engineer. - - Group members can send direct messages. + + Members can send direct messages. חברי הקבוצה יכולים לשלוח הודעות ישירות. No comment provided by engineer. - - Group members can send disappearing messages. + + Members can send disappearing messages. חברי הקבוצה יכולים לשלוח הודעות נעלמות. No comment provided by engineer. - - Group members can send voice messages. + + Members can send voice messages. חברי הקבוצה יכולים לשלוח הודעות קוליות. No comment provided by engineer. @@ -2252,8 +2252,8 @@ Available in v5.1 מחיקה בלתי הפיכה של הודעות אסורה בצ׳אט זה. No comment provided by engineer. - - Irreversible message deletion is prohibited in this group. + + Irreversible message deletion is prohibited. מחיקה בלתי הפיכה של הודעות אסורה בקבוצה זו. No comment provided by engineer. @@ -3859,8 +3859,8 @@ To connect, please ask your contact to create another connection link and check Voice messages are prohibited in this chat. No comment provided by engineer. - - Voice messages are prohibited in this group. + + Voice messages are prohibited. No comment provided by engineer. @@ -4958,8 +4958,8 @@ SimpleX servers cannot see your profile. נמחק No comment provided by engineer. - - Files and media are prohibited in this group. + + Files and media are prohibited. קבצים ומדיה אסורים בקבוצה זו. No comment provided by engineer. @@ -5018,13 +5018,13 @@ SimpleX servers cannot see your profile. הזמן חברים No comment provided by engineer. - - Group members can add message reactions. + + Members can add message reactions. חברי הקבוצה יכולים להוסיף תגובות אמוג׳י להודעות. No comment provided by engineer. - - Group members can send files and media. + + Members can send files and media. חברי הקבוצה יכולים לשלוח קבצים ומדיה. No comment provided by engineer. @@ -5222,8 +5222,8 @@ SimpleX servers cannot see your profile. תגובות אמוג׳י להודעות אסורות בצ׳אט זה. No comment provided by engineer. - - Message reactions are prohibited in this group. + + Message reactions are prohibited. תגובות אמוג׳י להודעות אסורות בקבוצה זו. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/hr.xcloc/Localized Contents/hr.xliff b/apps/ios/SimpleX Localizations/hr.xcloc/Localized Contents/hr.xliff index 7ae670185c..2fd96e3492 100644 --- a/apps/ios/SimpleX Localizations/hr.xcloc/Localized Contents/hr.xliff +++ b/apps/ios/SimpleX Localizations/hr.xcloc/Localized Contents/hr.xliff @@ -1034,8 +1034,8 @@ Direct messages chat feature - - Direct messages between members are prohibited in this group. + + Direct messages between members are prohibited. No comment provided by engineer. @@ -1050,8 +1050,8 @@ Disappearing messages are prohibited in this chat. No comment provided by engineer. - - Disappearing messages are prohibited in this group. + + Disappearing messages are prohibited. No comment provided by engineer. @@ -1414,16 +1414,16 @@ Group members can irreversibly delete sent messages. No comment provided by engineer. - - Group members can send direct messages. + + Members can send direct messages. No comment provided by engineer. - - Group members can send disappearing messages. + + Members can send disappearing messages. No comment provided by engineer. - - Group members can send voice messages. + + Members can send voice messages. No comment provided by engineer. @@ -1611,8 +1611,8 @@ Irreversible message deletion is prohibited in this chat. No comment provided by engineer. - - Irreversible message deletion is prohibited in this group. + + Irreversible message deletion is prohibited. No comment provided by engineer. @@ -2842,8 +2842,8 @@ To connect, please ask your contact to create another connection link and check Voice messages are prohibited in this chat. No comment provided by engineer. - - Voice messages are prohibited in this group. + + Voice messages are prohibited. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff index 44750dfbb2..a17a6430d1 100644 --- a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff @@ -2500,8 +2500,8 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Közvetlen üzenetek chat feature - - Direct messages between members are prohibited in this group. + + Direct messages between members are prohibited. A közvetlen üzenetek küldése a tagok között le van tiltva ebben a csoportban. No comment provided by engineer. @@ -2540,8 +2540,8 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Az eltűnő üzenetek küldése le van tiltva ebben a csevegésben. No comment provided by engineer. - - Disappearing messages are prohibited in this group. + + Disappearing messages are prohibited. Az eltűnő üzenetek küldése le van tiltva ebben a csoportban. No comment provided by engineer. @@ -3398,8 +3398,8 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Fájlok és médiatartalmak chat feature - - Files and media are prohibited in this group. + + Files and media are prohibited. A fájlok- és a médiatartalmak le vannak tiltva ebben a csoportban. No comment provided by engineer. @@ -3672,38 +3672,38 @@ Hiba: %2$@ Csoporthivatkozások No comment provided by engineer. - - Group members can add message reactions. + + Members can add message reactions. Csoporttagok üzenetreakciókat adhatnak hozzá. No comment provided by engineer. - - Group members can irreversibly delete sent messages. (24 hours) + + Members can irreversibly delete sent messages. (24 hours) A csoport tagjai véglegesen törölhetik az elküldött üzeneteiket. (24 óra) No comment provided by engineer. - - Group members can send SimpleX links. + + Members can send SimpleX links. A csoport tagjai küldhetnek SimpleX-hivatkozásokat. No comment provided by engineer. - - Group members can send direct messages. + + Members can send direct messages. A csoport tagjai küldhetnek egymásnak közvetlen üzeneteket. No comment provided by engineer. - - Group members can send disappearing messages. + + Members can send disappearing messages. A csoport tagjai küldhetnek eltűnő üzeneteket. No comment provided by engineer. - - Group members can send files and media. + + Members can send files and media. A csoport tagjai küldhetnek fájlokat és médiatartalmakat. No comment provided by engineer. - - Group members can send voice messages. + + Members can send voice messages. A csoport tagjai küldhetnek hangüzeneteket. No comment provided by engineer. @@ -4121,8 +4121,8 @@ További fejlesztések hamarosan! Az üzenetek végleges törlése le van tiltva ebben a csevegésben. No comment provided by engineer. - - Irreversible message deletion is prohibited in this group. + + Irreversible message deletion is prohibited. Az üzenetek végleges törlése le van tiltva ebben a csoportban. No comment provided by engineer. @@ -4459,8 +4459,8 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! Az üzenetreakciók küldése le van tiltva ebben a csevegésben. No comment provided by engineer. - - Message reactions are prohibited in this group. + + Message reactions are prohibited. Az üzenetreakciók küldése le van tiltva ebben a csoportban. No comment provided by engineer. @@ -6707,8 +6707,8 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. SimpleX-hivatkozások chat feature - - SimpleX links are prohibited in this group. + + SimpleX links are prohibited. A SimpleX-hivatkozások küldése le van tiltva ebben a csoportban. No comment provided by engineer. @@ -7804,8 +7804,8 @@ A kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapc A hangüzenetek küldése le van tiltva ebben a csevegésben. No comment provided by engineer. - - Voice messages are prohibited in this group. + + Voice messages are prohibited. A hangüzenetek küldése le van tiltva ebben a csoportban. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff index 8e54ba40dd..0992a1f6bc 100644 --- a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff +++ b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff @@ -2500,8 +2500,8 @@ Questo è il tuo link una tantum! Messaggi diretti chat feature - - Direct messages between members are prohibited in this group. + + Direct messages between members are prohibited. I messaggi diretti tra i membri sono vietati in questo gruppo. No comment provided by engineer. @@ -2540,8 +2540,8 @@ Questo è il tuo link una tantum! I messaggi a tempo sono vietati in questa chat. No comment provided by engineer. - - Disappearing messages are prohibited in this group. + + Disappearing messages are prohibited. I messaggi a tempo sono vietati in questo gruppo. No comment provided by engineer. @@ -3398,8 +3398,8 @@ Questo è il tuo link una tantum! File e multimediali chat feature - - Files and media are prohibited in this group. + + Files and media are prohibited. File e contenuti multimediali sono vietati in questo gruppo. No comment provided by engineer. @@ -3672,38 +3672,38 @@ Errore: %2$@ Link del gruppo No comment provided by engineer. - - Group members can add message reactions. + + Members can add message reactions. I membri del gruppo possono aggiungere reazioni ai messaggi. No comment provided by engineer. - - Group members can irreversibly delete sent messages. (24 hours) + + Members can irreversibly delete sent messages. (24 hours) I membri del gruppo possono eliminare irreversibilmente i messaggi inviati. (24 ore) No comment provided by engineer. - - Group members can send SimpleX links. + + Members can send SimpleX links. I membri del gruppo possono inviare link di Simplex. No comment provided by engineer. - - Group members can send direct messages. + + Members can send direct messages. I membri del gruppo possono inviare messaggi diretti. No comment provided by engineer. - - Group members can send disappearing messages. + + Members can send disappearing messages. I membri del gruppo possono inviare messaggi a tempo. No comment provided by engineer. - - Group members can send files and media. + + Members can send files and media. I membri del gruppo possono inviare file e contenuti multimediali. No comment provided by engineer. - - Group members can send voice messages. + + Members can send voice messages. I membri del gruppo possono inviare messaggi vocali. No comment provided by engineer. @@ -4121,8 +4121,8 @@ Altri miglioramenti sono in arrivo! L'eliminazione irreversibile dei messaggi è vietata in questa chat. No comment provided by engineer. - - Irreversible message deletion is prohibited in this group. + + Irreversible message deletion is prohibited. L'eliminazione irreversibile dei messaggi è vietata in questo gruppo. No comment provided by engineer. @@ -4459,8 +4459,8 @@ Questo è il tuo link per il gruppo %@! Le reazioni ai messaggi sono vietate in questa chat. No comment provided by engineer. - - Message reactions are prohibited in this group. + + Message reactions are prohibited. Le reazioni ai messaggi sono vietate in questo gruppo. No comment provided by engineer. @@ -6707,8 +6707,8 @@ Attivalo nelle impostazioni *Rete e server*. Link di SimpleX chat feature - - SimpleX links are prohibited in this group. + + SimpleX links are prohibited. I link di SimpleX sono vietati in questo gruppo. No comment provided by engineer. @@ -7804,8 +7804,8 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e I messaggi vocali sono vietati in questa chat. No comment provided by engineer. - - Voice messages are prohibited in this group. + + Voice messages are prohibited. I messaggi vocali sono vietati in questo gruppo. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff index 1a5ffdd680..32d2db371b 100644 --- a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff +++ b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff @@ -2348,8 +2348,8 @@ This is your own one-time link! ダイレクトメッセージ chat feature - - Direct messages between members are prohibited in this group. + + Direct messages between members are prohibited. このグループではメンバー間のダイレクトメッセージが使用禁止です。 No comment provided by engineer. @@ -2387,8 +2387,8 @@ This is your own one-time link! このチャットでは消えるメッセージが使用禁止です。 No comment provided by engineer. - - Disappearing messages are prohibited in this group. + + Disappearing messages are prohibited. このグループでは消えるメッセージが使用禁止です。 No comment provided by engineer. @@ -3181,8 +3181,8 @@ This is your own one-time link! ファイルとメディア chat feature - - Files and media are prohibited in this group. + + Files and media are prohibited. このグループでは、ファイルとメディアは禁止されています。 No comment provided by engineer. @@ -3426,37 +3426,37 @@ Error: %2$@ グループのリンク No comment provided by engineer. - - Group members can add message reactions. + + Members can add message reactions. グループメンバーはメッセージへのリアクションを追加できます。 No comment provided by engineer. - - Group members can irreversibly delete sent messages. (24 hours) + + Members can irreversibly delete sent messages. (24 hours) グループのメンバーがメッセージを完全削除することができます。(24時間) No comment provided by engineer. - - Group members can send SimpleX links. + + Members can send SimpleX links. No comment provided by engineer. - - Group members can send direct messages. + + Members can send direct messages. グループのメンバーがダイレクトメッセージを送信できます。 No comment provided by engineer. - - Group members can send disappearing messages. + + Members can send disappearing messages. グループのメンバーが消えるメッセージを送信できます。 No comment provided by engineer. - - Group members can send files and media. + + Members can send files and media. グループメンバーはファイルやメディアを送信できます。 No comment provided by engineer. - - Group members can send voice messages. + + Members can send voice messages. グループのメンバーが音声メッセージを送信できます。 No comment provided by engineer. @@ -3852,8 +3852,8 @@ More improvements are coming soon! このチャットではメッセージの完全削除が使用禁止です。 No comment provided by engineer. - - Irreversible message deletion is prohibited in this group. + + Irreversible message deletion is prohibited. このグループではメッセージの完全削除が使用禁止です。 No comment provided by engineer. @@ -4168,8 +4168,8 @@ This is your link for group %@! このチャットではメッセージへのリアクションは禁止されています。 No comment provided by engineer. - - Message reactions are prohibited in this group. + + Message reactions are prohibited. このグループではメッセージへのリアクションは禁止されています。 No comment provided by engineer. @@ -6217,8 +6217,8 @@ Enable in *Network & servers* settings. SimpleXリンク chat feature - - SimpleX links are prohibited in this group. + + SimpleX links are prohibited. No comment provided by engineer. @@ -7213,8 +7213,8 @@ To connect, please ask your contact to create another connection link and check このチャットでは音声メッセージが使用禁止です。 No comment provided by engineer. - - Voice messages are prohibited in this group. + + Voice messages are prohibited. このグループでは音声メッセージが使用禁止です。 No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/ko.xcloc/Localized Contents/ko.xliff b/apps/ios/SimpleX Localizations/ko.xcloc/Localized Contents/ko.xliff index ac9d83e24b..9aaa83afc3 100644 --- a/apps/ios/SimpleX Localizations/ko.xcloc/Localized Contents/ko.xliff +++ b/apps/ios/SimpleX Localizations/ko.xcloc/Localized Contents/ko.xliff @@ -1009,8 +1009,8 @@ Direct messages chat feature - - Direct messages between members are prohibited in this group. + + Direct messages between members are prohibited. No comment provided by engineer. @@ -1025,8 +1025,8 @@ Disappearing messages are prohibited in this chat. No comment provided by engineer. - - Disappearing messages are prohibited in this group. + + Disappearing messages are prohibited. No comment provided by engineer. @@ -1417,16 +1417,16 @@ Group members can irreversibly delete sent messages. No comment provided by engineer. - - Group members can send direct messages. + + Members can send direct messages. No comment provided by engineer. - - Group members can send disappearing messages. + + Members can send disappearing messages. No comment provided by engineer. - - Group members can send voice messages. + + Members can send voice messages. No comment provided by engineer. @@ -1638,8 +1638,8 @@ Irreversible message deletion is prohibited in this chat. No comment provided by engineer. - - Irreversible message deletion is prohibited in this group. + + Irreversible message deletion is prohibited. No comment provided by engineer. @@ -2964,8 +2964,8 @@ To connect, please ask your contact to create another connection link and check Voice messages are prohibited in this chat. No comment provided by engineer. - - Voice messages are prohibited in this group. + + Voice messages are prohibited. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/lt.xcloc/Localized Contents/lt.xliff b/apps/ios/SimpleX Localizations/lt.xcloc/Localized Contents/lt.xliff index e16f585da8..54a713478f 100644 --- a/apps/ios/SimpleX Localizations/lt.xcloc/Localized Contents/lt.xliff +++ b/apps/ios/SimpleX Localizations/lt.xcloc/Localized Contents/lt.xliff @@ -1029,8 +1029,8 @@ Direct messages chat feature - - Direct messages between members are prohibited in this group. + + Direct messages between members are prohibited. No comment provided by engineer. @@ -1045,8 +1045,8 @@ Disappearing messages are prohibited in this chat. No comment provided by engineer. - - Disappearing messages are prohibited in this group. + + Disappearing messages are prohibited. No comment provided by engineer. @@ -1413,16 +1413,16 @@ Group members can irreversibly delete sent messages. No comment provided by engineer. - - Group members can send direct messages. + + Members can send direct messages. No comment provided by engineer. - - Group members can send disappearing messages. + + Members can send disappearing messages. No comment provided by engineer. - - Group members can send voice messages. + + Members can send voice messages. No comment provided by engineer. @@ -1610,8 +1610,8 @@ Irreversible message deletion is prohibited in this chat. No comment provided by engineer. - - Irreversible message deletion is prohibited in this group. + + Irreversible message deletion is prohibited. No comment provided by engineer. @@ -2869,8 +2869,8 @@ To connect, please ask your contact to create another connection link and check Voice messages are prohibited in this chat. No comment provided by engineer. - - Voice messages are prohibited in this group. + + Voice messages are prohibited. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff index 50b3fdae3e..b9cba70c3a 100644 --- a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff @@ -2500,8 +2500,8 @@ Dit is uw eigen eenmalige link! Directe berichten chat feature - - Direct messages between members are prohibited in this group. + + Direct messages between members are prohibited. Directe berichten tussen leden zijn verboden in deze groep. No comment provided by engineer. @@ -2540,8 +2540,8 @@ Dit is uw eigen eenmalige link! Verdwijnende berichten zijn verboden in dit gesprek. No comment provided by engineer. - - Disappearing messages are prohibited in this group. + + Disappearing messages are prohibited. Verdwijnende berichten zijn verboden in deze groep. No comment provided by engineer. @@ -3398,8 +3398,8 @@ Dit is uw eigen eenmalige link! Bestanden en media chat feature - - Files and media are prohibited in this group. + + Files and media are prohibited. Bestanden en media zijn verboden in deze groep. No comment provided by engineer. @@ -3672,38 +3672,38 @@ Fout: %2$@ Groep links No comment provided by engineer. - - Group members can add message reactions. + + Members can add message reactions. Groepsleden kunnen bericht reacties toevoegen. No comment provided by engineer. - - Group members can irreversibly delete sent messages. (24 hours) + + Members can irreversibly delete sent messages. (24 hours) Groepsleden kunnen verzonden berichten onherroepelijk verwijderen. (24 uur) No comment provided by engineer. - - Group members can send SimpleX links. + + Members can send SimpleX links. Groepsleden kunnen SimpleX-links verzenden. No comment provided by engineer. - - Group members can send direct messages. + + Members can send direct messages. Groepsleden kunnen directe berichten sturen. No comment provided by engineer. - - Group members can send disappearing messages. + + Members can send disappearing messages. Groepsleden kunnen verdwijnende berichten sturen. No comment provided by engineer. - - Group members can send files and media. + + Members can send files and media. Groepsleden kunnen bestanden en media verzenden. No comment provided by engineer. - - Group members can send voice messages. + + Members can send voice messages. Groepsleden kunnen spraak berichten verzenden. No comment provided by engineer. @@ -4121,8 +4121,8 @@ Binnenkort meer verbeteringen! Het onomkeerbaar verwijderen van berichten is verboden in dit gesprek. No comment provided by engineer. - - Irreversible message deletion is prohibited in this group. + + Irreversible message deletion is prohibited. Het onomkeerbaar verwijderen van berichten is verboden in deze groep. No comment provided by engineer. @@ -4459,8 +4459,8 @@ Dit is jouw link voor groep %@! Reacties op berichten zijn verboden in deze chat. No comment provided by engineer. - - Message reactions are prohibited in this group. + + Message reactions are prohibited. Reacties op berichten zijn verboden in deze groep. No comment provided by engineer. @@ -6707,8 +6707,8 @@ Schakel dit in in *Netwerk en servers*-instellingen. SimpleX links chat feature - - SimpleX links are prohibited in this group. + + SimpleX links are prohibited. SimpleX-links zijn in deze groep verboden. No comment provided by engineer. @@ -7804,8 +7804,8 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Spraak berichten zijn verboden in deze chat. No comment provided by engineer. - - Voice messages are prohibited in this group. + + Voice messages are prohibited. Spraak berichten zijn verboden in deze groep. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff index 0095b5e031..bdfe502952 100644 --- a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff @@ -2465,8 +2465,8 @@ To jest twój jednorazowy link! Bezpośrednie wiadomości chat feature - - Direct messages between members are prohibited in this group. + + Direct messages between members are prohibited. Bezpośrednie wiadomości między członkami są zabronione w tej grupie. No comment provided by engineer. @@ -2505,8 +2505,8 @@ To jest twój jednorazowy link! Znikające wiadomości są zabronione na tym czacie. No comment provided by engineer. - - Disappearing messages are prohibited in this group. + + Disappearing messages are prohibited. Znikające wiadomości są zabronione w tej grupie. No comment provided by engineer. @@ -3355,8 +3355,8 @@ To jest twój jednorazowy link! Pliki i media chat feature - - Files and media are prohibited in this group. + + Files and media are prohibited. Pliki i media są zabronione w tej grupie. No comment provided by engineer. @@ -3624,38 +3624,38 @@ Błąd: %2$@ Linki grupowe No comment provided by engineer. - - Group members can add message reactions. + + Members can add message reactions. Członkowie grupy mogą dodawać reakcje wiadomości. No comment provided by engineer. - - Group members can irreversibly delete sent messages. (24 hours) + + Members can irreversibly delete sent messages. (24 hours) Członkowie grupy mogą nieodwracalnie usuwać wysłane wiadomości. (24 godziny) No comment provided by engineer. - - Group members can send SimpleX links. + + Members can send SimpleX links. Członkowie grupy mogą wysyłać linki SimpleX. No comment provided by engineer. - - Group members can send direct messages. + + Members can send direct messages. Członkowie grupy mogą wysyłać bezpośrednie wiadomości. No comment provided by engineer. - - Group members can send disappearing messages. + + Members can send disappearing messages. Członkowie grupy mogą wysyłać znikające wiadomości. No comment provided by engineer. - - Group members can send files and media. + + Members can send files and media. Członkowie grupy mogą wysyłać pliki i media. No comment provided by engineer. - - Group members can send voice messages. + + Members can send voice messages. Członkowie grupy mogą wysyłać wiadomości głosowe. No comment provided by engineer. @@ -4069,8 +4069,8 @@ More improvements are coming soon! Nieodwracalne usuwanie wiadomości jest na tym czacie zabronione. No comment provided by engineer. - - Irreversible message deletion is prohibited in this group. + + Irreversible message deletion is prohibited. Nieodwracalne usuwanie wiadomości jest w tej grupie zabronione. No comment provided by engineer. @@ -4407,8 +4407,8 @@ To jest twój link do grupy %@! Reakcje wiadomości są zabronione na tym czacie. No comment provided by engineer. - - Message reactions are prohibited in this group. + + Message reactions are prohibited. Reakcje wiadomości są zabronione w tej grupie. No comment provided by engineer. @@ -6623,8 +6623,8 @@ Włącz w ustawianiach *Sieć i serwery* . Linki SimpleX chat feature - - SimpleX links are prohibited in this group. + + SimpleX links are prohibited. Linki SimpleX są zablokowane na tej grupie. No comment provided by engineer. @@ -7699,8 +7699,8 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Wiadomości głosowe są zabronione na tym czacie. No comment provided by engineer. - - Voice messages are prohibited in this group. + + Voice messages are prohibited. Wiadomości głosowe są zabronione w tej grupie. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/pt-BR.xcloc/Localized Contents/pt-BR.xliff b/apps/ios/SimpleX Localizations/pt-BR.xcloc/Localized Contents/pt-BR.xliff index 40fc2cd4b3..9badf9c2e4 100644 --- a/apps/ios/SimpleX Localizations/pt-BR.xcloc/Localized Contents/pt-BR.xliff +++ b/apps/ios/SimpleX Localizations/pt-BR.xcloc/Localized Contents/pt-BR.xliff @@ -1204,8 +1204,8 @@ Mensagens diretas chat feature - - Direct messages between members are prohibited in this group. + + Direct messages between members are prohibited. Mensagens diretas entre membros são proibidas neste grupo. No comment provided by engineer. @@ -1224,8 +1224,8 @@ Mensagens temporárias são proibidas nesse chat. No comment provided by engineer. - - Disappearing messages are prohibited in this group. + + Disappearing messages are prohibited. Mensagens que temporárias são proibidas neste grupo. No comment provided by engineer. @@ -1638,18 +1638,18 @@ Os membros do grupo podem excluir mensagens enviadas de forma irreversível. No comment provided by engineer. - - Group members can send direct messages. + + Members can send direct messages. Os membros do grupo podem enviar DMs. No comment provided by engineer. - - Group members can send disappearing messages. + + Members can send disappearing messages. Os membros do grupo podem enviar mensagens que desaparecem. No comment provided by engineer. - - Group members can send voice messages. + + Members can send voice messages. Os membros do grupo podem enviar mensagens de voz. No comment provided by engineer. @@ -1873,8 +1873,8 @@ A exclusão irreversível de mensagens é proibida neste chat. No comment provided by engineer. - - Irreversible message deletion is prohibited in this group. + + Irreversible message deletion is prohibited. A exclusão irreversível de mensagens é proibida neste grupo. No comment provided by engineer. @@ -3269,8 +3269,8 @@ Para se conectar, peça ao seu contato para criar outro link de conexão e verif Mensagens de voz são proibidas neste chat. No comment provided by engineer. - - Voice messages are prohibited in this group. + + Voice messages are prohibited. Mensagens de voz são proibidas neste grupo. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/pt.xcloc/Localized Contents/pt.xliff b/apps/ios/SimpleX Localizations/pt.xcloc/Localized Contents/pt.xliff index e20181e4f7..de1787bdad 100644 --- a/apps/ios/SimpleX Localizations/pt.xcloc/Localized Contents/pt.xliff +++ b/apps/ios/SimpleX Localizations/pt.xcloc/Localized Contents/pt.xliff @@ -1227,8 +1227,8 @@ Available in v5.1 Direct messages chat feature - - Direct messages between members are prohibited in this group. + + Direct messages between members are prohibited. No comment provided by engineer. @@ -1243,8 +1243,8 @@ Available in v5.1 Disappearing messages are prohibited in this chat. No comment provided by engineer. - - Disappearing messages are prohibited in this group. + + Disappearing messages are prohibited. No comment provided by engineer. @@ -1679,16 +1679,16 @@ Available in v5.1 Group members can irreversibly delete sent messages. No comment provided by engineer. - - Group members can send direct messages. + + Members can send direct messages. No comment provided by engineer. - - Group members can send disappearing messages. + + Members can send disappearing messages. No comment provided by engineer. - - Group members can send voice messages. + + Members can send voice messages. No comment provided by engineer. @@ -1920,8 +1920,8 @@ Available in v5.1 Irreversible message deletion is prohibited in this chat. No comment provided by engineer. - - Irreversible message deletion is prohibited in this group. + + Irreversible message deletion is prohibited. No comment provided by engineer. @@ -3436,8 +3436,8 @@ To connect, please ask your contact to create another connection link and check Voice messages are prohibited in this chat. No comment provided by engineer. - - Voice messages are prohibited in this group. + + Voice messages are prohibited. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff index e7230dbcb2..6d57734259 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff @@ -2473,8 +2473,8 @@ This is your own one-time link! Прямые сообщения chat feature - - Direct messages between members are prohibited in this group. + + Direct messages between members are prohibited. Прямые сообщения между членами группы запрещены. No comment provided by engineer. @@ -2513,8 +2513,8 @@ This is your own one-time link! Исчезающие сообщения запрещены в этом чате. No comment provided by engineer. - - Disappearing messages are prohibited in this group. + + Disappearing messages are prohibited. Исчезающие сообщения запрещены в этой группе. No comment provided by engineer. @@ -3363,8 +3363,8 @@ This is your own one-time link! Файлы и медиа chat feature - - Files and media are prohibited in this group. + + Files and media are prohibited. Файлы и медиа запрещены в этой группе. No comment provided by engineer. @@ -3633,38 +3633,38 @@ Error: %2$@ Ссылки групп No comment provided by engineer. - - Group members can add message reactions. + + Members can add message reactions. Члены группы могут добавлять реакции на сообщения. No comment provided by engineer. - - Group members can irreversibly delete sent messages. (24 hours) + + Members can irreversibly delete sent messages. (24 hours) Члены группы могут необратимо удалять отправленные сообщения. (24 часа) No comment provided by engineer. - - Group members can send SimpleX links. + + Members can send SimpleX links. Члены группы могут отправлять ссылки SimpleX. No comment provided by engineer. - - Group members can send direct messages. + + Members can send direct messages. Члены группы могут посылать прямые сообщения. No comment provided by engineer. - - Group members can send disappearing messages. + + Members can send disappearing messages. Члены группы могут посылать исчезающие сообщения. No comment provided by engineer. - - Group members can send files and media. + + Members can send files and media. Члены группы могут слать файлы и медиа. No comment provided by engineer. - - Group members can send voice messages. + + Members can send voice messages. Члены группы могут отправлять голосовые сообщения. No comment provided by engineer. @@ -4079,8 +4079,8 @@ More improvements are coming soon! Необратимое удаление сообщений запрещено в этом чате. No comment provided by engineer. - - Irreversible message deletion is prohibited in this group. + + Irreversible message deletion is prohibited. Необратимое удаление сообщений запрещено в этой группе. No comment provided by engineer. @@ -4417,8 +4417,8 @@ This is your link for group %@! Реакции на сообщения в этом чате запрещены. No comment provided by engineer. - - Message reactions are prohibited in this group. + + Message reactions are prohibited. Реакции на сообщения запрещены в этой группе. No comment provided by engineer. @@ -6633,8 +6633,8 @@ Enable in *Network & servers* settings. SimpleX ссылки chat feature - - SimpleX links are prohibited in this group. + + SimpleX links are prohibited. Ссылки SimpleX запрещены в этой группе. No comment provided by engineer. @@ -7712,8 +7712,8 @@ To connect, please ask your contact to create another connection link and check Голосовые сообщения запрещены в этом чате. No comment provided by engineer. - - Voice messages are prohibited in this group. + + Voice messages are prohibited. Голосовые сообщения запрещены в этой группе. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff index f3097bf8f2..cec1637438 100644 --- a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff +++ b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff @@ -2290,8 +2290,8 @@ This is your own one-time link! ข้อความโดยตรง chat feature - - Direct messages between members are prohibited in this group. + + Direct messages between members are prohibited. ข้อความโดยตรงระหว่างสมาชิกเป็นสิ่งต้องห้ามในกลุ่มนี้ No comment provided by engineer. @@ -2329,8 +2329,8 @@ This is your own one-time link! ข้อความที่จะหายไปหลังเวลาที่กําหนด (disappearing message) เป็นสิ่งต้องห้ามในแชทนี้ No comment provided by engineer. - - Disappearing messages are prohibited in this group. + + Disappearing messages are prohibited. ข้อความที่จะหายไปหลังเวลาที่กําหนด (disappearing message) เป็นสิ่งต้องห้ามในกลุ่มนี้ No comment provided by engineer. @@ -3119,8 +3119,8 @@ This is your own one-time link! ไฟล์และสื่อ chat feature - - Files and media are prohibited in this group. + + Files and media are prohibited. ไฟล์และสื่อเป็นสิ่งต้องห้ามในกลุ่มนี้ No comment provided by engineer. @@ -3364,37 +3364,37 @@ Error: %2$@ ลิงค์กลุ่ม No comment provided by engineer. - - Group members can add message reactions. + + Members can add message reactions. สมาชิกกลุ่มสามารถเพิ่มการแสดงปฏิกิริยาต่อข้อความได้ No comment provided by engineer. - - Group members can irreversibly delete sent messages. (24 hours) + + Members can irreversibly delete sent messages. (24 hours) สมาชิกกลุ่มสามารถลบข้อความที่ส่งแล้วอย่างถาวร No comment provided by engineer. - - Group members can send SimpleX links. + + Members can send SimpleX links. No comment provided by engineer. - - Group members can send direct messages. + + Members can send direct messages. สมาชิกกลุ่มสามารถส่งข้อความโดยตรงได้ No comment provided by engineer. - - Group members can send disappearing messages. + + Members can send disappearing messages. สมาชิกกลุ่มสามารถส่งข้อความที่จะหายไปหลังจากเวลาที่กำหนดหลังการอ่าน (disappearing messages) ได้ No comment provided by engineer. - - Group members can send files and media. + + Members can send files and media. สมาชิกกลุ่มสามารถส่งไฟล์และสื่อ No comment provided by engineer. - - Group members can send voice messages. + + Members can send voice messages. สมาชิกกลุ่มสามารถส่งข้อความเสียง No comment provided by engineer. @@ -3788,8 +3788,8 @@ More improvements are coming soon! ไม่สามารถลบข้อความแบบแก้ไขไม่ได้ในแชทนี้ No comment provided by engineer. - - Irreversible message deletion is prohibited in this group. + + Irreversible message deletion is prohibited. การลบข้อความแบบแก้ไขไม่ได้เป็นสิ่งที่ห้ามในกลุ่มนี้ No comment provided by engineer. @@ -4105,8 +4105,8 @@ This is your link for group %@! ห้ามแสดงปฏิกิริยาบนข้อความในแชทนี้ No comment provided by engineer. - - Message reactions are prohibited in this group. + + Message reactions are prohibited. ปฏิกิริยาบนข้อความเป็นสิ่งต้องห้ามในกลุ่มนี้ No comment provided by engineer. @@ -6149,8 +6149,8 @@ Enable in *Network & servers* settings. ลิงก์ SimpleX chat feature - - SimpleX links are prohibited in this group. + + SimpleX links are prohibited. No comment provided by engineer. @@ -7141,8 +7141,8 @@ To connect, please ask your contact to create another connection link and check ห้ามส่งข้อความเสียงในแชทนี้ No comment provided by engineer. - - Voice messages are prohibited in this group. + + Voice messages are prohibited. ข้อความเสียงเป็นสิ่งต้องห้ามในกลุ่มนี้ No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff index eca1c67b85..8ab4b2419d 100644 --- a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff @@ -2472,8 +2472,8 @@ Bu senin kendi tek kullanımlık bağlantın! Doğrudan mesajlar chat feature - - Direct messages between members are prohibited in this group. + + Direct messages between members are prohibited. Bu grupta üyeler arasında direkt mesajlaşma yasaktır. No comment provided by engineer. @@ -2512,8 +2512,8 @@ Bu senin kendi tek kullanımlık bağlantın! Kaybolan mesajlar bu sohbette yasaklanmış. No comment provided by engineer. - - Disappearing messages are prohibited in this group. + + Disappearing messages are prohibited. Kaybolan mesajlar bu grupta yasaklanmış. No comment provided by engineer. @@ -3362,8 +3362,8 @@ Bu senin kendi tek kullanımlık bağlantın! Dosyalar ve medya chat feature - - Files and media are prohibited in this group. + + Files and media are prohibited. Dosyalar ve medya bu grupta yasaklandı. No comment provided by engineer. @@ -3632,38 +3632,38 @@ Hata: %2$@ Grup bağlantıları No comment provided by engineer. - - Group members can add message reactions. + + Members can add message reactions. Grup üyeleri mesaj tepkileri ekleyebilir. No comment provided by engineer. - - Group members can irreversibly delete sent messages. (24 hours) + + Members can irreversibly delete sent messages. (24 hours) Grup üyeleri, gönderilen mesajları kalıcı olarak silebilir. (24 saat içinde) No comment provided by engineer. - - Group members can send SimpleX links. + + Members can send SimpleX links. Grup üyeleri SimpleX bağlantıları gönderebilir. No comment provided by engineer. - - Group members can send direct messages. + + Members can send direct messages. Grup üyeleri doğrudan mesajlar gönderebilir. No comment provided by engineer. - - Group members can send disappearing messages. + + Members can send disappearing messages. Grup üyeleri kaybolan mesajlar gönderebilir. No comment provided by engineer. - - Group members can send files and media. + + Members can send files and media. Grup üyeleri dosyalar ve medya gönderebilir. No comment provided by engineer. - - Group members can send voice messages. + + Members can send voice messages. Grup üyeleri sesli mesajlar gönderebilir. No comment provided by engineer. @@ -4079,8 +4079,8 @@ Daha fazla iyileştirme yakında geliyor! Bu sohbette geri döndürülemez mesaj silme yasaktır. No comment provided by engineer. - - Irreversible message deletion is prohibited in this group. + + Irreversible message deletion is prohibited. Bu grupta geri döndürülemez mesaj silme yasaktır. No comment provided by engineer. @@ -4417,8 +4417,8 @@ Bu senin grup için bağlantın %@! Mesaj tepkileri bu sohbette yasaklandı. No comment provided by engineer. - - Message reactions are prohibited in this group. + + Message reactions are prohibited. Mesaj tepkileri bu grupta yasaklandı. No comment provided by engineer. @@ -6633,8 +6633,8 @@ Enable in *Network & servers* settings. SimpleX bağlantıları chat feature - - SimpleX links are prohibited in this group. + + SimpleX links are prohibited. SimpleX bağlantıları bu grupta yasaklandı. No comment provided by engineer. @@ -7712,8 +7712,8 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Bu sohbette sesli mesajlar yasaktır. No comment provided by engineer. - - Voice messages are prohibited in this group. + + Voice messages are prohibited. Bu grupta sesli mesajlar yasaktır. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff index 0d05edbfbe..801b3d0c79 100644 --- a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff @@ -2500,8 +2500,8 @@ This is your own one-time link! Прямі повідомлення chat feature - - Direct messages between members are prohibited in this group. + + Direct messages between members are prohibited. У цій групі заборонені прямі повідомлення між учасниками. No comment provided by engineer. @@ -2540,8 +2540,8 @@ This is your own one-time link! Зникаючі повідомлення в цьому чаті заборонені. No comment provided by engineer. - - Disappearing messages are prohibited in this group. + + Disappearing messages are prohibited. У цій групі заборонено зникаючі повідомлення. No comment provided by engineer. @@ -3398,8 +3398,8 @@ This is your own one-time link! Файли і медіа chat feature - - Files and media are prohibited in this group. + + Files and media are prohibited. Файли та медіа в цій групі заборонені. No comment provided by engineer. @@ -3672,38 +3672,38 @@ Error: %2$@ Групові посилання No comment provided by engineer. - - Group members can add message reactions. + + Members can add message reactions. Учасники групи можуть додавати реакції на повідомлення. No comment provided by engineer. - - Group members can irreversibly delete sent messages. (24 hours) + + Members can irreversibly delete sent messages. (24 hours) Учасники групи можуть безповоротно видаляти надіслані повідомлення. (24 години) No comment provided by engineer. - - Group members can send SimpleX links. + + Members can send SimpleX links. Учасники групи можуть надсилати посилання SimpleX. No comment provided by engineer. - - Group members can send direct messages. + + Members can send direct messages. Учасники групи можуть надсилати прямі повідомлення. No comment provided by engineer. - - Group members can send disappearing messages. + + Members can send disappearing messages. Учасники групи можуть надсилати зникаючі повідомлення. No comment provided by engineer. - - Group members can send files and media. + + Members can send files and media. Учасники групи можуть надсилати файли та медіа. No comment provided by engineer. - - Group members can send voice messages. + + Members can send voice messages. Учасники групи можуть надсилати голосові повідомлення. No comment provided by engineer. @@ -4121,8 +4121,8 @@ More improvements are coming soon! У цьому чаті заборонено безповоротне видалення повідомлень. No comment provided by engineer. - - Irreversible message deletion is prohibited in this group. + + Irreversible message deletion is prohibited. У цій групі заборонено безповоротне видалення повідомлень. No comment provided by engineer. @@ -4459,8 +4459,8 @@ This is your link for group %@! Реакції на повідомлення в цьому чаті заборонені. No comment provided by engineer. - - Message reactions are prohibited in this group. + + Message reactions are prohibited. Реакції на повідомлення в цій групі заборонені. No comment provided by engineer. @@ -6707,8 +6707,8 @@ Enable in *Network & servers* settings. Посилання SimpleX chat feature - - SimpleX links are prohibited in this group. + + SimpleX links are prohibited. У цій групі заборонені посилання на SimpleX. No comment provided by engineer. @@ -7804,8 +7804,8 @@ To connect, please ask your contact to create another connection link and check Голосові повідомлення в цьому чаті заборонені. No comment provided by engineer. - - Voice messages are prohibited in this group. + + Voice messages are prohibited. Голосові повідомлення в цій групі заборонені. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff index 300a33d7b4..d9cb36f971 100644 --- a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff +++ b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff @@ -2455,8 +2455,8 @@ This is your own one-time link! 私信 chat feature - - Direct messages between members are prohibited in this group. + + Direct messages between members are prohibited. 此群中禁止成员之间私信。 No comment provided by engineer. @@ -2495,8 +2495,8 @@ This is your own one-time link! 此聊天中禁止显示限时消息。 No comment provided by engineer. - - Disappearing messages are prohibited in this group. + + Disappearing messages are prohibited. 该组禁止限时消息。 No comment provided by engineer. @@ -3337,8 +3337,8 @@ This is your own one-time link! 文件和媒体 chat feature - - Files and media are prohibited in this group. + + Files and media are prohibited. 此群组中禁止文件和媒体。 No comment provided by engineer. @@ -3602,38 +3602,38 @@ Error: %2$@ 群组链接 No comment provided by engineer. - - Group members can add message reactions. + + Members can add message reactions. 群组成员可以添加信息回应。 No comment provided by engineer. - - Group members can irreversibly delete sent messages. (24 hours) + + Members can irreversibly delete sent messages. (24 hours) 群组成员可以不可撤回地删除已发送的消息 No comment provided by engineer. - - Group members can send SimpleX links. + + Members can send SimpleX links. 群成员可发送 SimpleX 链接。 No comment provided by engineer. - - Group members can send direct messages. + + Members can send direct messages. 群组成员可以私信。 No comment provided by engineer. - - Group members can send disappearing messages. + + Members can send disappearing messages. 群组成员可以发送限时消息。 No comment provided by engineer. - - Group members can send files and media. + + Members can send files and media. 群组成员可以发送文件和媒体。 No comment provided by engineer. - - Group members can send voice messages. + + Members can send voice messages. 群组成员可以发送语音消息。 No comment provided by engineer. @@ -4046,8 +4046,8 @@ More improvements are coming soon! 此聊天中禁止不可撤回消息移除。 No comment provided by engineer. - - Irreversible message deletion is prohibited in this group. + + Irreversible message deletion is prohibited. 此群组中禁止不可撤回消息移除。 No comment provided by engineer. @@ -4384,8 +4384,8 @@ This is your link for group %@! 该聊天禁用了消息回应。 No comment provided by engineer. - - Message reactions are prohibited in this group. + + Message reactions are prohibited. 该群组禁用了消息回应。 No comment provided by engineer. @@ -6580,8 +6580,8 @@ Enable in *Network & servers* settings. SimpleX 链接 chat feature - - SimpleX links are prohibited in this group. + + SimpleX links are prohibited. 此群禁止 SimpleX 链接。 No comment provided by engineer. @@ -7649,8 +7649,8 @@ To connect, please ask your contact to create another connection link and check 语音信息在此聊天中被禁止。 No comment provided by engineer. - - Voice messages are prohibited in this group. + + Voice messages are prohibited. 语音信息在该群组中被禁用。 No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/zh-Hant.xcloc/Localized Contents/zh-Hant.xliff b/apps/ios/SimpleX Localizations/zh-Hant.xcloc/Localized Contents/zh-Hant.xliff index da4f843974..93b9725131 100644 --- a/apps/ios/SimpleX Localizations/zh-Hant.xcloc/Localized Contents/zh-Hant.xliff +++ b/apps/ios/SimpleX Localizations/zh-Hant.xcloc/Localized Contents/zh-Hant.xliff @@ -1173,8 +1173,8 @@ 私訊 chat feature - - Direct messages between members are prohibited in this group. + + Direct messages between members are prohibited. 私訊群組內的成員於這個群組內是禁用的。 No comment provided by engineer. @@ -1193,8 +1193,8 @@ 自動銷毀訊息已被禁止於此聊天室。 No comment provided by engineer. - - Disappearing messages are prohibited in this group. + + Disappearing messages are prohibited. 自動銷毀訊息於這個群組內是禁用的。 No comment provided by engineer. @@ -1618,18 +1618,18 @@ 群組內的成員可以不可逆地刪除訊息。 No comment provided by engineer. - - Group members can send direct messages. + + Members can send direct messages. 群組內的成員可以私訊群組內的成員。 No comment provided by engineer. - - Group members can send disappearing messages. + + Members can send disappearing messages. 群組內的成員可以傳送自動銷毀的訊息。 No comment provided by engineer. - - Group members can send voice messages. + + Members can send voice messages. 群組內的成員可以傳送語音訊息。 No comment provided by engineer. @@ -1864,8 +1864,8 @@ 不可逆地刪除訊息於這個聊天室內是禁用的。 No comment provided by engineer. - - Irreversible message deletion is prohibited in this group. + + Irreversible message deletion is prohibited. 不可逆地刪除訊息於這個群組內是禁用的。 No comment provided by engineer. @@ -3316,8 +3316,8 @@ To connect, please ask your contact to create another connection link and check 語音訊息於這個聊天窒是禁用的。 No comment provided by engineer. - - Voice messages are prohibited in this group. + + Voice messages are prohibited. 語音訊息於這個群組內是禁用的。 No comment provided by engineer. @@ -5513,8 +5513,8 @@ It can happen because of some bug or when the connection is compromised.啟用自毀密碼 set passcode view - - Group members can add message reactions. + + Members can add message reactions. 群組內的成員可以新增訊息互動。 No comment provided by engineer. @@ -5689,8 +5689,8 @@ It can happen because of some bug or when the connection is compromised.已移除在 No comment provided by engineer. - - Message reactions are prohibited in this group. + + Message reactions are prohibited. 訊息互動於這個群組內是禁用的。 No comment provided by engineer. diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index b2532c1dc1..5379cce236 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -821,38 +821,38 @@ public enum GroupFeature: String, Decodable, Feature, Hashable { switch self { case .timedMessages: switch enabled { - case .on: return "Group members can send disappearing messages." - case .off: return "Disappearing messages are prohibited in this group." + case .on: return "Members can send disappearing messages." + case .off: return "Disappearing messages are prohibited." } case .directMessages: switch enabled { - case .on: return "Group members can send direct messages." - case .off: return "Direct messages between members are prohibited in this group." + case .on: return "Members can send direct messages." + case .off: return "Direct messages between members are prohibited." } case .fullDelete: switch enabled { - case .on: return "Group members can irreversibly delete sent messages. (24 hours)" - case .off: return "Irreversible message deletion is prohibited in this group." + case .on: return "Members can irreversibly delete sent messages. (24 hours)" + case .off: return "Irreversible message deletion is prohibited." } case .reactions: switch enabled { - case .on: return "Group members can add message reactions." - case .off: return "Message reactions are prohibited in this group." + case .on: return "Members can add message reactions." + case .off: return "Message reactions are prohibited." } case .voice: switch enabled { - case .on: return "Group members can send voice messages." - case .off: return "Voice messages are prohibited in this group." + case .on: return "Members can send voice messages." + case .off: return "Voice messages are prohibited." } case .files: switch enabled { - case .on: return "Group members can send files and media." - case .off: return "Files and media are prohibited in this group." + case .on: return "Members can send files and media." + case .off: return "Files and media are prohibited." } case .simplexLinks: switch enabled { - case .on: return "Group members can send SimpleX links." - case .off: return "SimpleX links are prohibited in this group." + case .on: return "Members can send SimpleX links." + case .off: return "SimpleX links are prohibited." } case .history: switch enabled { diff --git a/apps/ios/bg.lproj/Localizable.strings b/apps/ios/bg.lproj/Localizable.strings index aa955d7a7a..a63ca87a99 100644 --- a/apps/ios/bg.lproj/Localizable.strings +++ b/apps/ios/bg.lproj/Localizable.strings @@ -1299,7 +1299,7 @@ "Direct messages" = "Лични съобщения"; /* No comment provided by engineer. */ -"Direct messages between members are prohibited in this group." = "Личните съобщения между членовете са забранени в тази група."; +"Direct messages between members are prohibited." = "Личните съобщения между членовете са забранени в тази група."; /* No comment provided by engineer. */ "Disable (keep overrides)" = "Деактивиране (запазване на промените)"; @@ -1323,7 +1323,7 @@ "Disappearing messages are prohibited in this chat." = "Изчезващите съобщения са забранени в този чат."; /* No comment provided by engineer. */ -"Disappearing messages are prohibited in this group." = "Изчезващите съобщения са забранени в тази група."; +"Disappearing messages are prohibited." = "Изчезващите съобщения са забранени в тази група."; /* No comment provided by engineer. */ "Disappears at" = "Изчезва в"; @@ -1786,7 +1786,7 @@ "Files and media" = "Файлове и медия"; /* No comment provided by engineer. */ -"Files and media are prohibited in this group." = "Файловете и медията са забранени в тази група."; +"Files and media are prohibited." = "Файловете и медията са забранени в тази група."; /* No comment provided by engineer. */ "Files and media not allowed" = "Файлове и медия не са разрешени"; @@ -1906,25 +1906,25 @@ "Group links" = "Групови линкове"; /* No comment provided by engineer. */ -"Group members can add message reactions." = "Членовете на групата могат да добавят реакции към съобщенията."; +"Members can add message reactions." = "Членовете на групата могат да добавят реакции към съобщенията."; /* No comment provided by engineer. */ -"Group members can irreversibly delete sent messages. (24 hours)" = "Членовете на групата могат необратимо да изтриват изпратените съобщения. (24 часа)"; +"Members can irreversibly delete sent messages. (24 hours)" = "Членовете на групата могат необратимо да изтриват изпратените съобщения. (24 часа)"; /* No comment provided by engineer. */ -"Group members can send direct messages." = "Членовете на групата могат да изпращат лични съобщения."; +"Members can send direct messages." = "Членовете на групата могат да изпращат лични съобщения."; /* No comment provided by engineer. */ -"Group members can send disappearing messages." = "Членовете на групата могат да изпращат изчезващи съобщения."; +"Members can send disappearing messages." = "Членовете на групата могат да изпращат изчезващи съобщения."; /* No comment provided by engineer. */ -"Group members can send files and media." = "Членовете на групата могат да изпращат файлове и медия."; +"Members can send files and media." = "Членовете на групата могат да изпращат файлове и медия."; /* No comment provided by engineer. */ -"Group members can send SimpleX links." = "Членовете на групата могат да изпращат SimpleX линкове."; +"Members can send SimpleX links." = "Членовете на групата могат да изпращат SimpleX линкове."; /* No comment provided by engineer. */ -"Group members can send voice messages." = "Членовете на групата могат да изпращат гласови съобщения."; +"Members can send voice messages." = "Членовете на групата могат да изпращат гласови съобщения."; /* notification */ "Group message:" = "Групово съобщение:"; @@ -2203,7 +2203,7 @@ "Irreversible message deletion is prohibited in this chat." = "Необратимото изтриване на съобщения е забранено в този чат."; /* No comment provided by engineer. */ -"Irreversible message deletion is prohibited in this group." = "Необратимото изтриване на съобщения е забранено в тази група."; +"Irreversible message deletion is prohibited." = "Необратимото изтриване на съобщения е забранено в тази група."; /* No comment provided by engineer. */ "It allows having many anonymous connections without any shared data between them in a single chat profile." = "Позволява да имате много анонимни връзки без споделени данни между тях в един чат профил ."; @@ -2392,7 +2392,7 @@ "Message reactions are prohibited in this chat." = "Реакциите на съобщения са забранени в този чат."; /* No comment provided by engineer. */ -"Message reactions are prohibited in this group." = "Реакциите на съобщения са забранени в тази група."; +"Message reactions are prohibited." = "Реакциите на съобщения са забранени в тази група."; /* notification */ "message received" = "получено съобщение"; @@ -3450,7 +3450,7 @@ "SimpleX links" = "SimpleX линкове"; /* No comment provided by engineer. */ -"SimpleX links are prohibited in this group." = "SimpleX линкове са забранени в тази група."; +"SimpleX links are prohibited." = "SimpleX линкове са забранени в тази група."; /* No comment provided by engineer. */ "SimpleX links not allowed" = "SimpleX линковете не са разрешени"; @@ -3987,7 +3987,7 @@ "Voice messages are prohibited in this chat." = "Гласовите съобщения са забранени в този чат."; /* No comment provided by engineer. */ -"Voice messages are prohibited in this group." = "Гласовите съобщения са забранени в тази група."; +"Voice messages are prohibited." = "Гласовите съобщения са забранени в тази група."; /* No comment provided by engineer. */ "Voice messages not allowed" = "Гласовите съобщения не са разрешени"; diff --git a/apps/ios/cs.lproj/Localizable.strings b/apps/ios/cs.lproj/Localizable.strings index 9e96aafcfd..a150c2427f 100644 --- a/apps/ios/cs.lproj/Localizable.strings +++ b/apps/ios/cs.lproj/Localizable.strings @@ -1059,7 +1059,7 @@ "Direct messages" = "Přímé zprávy"; /* No comment provided by engineer. */ -"Direct messages between members are prohibited in this group." = "Přímé zprávy mezi členy jsou v této skupině zakázány."; +"Direct messages between members are prohibited." = "Přímé zprávy mezi členy jsou v této skupině zakázány."; /* No comment provided by engineer. */ "Disable (keep overrides)" = "Vypnout (zachovat přepsání)"; @@ -1083,7 +1083,7 @@ "Disappearing messages are prohibited in this chat." = "Mizící zprávy jsou v tomto chatu zakázány."; /* No comment provided by engineer. */ -"Disappearing messages are prohibited in this group." = "Mizící zprávy jsou v této skupině zakázány."; +"Disappearing messages are prohibited." = "Mizící zprávy jsou v této skupině zakázány."; /* No comment provided by engineer. */ "Disappears at" = "Zmizí v"; @@ -1461,7 +1461,7 @@ "Files and media" = "Soubory a média"; /* No comment provided by engineer. */ -"Files and media are prohibited in this group." = "Soubory a média jsou zakázány v této skupině."; +"Files and media are prohibited." = "Soubory a média jsou zakázány v této skupině."; /* No comment provided by engineer. */ "Files and media prohibited!" = "Soubory a média jsou zakázány!"; @@ -1545,22 +1545,22 @@ "Group links" = "Odkazy na skupiny"; /* No comment provided by engineer. */ -"Group members can add message reactions." = "Členové skupin mohou přidávat reakce na zprávy."; +"Members can add message reactions." = "Členové skupin mohou přidávat reakce na zprávy."; /* No comment provided by engineer. */ -"Group members can irreversibly delete sent messages. (24 hours)" = "Členové skupiny mohou nevratně mazat odeslané zprávy. (24 hodin)"; +"Members can irreversibly delete sent messages. (24 hours)" = "Členové skupiny mohou nevratně mazat odeslané zprávy. (24 hodin)"; /* No comment provided by engineer. */ -"Group members can send direct messages." = "Členové skupiny mohou posílat přímé zprávy."; +"Members can send direct messages." = "Členové skupiny mohou posílat přímé zprávy."; /* No comment provided by engineer. */ -"Group members can send disappearing messages." = "Členové skupiny mohou posílat mizící zprávy."; +"Members can send disappearing messages." = "Členové skupiny mohou posílat mizící zprávy."; /* No comment provided by engineer. */ -"Group members can send files and media." = "Členové skupiny mohou posílat soubory a média."; +"Members can send files and media." = "Členové skupiny mohou posílat soubory a média."; /* No comment provided by engineer. */ -"Group members can send voice messages." = "Členové skupiny mohou posílat hlasové zprávy."; +"Members can send voice messages." = "Členové skupiny mohou posílat hlasové zprávy."; /* notification */ "Group message:" = "Skupinová zpráva:"; @@ -1794,7 +1794,7 @@ "Irreversible message deletion is prohibited in this chat." = "Nevratné mazání zpráv je v tomto chatu zakázáno."; /* No comment provided by engineer. */ -"Irreversible message deletion is prohibited in this group." = "Nevratné mazání zpráv je v této skupině zakázáno."; +"Irreversible message deletion is prohibited." = "Nevratné mazání zpráv je v této skupině zakázáno."; /* No comment provided by engineer. */ "It allows having many anonymous connections without any shared data between them in a single chat profile." = "Umožňuje mít v jednom profilu chatu mnoho anonymních spojení bez jakýchkoli sdílených údajů mezi nimi."; @@ -1950,7 +1950,7 @@ "Message reactions are prohibited in this chat." = "Reakce na zprávy jsou v tomto chatu zakázány."; /* No comment provided by engineer. */ -"Message reactions are prohibited in this group." = "Reakce na zprávy jsou v této skupině zakázány."; +"Message reactions are prohibited." = "Reakce na zprávy jsou v této skupině zakázány."; /* notification */ "message received" = "zpráva přijata"; @@ -3206,7 +3206,7 @@ "Voice messages are prohibited in this chat." = "Hlasové zprávy jsou v tomto chatu zakázány."; /* No comment provided by engineer. */ -"Voice messages are prohibited in this group." = "Hlasové zprávy jsou v této skupině zakázány."; +"Voice messages are prohibited." = "Hlasové zprávy jsou v této skupině zakázány."; /* No comment provided by engineer. */ "Voice messages prohibited!" = "Hlasové zprávy jsou zakázány!"; diff --git a/apps/ios/de.lproj/Localizable.strings b/apps/ios/de.lproj/Localizable.strings index 1e92d094b4..6231000330 100644 --- a/apps/ios/de.lproj/Localizable.strings +++ b/apps/ios/de.lproj/Localizable.strings @@ -1656,7 +1656,7 @@ "Direct messages" = "Direkte Nachrichten"; /* No comment provided by engineer. */ -"Direct messages between members are prohibited in this group." = "In dieser Gruppe sind Direktnachrichten zwischen Mitgliedern nicht erlaubt."; +"Direct messages between members are prohibited." = "In dieser Gruppe sind Direktnachrichten zwischen Mitgliedern nicht erlaubt."; /* No comment provided by engineer. */ "Disable (keep overrides)" = "Deaktivieren (vorgenommene Einstellungen bleiben erhalten)"; @@ -1683,7 +1683,7 @@ "Disappearing messages are prohibited in this chat." = "In diesem Chat sind verschwindende Nachrichten nicht erlaubt."; /* No comment provided by engineer. */ -"Disappearing messages are prohibited in this group." = "In dieser Gruppe sind verschwindende Nachrichten nicht erlaubt."; +"Disappearing messages are prohibited." = "In dieser Gruppe sind verschwindende Nachrichten nicht erlaubt."; /* No comment provided by engineer. */ "Disappears at" = "Verschwindet um"; @@ -2254,7 +2254,7 @@ "Files and media" = "Dateien und Medien"; /* No comment provided by engineer. */ -"Files and media are prohibited in this group." = "In dieser Gruppe sind Dateien und Medien nicht erlaubt."; +"Files and media are prohibited." = "In dieser Gruppe sind Dateien und Medien nicht erlaubt."; /* No comment provided by engineer. */ "Files and media not allowed" = "Dateien und Medien sind nicht erlaubt"; @@ -2425,25 +2425,25 @@ "Group links" = "Gruppen-Links"; /* No comment provided by engineer. */ -"Group members can add message reactions." = "Gruppenmitglieder können eine Reaktion auf Nachrichten geben."; +"Members can add message reactions." = "Gruppenmitglieder können eine Reaktion auf Nachrichten geben."; /* No comment provided by engineer. */ -"Group members can irreversibly delete sent messages. (24 hours)" = "Gruppenmitglieder können gesendete Nachrichten unwiederbringlich löschen. (24 Stunden)"; +"Members can irreversibly delete sent messages. (24 hours)" = "Gruppenmitglieder können gesendete Nachrichten unwiederbringlich löschen. (24 Stunden)"; /* No comment provided by engineer. */ -"Group members can send direct messages." = "Gruppenmitglieder können Direktnachrichten versenden."; +"Members can send direct messages." = "Gruppenmitglieder können Direktnachrichten versenden."; /* No comment provided by engineer. */ -"Group members can send disappearing messages." = "Gruppenmitglieder können verschwindende Nachrichten senden."; +"Members can send disappearing messages." = "Gruppenmitglieder können verschwindende Nachrichten senden."; /* No comment provided by engineer. */ -"Group members can send files and media." = "Gruppenmitglieder können Dateien und Medien senden."; +"Members can send files and media." = "Gruppenmitglieder können Dateien und Medien senden."; /* No comment provided by engineer. */ -"Group members can send SimpleX links." = "Gruppenmitglieder können SimpleX-Links senden."; +"Members can send SimpleX links." = "Gruppenmitglieder können SimpleX-Links senden."; /* No comment provided by engineer. */ -"Group members can send voice messages." = "Gruppenmitglieder können Sprachnachrichten versenden."; +"Members can send voice messages." = "Gruppenmitglieder können Sprachnachrichten versenden."; /* notification */ "Group message:" = "Grppennachricht:"; @@ -2746,7 +2746,7 @@ "Irreversible message deletion is prohibited in this chat." = "In diesem Chat ist das unwiederbringliche Löschen von Nachrichten nicht erlaubt."; /* No comment provided by engineer. */ -"Irreversible message deletion is prohibited in this group." = "In dieser Gruppe ist das unwiederbringliche Löschen von Nachrichten nicht erlaubt."; +"Irreversible message deletion is prohibited." = "In dieser Gruppe ist das unwiederbringliche Löschen von Nachrichten nicht erlaubt."; /* No comment provided by engineer. */ "It allows having many anonymous connections without any shared data between them in a single chat profile." = "Er ermöglicht mehrere anonyme Verbindungen in einem einzigen Chat-Profil ohne Daten zwischen diesen zu teilen."; @@ -2968,7 +2968,7 @@ "Message reactions are prohibited in this chat." = "In diesem Chat sind Reaktionen auf Nachrichten nicht erlaubt."; /* No comment provided by engineer. */ -"Message reactions are prohibited in this group." = "In dieser Gruppe sind Reaktionen auf Nachrichten nicht erlaubt."; +"Message reactions are prohibited." = "In dieser Gruppe sind Reaktionen auf Nachrichten nicht erlaubt."; /* notification */ "message received" = "Nachricht empfangen"; @@ -4416,7 +4416,7 @@ "SimpleX links" = "SimpleX-Links"; /* No comment provided by engineer. */ -"SimpleX links are prohibited in this group." = "In dieser Gruppe sind SimpleX-Links nicht erlaubt."; +"SimpleX links are prohibited." = "In dieser Gruppe sind SimpleX-Links nicht erlaubt."; /* No comment provided by engineer. */ "SimpleX links not allowed" = "SimpleX-Links sind nicht erlaubt"; @@ -5148,7 +5148,7 @@ "Voice messages are prohibited in this chat." = "In diesem Chat sind Sprachnachrichten nicht erlaubt."; /* No comment provided by engineer. */ -"Voice messages are prohibited in this group." = "In dieser Gruppe sind Sprachnachrichten nicht erlaubt."; +"Voice messages are prohibited." = "In dieser Gruppe sind Sprachnachrichten nicht erlaubt."; /* No comment provided by engineer. */ "Voice messages not allowed" = "Sprachnachrichten sind nicht erlaubt"; diff --git a/apps/ios/es.lproj/Localizable.strings b/apps/ios/es.lproj/Localizable.strings index d02497515e..d15e6d75ce 100644 --- a/apps/ios/es.lproj/Localizable.strings +++ b/apps/ios/es.lproj/Localizable.strings @@ -1656,7 +1656,7 @@ "Direct messages" = "Mensajes directos"; /* No comment provided by engineer. */ -"Direct messages between members are prohibited in this group." = "Los mensajes directos entre miembros del grupo no están permitidos."; +"Direct messages between members are prohibited." = "Los mensajes directos entre miembros del grupo no están permitidos."; /* No comment provided by engineer. */ "Disable (keep overrides)" = "Desactivar (conservando anulaciones)"; @@ -1683,7 +1683,7 @@ "Disappearing messages are prohibited in this chat." = "Los mensajes temporales no están permitidos en este chat."; /* No comment provided by engineer. */ -"Disappearing messages are prohibited in this group." = "Los mensajes temporales no están permitidos en este grupo."; +"Disappearing messages are prohibited." = "Los mensajes temporales no están permitidos en este grupo."; /* No comment provided by engineer. */ "Disappears at" = "Desaparecerá"; @@ -2254,7 +2254,7 @@ "Files and media" = "Archivos y multimedia"; /* No comment provided by engineer. */ -"Files and media are prohibited in this group." = "Los archivos y multimedia no están permitidos en este grupo."; +"Files and media are prohibited." = "Los archivos y multimedia no están permitidos en este grupo."; /* No comment provided by engineer. */ "Files and media not allowed" = "Archivos y multimedia no permitidos"; @@ -2425,25 +2425,25 @@ "Group links" = "Enlaces de grupo"; /* No comment provided by engineer. */ -"Group members can add message reactions." = "Los miembros pueden añadir reacciones a los mensajes."; +"Members can add message reactions." = "Los miembros pueden añadir reacciones a los mensajes."; /* No comment provided by engineer. */ -"Group members can irreversibly delete sent messages. (24 hours)" = "Los miembros del grupo pueden eliminar mensajes de forma irreversible. (24 horas)"; +"Members can irreversibly delete sent messages. (24 hours)" = "Los miembros del grupo pueden eliminar mensajes de forma irreversible. (24 horas)"; /* No comment provided by engineer. */ -"Group members can send direct messages." = "Los miembros del grupo pueden enviar mensajes directos."; +"Members can send direct messages." = "Los miembros del grupo pueden enviar mensajes directos."; /* No comment provided by engineer. */ -"Group members can send disappearing messages." = "Los miembros del grupo pueden enviar mensajes temporales."; +"Members can send disappearing messages." = "Los miembros del grupo pueden enviar mensajes temporales."; /* No comment provided by engineer. */ -"Group members can send files and media." = "Los miembros del grupo pueden enviar archivos y multimedia."; +"Members can send files and media." = "Los miembros del grupo pueden enviar archivos y multimedia."; /* No comment provided by engineer. */ -"Group members can send SimpleX links." = "Los miembros del grupo pueden enviar enlaces SimpleX."; +"Members can send SimpleX links." = "Los miembros del grupo pueden enviar enlaces SimpleX."; /* No comment provided by engineer. */ -"Group members can send voice messages." = "Los miembros del grupo pueden enviar mensajes de voz."; +"Members can send voice messages." = "Los miembros del grupo pueden enviar mensajes de voz."; /* notification */ "Group message:" = "Mensaje de grupo:"; @@ -2746,7 +2746,7 @@ "Irreversible message deletion is prohibited in this chat." = "La eliminación irreversible de mensajes no está permitida en este chat."; /* No comment provided by engineer. */ -"Irreversible message deletion is prohibited in this group." = "La eliminación irreversible de mensajes no está permitida en este grupo."; +"Irreversible message deletion is prohibited." = "La eliminación irreversible de mensajes no está permitida en este grupo."; /* No comment provided by engineer. */ "It allows having many anonymous connections without any shared data between them in a single chat profile." = "Permite tener varias conexiones anónimas sin datos compartidos entre estas dentro del mismo perfil."; @@ -2968,7 +2968,7 @@ "Message reactions are prohibited in this chat." = "Las reacciones a los mensajes no están permitidas en este chat."; /* No comment provided by engineer. */ -"Message reactions are prohibited in this group." = "Las reacciones a los mensajes no están permitidas en este grupo."; +"Message reactions are prohibited." = "Las reacciones a los mensajes no están permitidas en este grupo."; /* notification */ "message received" = "mensaje recibido"; @@ -4416,7 +4416,7 @@ "SimpleX links" = "Enlaces SimpleX"; /* No comment provided by engineer. */ -"SimpleX links are prohibited in this group." = "Los enlaces SimpleX no se permiten en este grupo."; +"SimpleX links are prohibited." = "Los enlaces SimpleX no se permiten en este grupo."; /* No comment provided by engineer. */ "SimpleX links not allowed" = "Enlaces SimpleX no permitidos"; @@ -5148,7 +5148,7 @@ "Voice messages are prohibited in this chat." = "Los mensajes de voz no están permitidos en este chat."; /* No comment provided by engineer. */ -"Voice messages are prohibited in this group." = "Los mensajes de voz no están permitidos en este grupo."; +"Voice messages are prohibited." = "Los mensajes de voz no están permitidos en este grupo."; /* No comment provided by engineer. */ "Voice messages not allowed" = "Mensajes de voz no permitidos"; diff --git a/apps/ios/fi.lproj/Localizable.strings b/apps/ios/fi.lproj/Localizable.strings index 6f28ddd3b0..8946be02b4 100644 --- a/apps/ios/fi.lproj/Localizable.strings +++ b/apps/ios/fi.lproj/Localizable.strings @@ -1041,7 +1041,7 @@ "Direct messages" = "Yksityisviestit"; /* No comment provided by engineer. */ -"Direct messages between members are prohibited in this group." = "Yksityisviestit jäsenten välillä ovat kiellettyjä tässä ryhmässä."; +"Direct messages between members are prohibited." = "Yksityisviestit jäsenten välillä ovat kiellettyjä tässä ryhmässä."; /* No comment provided by engineer. */ "Disable (keep overrides)" = "Poista käytöstä (pidä ohitukset)"; @@ -1065,7 +1065,7 @@ "Disappearing messages are prohibited in this chat." = "Katoavat viestit ovat kiellettyjä tässä keskustelussa."; /* No comment provided by engineer. */ -"Disappearing messages are prohibited in this group." = "Katoavat viestit ovat kiellettyjä tässä ryhmässä."; +"Disappearing messages are prohibited." = "Katoavat viestit ovat kiellettyjä tässä ryhmässä."; /* No comment provided by engineer. */ "Disappears at" = "Katoaa klo"; @@ -1437,7 +1437,7 @@ "Files and media" = "Tiedostot ja media"; /* No comment provided by engineer. */ -"Files and media are prohibited in this group." = "Tiedostot ja media ovat tässä ryhmässä kiellettyjä."; +"Files and media are prohibited." = "Tiedostot ja media ovat tässä ryhmässä kiellettyjä."; /* No comment provided by engineer. */ "Files and media prohibited!" = "Tiedostot ja media kielletty!"; @@ -1521,22 +1521,22 @@ "Group links" = "Ryhmälinkit"; /* No comment provided by engineer. */ -"Group members can add message reactions." = "Ryhmän jäsenet voivat lisätä viestireaktioita."; +"Members can add message reactions." = "Ryhmän jäsenet voivat lisätä viestireaktioita."; /* No comment provided by engineer. */ -"Group members can irreversibly delete sent messages. (24 hours)" = "Ryhmän jäsenet voivat poistaa lähetetyt viestit peruuttamattomasti. (24 tuntia)"; +"Members can irreversibly delete sent messages. (24 hours)" = "Ryhmän jäsenet voivat poistaa lähetetyt viestit peruuttamattomasti. (24 tuntia)"; /* No comment provided by engineer. */ -"Group members can send direct messages." = "Ryhmän jäsenet voivat lähettää suoraviestejä."; +"Members can send direct messages." = "Ryhmän jäsenet voivat lähettää suoraviestejä."; /* No comment provided by engineer. */ -"Group members can send disappearing messages." = "Ryhmän jäsenet voivat lähettää katoavia viestejä."; +"Members can send disappearing messages." = "Ryhmän jäsenet voivat lähettää katoavia viestejä."; /* No comment provided by engineer. */ -"Group members can send files and media." = "Ryhmän jäsenet voivat lähettää tiedostoja ja mediaa."; +"Members can send files and media." = "Ryhmän jäsenet voivat lähettää tiedostoja ja mediaa."; /* No comment provided by engineer. */ -"Group members can send voice messages." = "Ryhmän jäsenet voivat lähettää ääniviestejä."; +"Members can send voice messages." = "Ryhmän jäsenet voivat lähettää ääniviestejä."; /* notification */ "Group message:" = "Ryhmäviesti:"; @@ -1770,7 +1770,7 @@ "Irreversible message deletion is prohibited in this chat." = "Viestien peruuttamaton poisto on kielletty tässä keskustelussa."; /* No comment provided by engineer. */ -"Irreversible message deletion is prohibited in this group." = "Viestien peruuttamaton poisto on kielletty tässä ryhmässä."; +"Irreversible message deletion is prohibited." = "Viestien peruuttamaton poisto on kielletty tässä ryhmässä."; /* No comment provided by engineer. */ "It allows having many anonymous connections without any shared data between them in a single chat profile." = "Se mahdollistaa useiden nimettömien yhteyksien muodostamisen yhdessä keskusteluprofiilissa ilman, että niiden välillä on jaettuja tietoja."; @@ -1926,7 +1926,7 @@ "Message reactions are prohibited in this chat." = "Viestireaktiot ovat kiellettyjä tässä keskustelussa."; /* No comment provided by engineer. */ -"Message reactions are prohibited in this group." = "Viestireaktiot ovat kiellettyjä tässä ryhmässä."; +"Message reactions are prohibited." = "Viestireaktiot ovat kiellettyjä tässä ryhmässä."; /* notification */ "message received" = "viesti vastaanotettu"; @@ -3164,7 +3164,7 @@ "Voice messages are prohibited in this chat." = "Ääniviestit ovat kiellettyjä tässä keskustelussa."; /* No comment provided by engineer. */ -"Voice messages are prohibited in this group." = "Ääniviestit ovat kiellettyjä tässä ryhmässä."; +"Voice messages are prohibited." = "Ääniviestit ovat kiellettyjä tässä ryhmässä."; /* No comment provided by engineer. */ "Voice messages prohibited!" = "Ääniviestit kielletty!"; diff --git a/apps/ios/fr.lproj/Localizable.strings b/apps/ios/fr.lproj/Localizable.strings index 273fb76d6e..f1a8e97758 100644 --- a/apps/ios/fr.lproj/Localizable.strings +++ b/apps/ios/fr.lproj/Localizable.strings @@ -1572,7 +1572,7 @@ "Direct messages" = "Messages directs"; /* No comment provided by engineer. */ -"Direct messages between members are prohibited in this group." = "Les messages directs entre membres sont interdits dans ce groupe."; +"Direct messages between members are prohibited." = "Les messages directs entre membres sont interdits dans ce groupe."; /* No comment provided by engineer. */ "Disable (keep overrides)" = "Désactiver (conserver les remplacements)"; @@ -1599,7 +1599,7 @@ "Disappearing messages are prohibited in this chat." = "Les messages éphémères sont interdits dans cette discussion."; /* No comment provided by engineer. */ -"Disappearing messages are prohibited in this group." = "Les messages éphémères sont interdits dans ce groupe."; +"Disappearing messages are prohibited." = "Les messages éphémères sont interdits dans ce groupe."; /* No comment provided by engineer. */ "Disappears at" = "Disparaîtra le"; @@ -2146,7 +2146,7 @@ "Files and media" = "Fichiers et médias"; /* No comment provided by engineer. */ -"Files and media are prohibited in this group." = "Les fichiers et les médias sont interdits dans ce groupe."; +"Files and media are prohibited." = "Les fichiers et les médias sont interdits dans ce groupe."; /* No comment provided by engineer. */ "Files and media not allowed" = "Fichiers et médias non autorisés"; @@ -2302,25 +2302,25 @@ "Group links" = "Liens de groupe"; /* No comment provided by engineer. */ -"Group members can add message reactions." = "Les membres du groupe peuvent ajouter des réactions aux messages."; +"Members can add message reactions." = "Les membres du groupe peuvent ajouter des réactions aux messages."; /* No comment provided by engineer. */ -"Group members can irreversibly delete sent messages. (24 hours)" = "Les membres du groupe peuvent supprimer de manière irréversible les messages envoyés. (24 heures)"; +"Members can irreversibly delete sent messages. (24 hours)" = "Les membres du groupe peuvent supprimer de manière irréversible les messages envoyés. (24 heures)"; /* No comment provided by engineer. */ -"Group members can send direct messages." = "Les membres du groupe peuvent envoyer des messages directs."; +"Members can send direct messages." = "Les membres du groupe peuvent envoyer des messages directs."; /* No comment provided by engineer. */ -"Group members can send disappearing messages." = "Les membres du groupes peuvent envoyer des messages éphémères."; +"Members can send disappearing messages." = "Les membres du groupes peuvent envoyer des messages éphémères."; /* No comment provided by engineer. */ -"Group members can send files and media." = "Les membres du groupe peuvent envoyer des fichiers et des médias."; +"Members can send files and media." = "Les membres du groupe peuvent envoyer des fichiers et des médias."; /* No comment provided by engineer. */ -"Group members can send SimpleX links." = "Les membres du groupe peuvent envoyer des liens SimpleX."; +"Members can send SimpleX links." = "Les membres du groupe peuvent envoyer des liens SimpleX."; /* No comment provided by engineer. */ -"Group members can send voice messages." = "Les membres du groupe peuvent envoyer des messages vocaux."; +"Members can send voice messages." = "Les membres du groupe peuvent envoyer des messages vocaux."; /* notification */ "Group message:" = "Message du groupe :"; @@ -2617,7 +2617,7 @@ "Irreversible message deletion is prohibited in this chat." = "La suppression irréversible de message est interdite dans ce chat."; /* No comment provided by engineer. */ -"Irreversible message deletion is prohibited in this group." = "La suppression irréversible de messages est interdite dans ce groupe."; +"Irreversible message deletion is prohibited." = "La suppression irréversible de messages est interdite dans ce groupe."; /* No comment provided by engineer. */ "It allows having many anonymous connections without any shared data between them in a single chat profile." = "Cela permet d'avoir plusieurs connections anonymes sans aucune données partagées entre elles sur un même profil."; @@ -2839,7 +2839,7 @@ "Message reactions are prohibited in this chat." = "Les réactions aux messages sont interdites dans ce chat."; /* No comment provided by engineer. */ -"Message reactions are prohibited in this group." = "Les réactions aux messages sont interdites dans ce groupe."; +"Message reactions are prohibited." = "Les réactions aux messages sont interdites dans ce groupe."; /* notification */ "message received" = "message reçu"; @@ -4191,7 +4191,7 @@ "SimpleX links" = "Liens SimpleX"; /* No comment provided by engineer. */ -"SimpleX links are prohibited in this group." = "Les liens SimpleX sont interdits dans ce groupe."; +"SimpleX links are prohibited." = "Les liens SimpleX sont interdits dans ce groupe."; /* No comment provided by engineer. */ "SimpleX links not allowed" = "Les liens SimpleX ne sont pas autorisés"; @@ -4872,7 +4872,7 @@ "Voice messages are prohibited in this chat." = "Les messages vocaux sont interdits dans ce chat."; /* No comment provided by engineer. */ -"Voice messages are prohibited in this group." = "Les messages vocaux sont interdits dans ce groupe."; +"Voice messages are prohibited." = "Les messages vocaux sont interdits dans ce groupe."; /* No comment provided by engineer. */ "Voice messages not allowed" = "Les messages vocaux ne sont pas autorisés"; diff --git a/apps/ios/hu.lproj/Localizable.strings b/apps/ios/hu.lproj/Localizable.strings index 9103f4baf3..c1008ad30b 100644 --- a/apps/ios/hu.lproj/Localizable.strings +++ b/apps/ios/hu.lproj/Localizable.strings @@ -1656,7 +1656,7 @@ "Direct messages" = "Közvetlen üzenetek"; /* No comment provided by engineer. */ -"Direct messages between members are prohibited in this group." = "A közvetlen üzenetek küldése a tagok között le van tiltva ebben a csoportban."; +"Direct messages between members are prohibited." = "A közvetlen üzenetek küldése a tagok között le van tiltva ebben a csoportban."; /* No comment provided by engineer. */ "Disable (keep overrides)" = "Letiltás (felülírások megtartásával)"; @@ -1683,7 +1683,7 @@ "Disappearing messages are prohibited in this chat." = "Az eltűnő üzenetek küldése le van tiltva ebben a csevegésben."; /* No comment provided by engineer. */ -"Disappearing messages are prohibited in this group." = "Az eltűnő üzenetek küldése le van tiltva ebben a csoportban."; +"Disappearing messages are prohibited." = "Az eltűnő üzenetek küldése le van tiltva ebben a csoportban."; /* No comment provided by engineer. */ "Disappears at" = "Eltűnik ekkor:"; @@ -2254,7 +2254,7 @@ "Files and media" = "Fájlok és médiatartalmak"; /* No comment provided by engineer. */ -"Files and media are prohibited in this group." = "A fájlok- és a médiatartalmak le vannak tiltva ebben a csoportban."; +"Files and media are prohibited." = "A fájlok- és a médiatartalmak le vannak tiltva ebben a csoportban."; /* No comment provided by engineer. */ "Files and media not allowed" = "A fájlok- és médiatartalmak nincsenek engedélyezve"; @@ -2425,25 +2425,25 @@ "Group links" = "Csoporthivatkozások"; /* No comment provided by engineer. */ -"Group members can add message reactions." = "Csoporttagok üzenetreakciókat adhatnak hozzá."; +"Members can add message reactions." = "Csoporttagok üzenetreakciókat adhatnak hozzá."; /* No comment provided by engineer. */ -"Group members can irreversibly delete sent messages. (24 hours)" = "A csoport tagjai véglegesen törölhetik az elküldött üzeneteiket. (24 óra)"; +"Members can irreversibly delete sent messages. (24 hours)" = "A csoport tagjai véglegesen törölhetik az elküldött üzeneteiket. (24 óra)"; /* No comment provided by engineer. */ -"Group members can send direct messages." = "A csoport tagjai küldhetnek egymásnak közvetlen üzeneteket."; +"Members can send direct messages." = "A csoport tagjai küldhetnek egymásnak közvetlen üzeneteket."; /* No comment provided by engineer. */ -"Group members can send disappearing messages." = "A csoport tagjai küldhetnek eltűnő üzeneteket."; +"Members can send disappearing messages." = "A csoport tagjai küldhetnek eltűnő üzeneteket."; /* No comment provided by engineer. */ -"Group members can send files and media." = "A csoport tagjai küldhetnek fájlokat és médiatartalmakat."; +"Members can send files and media." = "A csoport tagjai küldhetnek fájlokat és médiatartalmakat."; /* No comment provided by engineer. */ -"Group members can send SimpleX links." = "A csoport tagjai küldhetnek SimpleX-hivatkozásokat."; +"Members can send SimpleX links." = "A csoport tagjai küldhetnek SimpleX-hivatkozásokat."; /* No comment provided by engineer. */ -"Group members can send voice messages." = "A csoport tagjai küldhetnek hangüzeneteket."; +"Members can send voice messages." = "A csoport tagjai küldhetnek hangüzeneteket."; /* notification */ "Group message:" = "Csoport üzenet:"; @@ -2746,7 +2746,7 @@ "Irreversible message deletion is prohibited in this chat." = "Az üzenetek végleges törlése le van tiltva ebben a csevegésben."; /* No comment provided by engineer. */ -"Irreversible message deletion is prohibited in this group." = "Az üzenetek végleges törlése le van tiltva ebben a csoportban."; +"Irreversible message deletion is prohibited." = "Az üzenetek végleges törlése le van tiltva ebben a csoportban."; /* No comment provided by engineer. */ "It allows having many anonymous connections without any shared data between them in a single chat profile." = "Lehetővé teszi, hogy egyetlen csevegőprofilon belül több névtelen kapcsolat legyen, anélkül, hogy megosztott adatok lennének közöttük."; @@ -2968,7 +2968,7 @@ "Message reactions are prohibited in this chat." = "Az üzenetreakciók küldése le van tiltva ebben a csevegésben."; /* No comment provided by engineer. */ -"Message reactions are prohibited in this group." = "Az üzenetreakciók küldése le van tiltva ebben a csoportban."; +"Message reactions are prohibited." = "Az üzenetreakciók küldése le van tiltva ebben a csoportban."; /* notification */ "message received" = "üzenet érkezett"; @@ -4416,7 +4416,7 @@ "SimpleX links" = "SimpleX-hivatkozások"; /* No comment provided by engineer. */ -"SimpleX links are prohibited in this group." = "A SimpleX-hivatkozások küldése le van tiltva ebben a csoportban."; +"SimpleX links are prohibited." = "A SimpleX-hivatkozások küldése le van tiltva ebben a csoportban."; /* No comment provided by engineer. */ "SimpleX links not allowed" = "A SimpleX-hivatkozások küldése le van tiltva"; @@ -5148,7 +5148,7 @@ "Voice messages are prohibited in this chat." = "A hangüzenetek küldése le van tiltva ebben a csevegésben."; /* No comment provided by engineer. */ -"Voice messages are prohibited in this group." = "A hangüzenetek küldése le van tiltva ebben a csoportban."; +"Voice messages are prohibited." = "A hangüzenetek küldése le van tiltva ebben a csoportban."; /* No comment provided by engineer. */ "Voice messages not allowed" = "A hangüzenetek küldése le van tiltva"; diff --git a/apps/ios/it.lproj/Localizable.strings b/apps/ios/it.lproj/Localizable.strings index 9abd660f0c..42f399b710 100644 --- a/apps/ios/it.lproj/Localizable.strings +++ b/apps/ios/it.lproj/Localizable.strings @@ -1656,7 +1656,7 @@ "Direct messages" = "Messaggi diretti"; /* No comment provided by engineer. */ -"Direct messages between members are prohibited in this group." = "I messaggi diretti tra i membri sono vietati in questo gruppo."; +"Direct messages between members are prohibited." = "I messaggi diretti tra i membri sono vietati in questo gruppo."; /* No comment provided by engineer. */ "Disable (keep overrides)" = "Disattiva (mantieni sostituzioni)"; @@ -1683,7 +1683,7 @@ "Disappearing messages are prohibited in this chat." = "I messaggi a tempo sono vietati in questa chat."; /* No comment provided by engineer. */ -"Disappearing messages are prohibited in this group." = "I messaggi a tempo sono vietati in questo gruppo."; +"Disappearing messages are prohibited." = "I messaggi a tempo sono vietati in questo gruppo."; /* No comment provided by engineer. */ "Disappears at" = "Scompare il"; @@ -2254,7 +2254,7 @@ "Files and media" = "File e multimediali"; /* No comment provided by engineer. */ -"Files and media are prohibited in this group." = "File e contenuti multimediali sono vietati in questo gruppo."; +"Files and media are prohibited." = "File e contenuti multimediali sono vietati in questo gruppo."; /* No comment provided by engineer. */ "Files and media not allowed" = "File e multimediali non consentiti"; @@ -2425,25 +2425,25 @@ "Group links" = "Link del gruppo"; /* No comment provided by engineer. */ -"Group members can add message reactions." = "I membri del gruppo possono aggiungere reazioni ai messaggi."; +"Members can add message reactions." = "I membri del gruppo possono aggiungere reazioni ai messaggi."; /* No comment provided by engineer. */ -"Group members can irreversibly delete sent messages. (24 hours)" = "I membri del gruppo possono eliminare irreversibilmente i messaggi inviati. (24 ore)"; +"Members can irreversibly delete sent messages. (24 hours)" = "I membri del gruppo possono eliminare irreversibilmente i messaggi inviati. (24 ore)"; /* No comment provided by engineer. */ -"Group members can send direct messages." = "I membri del gruppo possono inviare messaggi diretti."; +"Members can send direct messages." = "I membri del gruppo possono inviare messaggi diretti."; /* No comment provided by engineer. */ -"Group members can send disappearing messages." = "I membri del gruppo possono inviare messaggi a tempo."; +"Members can send disappearing messages." = "I membri del gruppo possono inviare messaggi a tempo."; /* No comment provided by engineer. */ -"Group members can send files and media." = "I membri del gruppo possono inviare file e contenuti multimediali."; +"Members can send files and media." = "I membri del gruppo possono inviare file e contenuti multimediali."; /* No comment provided by engineer. */ -"Group members can send SimpleX links." = "I membri del gruppo possono inviare link di Simplex."; +"Members can send SimpleX links." = "I membri del gruppo possono inviare link di Simplex."; /* No comment provided by engineer. */ -"Group members can send voice messages." = "I membri del gruppo possono inviare messaggi vocali."; +"Members can send voice messages." = "I membri del gruppo possono inviare messaggi vocali."; /* notification */ "Group message:" = "Messaggio del gruppo:"; @@ -2746,7 +2746,7 @@ "Irreversible message deletion is prohibited in this chat." = "L'eliminazione irreversibile dei messaggi è vietata in questa chat."; /* No comment provided by engineer. */ -"Irreversible message deletion is prohibited in this group." = "L'eliminazione irreversibile dei messaggi è vietata in questo gruppo."; +"Irreversible message deletion is prohibited." = "L'eliminazione irreversibile dei messaggi è vietata in questo gruppo."; /* No comment provided by engineer. */ "It allows having many anonymous connections without any shared data between them in a single chat profile." = "Permette di avere molte connessioni anonime senza dati condivisi tra di loro in un unico profilo di chat."; @@ -2968,7 +2968,7 @@ "Message reactions are prohibited in this chat." = "Le reazioni ai messaggi sono vietate in questa chat."; /* No comment provided by engineer. */ -"Message reactions are prohibited in this group." = "Le reazioni ai messaggi sono vietate in questo gruppo."; +"Message reactions are prohibited." = "Le reazioni ai messaggi sono vietate in questo gruppo."; /* notification */ "message received" = "messaggio ricevuto"; @@ -4416,7 +4416,7 @@ "SimpleX links" = "Link di SimpleX"; /* No comment provided by engineer. */ -"SimpleX links are prohibited in this group." = "I link di SimpleX sono vietati in questo gruppo."; +"SimpleX links are prohibited." = "I link di SimpleX sono vietati in questo gruppo."; /* No comment provided by engineer. */ "SimpleX links not allowed" = "Link di SimpleX non consentiti"; @@ -5148,7 +5148,7 @@ "Voice messages are prohibited in this chat." = "I messaggi vocali sono vietati in questa chat."; /* No comment provided by engineer. */ -"Voice messages are prohibited in this group." = "I messaggi vocali sono vietati in questo gruppo."; +"Voice messages are prohibited." = "I messaggi vocali sono vietati in questo gruppo."; /* No comment provided by engineer. */ "Voice messages not allowed" = "Messaggi vocali non consentiti"; diff --git a/apps/ios/ja.lproj/Localizable.strings b/apps/ios/ja.lproj/Localizable.strings index 5bcd706702..019472b804 100644 --- a/apps/ios/ja.lproj/Localizable.strings +++ b/apps/ios/ja.lproj/Localizable.strings @@ -1179,7 +1179,7 @@ "Direct messages" = "ダイレクトメッセージ"; /* No comment provided by engineer. */ -"Direct messages between members are prohibited in this group." = "このグループではメンバー間のダイレクトメッセージが使用禁止です。"; +"Direct messages between members are prohibited." = "このグループではメンバー間のダイレクトメッセージが使用禁止です。"; /* No comment provided by engineer. */ "Disable (keep overrides)" = "無効にする(設定の優先を維持)"; @@ -1203,7 +1203,7 @@ "Disappearing messages are prohibited in this chat." = "このチャットでは消えるメッセージが使用禁止です。"; /* No comment provided by engineer. */ -"Disappearing messages are prohibited in this group." = "このグループでは消えるメッセージが使用禁止です。"; +"Disappearing messages are prohibited." = "このグループでは消えるメッセージが使用禁止です。"; /* No comment provided by engineer. */ "Disappears at" = "に消えます"; @@ -1578,7 +1578,7 @@ "Files and media" = "ファイルとメディア"; /* No comment provided by engineer. */ -"Files and media are prohibited in this group." = "このグループでは、ファイルとメディアは禁止されています。"; +"Files and media are prohibited." = "このグループでは、ファイルとメディアは禁止されています。"; /* No comment provided by engineer. */ "Files and media prohibited!" = "ファイルとメディアは禁止されています!"; @@ -1662,22 +1662,22 @@ "Group links" = "グループのリンク"; /* No comment provided by engineer. */ -"Group members can add message reactions." = "グループメンバーはメッセージへのリアクションを追加できます。"; +"Members can add message reactions." = "グループメンバーはメッセージへのリアクションを追加できます。"; /* No comment provided by engineer. */ -"Group members can irreversibly delete sent messages. (24 hours)" = "グループのメンバーがメッセージを完全削除することができます。(24時間)"; +"Members can irreversibly delete sent messages. (24 hours)" = "グループのメンバーがメッセージを完全削除することができます。(24時間)"; /* No comment provided by engineer. */ -"Group members can send direct messages." = "グループのメンバーがダイレクトメッセージを送信できます。"; +"Members can send direct messages." = "グループのメンバーがダイレクトメッセージを送信できます。"; /* No comment provided by engineer. */ -"Group members can send disappearing messages." = "グループのメンバーが消えるメッセージを送信できます。"; +"Members can send disappearing messages." = "グループのメンバーが消えるメッセージを送信できます。"; /* No comment provided by engineer. */ -"Group members can send files and media." = "グループメンバーはファイルやメディアを送信できます。"; +"Members can send files and media." = "グループメンバーはファイルやメディアを送信できます。"; /* No comment provided by engineer. */ -"Group members can send voice messages." = "グループのメンバーが音声メッセージを送信できます。"; +"Members can send voice messages." = "グループのメンバーが音声メッセージを送信できます。"; /* notification */ "Group message:" = "グループメッセージ:"; @@ -1911,7 +1911,7 @@ "Irreversible message deletion is prohibited in this chat." = "このチャットではメッセージの完全削除が使用禁止です。"; /* No comment provided by engineer. */ -"Irreversible message deletion is prohibited in this group." = "このグループではメッセージの完全削除が使用禁止です。"; +"Irreversible message deletion is prohibited." = "このグループではメッセージの完全削除が使用禁止です。"; /* No comment provided by engineer. */ "It allows having many anonymous connections without any shared data between them in a single chat profile." = "これにより単一のチャット プロファイル内で、データを共有せずに多数の匿名の接続をすることができます。"; @@ -2064,7 +2064,7 @@ "Message reactions are prohibited in this chat." = "このチャットではメッセージへのリアクションは禁止されています。"; /* No comment provided by engineer. */ -"Message reactions are prohibited in this group." = "このグループではメッセージへのリアクションは禁止されています。"; +"Message reactions are prohibited." = "このグループではメッセージへのリアクションは禁止されています。"; /* notification */ "message received" = "メッセージを受信"; @@ -3290,7 +3290,7 @@ "Voice messages are prohibited in this chat." = "このチャットでは音声メッセージが使用禁止です。"; /* No comment provided by engineer. */ -"Voice messages are prohibited in this group." = "このグループでは音声メッセージが使用禁止です。"; +"Voice messages are prohibited." = "このグループでは音声メッセージが使用禁止です。"; /* No comment provided by engineer. */ "Voice messages prohibited!" = "音声メッセージは使用禁止です!"; diff --git a/apps/ios/nl.lproj/Localizable.strings b/apps/ios/nl.lproj/Localizable.strings index fadec1b09b..652ccdf63c 100644 --- a/apps/ios/nl.lproj/Localizable.strings +++ b/apps/ios/nl.lproj/Localizable.strings @@ -1656,7 +1656,7 @@ "Direct messages" = "Directe berichten"; /* No comment provided by engineer. */ -"Direct messages between members are prohibited in this group." = "Directe berichten tussen leden zijn verboden in deze groep."; +"Direct messages between members are prohibited." = "Directe berichten tussen leden zijn verboden in deze groep."; /* No comment provided by engineer. */ "Disable (keep overrides)" = "Uitschakelen (overschrijvingen behouden)"; @@ -1683,7 +1683,7 @@ "Disappearing messages are prohibited in this chat." = "Verdwijnende berichten zijn verboden in dit gesprek."; /* No comment provided by engineer. */ -"Disappearing messages are prohibited in this group." = "Verdwijnende berichten zijn verboden in deze groep."; +"Disappearing messages are prohibited." = "Verdwijnende berichten zijn verboden in deze groep."; /* No comment provided by engineer. */ "Disappears at" = "Verdwijnt op"; @@ -2254,7 +2254,7 @@ "Files and media" = "Bestanden en media"; /* No comment provided by engineer. */ -"Files and media are prohibited in this group." = "Bestanden en media zijn verboden in deze groep."; +"Files and media are prohibited." = "Bestanden en media zijn verboden in deze groep."; /* No comment provided by engineer. */ "Files and media not allowed" = "Bestanden en media niet toegestaan"; @@ -2425,25 +2425,25 @@ "Group links" = "Groep links"; /* No comment provided by engineer. */ -"Group members can add message reactions." = "Groepsleden kunnen bericht reacties toevoegen."; +"Members can add message reactions." = "Groepsleden kunnen bericht reacties toevoegen."; /* No comment provided by engineer. */ -"Group members can irreversibly delete sent messages. (24 hours)" = "Groepsleden kunnen verzonden berichten onherroepelijk verwijderen. (24 uur)"; +"Members can irreversibly delete sent messages. (24 hours)" = "Groepsleden kunnen verzonden berichten onherroepelijk verwijderen. (24 uur)"; /* No comment provided by engineer. */ -"Group members can send direct messages." = "Groepsleden kunnen directe berichten sturen."; +"Members can send direct messages." = "Groepsleden kunnen directe berichten sturen."; /* No comment provided by engineer. */ -"Group members can send disappearing messages." = "Groepsleden kunnen verdwijnende berichten sturen."; +"Members can send disappearing messages." = "Groepsleden kunnen verdwijnende berichten sturen."; /* No comment provided by engineer. */ -"Group members can send files and media." = "Groepsleden kunnen bestanden en media verzenden."; +"Members can send files and media." = "Groepsleden kunnen bestanden en media verzenden."; /* No comment provided by engineer. */ -"Group members can send SimpleX links." = "Groepsleden kunnen SimpleX-links verzenden."; +"Members can send SimpleX links." = "Groepsleden kunnen SimpleX-links verzenden."; /* No comment provided by engineer. */ -"Group members can send voice messages." = "Groepsleden kunnen spraak berichten verzenden."; +"Members can send voice messages." = "Groepsleden kunnen spraak berichten verzenden."; /* notification */ "Group message:" = "Groep bericht:"; @@ -2746,7 +2746,7 @@ "Irreversible message deletion is prohibited in this chat." = "Het onomkeerbaar verwijderen van berichten is verboden in dit gesprek."; /* No comment provided by engineer. */ -"Irreversible message deletion is prohibited in this group." = "Het onomkeerbaar verwijderen van berichten is verboden in deze groep."; +"Irreversible message deletion is prohibited." = "Het onomkeerbaar verwijderen van berichten is verboden in deze groep."; /* No comment provided by engineer. */ "It allows having many anonymous connections without any shared data between them in a single chat profile." = "Het maakt het mogelijk om veel anonieme verbindingen te hebben zonder enige gedeelde gegevens tussen hen in een enkel chatprofiel."; @@ -2968,7 +2968,7 @@ "Message reactions are prohibited in this chat." = "Reacties op berichten zijn verboden in deze chat."; /* No comment provided by engineer. */ -"Message reactions are prohibited in this group." = "Reacties op berichten zijn verboden in deze groep."; +"Message reactions are prohibited." = "Reacties op berichten zijn verboden in deze groep."; /* notification */ "message received" = "bericht ontvangen"; @@ -4416,7 +4416,7 @@ "SimpleX links" = "SimpleX links"; /* No comment provided by engineer. */ -"SimpleX links are prohibited in this group." = "SimpleX-links zijn in deze groep verboden."; +"SimpleX links are prohibited." = "SimpleX-links zijn in deze groep verboden."; /* No comment provided by engineer. */ "SimpleX links not allowed" = "SimpleX-links zijn niet toegestaan"; @@ -5148,7 +5148,7 @@ "Voice messages are prohibited in this chat." = "Spraak berichten zijn verboden in deze chat."; /* No comment provided by engineer. */ -"Voice messages are prohibited in this group." = "Spraak berichten zijn verboden in deze groep."; +"Voice messages are prohibited." = "Spraak berichten zijn verboden in deze groep."; /* No comment provided by engineer. */ "Voice messages not allowed" = "Spraakberichten niet toegestaan"; diff --git a/apps/ios/pl.lproj/Localizable.strings b/apps/ios/pl.lproj/Localizable.strings index 6650b1d8c8..04279064ee 100644 --- a/apps/ios/pl.lproj/Localizable.strings +++ b/apps/ios/pl.lproj/Localizable.strings @@ -1551,7 +1551,7 @@ "Direct messages" = "Bezpośrednie wiadomości"; /* No comment provided by engineer. */ -"Direct messages between members are prohibited in this group." = "Bezpośrednie wiadomości między członkami są zabronione w tej grupie."; +"Direct messages between members are prohibited." = "Bezpośrednie wiadomości między członkami są zabronione w tej grupie."; /* No comment provided by engineer. */ "Disable (keep overrides)" = "Wyłącz (zachowaj nadpisania)"; @@ -1578,7 +1578,7 @@ "Disappearing messages are prohibited in this chat." = "Znikające wiadomości są zabronione na tym czacie."; /* No comment provided by engineer. */ -"Disappearing messages are prohibited in this group." = "Znikające wiadomości są zabronione w tej grupie."; +"Disappearing messages are prohibited." = "Znikające wiadomości są zabronione w tej grupie."; /* No comment provided by engineer. */ "Disappears at" = "Znika o"; @@ -2125,7 +2125,7 @@ "Files and media" = "Pliki i media"; /* No comment provided by engineer. */ -"Files and media are prohibited in this group." = "Pliki i media są zabronione w tej grupie."; +"Files and media are prohibited." = "Pliki i media są zabronione w tej grupie."; /* No comment provided by engineer. */ "Files and media not allowed" = "Pliki i multimedia nie są dozwolone"; @@ -2278,25 +2278,25 @@ "Group links" = "Linki grupowe"; /* No comment provided by engineer. */ -"Group members can add message reactions." = "Członkowie grupy mogą dodawać reakcje wiadomości."; +"Members can add message reactions." = "Członkowie grupy mogą dodawać reakcje wiadomości."; /* No comment provided by engineer. */ -"Group members can irreversibly delete sent messages. (24 hours)" = "Członkowie grupy mogą nieodwracalnie usuwać wysłane wiadomości. (24 godziny)"; +"Members can irreversibly delete sent messages. (24 hours)" = "Członkowie grupy mogą nieodwracalnie usuwać wysłane wiadomości. (24 godziny)"; /* No comment provided by engineer. */ -"Group members can send direct messages." = "Członkowie grupy mogą wysyłać bezpośrednie wiadomości."; +"Members can send direct messages." = "Członkowie grupy mogą wysyłać bezpośrednie wiadomości."; /* No comment provided by engineer. */ -"Group members can send disappearing messages." = "Członkowie grupy mogą wysyłać znikające wiadomości."; +"Members can send disappearing messages." = "Członkowie grupy mogą wysyłać znikające wiadomości."; /* No comment provided by engineer. */ -"Group members can send files and media." = "Członkowie grupy mogą wysyłać pliki i media."; +"Members can send files and media." = "Członkowie grupy mogą wysyłać pliki i media."; /* No comment provided by engineer. */ -"Group members can send SimpleX links." = "Członkowie grupy mogą wysyłać linki SimpleX."; +"Members can send SimpleX links." = "Członkowie grupy mogą wysyłać linki SimpleX."; /* No comment provided by engineer. */ -"Group members can send voice messages." = "Członkowie grupy mogą wysyłać wiadomości głosowe."; +"Members can send voice messages." = "Członkowie grupy mogą wysyłać wiadomości głosowe."; /* notification */ "Group message:" = "Wiadomość grupowa:"; @@ -2590,7 +2590,7 @@ "Irreversible message deletion is prohibited in this chat." = "Nieodwracalne usuwanie wiadomości jest na tym czacie zabronione."; /* No comment provided by engineer. */ -"Irreversible message deletion is prohibited in this group." = "Nieodwracalne usuwanie wiadomości jest w tej grupie zabronione."; +"Irreversible message deletion is prohibited." = "Nieodwracalne usuwanie wiadomości jest w tej grupie zabronione."; /* No comment provided by engineer. */ "It allows having many anonymous connections without any shared data between them in a single chat profile." = "To pozwala na posiadanie wielu anonimowych połączeń bez żadnych wspólnych danych między nimi w pojedynczym profilu czatu."; @@ -2812,7 +2812,7 @@ "Message reactions are prohibited in this chat." = "Reakcje wiadomości są zabronione na tym czacie."; /* No comment provided by engineer. */ -"Message reactions are prohibited in this group." = "Reakcje wiadomości są zabronione w tej grupie."; +"Message reactions are prohibited." = "Reakcje wiadomości są zabronione w tej grupie."; /* notification */ "message received" = "wiadomość otrzymana"; @@ -4164,7 +4164,7 @@ "SimpleX links" = "Linki SimpleX"; /* No comment provided by engineer. */ -"SimpleX links are prohibited in this group." = "Linki SimpleX są zablokowane na tej grupie."; +"SimpleX links are prohibited." = "Linki SimpleX są zablokowane na tej grupie."; /* No comment provided by engineer. */ "SimpleX links not allowed" = "Linki SimpleX są niedozwolone"; @@ -4836,7 +4836,7 @@ "Voice messages are prohibited in this chat." = "Wiadomości głosowe są zabronione na tym czacie."; /* No comment provided by engineer. */ -"Voice messages are prohibited in this group." = "Wiadomości głosowe są zabronione w tej grupie."; +"Voice messages are prohibited." = "Wiadomości głosowe są zabronione w tej grupie."; /* No comment provided by engineer. */ "Voice messages not allowed" = "Wiadomości głosowe są niedozwolone"; diff --git a/apps/ios/ru.lproj/Localizable.strings b/apps/ios/ru.lproj/Localizable.strings index 6db8181e8d..bb7ec81ace 100644 --- a/apps/ios/ru.lproj/Localizable.strings +++ b/apps/ios/ru.lproj/Localizable.strings @@ -1575,7 +1575,7 @@ "Direct messages" = "Прямые сообщения"; /* No comment provided by engineer. */ -"Direct messages between members are prohibited in this group." = "Прямые сообщения между членами группы запрещены."; +"Direct messages between members are prohibited." = "Прямые сообщения между членами группы запрещены."; /* No comment provided by engineer. */ "Disable (keep overrides)" = "Выключить (кроме исключений)"; @@ -1602,7 +1602,7 @@ "Disappearing messages are prohibited in this chat." = "Исчезающие сообщения запрещены в этом чате."; /* No comment provided by engineer. */ -"Disappearing messages are prohibited in this group." = "Исчезающие сообщения запрещены в этой группе."; +"Disappearing messages are prohibited." = "Исчезающие сообщения запрещены в этой группе."; /* No comment provided by engineer. */ "Disappears at" = "Исчезает"; @@ -2149,7 +2149,7 @@ "Files and media" = "Файлы и медиа"; /* No comment provided by engineer. */ -"Files and media are prohibited in this group." = "Файлы и медиа запрещены в этой группе."; +"Files and media are prohibited." = "Файлы и медиа запрещены в этой группе."; /* No comment provided by engineer. */ "Files and media not allowed" = "Файлы и медиа не разрешены"; @@ -2305,25 +2305,25 @@ "Group links" = "Ссылки групп"; /* No comment provided by engineer. */ -"Group members can add message reactions." = "Члены группы могут добавлять реакции на сообщения."; +"Members can add message reactions." = "Члены группы могут добавлять реакции на сообщения."; /* No comment provided by engineer. */ -"Group members can irreversibly delete sent messages. (24 hours)" = "Члены группы могут необратимо удалять отправленные сообщения. (24 часа)"; +"Members can irreversibly delete sent messages. (24 hours)" = "Члены группы могут необратимо удалять отправленные сообщения. (24 часа)"; /* No comment provided by engineer. */ -"Group members can send direct messages." = "Члены группы могут посылать прямые сообщения."; +"Members can send direct messages." = "Члены группы могут посылать прямые сообщения."; /* No comment provided by engineer. */ -"Group members can send disappearing messages." = "Члены группы могут посылать исчезающие сообщения."; +"Members can send disappearing messages." = "Члены группы могут посылать исчезающие сообщения."; /* No comment provided by engineer. */ -"Group members can send files and media." = "Члены группы могут слать файлы и медиа."; +"Members can send files and media." = "Члены группы могут слать файлы и медиа."; /* No comment provided by engineer. */ -"Group members can send SimpleX links." = "Члены группы могут отправлять ссылки SimpleX."; +"Members can send SimpleX links." = "Члены группы могут отправлять ссылки SimpleX."; /* No comment provided by engineer. */ -"Group members can send voice messages." = "Члены группы могут отправлять голосовые сообщения."; +"Members can send voice messages." = "Члены группы могут отправлять голосовые сообщения."; /* notification */ "Group message:" = "Групповое сообщение:"; @@ -2620,7 +2620,7 @@ "Irreversible message deletion is prohibited in this chat." = "Необратимое удаление сообщений запрещено в этом чате."; /* No comment provided by engineer. */ -"Irreversible message deletion is prohibited in this group." = "Необратимое удаление сообщений запрещено в этой группе."; +"Irreversible message deletion is prohibited." = "Необратимое удаление сообщений запрещено в этой группе."; /* No comment provided by engineer. */ "It allows having many anonymous connections without any shared data between them in a single chat profile." = "Это позволяет иметь много анонимных соединений без общих данных между ними в одном профиле пользователя."; @@ -2842,7 +2842,7 @@ "Message reactions are prohibited in this chat." = "Реакции на сообщения в этом чате запрещены."; /* No comment provided by engineer. */ -"Message reactions are prohibited in this group." = "Реакции на сообщения запрещены в этой группе."; +"Message reactions are prohibited." = "Реакции на сообщения запрещены в этой группе."; /* notification */ "message received" = "получено сообщение"; @@ -4194,7 +4194,7 @@ "SimpleX links" = "SimpleX ссылки"; /* No comment provided by engineer. */ -"SimpleX links are prohibited in this group." = "Ссылки SimpleX запрещены в этой группе."; +"SimpleX links are prohibited." = "Ссылки SimpleX запрещены в этой группе."; /* No comment provided by engineer. */ "SimpleX links not allowed" = "Ссылки SimpleX не разрешены"; @@ -4875,7 +4875,7 @@ "Voice messages are prohibited in this chat." = "Голосовые сообщения запрещены в этом чате."; /* No comment provided by engineer. */ -"Voice messages are prohibited in this group." = "Голосовые сообщения запрещены в этой группе."; +"Voice messages are prohibited." = "Голосовые сообщения запрещены в этой группе."; /* No comment provided by engineer. */ "Voice messages not allowed" = "Голосовые сообщения не разрешены"; diff --git a/apps/ios/th.lproj/Localizable.strings b/apps/ios/th.lproj/Localizable.strings index 1b3dec5ee1..962c64b710 100644 --- a/apps/ios/th.lproj/Localizable.strings +++ b/apps/ios/th.lproj/Localizable.strings @@ -1005,7 +1005,7 @@ "Direct messages" = "ข้อความโดยตรง"; /* No comment provided by engineer. */ -"Direct messages between members are prohibited in this group." = "ข้อความโดยตรงระหว่างสมาชิกเป็นสิ่งต้องห้ามในกลุ่มนี้"; +"Direct messages between members are prohibited." = "ข้อความโดยตรงระหว่างสมาชิกเป็นสิ่งต้องห้ามในกลุ่มนี้"; /* No comment provided by engineer. */ "Disable (keep overrides)" = "ปิดใช้งาน (เก็บการแทนที่)"; @@ -1026,7 +1026,7 @@ "Disappearing messages are prohibited in this chat." = "ข้อความที่จะหายไปหลังเวลาที่กําหนด (disappearing message) เป็นสิ่งต้องห้ามในแชทนี้"; /* No comment provided by engineer. */ -"Disappearing messages are prohibited in this group." = "ข้อความที่จะหายไปหลังเวลาที่กําหนด (disappearing message) เป็นสิ่งต้องห้ามในกลุ่มนี้"; +"Disappearing messages are prohibited." = "ข้อความที่จะหายไปหลังเวลาที่กําหนด (disappearing message) เป็นสิ่งต้องห้ามในกลุ่มนี้"; /* No comment provided by engineer. */ "Disappears at" = "หายไปที่"; @@ -1386,7 +1386,7 @@ "Files and media" = "ไฟล์และสื่อ"; /* No comment provided by engineer. */ -"Files and media are prohibited in this group." = "ไฟล์และสื่อเป็นสิ่งต้องห้ามในกลุ่มนี้"; +"Files and media are prohibited." = "ไฟล์และสื่อเป็นสิ่งต้องห้ามในกลุ่มนี้"; /* No comment provided by engineer. */ "Files and media prohibited!" = "ไฟล์และสื่อต้องห้าม!"; @@ -1470,22 +1470,22 @@ "Group links" = "ลิงค์กลุ่ม"; /* No comment provided by engineer. */ -"Group members can add message reactions." = "สมาชิกกลุ่มสามารถเพิ่มการแสดงปฏิกิริยาต่อข้อความได้"; +"Members can add message reactions." = "สมาชิกกลุ่มสามารถเพิ่มการแสดงปฏิกิริยาต่อข้อความได้"; /* No comment provided by engineer. */ -"Group members can irreversibly delete sent messages. (24 hours)" = "สมาชิกกลุ่มสามารถลบข้อความที่ส่งแล้วอย่างถาวร"; +"Members can irreversibly delete sent messages. (24 hours)" = "สมาชิกกลุ่มสามารถลบข้อความที่ส่งแล้วอย่างถาวร"; /* No comment provided by engineer. */ -"Group members can send direct messages." = "สมาชิกกลุ่มสามารถส่งข้อความโดยตรงได้"; +"Members can send direct messages." = "สมาชิกกลุ่มสามารถส่งข้อความโดยตรงได้"; /* No comment provided by engineer. */ -"Group members can send disappearing messages." = "สมาชิกกลุ่มสามารถส่งข้อความที่จะหายไปหลังจากเวลาที่กำหนดหลังการอ่าน (disappearing messages) ได้"; +"Members can send disappearing messages." = "สมาชิกกลุ่มสามารถส่งข้อความที่จะหายไปหลังจากเวลาที่กำหนดหลังการอ่าน (disappearing messages) ได้"; /* No comment provided by engineer. */ -"Group members can send files and media." = "สมาชิกกลุ่มสามารถส่งไฟล์และสื่อ"; +"Members can send files and media." = "สมาชิกกลุ่มสามารถส่งไฟล์และสื่อ"; /* No comment provided by engineer. */ -"Group members can send voice messages." = "สมาชิกกลุ่มสามารถส่งข้อความเสียง"; +"Members can send voice messages." = "สมาชิกกลุ่มสามารถส่งข้อความเสียง"; /* notification */ "Group message:" = "ข้อความกลุ่ม:"; @@ -1713,7 +1713,7 @@ "Irreversible message deletion is prohibited in this chat." = "ไม่สามารถลบข้อความแบบแก้ไขไม่ได้ในแชทนี้"; /* No comment provided by engineer. */ -"Irreversible message deletion is prohibited in this group." = "การลบข้อความแบบแก้ไขไม่ได้เป็นสิ่งที่ห้ามในกลุ่มนี้"; +"Irreversible message deletion is prohibited." = "การลบข้อความแบบแก้ไขไม่ได้เป็นสิ่งที่ห้ามในกลุ่มนี้"; /* No comment provided by engineer. */ "It allows having many anonymous connections without any shared data between them in a single chat profile." = "อนุญาตให้มีการเชื่อมต่อที่ไม่ระบุตัวตนจำนวนมากโดยไม่มีข้อมูลที่ใช้ร่วมกันระหว่างกันในโปรไฟล์การแชทเดียว"; @@ -1869,7 +1869,7 @@ "Message reactions are prohibited in this chat." = "ห้ามแสดงปฏิกิริยาบนข้อความในแชทนี้"; /* No comment provided by engineer. */ -"Message reactions are prohibited in this group." = "ปฏิกิริยาบนข้อความเป็นสิ่งต้องห้ามในกลุ่มนี้"; +"Message reactions are prohibited." = "ปฏิกิริยาบนข้อความเป็นสิ่งต้องห้ามในกลุ่มนี้"; /* notification */ "message received" = "ข้อความที่ได้รับ"; @@ -3071,7 +3071,7 @@ "Voice messages are prohibited in this chat." = "ห้ามส่งข้อความเสียงในแชทนี้"; /* No comment provided by engineer. */ -"Voice messages are prohibited in this group." = "ข้อความเสียงเป็นสิ่งต้องห้ามในกลุ่มนี้"; +"Voice messages are prohibited." = "ข้อความเสียงเป็นสิ่งต้องห้ามในกลุ่มนี้"; /* No comment provided by engineer. */ "Voice messages prohibited!" = "ห้ามข้อความเสียง!"; diff --git a/apps/ios/tr.lproj/Localizable.strings b/apps/ios/tr.lproj/Localizable.strings index b849dda85a..ec29de0cf3 100644 --- a/apps/ios/tr.lproj/Localizable.strings +++ b/apps/ios/tr.lproj/Localizable.strings @@ -1572,7 +1572,7 @@ "Direct messages" = "Doğrudan mesajlar"; /* No comment provided by engineer. */ -"Direct messages between members are prohibited in this group." = "Bu grupta üyeler arasında direkt mesajlaşma yasaktır."; +"Direct messages between members are prohibited." = "Bu grupta üyeler arasında direkt mesajlaşma yasaktır."; /* No comment provided by engineer. */ "Disable (keep overrides)" = "Devre dışı bırak (geçersiz kılmaları koru)"; @@ -1599,7 +1599,7 @@ "Disappearing messages are prohibited in this chat." = "Kaybolan mesajlar bu sohbette yasaklanmış."; /* No comment provided by engineer. */ -"Disappearing messages are prohibited in this group." = "Kaybolan mesajlar bu grupta yasaklanmış."; +"Disappearing messages are prohibited." = "Kaybolan mesajlar bu grupta yasaklanmış."; /* No comment provided by engineer. */ "Disappears at" = "da kaybolur"; @@ -2146,7 +2146,7 @@ "Files and media" = "Dosyalar ve medya"; /* No comment provided by engineer. */ -"Files and media are prohibited in this group." = "Dosyalar ve medya bu grupta yasaklandı."; +"Files and media are prohibited." = "Dosyalar ve medya bu grupta yasaklandı."; /* No comment provided by engineer. */ "Files and media not allowed" = "Dosyalar ve medyaya izin verilmiyor"; @@ -2302,25 +2302,25 @@ "Group links" = "Grup bağlantıları"; /* No comment provided by engineer. */ -"Group members can add message reactions." = "Grup üyeleri mesaj tepkileri ekleyebilir."; +"Members can add message reactions." = "Grup üyeleri mesaj tepkileri ekleyebilir."; /* No comment provided by engineer. */ -"Group members can irreversibly delete sent messages. (24 hours)" = "Grup üyeleri, gönderilen mesajları kalıcı olarak silebilir. (24 saat içinde)"; +"Members can irreversibly delete sent messages. (24 hours)" = "Grup üyeleri, gönderilen mesajları kalıcı olarak silebilir. (24 saat içinde)"; /* No comment provided by engineer. */ -"Group members can send direct messages." = "Grup üyeleri doğrudan mesajlar gönderebilir."; +"Members can send direct messages." = "Grup üyeleri doğrudan mesajlar gönderebilir."; /* No comment provided by engineer. */ -"Group members can send disappearing messages." = "Grup üyeleri kaybolan mesajlar gönderebilir."; +"Members can send disappearing messages." = "Grup üyeleri kaybolan mesajlar gönderebilir."; /* No comment provided by engineer. */ -"Group members can send files and media." = "Grup üyeleri dosyalar ve medya gönderebilir."; +"Members can send files and media." = "Grup üyeleri dosyalar ve medya gönderebilir."; /* No comment provided by engineer. */ -"Group members can send SimpleX links." = "Grup üyeleri SimpleX bağlantıları gönderebilir."; +"Members can send SimpleX links." = "Grup üyeleri SimpleX bağlantıları gönderebilir."; /* No comment provided by engineer. */ -"Group members can send voice messages." = "Grup üyeleri sesli mesajlar gönderebilir."; +"Members can send voice messages." = "Grup üyeleri sesli mesajlar gönderebilir."; /* notification */ "Group message:" = "Grup mesajı:"; @@ -2617,7 +2617,7 @@ "Irreversible message deletion is prohibited in this chat." = "Bu sohbette geri döndürülemez mesaj silme yasaktır."; /* No comment provided by engineer. */ -"Irreversible message deletion is prohibited in this group." = "Bu grupta geri döndürülemez mesaj silme yasaktır."; +"Irreversible message deletion is prohibited." = "Bu grupta geri döndürülemez mesaj silme yasaktır."; /* No comment provided by engineer. */ "It allows having many anonymous connections without any shared data between them in a single chat profile." = "Tek bir sohbet profilinde aralarında herhangi bir veri paylaşımı olmadan birçok anonim bağlantıya sahip olmaya izin verir."; @@ -2839,7 +2839,7 @@ "Message reactions are prohibited in this chat." = "Mesaj tepkileri bu sohbette yasaklandı."; /* No comment provided by engineer. */ -"Message reactions are prohibited in this group." = "Mesaj tepkileri bu grupta yasaklandı."; +"Message reactions are prohibited." = "Mesaj tepkileri bu grupta yasaklandı."; /* notification */ "message received" = "mesaj alındı"; @@ -4191,7 +4191,7 @@ "SimpleX links" = "SimpleX bağlantıları"; /* No comment provided by engineer. */ -"SimpleX links are prohibited in this group." = "SimpleX bağlantıları bu grupta yasaklandı."; +"SimpleX links are prohibited." = "SimpleX bağlantıları bu grupta yasaklandı."; /* No comment provided by engineer. */ "SimpleX links not allowed" = "SimpleX bağlantılarına izin verilmiyor"; @@ -4872,7 +4872,7 @@ "Voice messages are prohibited in this chat." = "Bu sohbette sesli mesajlar yasaktır."; /* No comment provided by engineer. */ -"Voice messages are prohibited in this group." = "Bu grupta sesli mesajlar yasaktır."; +"Voice messages are prohibited." = "Bu grupta sesli mesajlar yasaktır."; /* No comment provided by engineer. */ "Voice messages not allowed" = "Sesli mesajlara izin verilmiyor"; diff --git a/apps/ios/uk.lproj/Localizable.strings b/apps/ios/uk.lproj/Localizable.strings index 7b66aa5efb..28cc2839e8 100644 --- a/apps/ios/uk.lproj/Localizable.strings +++ b/apps/ios/uk.lproj/Localizable.strings @@ -1656,7 +1656,7 @@ "Direct messages" = "Прямі повідомлення"; /* No comment provided by engineer. */ -"Direct messages between members are prohibited in this group." = "У цій групі заборонені прямі повідомлення між учасниками."; +"Direct messages between members are prohibited." = "У цій групі заборонені прямі повідомлення між учасниками."; /* No comment provided by engineer. */ "Disable (keep overrides)" = "Вимкнути (зберегти перевизначення)"; @@ -1683,7 +1683,7 @@ "Disappearing messages are prohibited in this chat." = "Зникаючі повідомлення в цьому чаті заборонені."; /* No comment provided by engineer. */ -"Disappearing messages are prohibited in this group." = "У цій групі заборонено зникаючі повідомлення."; +"Disappearing messages are prohibited." = "У цій групі заборонено зникаючі повідомлення."; /* No comment provided by engineer. */ "Disappears at" = "Зникає за"; @@ -2254,7 +2254,7 @@ "Files and media" = "Файли і медіа"; /* No comment provided by engineer. */ -"Files and media are prohibited in this group." = "Файли та медіа в цій групі заборонені."; +"Files and media are prohibited." = "Файли та медіа в цій групі заборонені."; /* No comment provided by engineer. */ "Files and media not allowed" = "Файли та медіафайли заборонені"; @@ -2425,25 +2425,25 @@ "Group links" = "Групові посилання"; /* No comment provided by engineer. */ -"Group members can add message reactions." = "Учасники групи можуть додавати реакції на повідомлення."; +"Members can add message reactions." = "Учасники групи можуть додавати реакції на повідомлення."; /* No comment provided by engineer. */ -"Group members can irreversibly delete sent messages. (24 hours)" = "Учасники групи можуть безповоротно видаляти надіслані повідомлення. (24 години)"; +"Members can irreversibly delete sent messages. (24 hours)" = "Учасники групи можуть безповоротно видаляти надіслані повідомлення. (24 години)"; /* No comment provided by engineer. */ -"Group members can send direct messages." = "Учасники групи можуть надсилати прямі повідомлення."; +"Members can send direct messages." = "Учасники групи можуть надсилати прямі повідомлення."; /* No comment provided by engineer. */ -"Group members can send disappearing messages." = "Учасники групи можуть надсилати зникаючі повідомлення."; +"Members can send disappearing messages." = "Учасники групи можуть надсилати зникаючі повідомлення."; /* No comment provided by engineer. */ -"Group members can send files and media." = "Учасники групи можуть надсилати файли та медіа."; +"Members can send files and media." = "Учасники групи можуть надсилати файли та медіа."; /* No comment provided by engineer. */ -"Group members can send SimpleX links." = "Учасники групи можуть надсилати посилання SimpleX."; +"Members can send SimpleX links." = "Учасники групи можуть надсилати посилання SimpleX."; /* No comment provided by engineer. */ -"Group members can send voice messages." = "Учасники групи можуть надсилати голосові повідомлення."; +"Members can send voice messages." = "Учасники групи можуть надсилати голосові повідомлення."; /* notification */ "Group message:" = "Групове повідомлення:"; @@ -2746,7 +2746,7 @@ "Irreversible message deletion is prohibited in this chat." = "У цьому чаті заборонено безповоротне видалення повідомлень."; /* No comment provided by engineer. */ -"Irreversible message deletion is prohibited in this group." = "У цій групі заборонено безповоротне видалення повідомлень."; +"Irreversible message deletion is prohibited." = "У цій групі заборонено безповоротне видалення повідомлень."; /* No comment provided by engineer. */ "It allows having many anonymous connections without any shared data between them in a single chat profile." = "Це дозволяє мати багато анонімних з'єднань без будь-яких спільних даних між ними в одному профілі чату."; @@ -2968,7 +2968,7 @@ "Message reactions are prohibited in this chat." = "Реакції на повідомлення в цьому чаті заборонені."; /* No comment provided by engineer. */ -"Message reactions are prohibited in this group." = "Реакції на повідомлення в цій групі заборонені."; +"Message reactions are prohibited." = "Реакції на повідомлення в цій групі заборонені."; /* notification */ "message received" = "повідомлення отримано"; @@ -4416,7 +4416,7 @@ "SimpleX links" = "Посилання SimpleX"; /* No comment provided by engineer. */ -"SimpleX links are prohibited in this group." = "У цій групі заборонені посилання на SimpleX."; +"SimpleX links are prohibited." = "У цій групі заборонені посилання на SimpleX."; /* No comment provided by engineer. */ "SimpleX links not allowed" = "Посилання SimpleX заборонені"; @@ -5148,7 +5148,7 @@ "Voice messages are prohibited in this chat." = "Голосові повідомлення в цьому чаті заборонені."; /* No comment provided by engineer. */ -"Voice messages are prohibited in this group." = "Голосові повідомлення в цій групі заборонені."; +"Voice messages are prohibited." = "Голосові повідомлення в цій групі заборонені."; /* No comment provided by engineer. */ "Voice messages not allowed" = "Голосові повідомлення заборонені"; diff --git a/apps/ios/zh-Hans.lproj/Localizable.strings b/apps/ios/zh-Hans.lproj/Localizable.strings index a524b5739d..5fac1a8577 100644 --- a/apps/ios/zh-Hans.lproj/Localizable.strings +++ b/apps/ios/zh-Hans.lproj/Localizable.strings @@ -1521,7 +1521,7 @@ "Direct messages" = "私信"; /* No comment provided by engineer. */ -"Direct messages between members are prohibited in this group." = "此群中禁止成员之间私信。"; +"Direct messages between members are prohibited." = "此群中禁止成员之间私信。"; /* No comment provided by engineer. */ "Disable (keep overrides)" = "禁用(保留覆盖)"; @@ -1548,7 +1548,7 @@ "Disappearing messages are prohibited in this chat." = "此聊天中禁止显示限时消息。"; /* No comment provided by engineer. */ -"Disappearing messages are prohibited in this group." = "该组禁止限时消息。"; +"Disappearing messages are prohibited." = "该组禁止限时消息。"; /* No comment provided by engineer. */ "Disappears at" = "消失于"; @@ -2074,7 +2074,7 @@ "Files and media" = "文件和媒体"; /* No comment provided by engineer. */ -"Files and media are prohibited in this group." = "此群组中禁止文件和媒体。"; +"Files and media are prohibited." = "此群组中禁止文件和媒体。"; /* No comment provided by engineer. */ "Files and media not allowed" = "不允许文件和媒体"; @@ -2215,25 +2215,25 @@ "Group links" = "群组链接"; /* No comment provided by engineer. */ -"Group members can add message reactions." = "群组成员可以添加信息回应。"; +"Members can add message reactions." = "群组成员可以添加信息回应。"; /* No comment provided by engineer. */ -"Group members can irreversibly delete sent messages. (24 hours)" = "群组成员可以不可撤回地删除已发送的消息"; +"Members can irreversibly delete sent messages. (24 hours)" = "群组成员可以不可撤回地删除已发送的消息"; /* No comment provided by engineer. */ -"Group members can send direct messages." = "群组成员可以私信。"; +"Members can send direct messages." = "群组成员可以私信。"; /* No comment provided by engineer. */ -"Group members can send disappearing messages." = "群组成员可以发送限时消息。"; +"Members can send disappearing messages." = "群组成员可以发送限时消息。"; /* No comment provided by engineer. */ -"Group members can send files and media." = "群组成员可以发送文件和媒体。"; +"Members can send files and media." = "群组成员可以发送文件和媒体。"; /* No comment provided by engineer. */ -"Group members can send SimpleX links." = "群成员可发送 SimpleX 链接。"; +"Members can send SimpleX links." = "群成员可发送 SimpleX 链接。"; /* No comment provided by engineer. */ -"Group members can send voice messages." = "群组成员可以发送语音消息。"; +"Members can send voice messages." = "群组成员可以发送语音消息。"; /* notification */ "Group message:" = "群组消息:"; @@ -2524,7 +2524,7 @@ "Irreversible message deletion is prohibited in this chat." = "此聊天中禁止不可撤回消息移除。"; /* No comment provided by engineer. */ -"Irreversible message deletion is prohibited in this group." = "此群组中禁止不可撤回消息移除。"; +"Irreversible message deletion is prohibited." = "此群组中禁止不可撤回消息移除。"; /* No comment provided by engineer. */ "It allows having many anonymous connections without any shared data between them in a single chat profile." = "它允许在一个聊天资料中有多个匿名连接,而它们之间没有任何共享数据。"; @@ -2746,7 +2746,7 @@ "Message reactions are prohibited in this chat." = "该聊天禁用了消息回应。"; /* No comment provided by engineer. */ -"Message reactions are prohibited in this group." = "该群组禁用了消息回应。"; +"Message reactions are prohibited." = "该群组禁用了消息回应。"; /* notification */ "message received" = "消息已收到"; @@ -4044,7 +4044,7 @@ "SimpleX links" = "SimpleX 链接"; /* No comment provided by engineer. */ -"SimpleX links are prohibited in this group." = "此群禁止 SimpleX 链接。"; +"SimpleX links are prohibited." = "此群禁止 SimpleX 链接。"; /* No comment provided by engineer. */ "SimpleX links not allowed" = "不允许SimpleX 链接"; @@ -4692,7 +4692,7 @@ "Voice messages are prohibited in this chat." = "语音信息在此聊天中被禁止。"; /* No comment provided by engineer. */ -"Voice messages are prohibited in this group." = "语音信息在该群组中被禁用。"; +"Voice messages are prohibited." = "语音信息在该群组中被禁用。"; /* No comment provided by engineer. */ "Voice messages not allowed" = "不允许语音消息"; diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index f0f63f2c72..9531712554 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -5001,7 +5001,7 @@ enum class GroupFeature: Feature { } DirectMessages -> when(enabled) { GroupFeatureEnabled.ON -> generalGetString(MR.strings.group_members_can_send_dms) - GroupFeatureEnabled.OFF -> generalGetString(MR.strings.direct_messages_are_prohibited_in_chat) + GroupFeatureEnabled.OFF -> generalGetString(MR.strings.direct_messages_are_prohibited) } FullDelete -> when(enabled) { GroupFeatureEnabled.ON -> generalGetString(MR.strings.group_members_can_delete) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt index b351f56c29..25661f00a0 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt @@ -33,6 +33,7 @@ import chat.simplex.common.views.usersettings.SettingsActionItem import chat.simplex.common.model.GroupInfo import chat.simplex.common.platform.* import chat.simplex.res.MR +import dev.icerock.moko.resources.StringResource @Composable fun AddGroupMembersView(rhId: Long?, groupInfo: GroupInfo, creatingGroup: Boolean = false, chatModel: ChatModel, close: () -> Unit) { @@ -126,7 +127,8 @@ fun AddGroupMembersLayout( tint = MaterialTheme.colors.secondary, modifier = Modifier.padding(end = 10.dp).size(20.dp) ) - Text(generalGetString(MR.strings.group_main_profile_sent), textAlign = TextAlign.Center, style = MaterialTheme.typography.body2) + val textId = if (groupInfo.businessChat == null) MR.strings.group_main_profile_sent else MR.strings.chat_main_profile_sent + Text(generalGetString(textId), textAlign = TextAlign.Center, style = MaterialTheme.typography.body2) } } @@ -168,7 +170,8 @@ fun AddGroupMembersLayout( if (creatingGroup && selectedContacts.isEmpty()) { SkipInvitingButton(close) } else { - InviteMembersButton(inviteMembers, disabled = selectedContacts.isEmpty() || !allowModifyMembers) + val titleId = if (groupInfo.businessChat == null) MR.strings.invite_to_group_button else MR.strings.invite_to_chat_button + InviteMembersButton(titleId, inviteMembers, disabled = selectedContacts.isEmpty() || !allowModifyMembers) } } SectionCustomFooter { @@ -220,10 +223,10 @@ private fun RoleSelectionRow(groupInfo: GroupInfo, selectedRole: MutableState Unit, disabled: Boolean) { +fun InviteMembersButton(titleId: StringResource, onClick: () -> Unit, disabled: Boolean) { SettingsActionItem( painterResource(MR.images.ic_check), - stringResource(MR.strings.invite_to_group_button), + stringResource(titleId), click = onClick, textColor = MaterialTheme.colors.primary, iconColor = MaterialTheme.colors.primary, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt index 870df388b2..804222f264 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt @@ -123,12 +123,18 @@ fun ModalData.GroupChatInfoView(chatModel: ChatModel, rhId: Long?, chatId: Strin fun deleteGroupDialog(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, close: (() -> Unit)? = null) { val chatInfo = chat.chatInfo - val alertTextKey = - if (groupInfo.membership.memberCurrent) MR.strings.delete_group_for_all_members_cannot_undo_warning - else MR.strings.delete_group_for_self_cannot_undo_warning + val titleId = if (groupInfo.businessChat == null) MR.strings.delete_group_question else MR.strings.delete_chat_question + val messageId = + if (groupInfo.businessChat == null) { + if (groupInfo.membership.memberCurrent) MR.strings.delete_group_for_all_members_cannot_undo_warning + else MR.strings.delete_group_for_self_cannot_undo_warning + } else { + if (groupInfo.membership.memberCurrent) MR.strings.delete_chat_for_all_members_cannot_undo_warning + else MR.strings.delete_chat_for_self_cannot_undo_warning + } AlertManager.shared.showAlertDialog( - title = generalGetString(MR.strings.delete_group_question), - text = generalGetString(alertTextKey), + title = generalGetString(titleId), + text = generalGetString(messageId), confirmText = generalGetString(MR.strings.delete_verb), onConfirm = { withBGApi { @@ -151,9 +157,14 @@ fun deleteGroupDialog(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, cl } fun leaveGroupDialog(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, close: (() -> Unit)? = null) { + val titleId = if (groupInfo.businessChat == null) MR.strings.leave_group_question else MR.strings.leave_chat_question + val messageId = if (groupInfo.businessChat == null) + MR.strings.you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved + else + MR.strings.you_will_stop_receiving_messages_from_this_chat_chat_history_will_be_preserved AlertManager.shared.showAlertDialog( - title = generalGetString(MR.strings.leave_group_question), - text = generalGetString(MR.strings.you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved), + title = generalGetString(titleId), + text = generalGetString(messageId), confirmText = generalGetString(MR.strings.leave_group_button), onConfirm = { withLongRunningApi(60_000) { @@ -166,9 +177,13 @@ fun leaveGroupDialog(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, cl } private fun removeMemberAlert(rhId: Long?, groupInfo: GroupInfo, mem: GroupMember) { + val messageId = if (groupInfo.businessChat == null) + MR.strings.member_will_be_removed_from_group_cannot_be_undone + else + MR.strings.member_will_be_removed_from_chat_cannot_be_undone AlertManager.shared.showAlertDialog( title = generalGetString(MR.strings.button_remove_member_question), - text = generalGetString(MR.strings.member_will_be_removed_from_group_cannot_be_undone), + text = generalGetString(messageId), confirmText = generalGetString(MR.strings.remove_member_confirmation), onConfirm = { withBGApi { @@ -353,7 +368,8 @@ fun ModalData.GroupChatInfoLayout( } } } - SectionTextFooter(stringResource(MR.strings.only_group_owners_can_change_prefs)) + val footerId = if (groupInfo.businessChat == null) MR.strings.only_group_owners_can_change_prefs else MR.strings.only_chat_owners_can_change_prefs + SectionTextFooter(stringResource(footerId)) SectionDividerSpaced(maxTopPadding = true) SectionView(title = String.format(generalGetString(MR.strings.group_info_section_title_num_members), members.count() + 1)) { @@ -397,10 +413,12 @@ fun ModalData.GroupChatInfoLayout( SectionView { ClearChatButton(clearChat) if (groupInfo.canDelete) { - DeleteGroupButton(deleteGroup) + val titleId = if (groupInfo.businessChat == null) MR.strings.button_delete_group else MR.strings.button_delete_chat + DeleteGroupButton(titleId, deleteGroup) } if (groupInfo.membership.memberCurrent) { - LeaveGroupButton(leaveGroup) + val titleId = if (groupInfo.businessChat == null) MR.strings.button_leave_group else MR.strings.button_leave_chat + LeaveGroupButton(titleId, leaveGroup) } } @@ -655,10 +673,10 @@ private fun AddOrEditWelcomeMessage(welcomeMessage: String?, onClick: () -> Unit } @Composable -private fun LeaveGroupButton(onClick: () -> Unit) { +private fun LeaveGroupButton(titleId: StringResource, onClick: () -> Unit) { SettingsActionItem( painterResource(MR.images.ic_logout), - stringResource(MR.strings.button_leave_group), + stringResource(titleId), onClick, iconColor = Color.Red, textColor = Color.Red @@ -666,10 +684,10 @@ private fun LeaveGroupButton(onClick: () -> Unit) { } @Composable -private fun DeleteGroupButton(onClick: () -> Unit) { +private fun DeleteGroupButton(titleId: StringResource, onClick: () -> Unit) { SettingsActionItem( painterResource(MR.images.ic_delete), - stringResource(MR.strings.button_delete_group), + stringResource(titleId), onClick, iconColor = Color.Red, textColor = Color.Red diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt index a78dd36887..7f0d5f088e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt @@ -38,6 +38,7 @@ import chat.simplex.common.model.GroupInfo import chat.simplex.common.platform.* import chat.simplex.common.views.chatlist.openLoadedChat import chat.simplex.res.MR +import dev.icerock.moko.resources.StringResource import kotlinx.datetime.Clock @Composable @@ -104,7 +105,7 @@ fun GroupMemberInfoView( if (it == newRole.value) return@GroupMemberInfoLayout val prevValue = newRole.value newRole.value = it - updateMemberRoleDialog(it, member, onDismiss = { + updateMemberRoleDialog(it, groupInfo, member, onDismiss = { newRole.value = prevValue }) { withBGApi { @@ -211,9 +212,13 @@ fun GroupMemberInfoView( } fun removeMemberDialog(rhId: Long?, groupInfo: GroupInfo, member: GroupMember, chatModel: ChatModel, close: (() -> Unit)? = null) { + val messageId = if (groupInfo.businessChat == null) + MR.strings.member_will_be_removed_from_group_cannot_be_undone + else + MR.strings.member_will_be_removed_from_chat_cannot_be_undone AlertManager.shared.showAlertDialog( title = generalGetString(MR.strings.button_remove_member), - text = generalGetString(MR.strings.member_will_be_removed_from_group_cannot_be_undone), + text = generalGetString(messageId), confirmText = generalGetString(MR.strings.remove_member_confirmation), onConfirm = { withBGApi { @@ -346,14 +351,15 @@ fun GroupMemberInfoLayout( showSendMessageToEnableCallsAlert() }) } else { // no known contact chat && directMessages are off + val messageId = if (groupInfo.businessChat == null) MR.strings.direct_messages_are_prohibited_in_group else MR.strings.direct_messages_are_prohibited_in_chat InfoViewActionButton(modifier = Modifier.fillMaxWidth(0.33f), painterResource(MR.images.ic_chat_bubble), generalGetString(MR.strings.info_view_message_button), disabled = false, disabledLook = true, onClick = { - showDirectMessagesProhibitedAlert(generalGetString(MR.strings.cant_send_message_to_member_alert_title)) + showDirectMessagesProhibitedAlert(generalGetString(MR.strings.cant_send_message_to_member_alert_title), messageId) }) InfoViewActionButton(modifier = Modifier.fillMaxWidth(0.5f), painterResource(MR.images.ic_call), generalGetString(MR.strings.info_view_call_button), disabled = false, disabledLook = true, onClick = { - showDirectMessagesProhibitedAlert(generalGetString(MR.strings.cant_call_member_alert_title)) + showDirectMessagesProhibitedAlert(generalGetString(MR.strings.cant_call_member_alert_title), messageId) }) InfoViewActionButton(modifier = Modifier.fillMaxWidth(1f), painterResource(MR.images.ic_videocam), generalGetString(MR.strings.info_view_video_button), disabled = false, disabledLook = true, onClick = { - showDirectMessagesProhibitedAlert(generalGetString(MR.strings.cant_call_member_alert_title)) + showDirectMessagesProhibitedAlert(generalGetString(MR.strings.cant_call_member_alert_title), messageId) }) } } @@ -394,7 +400,8 @@ fun GroupMemberInfoLayout( } SectionView(title = stringResource(MR.strings.member_info_section_title_member)) { - InfoRow(stringResource(MR.strings.info_row_group), groupInfo.displayName) + val titleId = if (groupInfo.businessChat == null) MR.strings.info_row_group else MR.strings.info_row_chat + InfoRow(stringResource(titleId), groupInfo.displayName) val roles = remember { member.canChangeRoleTo(groupInfo) } if (roles != null) { RoleSelectionRow(roles, newRole, onRoleSelected) @@ -470,10 +477,10 @@ private fun showSendMessageToEnableCallsAlert() { ) } -private fun showDirectMessagesProhibitedAlert(title: String) { +private fun showDirectMessagesProhibitedAlert(title: String, messageId: StringResource) { AlertManager.shared.showAlertMsg( title = title, - text = generalGetString(MR.strings.direct_messages_are_prohibited_in_chat) + text = generalGetString(messageId) ) } @@ -635,15 +642,19 @@ fun MemberProfileImage( private fun updateMemberRoleDialog( newRole: GroupMemberRole, + groupInfo: GroupInfo, member: GroupMember, onDismiss: () -> Unit, onConfirm: () -> Unit ) { AlertManager.shared.showAlertDialog( title = generalGetString(MR.strings.change_member_role_question), - text = if (member.memberCurrent) - String.format(generalGetString(MR.strings.member_role_will_be_changed_with_notification), newRole.text) - else + text = if (member.memberCurrent) { + if (groupInfo.businessChat == null) + String.format(generalGetString(MR.strings.member_role_will_be_changed_with_notification), newRole.text) + else + String.format(generalGetString(MR.strings.member_role_will_be_changed_with_notification_chat), newRole.text) + } else String.format(generalGetString(MR.strings.member_role_will_be_changed_with_invitation), newRole.text), confirmText = generalGetString(MR.strings.change_verb), onDismiss = onDismiss, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/AdvancedNetworkSettings.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/AdvancedNetworkSettings.kt index 838cac0172..fc042cc46c 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/AdvancedNetworkSettings.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/AdvancedNetworkSettings.kt @@ -418,7 +418,7 @@ fun IntSettingRow(title: String, selection: MutableState, values: List Spacer(Modifier.size(4.dp)) Icon( if (!expanded.value) painterResource(MR.images.ic_arrow_drop_down) else painterResource(MR.images.ic_arrow_drop_up), - generalGetString(MR.strings.invite_to_group_button), + contentDescription = null, modifier = Modifier.padding(start = 8.dp), tint = MaterialTheme.colors.secondary ) @@ -478,7 +478,7 @@ fun TimeoutSettingRow(title: String, selection: MutableState, values: List Spacer(Modifier.size(4.dp)) Icon( if (!expanded.value) painterResource(MR.images.ic_arrow_drop_down) else painterResource(MR.images.ic_arrow_drop_up), - generalGetString(MR.strings.invite_to_group_button), + contentDescription = null, modifier = Modifier.padding(start = 8.dp), tint = MaterialTheme.colors.secondary ) diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml index c8d995cfd9..1171b1d1ae 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml @@ -621,7 +621,7 @@ %d أسبوع لا يمكن أن يحتوي اسم العرض على مسافة فارغة. مكالمة فيديو مُعمّاة بين الطريفين - الرسائل المباشرة بين الأعضاء ممنوعة في هذه المجموعة. + الرسائل المباشرة بين الأعضاء ممنوعة في هذه المجموعة. %d ساعة %d ساعة %d ساعات diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 53df6d4818..7291c8cbb1 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -1425,7 +1425,9 @@ You joined this group. Connecting to inviting group member. Leave Leave group? + Leave chat? You will stop receiving messages from this group. Chat history will be preserved. + You will stop receiving messages from this chat. Chat history will be preserved. Invite members Group inactive Invitation expired! @@ -1542,6 +1544,7 @@ Initial role Expand role selection Invite to group + Invite to chat Skip inviting members Select contacts Contact checked @@ -1558,10 +1561,15 @@ %1$s MEMBERS you: %1$s Delete group + Delete chat Delete group? + Delete chat? Group will be deleted for all members - this cannot be undone! + Chat will be deleted for all members - this cannot be undone! Group will be deleted for you - this cannot be undone! + Chat will be deleted for you - this cannot be undone! Leave group + Leave chat Edit group profile Add welcome message Welcome message @@ -1578,6 +1586,7 @@ Error creating member contact Error sending invitation Only group owners can change group preferences. + Only chat owners can change preferences. Address Share address You can share this address with your contacts to let them connect with %s. @@ -1624,6 +1633,7 @@ Send direct message Member will be removed from group - this cannot be undone! + Member will be removed from chat - this cannot be undone! Remove Remove member Block member? @@ -1649,6 +1659,7 @@ Switch Change group role? The role will be changed to "%s". Everyone in the group will be notified. + The role will be changed to "%s". Everyone in the chat will be notified. The role will be changed to "%s". The member will receive a new invitation. Connect directly? Сonnection request will be sent to this group member. @@ -1656,6 +1667,7 @@ Error changing role Error blocking member for all Group + Chat Connection direct indirect (%1$s) @@ -1703,6 +1715,7 @@ Enter group name: Group full name: Your chat profile will be sent to group members + Your chat profile will be sent to chat members Create group @@ -1957,20 +1970,22 @@ Prohibit sending SimpleX links Send up to 100 last messages to new members. Do not send history to new members. - Group members can send disappearing messages. - Disappearing messages are prohibited in this group. - Group members can send direct messages. - Direct messages between members are prohibited in this group. - Group members can irreversibly delete sent messages. (24 hours) - Irreversible message deletion is prohibited in this group. - Group members can send voice messages. - Voice messages are prohibited in this group. - Group members can add message reactions. - Message reactions are prohibited in this group. - Group members can send files and media. - Files and media are prohibited in this group. - Group members can send SimpleX links. - SimpleX links are prohibited in this group. + Members can send disappearing messages. + Disappearing messages are prohibited. + Members can send direct messages. + Direct messages between members are prohibited. + Direct messages between members are prohibited in this group. + Direct messages between members are prohibited in this chat. + Members can irreversibly delete sent messages. (24 hours) + Irreversible message deletion is prohibited. + Members can send voice messages. + Voice messages are prohibited. + Members can add message reactions. + Message reactions are prohibited. + Members can send files and media. + Files and media are prohibited. + Members can send SimpleX links. + SimpleX links are prohibited. Up to 100 last messages are sent to new members. History is not sent to new members. Delete after diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/bg/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/bg/strings.xml index d59867574d..22e93b041a 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/bg/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/bg/strings.xml @@ -460,7 +460,7 @@ Изчезващи съобщения активирано активирано за контакт - Личните съобщения между членовете са забранени в тази група. + Личните съобщения между членовете са забранени в тази група. Различни имена, аватари и транспортна изолация. Оправяне на криптирането след възстановяване от резервни копия. Потвърждениe за доставка! diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml index b107ea1df7..a6ea5b1208 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml @@ -897,7 +897,7 @@ Členové skupiny mohou posílat mizící zprávy. Mizící zprávy jsou v této skupině zakázány. Členové skupiny mohou posílat přímé zprávy. - Přímé zprávy mezi členy jsou v této skupině zakázány. + Přímé zprávy mezi členy jsou v této skupině zakázány. Členové skupin mohou nevratně mazat odeslané zprávy. (24 hodin) Nevratné mazání zpráv je v této skupině zakázáno. Členové skupiny mohou posílat hlasové zprávy. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml index 29812d0a3e..3c3711be25 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml @@ -875,7 +875,7 @@ Das Senden von Sprachnachrichten erlauben. Das Senden von Sprachnachrichten nicht erlauben. Gruppenmitglieder können Direktnachrichten versenden. - In dieser Gruppe sind Direktnachrichten zwischen Mitgliedern nicht erlaubt. + In dieser Gruppe sind Direktnachrichten zwischen Mitgliedern nicht erlaubt. Gruppenmitglieder können gesendete Nachrichten unwiederbringlich löschen (bis zu 24 Stunden). In dieser Gruppe ist das unwiederbringliche Löschen von Nachrichten nicht erlaubt. Gruppenmitglieder können Sprachnachrichten versenden. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml index 3d22d1fcc5..98f34e4c81 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml @@ -82,7 +82,7 @@ Crea grupo secreto La contraseña de cifrado de la base de datos será actualizada. ID base de datos - Los mensajes directos entre miembros del grupo no están permitidos. + Los mensajes directos entre miembros del grupo no están permitidos. La contraseña de la base de datos es diferente a la almacenada en Keystore. La base de datos será cifrada y la contraseña se guardará en Keystore. ¿Eliminar contacto\? diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml index 8850e33a3e..7e59b69082 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml @@ -1415,7 +1415,7 @@ عدم ارسال تاریخچه به اعضای جدید. اعضای گروه می‌توانند پیام‌های ناپدید شونده ارسال کنند. اعضای گروه می‌توانند پیام‌های مستقیم ارسال کنند. - پیام‌های مستقیم بین اعضا در این گروه ممنوع هستند. + پیام‌های مستقیم بین اعضا در این گروه ممنوع هستند. حذف غیرقابل برگشت در این گروه ممنوع است. پیام‌های صوتی در این گروه ممنوع هستند. واکنش‌های پیام در این گروه ممنوع هستند. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/fi/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/fi/strings.xml index f933b2d6cc..62a02986b2 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/fi/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/fi/strings.xml @@ -339,7 +339,7 @@ Sekä sinä että kontaktisi voivat käyttää viestireaktioita. Sekä sinä että kontaktisi voitte peruuttamattomasti poistaa lähetetyt viestit. Sekä sinä että kontaktisi voitte soittaa puheluita. - Yksityisviestit jäsenten välillä ovat kiellettyjä tässä ryhmässä. + Yksityisviestit jäsenten välillä ovat kiellettyjä tässä ryhmässä. Chat-profiilin (oletus) tai yhteyden (BETA) perusteella. peruttu %s %d päivä diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml index 6caf5b61ef..480aa2e10c 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml @@ -856,7 +856,7 @@ Interdire l’envoi de messages éphémères. Interdire la suppression irréversible des messages. Les membres du groupe peuvent envoyer des messages directs. - Les messages directs entre membres sont interdits dans ce groupe. + Les messages directs entre membres sont interdits dans ce groupe. Les destinataires voient les mises à jour au fur et à mesure que vous les tapez. Vérifier la sécurité de la connexion Comparez les codes de sécurité avec vos contacts. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml index d8ce80884e..4f80e33166 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml @@ -402,7 +402,7 @@ %d ismerős kiválasztva Engedélyezés %dhónap - A közvetlen üzenetek küldése a tagok között le van tiltva ebben a csoportban. + A közvetlen üzenetek küldése a tagok között le van tiltva ebben a csoportban. %d perc Az adatbázis egy véletlenszerű jelmondattal van titkosítva. Exportálás előtt változtassa meg a jelmondatot. Kézbesítés jelentések letiltása a csoportok számára? diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml index a081eb37bf..1050f1d575 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml @@ -564,7 +564,7 @@ Anggota grup dapat hapus pesan terkirim secara permanen. (24 jam) Anggota grup dapat mengirim pesan suara. Hapus pesan yang tidak dapat dibatalkan dilarang di grup ini. - Pesan pribadi antar anggota dilarang di grup ini. + Pesan pribadi antar anggota dilarang di grup ini. Anggota grup dapat kirim tautan SimpleX. %d jam %d jam diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml index 0b827df4d7..2f41824bd1 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml @@ -289,7 +289,7 @@ Il database è crittografato con una password casuale, puoi cambiarla. La password del database è necessaria per aprire la chat. Elimina - I messaggi diretti tra i membri sono vietati in questo gruppo. + I messaggi diretti tra i membri sono vietati in questo gruppo. Inserisci il tuo nome: File Svuota chat @@ -2156,7 +2156,7 @@ Condizioni accettate il: %s. Operatore %s.]]> - %s.]]> + %s.]]> %s.]]> %s.]]> Accetta le condizioni @@ -2168,7 +2168,7 @@ Usa per i messaggi Vedi le condizioni %s.]]> - %s, accetta le condizioni d'uso.]]> + %s, accetta le condizioni d\'uso.]]> Condizioni d\'uso Apri le modifiche Apri le condizioni @@ -2188,7 +2188,7 @@ Server di multimediali e file aggiunti Indirizzo o link una tantum? Impostazioni dell\'indirizzo - con un solo contatto - condividilo di persona o tramite un messenger.]]> + con un solo contatto - condividilo di persona o tramite un messenger.]]> Le condizioni verranno accettate per gli operatori attivati dopo 30 giorni. Le condizioni verranno accettate il: %s. Errore di accettazione delle condizioni diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/iw/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/iw/strings.xml index 01c19a20f0..d9ddd08a57 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/iw/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/iw/strings.xml @@ -382,7 +382,7 @@ %d שבועות שמות שונים, אווטארים ובידוד תעבורה. ישיר - הודעות ישירות בין חברי קבוצה אסורות בקבוצה זו. + הודעות ישירות בין חברי קבוצה אסורות בקבוצה זו. הזן את שמך: שם תצוגה אינו יכול להכיל רווחים. %d חודשים diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml index 83b29a0b4c..c6775f6639 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml @@ -250,7 +250,7 @@ 招待が期限切れました! サーバを削除 端末認証がオフです。SimpleXロックを解除します。 - このグループではメンバー間のダイレクトメッセージが無効です。 + このグループではメンバー間のダイレクトメッセージが無効です。 このグループでは消えるメッセージが無効です。 %d 分 %d 週 diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ko/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ko/strings.xml index 8395f5ea48..c03b4e648b 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ko/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ko/strings.xml @@ -290,7 +290,7 @@ 다운그레이드하고 채팅 열기 다이렉트 메시지 사라지는 메시지 - 이 그룹에서는 멤버들의 다이렉트 메시지가 금지되어 있어요. + 이 그룹에서는 멤버들의 다이렉트 메시지가 금지되어 있어요. %d초 %d 초 %d시 diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/lt/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/lt/strings.xml index da7738f49e..5db1442fc6 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/lt/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/lt/strings.xml @@ -507,7 +507,7 @@ Išeiti iš grupės\? Nežinoma duomenų bazės klaida: %s Profilis ir ryšiai su serveriu - Tiesioginės žinutės tarp narių šioje grupėje yra uždraustos. + Tiesioginės žinutės tarp narių šioje grupėje yra uždraustos. Garso/vaizdo skambučiai " \nPrieinama versijoje v5.1" diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml index 3d3aac1957..26ec40da60 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml @@ -254,7 +254,7 @@ Apparaatverificatie is uitgeschakeld. SimpleX Vergrendelen uitschakelen. Vul uw naam in: Apparaatverificatie is niet ingeschakeld. Je kunt SimpleX Vergrendelen inschakelen via Instellingen zodra je apparaatverificatie hebt ingeschakeld. - Directe berichten tussen leden zijn verboden in deze groep. + Directe berichten tussen leden zijn verboden in deze groep. %d bestand(en) met een totale grootte van %s %d uur Uitzetten diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml index 10f4f79d47..7b46d10921 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml @@ -807,7 +807,7 @@ Zarówno Ty, jak i Twój kontakt możecie wysyłać znikające wiadomości. Kontakty mogą oznaczać wiadomości do usunięcia; będziesz mógł je zobaczyć. Usuń po - Bezpośrednie wiadomości między członkami są zabronione w tej grupie. + Bezpośrednie wiadomości między członkami są zabronione w tej grupie. Znikające wiadomości są zabronione na tym czacie. %dm %d min diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/pt-rBR/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/pt-rBR/strings.xml index 8b16e01b4e..034e5d19db 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/pt-rBR/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/pt-rBR/strings.xml @@ -333,7 +333,7 @@ Funcionalidades experimentais Erro ao criar o link de grupo Erro ao excluir o link de grupo - Mensagens diretas entre membros são proibidas neste grupo. + Mensagens diretas entre membros são proibidas neste grupo. %dh %d horas anônimo via link de endereço de contato diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/pt/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/pt/strings.xml index 02e5547e04..5cdc67e9e5 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/pt/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/pt/strings.xml @@ -486,7 +486,7 @@ Atualização da base de dados %d semana %d semanas - Mensagens diretas entre membros são proibidas neste grupo. + Mensagens diretas entre membros são proibidas neste grupo. Eliminar fila Transferir ficheiro Ficheiro diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml index 59b355fd2b..bcd0848e8d 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml @@ -879,7 +879,7 @@ Разрешить отправлять голосовые сообщения. Запретить отправлять голосовые сообщений. Члены группы могут посылать прямые сообщения. - Прямые сообщения между членами группы запрещены. + Прямые сообщения между членами группы запрещены. Члены группы могут необратимо удалять отправленные сообщения. (24 часа) Необратимое удаление сообщений запрещено в этой группе. Члены группы могут отправлять голосовые сообщения. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/th/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/th/strings.xml index 2487c7d5cd..a0027df1d8 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/th/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/th/strings.xml @@ -480,7 +480,7 @@ สมาชิกกลุ่มสามารถส่งข้อความที่จะหายไปหลังจากเวลาที่กำหนดหลังการอ่าน (disappearing messages) ได้ สมาชิกกลุ่มสามารถส่งข้อความโดยตรงได้ สมาชิกกลุ่มสามารถส่งข้อความเสียง - ข้อความโดยตรงระหว่างสมาชิกเป็นสิ่งต้องห้ามในกลุ่มนี้ + ข้อความโดยตรงระหว่างสมาชิกเป็นสิ่งต้องห้ามในกลุ่มนี้ ข้อความที่จะหายไปหลังจากเวลาที่กำหนดหลังการอ่าน (disappearing messages) เป็นสิ่งต้องห้ามในกลุ่มนี้ สมาชิกกลุ่มสามารถเพิ่มการแสดงปฏิกิริยาต่อข้อความได้ ลบหลังจาก diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml index 8df7457e0f..67b6226b0e 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml @@ -524,7 +524,7 @@ %s üyesi için şifreleme kabul edildi doğrudan Yeniden gösterme - Bu grupta üyeler arası doğrudan mesajlaşma yasaklıdır. + Bu grupta üyeler arası doğrudan mesajlaşma yasaklıdır. konuşulan kişi için etkinleşti senin için etkinleştirildi %d sn diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml index fcae74db1b..73daa373c1 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml @@ -748,7 +748,7 @@ Заборонити реакції на повідомлення. Самознищувальні повідомлення заборонені в цій групі. Учасники групи можуть надсилати приватні повідомлення. - Приватні повідомлення між учасниками заборонені в цій групі. + Приватні повідомлення між учасниками заборонені в цій групі. Учасники групи можуть назавжди видаляти відправлені повідомлення. (24 години) Назавжди видалення повідомлень заборонене в цій групі. Голосові повідомлення заборонені в цій групі. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml index f02f7abc15..c9b30c652a 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml @@ -503,7 +503,7 @@ Đã xóa Lỗi xóa %d sự kiện nhóm - Tin nhắn trực tiếp giữa các thành viên bị cấm trong nhóm này. + Tin nhắn trực tiếp giữa các thành viên bị cấm trong nhóm này. %d tệp với tổng kích thước là %s phần di dời khác nhau trong ứng dụng/cơ sở dữ liệu: %s / %s Tin nhắn trực tiếp diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml index 4c0b66d216..9df7b74c33 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml @@ -423,7 +423,7 @@ 翻转相机 启用自动删除消息? 用于控制台 - 此群中禁止成员之间私信。 + 此群中禁止成员之间私信。 该组禁止限时消息。 群组成员可以不可逆地删除已发送的消息。(24小时) 群组成员可以私信。 diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml index 22d226829e..11a086f795 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml @@ -752,7 +752,7 @@ 自動銷毀訊息於這個聊天室內是禁用的。 不可逆地刪除訊息於這個聊天室內是禁用的。 只有你可以傳送語音訊息。 - 私訊群組內的成員於這個群組內是禁用的。 + 私訊群組內的成員於這個群組內是禁用的。 群組內的成員可以不可逆地刪除訊息。(24小時) 語音訊息 改善伺服器配置 From a182cf5730e1d11575a9229dd2213bf0156645b8 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Tue, 3 Dec 2024 18:23:24 +0000 Subject: [PATCH 117/167] ui, site: v6.2 whats new, business (#5309) * ui, site: v6.2 whats new, business * icon Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> * business Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> * typo Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> * typo Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> --------- Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> --- .../Views/Onboarding/WhatsNewView.swift | 9 ++- .../common/views/onboarding/WhatsNewView.kt | 9 ++- .../commonMain/resources/MR/base/strings.xml | 2 + ...ork-v6-2-servers-by-flux-business-chats.md | 60 +++++++++++++++++++ docs/BUSINESS.md | 60 +++++++++++++++++++ website/langs/en.json | 1 + website/src/_data/docs_dropdown.json | 12 ++-- website/src/_data/docs_sidebar.json | 1 + website/src/_includes/navbar.html | 8 +++ 9 files changed, 150 insertions(+), 12 deletions(-) create mode 100644 blog/20241210-simplex-network-v6-2-servers-by-flux-business-chats.md create mode 100755 docs/BUSINESS.md diff --git a/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift b/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift index 92b2820681..182c5652d7 100644 --- a/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift +++ b/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift @@ -520,14 +520,19 @@ private let versionDescriptions: [VersionDescription] = [ ] ), VersionDescription( - version: "v6.2 (beta.1)", - post: URL(string: "https://simplex.chat/blog/20241125-servers-operated-by-flux-true-privacy-and-decentralization-for-all-users.html"), + version: "v6.2", + post: URL(string: "https://simplex.chat/blog/20241210-simplex-network-v6-2-servers-by-flux-business-chats.html"), features: [ .view(FeatureView( icon: nil, title: "Network decentralization", view: { NewOperatorsView() } )), + .feature(Description( + icon: "briefcase", + title: "Business chats", + description: "Privacy for your customers." + )), .feature(Description( icon: "bolt", title: "More reliable notifications", diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt index 8eb89931b3..a5cb944f0a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt @@ -724,8 +724,8 @@ private val versionDescriptions: List = listOf( ), ), VersionDescription( - version = "v6.2-beta.1", - post = "https://simplex.chat/blog/20241125-servers-operated-by-flux-true-privacy-and-decentralization-for-all-users.html", + version = "v6.2", + post = "https://simplex.chat/blog/20241210-simplex-network-v6-2-servers-by-flux-business-chats.html", features = listOf( VersionFeature.FeatureView( icon = null, @@ -749,6 +749,11 @@ private val versionDescriptions: List = listOf( } } ), + VersionFeature.FeatureDescription( + icon = MR.images.ic_work, + titleId = MR.strings.v6_2_business_chats, + descrId = MR.strings.v6_2_business_chats_descr + ), VersionFeature.FeatureDescription( icon = MR.images.ic_chat, titleId = MR.strings.v6_2_improved_chat_navigation, diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 7291c8cbb1..ddf8805e8a 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -2175,6 +2175,8 @@ for better metadata privacy. Improved chat navigation - Open chat on the first unread message.\n- Jump to quoted messages. + Business chats + Privacy for your customers. View updated conditions diff --git a/blog/20241210-simplex-network-v6-2-servers-by-flux-business-chats.md b/blog/20241210-simplex-network-v6-2-servers-by-flux-business-chats.md new file mode 100644 index 0000000000..d59d8e6003 --- /dev/null +++ b/blog/20241210-simplex-network-v6-2-servers-by-flux-business-chats.md @@ -0,0 +1,60 @@ +--- +layout: layouts/article.html +title: "Servers operated by Flux - true privacy and decentralization for all users" +date: 2024-12-10 +# previewBody: blog_previews/20241210.html +# image: images/simplexonflux.png +# imageWide: true +draft: true +permalink: "/blog/20241210-simplex-network-v6-2-servers-by-flux-business-chats.html" +--- + +# SimpleX network: preset servers operated by Flux, business chats and more with v6.2 of the apps + +**Will be published:** Dec 10, 2024 + +This is a placeholder page for the upcoming v6.2 release announcement! + +- Preset servers are now operated by two companies - SimpleX Chat and Flux. Read [this post](./20241125-servers-operated-by-flux-true-privacy-and-decentralization-for-all-users.md). +- Business chats to provide support from your business to users of SimpleX network. Read [this page](../docs/BUSINESS.md). +- and more! + +## SimpleX network + +Some links to answer the most common questions: + +[How can SimpleX deliver messages without user identifiers](./20220511-simplex-chat-v2-images-files.md#the-first-messaging-platform-without-user-identifiers). + +[What are the risks to have identifiers assigned to the users](./20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md#why-having-users-identifiers-is-bad-for-the-users). + +[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-and-security-technical-details-and-limitations). + +[Frequently asked questions](../docs/FAQ.md). + +Please also see our [website](https://simplex.chat). + +## Please support us with your donations + +Huge *thank you* to everybody who donated to SimpleX Chat! + +Prioritizing users privacy and security, and also raising the investment, would have been impossible without your support and donations. + +Also, funding the work to transition the protocols to non-profit governance model would not have been possible without the donations we received from the users. + +Our pledge to our users is that SimpleX protocols are and will remain open, and in public domain, so anybody can build the future implementations of the clients and the servers. We are building SimpleX platform based on the same principles as email and web, but much more private and secure. + +Your donations help us raise more funds — any amount, even the price of the cup of coffee, makes a big difference for us. + +See [this section](https://github.com/simplex-chat/simplex-chat/#please-support-us-with-your-donations) for the ways to donate. + +Thank you, + +Evgeny + +SimpleX Chat founder + +[1] You can also to self-host your own SimpleX servers on [Flux decentralized cloud](https://home.runonflux.io/apps/marketplace?q=simplex). + +[2] The probability of connection being de-anonymized and the number of random server choices follow this equation: `(1 - s ^ 2) ^ n = 1 - p`, where `s` is the share of attacker-controlled servers in the network, `n` is the number of random choices of entry and exit nodes for the circuit, and `p` is the probability of both entry and exit nodes, and the connection privacy being compromised. Substituting `0.02` (2%) for `s`, `0.5` (50%) for `p`, and solving this equation for `n` we obtain that `1733` random circuits have 50% probability of privacy being compromised. + +Also see [this presentation about Tor](https://ritter.vg/p/tor-v1.6.pdf), specifically the approximate calculations on page 76, and also [Tor project post](https://blog.torproject.org/announcing-vanguards-add-onion-services/) about the changes that made attack on hidden service anonymity harder, but still viable in case the it is used for a long time. diff --git a/docs/BUSINESS.md b/docs/BUSINESS.md new file mode 100755 index 0000000000..aed5fd877c --- /dev/null +++ b/docs/BUSINESS.md @@ -0,0 +1,60 @@ +--- +title: SimpleX for business +revision: 03.12.2024 +--- + +# Using SimpleX Chat in business + +SimpleX Chat (aka SimpleX) is a decentralized communication network that provides private and secure messaging. Its users are rapidly growing, and providing customer services via SimpleX can offer you a unique opportunity to engage people who are the most enthusiastic about trying out early stage technology products and services. + +This document aims to help you make the best use of SimpleX Chat if you choose to engage with its users. + +## Communcate with customers via business address + +In the same way you can connect to our "SimpleX Chat team" profile via the app, you can provide the address for your existing and prospective customers: +- to buy your product and services via chat, +- to ask any questions, make suggestions and provide feedback, +- to discover more information about your business. + +Customers who value privacy and security, and want to engage with you without sharing any personal data and minimizing any metadata that is shared with you, will be really happy to use this communication channel. + +From v6.2 SimpleX Chat supports business addresses. Their design allows you to accept requests from multiple customers, with the app creating a new business chat with each of them. + +Business chats operate in a way similar to dedicated customer support systems by combining features of direct conversations and groups, and the only widely used messenger that provides such functionality is WeChat with Chinese business accounts. + +When a customer connects to your business via the business contact address, a new conversation is created. Similarly to how direct chats work, the customer will see the name and logo of your business, and you will see the name and avatar of your customer. + +But the business conversation works as a group - once the customer is connected, other people from the business can be added to the conversation, and the customers will see who are they talking with. This can be used to transfer business conversation to another person, or for escalation - in the same way as with the dedicated support systems. + +SimpleX Chat profile with the business address can be used in one of these ways: +- for small teams it can be managed by one person running the app on their desktop computer, who would respond to customer questions and manually add to the conversation other people in the business, as required. +- if you have multiple support agents, you can run business profile in CLI client running in cloud VM or on any machine with high speed Internet (see Technical advice below), and they can connect to this client from desktop client, in turns. This is how we use our business profile ourselves, even though it requires some configuration. You can manage 100s of thousands of connected customers in this way. +- For larger teams, it would be appropriate to have this profile managed by chat bot that can reply to some simple questions, and to add support agents, based on their availability and the questions asked. These scenarios would require programming a chat bot, and we are currently working to simplify it. + +In any case, it is important that the client application remains running and connected to the Internet for you to receive support requests. + +## Customer broadcasts + +While currently supported only via CLI clients (or via chat console in desktop and mobile clients), it can be used to broadcast important announcements to all connected customers. We will be adding this feature to desktop clients soon. We use it to broadcast release updates to a very large number of users who are connected to our own support profile. + +## Community groups and promotion in group directory + +In addition to providing support to clients individually, you can create a community group, and promote it via our experimental and growing [directory of public groups](./DIRECTORY.md). Community groups require ongoing moderation. + +## Limitations + +With all advantages in privacy and security of e2e encryption in SimpleX Chat, there are some important limitations: +- **protecting your data from loss is your responsibility**. This is the price of privacy - if you lose your device, or database passphrase, there is absolutely no way we would be able to support you to recover access. There are ways to work around these limitations. +- **you cannot access the same profile from multiple devices**. For all communication products it's a basic expectation, and yet there is not a single one that delivered it without some very serious privacy and security compromises. Better solutions are possible, and we will be implementing it, but reasonably secure approach is much more complex to implement than what is affordable at the current stage. You can access mobile or CLI profile from desktop, and the latter allows to use one profile by multiple people in turns, as we explain below. +- **your owner role in the groups cannot be restored if you lose the device**. The solution is to create owner profiles on multiple devices for all your important groups. This way if you lose device or data for one of profiles, you won't lose control of the group, and you can add a new one. Think about it as about keys to your cryptowallet. +- **current groups are highly experimental**. Message delivery can be delayed or fail in some cases, lists of members can be out of sync. There are approaches to make them more stable we use for our groups. + +## Technical advice + +### Running SimpleX Chat in the cloud + +### Using remote profiles via Desktop app + +## Organizations using SimpleX Chat for customer service, support and sales + +Please let us know if you use SimpleX Chat to communicate with your customers and want to be included in this list. diff --git a/website/langs/en.json b/website/langs/en.json index a9b31f2a6e..e57b3375de 100644 --- a/website/langs/en.json +++ b/website/langs/en.json @@ -238,6 +238,7 @@ "docs-dropdown-10": "Transparency", "docs-dropdown-11": "FAQ", "docs-dropdown-12": "Security", + "docs-dropdown-14": "SimpleX for business", "newer-version-of-eng-msg": "There is a newer version of this page in English.", "click-to-see": "Click to see", "menu": "Menu", diff --git a/website/src/_data/docs_dropdown.json b/website/src/_data/docs_dropdown.json index 88fd5826dd..f97c2aeff2 100644 --- a/website/src/_data/docs_dropdown.json +++ b/website/src/_data/docs_dropdown.json @@ -5,12 +5,12 @@ "url": "/docs/simplex.html" }, { - "title": "docs-dropdown-8", - "url": "/docs/directory.html" + "title": "docs-dropdown-14", + "url": "/docs/business.html" }, { - "title": "docs-dropdown-2", - "url": "/docs/android.html" + "title": "docs-dropdown-8", + "url": "/docs/directory.html" }, { "title": "docs-dropdown-3", @@ -28,10 +28,6 @@ "title": "docs-dropdown-6", "url": "/docs/webrtc.html" }, - { - "title": "docs-dropdown-7", - "url": "/docs/translations.html" - }, { "title": "docs-dropdown-9", "url": "/downloads/" diff --git a/website/src/_data/docs_sidebar.json b/website/src/_data/docs_sidebar.json index e9ccb7ce02..e370ccc078 100644 --- a/website/src/_data/docs_sidebar.json +++ b/website/src/_data/docs_sidebar.json @@ -18,6 +18,7 @@ "menu": "Reference", "data": [ "SIMPLEX.md", + "BUSINESS.md", "DIRECTORY.md", "ANDROID.md", "CLI.md", diff --git a/website/src/_includes/navbar.html b/website/src/_includes/navbar.html index 5f9d4be3f3..6e69c559b0 100644 --- a/website/src/_includes/navbar.html +++ b/website/src/_includes/navbar.html @@ -80,6 +80,14 @@
+
  • {{ "docs-dropdown-7" | i18n({}, lang ) | safe }} +
  • +
  • {{ "docs-dropdown-2" | i18n({}, lang ) | safe }} +
  • {{ "chat-bot-example" | i18n({}, lang ) | safe }} From f3be723cde619ff4ce93402dc0e15cdeea42a85f Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Tue, 3 Dec 2024 18:40:58 +0000 Subject: [PATCH 118/167] ios: export localizations --- .../bg.xcloc/Localized Contents/bg.xliff | 130 +++++++++++----- .../cs.xcloc/Localized Contents/cs.xliff | 128 ++++++++++++---- .../de.xcloc/Localized Contents/de.xliff | 130 +++++++++++----- .../en.xcloc/Localized Contents/en.xliff | 145 +++++++++++++----- .../es.xcloc/Localized Contents/es.xliff | 130 +++++++++++----- .../fi.xcloc/Localized Contents/fi.xliff | 128 ++++++++++++---- .../fr.xcloc/Localized Contents/fr.xliff | 130 +++++++++++----- .../hu.xcloc/Localized Contents/hu.xliff | 130 +++++++++++----- .../it.xcloc/Localized Contents/it.xliff | 130 +++++++++++----- .../ja.xcloc/Localized Contents/ja.xliff | 128 ++++++++++++---- .../nl.xcloc/Localized Contents/nl.xliff | 130 +++++++++++----- .../pl.xcloc/Localized Contents/pl.xliff | 130 +++++++++++----- .../ru.xcloc/Localized Contents/ru.xliff | 130 +++++++++++----- .../th.xcloc/Localized Contents/th.xliff | 128 ++++++++++++---- .../tr.xcloc/Localized Contents/tr.xliff | 130 +++++++++++----- .../uk.xcloc/Localized Contents/uk.xliff | 130 +++++++++++----- .../Localized Contents/zh-Hans.xliff | 130 +++++++++++----- 17 files changed, 1626 insertions(+), 591 deletions(-) diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff index e483406fe5..0e8e30d8aa 100644 --- a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff @@ -1199,6 +1199,10 @@ Business address No comment provided by engineer. + + Business chats + 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). Чрез чат профил (по подразбиране) или [чрез връзка](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (БЕТА). @@ -1334,6 +1338,10 @@ Change user profiles authentication reason + + Chat + No comment provided by engineer. + Chat already exists No comment provided by engineer. @@ -1412,6 +1420,14 @@ Chat theme No comment provided by engineer. + + Chat will be deleted for all members - this cannot be undone! + No comment provided by engineer. + + + Chat will be deleted for you - this cannot be undone! + No comment provided by engineer. + Chats Чатове @@ -2122,6 +2138,10 @@ This is your own one-time link! Изтрий и уведоми контакт No comment provided by engineer. + + Delete chat + No comment provided by engineer. + Delete chat profile Изтрий чат профила @@ -2132,6 +2152,10 @@ This is your own one-time link! Изтриване на чат профила? No comment provided by engineer. + + Delete chat? + No comment provided by engineer. + Delete connection Изтрий връзката @@ -2384,6 +2408,10 @@ This is your own one-time link! Лични съобщения chat feature + + Direct messages between members are prohibited in this chat. + No comment provided by engineer. + Direct messages between members are prohibited. Личните съобщения между членовете са забранени в тази група. @@ -3502,41 +3530,6 @@ Error: %2$@ Групови линкове No comment provided by engineer. - - Members can add message reactions. - Членовете на групата могат да добавят реакции към съобщенията. - No comment provided by engineer. - - - Members can irreversibly delete sent messages. (24 hours) - Членовете на групата могат необратимо да изтриват изпратените съобщения. (24 часа) - No comment provided by engineer. - - - Members can send SimpleX links. - Членовете на групата могат да изпращат SimpleX линкове. - No comment provided by engineer. - - - Members can send direct messages. - Членовете на групата могат да изпращат лични съобщения. - No comment provided by engineer. - - - Members can send disappearing messages. - Членовете на групата могат да изпращат изчезващи съобщения. - No comment provided by engineer. - - - Members can send files and media. - Членовете на групата могат да изпращат файлове и медия. - No comment provided by engineer. - - - Members can send voice messages. - Членовете на групата могат да изпращат гласови съобщения. - No comment provided by engineer. - Group message: Групово съобщение: @@ -3929,6 +3922,10 @@ More improvements are coming soon! Покани членове No comment provided by engineer. + + Invite to chat + No comment provided by engineer. + Invite to group Покани в групата @@ -4085,6 +4082,14 @@ This is your link for group %@! Напусни swipe action + + Leave chat + No comment provided by engineer. + + + Leave chat? + No comment provided by engineer. + Leave group Напусни групата @@ -4212,6 +4217,10 @@ This is your link for group %@! Member inactive item status text + + Member role will be changed to "%@". All chat members will be notified. + No comment provided by engineer. + Member role will be changed to "%@". All group members will be notified. Ролята на члена ще бъде променена на "%@". Всички членове на групата ще бъдат уведомени. @@ -4222,11 +4231,50 @@ This is your link for group %@! Ролята на члена ще бъде променена на "%@". Членът ще получи нова покана. No comment provided by engineer. + + Member will be removed from chat - this cannot be undone! + No comment provided by engineer. + Member will be removed from group - this cannot be undone! Членът ще бъде премахнат от групата - това не може да бъде отменено! No comment provided by engineer. + + Members can add message reactions. + Членовете на групата могат да добавят реакции към съобщенията. + No comment provided by engineer. + + + Members can irreversibly delete sent messages. (24 hours) + Членовете на групата могат необратимо да изтриват изпратените съобщения. (24 часа) + No comment provided by engineer. + + + Members can send SimpleX links. + Членовете на групата могат да изпращат SimpleX линкове. + No comment provided by engineer. + + + Members can send direct messages. + Членовете на групата могат да изпращат лични съобщения. + No comment provided by engineer. + + + Members can send disappearing messages. + Членовете на групата могат да изпращат изчезващи съобщения. + No comment provided by engineer. + + + Members can send files and media. + Членовете на групата могат да изпращат файлове и медия. + No comment provided by engineer. + + + Members can send voice messages. + Членовете на групата могат да изпращат гласови съобщения. + No comment provided by engineer. + Menus No comment provided by engineer. @@ -4764,6 +4812,10 @@ Requires compatible VPN. Няма се използват Onion хостове. No comment provided by engineer. + + Only chat owners can change preferences. + No comment provided by engineer. + Only client devices store user profiles, contacts, groups, and messages. Само потребителските устройства съхраняват потребителски профили, контакти, групи и съобщения, изпратени с **двуслойно криптиране от край до край**. @@ -5142,6 +5194,10 @@ Error: %@ Поверителност и сигурност No comment provided by engineer. + + Privacy for your customers. + No comment provided by engineer. + Privacy redefined Поверителността преосмислена @@ -7882,6 +7938,10 @@ Repeat connection request? Все още ще получавате обаждания и известия от заглушени профили, когато са активни. No comment provided by engineer. + + You will stop receiving messages from this chat. Chat history will be preserved. + No comment provided by engineer. + You will stop receiving messages from this group. Chat history will be preserved. Ще спрете да получавате съобщения от тази група. Историята на чата ще бъде запазена. diff --git a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff index 7efd941d11..10f6355692 100644 --- a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff @@ -1161,6 +1161,10 @@ Business address No comment provided by engineer. + + Business chats + 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). Podle chat profilu (výchozí) nebo [podle připojení](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA). @@ -1293,6 +1297,10 @@ Change user profiles authentication reason + + Chat + No comment provided by engineer. + Chat already exists No comment provided by engineer. @@ -1369,6 +1377,14 @@ Chat theme No comment provided by engineer. + + Chat will be deleted for all members - this cannot be undone! + No comment provided by engineer. + + + Chat will be deleted for you - this cannot be undone! + No comment provided by engineer. + Chats Chaty @@ -2051,6 +2067,10 @@ This is your own one-time link! Delete and notify contact No comment provided by engineer. + + Delete chat + No comment provided by engineer. + Delete chat profile Smazat chat profil @@ -2061,6 +2081,10 @@ This is your own one-time link! Smazat chat profil? No comment provided by engineer. + + Delete chat? + No comment provided by engineer. + Delete connection Smazat připojení @@ -2309,6 +2333,10 @@ This is your own one-time link! Přímé zprávy chat feature + + Direct messages between members are prohibited in this chat. + No comment provided by engineer. + Direct messages between members are prohibited. Přímé zprávy mezi členy jsou v této skupině zakázány. @@ -3389,40 +3417,6 @@ Error: %2$@ Odkazy na skupiny No comment provided by engineer. - - Members can add message reactions. - Členové skupin mohou přidávat reakce na zprávy. - No comment provided by engineer. - - - Members can irreversibly delete sent messages. (24 hours) - Členové skupiny mohou nevratně mazat odeslané zprávy. (24 hodin) - No comment provided by engineer. - - - Members can send SimpleX links. - No comment provided by engineer. - - - Members can send direct messages. - Členové skupiny mohou posílat přímé zprávy. - No comment provided by engineer. - - - Members can send disappearing messages. - Členové skupiny mohou posílat mizící zprávy. - No comment provided by engineer. - - - Members can send files and media. - Členové skupiny mohou posílat soubory a média. - No comment provided by engineer. - - - Members can send voice messages. - Členové skupiny mohou posílat hlasové zprávy. - No comment provided by engineer. - Group message: Skupinová zpráva: @@ -3800,6 +3794,10 @@ More improvements are coming soon! Pozvat členy No comment provided by engineer. + + Invite to chat + No comment provided by engineer. + Invite to group Pozvat do skupiny @@ -3948,6 +3946,14 @@ This is your link for group %@! Opustit swipe action + + Leave chat + No comment provided by engineer. + + + Leave chat? + No comment provided by engineer. + Leave group Opustit skupinu @@ -4072,6 +4078,10 @@ This is your link for group %@! Member inactive item status text + + Member role will be changed to "%@". All chat members will be notified. + No comment provided by engineer. + Member role will be changed to "%@". All group members will be notified. Role člena se změní na "%@". Všichni členové skupiny budou upozorněni. @@ -4082,11 +4092,49 @@ This is your link for group %@! Role člena se změní na "%@". Člen obdrží novou pozvánku. No comment provided by engineer. + + Member will be removed from chat - this cannot be undone! + No comment provided by engineer. + Member will be removed from group - this cannot be undone! Člen bude odstraněn ze skupiny - toto nelze vzít zpět! No comment provided by engineer. + + Members can add message reactions. + Členové skupin mohou přidávat reakce na zprávy. + No comment provided by engineer. + + + Members can irreversibly delete sent messages. (24 hours) + Členové skupiny mohou nevratně mazat odeslané zprávy. (24 hodin) + No comment provided by engineer. + + + Members can send SimpleX links. + No comment provided by engineer. + + + Members can send direct messages. + Členové skupiny mohou posílat přímé zprávy. + No comment provided by engineer. + + + Members can send disappearing messages. + Členové skupiny mohou posílat mizící zprávy. + No comment provided by engineer. + + + Members can send files and media. + Členové skupiny mohou posílat soubory a média. + No comment provided by engineer. + + + Members can send voice messages. + Členové skupiny mohou posílat hlasové zprávy. + No comment provided by engineer. + Menus No comment provided by engineer. @@ -4605,6 +4653,10 @@ Vyžaduje povolení sítě VPN. Onion hostitelé nebudou použiti. No comment provided by engineer. + + Only chat owners can change preferences. + No comment provided by engineer. + Only client devices store user profiles, contacts, groups, and messages. Pouze klientská zařízení ukládají uživatelské profily, kontakty, skupiny a zprávy odeslané s **2vrstvým šifrováním typu end-to-end**. @@ -4967,6 +5019,10 @@ Error: %@ Ochrana osobních údajů a zabezpečení No comment provided by engineer. + + Privacy for your customers. + No comment provided by engineer. + Privacy redefined Nové vymezení soukromí @@ -7614,6 +7670,10 @@ Repeat connection request? Stále budete přijímat volání a upozornění od umlčených profilů pokud budou aktivní. No comment provided by engineer. + + You will stop receiving messages from this chat. Chat history will be preserved. + No comment provided by engineer. + You will stop receiving messages from this group. Chat history will be preserved. Přestanete dostávat zprávy z této skupiny. Historie chatu bude zachována. diff --git a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff index afce946eea..35e11b4861 100644 --- a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff +++ b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff @@ -1246,6 +1246,10 @@ Business address No comment provided by engineer. + + Business chats + 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). Per Chat-Profil (Voreinstellung) oder [per Verbindung](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA). @@ -1388,6 +1392,10 @@ Chat-Profile wechseln authentication reason + + Chat + No comment provided by engineer. + Chat already exists No comment provided by engineer. @@ -1471,6 +1479,14 @@ Chat-Design No comment provided by engineer. + + Chat will be deleted for all members - this cannot be undone! + No comment provided by engineer. + + + Chat will be deleted for you - this cannot be undone! + No comment provided by engineer. + Chats Chats @@ -2225,6 +2241,10 @@ Das ist Ihr eigener Einmal-Link! Kontakt löschen und benachrichtigen No comment provided by engineer. + + Delete chat + No comment provided by engineer. + Delete chat profile Chat-Profil löschen @@ -2235,6 +2255,10 @@ Das ist Ihr eigener Einmal-Link! Chat-Profil löschen? No comment provided by engineer. + + Delete chat? + No comment provided by engineer. + Delete connection Verbindung löschen @@ -2500,6 +2524,10 @@ Das ist Ihr eigener Einmal-Link! Direkte Nachrichten chat feature + + Direct messages between members are prohibited in this chat. + No comment provided by engineer. + Direct messages between members are prohibited. In dieser Gruppe sind Direktnachrichten zwischen Mitgliedern nicht erlaubt. @@ -3672,41 +3700,6 @@ Fehler: %2$@ Gruppen-Links No comment provided by engineer. - - Members can add message reactions. - Gruppenmitglieder können eine Reaktion auf Nachrichten geben. - No comment provided by engineer. - - - Members can irreversibly delete sent messages. (24 hours) - Gruppenmitglieder können gesendete Nachrichten unwiederbringlich löschen. (24 Stunden) - No comment provided by engineer. - - - Members can send SimpleX links. - Gruppenmitglieder können SimpleX-Links senden. - No comment provided by engineer. - - - Members can send direct messages. - Gruppenmitglieder können Direktnachrichten versenden. - No comment provided by engineer. - - - Members can send disappearing messages. - Gruppenmitglieder können verschwindende Nachrichten senden. - No comment provided by engineer. - - - Members can send files and media. - Gruppenmitglieder können Dateien und Medien senden. - No comment provided by engineer. - - - Members can send voice messages. - Gruppenmitglieder können Sprachnachrichten versenden. - No comment provided by engineer. - Group message: Grppennachricht: @@ -4106,6 +4099,10 @@ Weitere Verbesserungen sind bald verfügbar! Mitglieder einladen No comment provided by engineer. + + Invite to chat + No comment provided by engineer. + Invite to group In Gruppe einladen @@ -4264,6 +4261,14 @@ Das ist Ihr Link für die Gruppe %@! Verlassen swipe action + + Leave chat + No comment provided by engineer. + + + Leave chat? + No comment provided by engineer. + Leave group Gruppe verlassen @@ -4394,6 +4399,10 @@ Das ist Ihr Link für die Gruppe %@! Mitglied inaktiv item status text + + Member role will be changed to "%@". All chat members will be notified. + No comment provided by engineer. + Member role will be changed to "%@". All group members will be notified. Die Mitgliederrolle wird auf "%@" geändert. Alle Mitglieder der Gruppe werden benachrichtigt. @@ -4404,11 +4413,50 @@ Das ist Ihr Link für die Gruppe %@! Die Mitgliederrolle wird auf "%@" geändert. Das Mitglied wird eine neue Einladung erhalten. No comment provided by engineer. + + Member will be removed from chat - this cannot be undone! + No comment provided by engineer. + Member will be removed from group - this cannot be undone! Das Mitglied wird aus der Gruppe entfernt. Dies kann nicht rückgängig gemacht werden! No comment provided by engineer. + + Members can add message reactions. + Gruppenmitglieder können eine Reaktion auf Nachrichten geben. + No comment provided by engineer. + + + Members can irreversibly delete sent messages. (24 hours) + Gruppenmitglieder können gesendete Nachrichten unwiederbringlich löschen. (24 Stunden) + No comment provided by engineer. + + + Members can send SimpleX links. + Gruppenmitglieder können SimpleX-Links senden. + No comment provided by engineer. + + + Members can send direct messages. + Gruppenmitglieder können Direktnachrichten versenden. + No comment provided by engineer. + + + Members can send disappearing messages. + Gruppenmitglieder können verschwindende Nachrichten senden. + No comment provided by engineer. + + + Members can send files and media. + Gruppenmitglieder können Dateien und Medien senden. + No comment provided by engineer. + + + Members can send voice messages. + Gruppenmitglieder können Sprachnachrichten versenden. + No comment provided by engineer. + Menus Menüs @@ -4982,6 +5030,10 @@ Dies erfordert die Aktivierung eines VPNs. Onion-Hosts werden nicht verwendet. No comment provided by engineer. + + Only chat owners can change preferences. + No comment provided by engineer. + Only client devices store user profiles, contacts, groups, and messages. Nur die Endgeräte speichern die Benutzerprofile, Kontakte, Gruppen und Nachrichten, welche über eine **2-Schichten Ende-zu-Ende-Verschlüsselung** gesendet werden. @@ -5377,6 +5429,10 @@ Fehler: %@ Datenschutz & Sicherheit No comment provided by engineer. + + Privacy for your customers. + No comment provided by engineer. + Privacy redefined Datenschutz neu definiert @@ -8277,6 +8333,10 @@ Verbindungsanfrage wiederholen? Sie können Anrufe und Benachrichtigungen auch von stummgeschalteten Profilen empfangen, solange diese aktiv sind. No comment provided by engineer. + + You will stop receiving messages from this chat. Chat history will be preserved. + No comment provided by engineer. + You will stop receiving messages from this group. Chat history will be preserved. Sie werden von dieser Gruppe keine Nachrichten mehr erhalten. Der Nachrichtenverlauf wird beibehalten. diff --git a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff index 2301f671a4..1d7f3f16be 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff +++ b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff @@ -1250,6 +1250,11 @@ Business address No comment provided by engineer. + + Business chats + Business chats + 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). By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA). @@ -1392,6 +1397,11 @@ Change user profiles authentication reason + + Chat + Chat + No comment provided by engineer. + Chat already exists Chat already exists @@ -1477,6 +1487,16 @@ Chat theme No comment provided by engineer. + + Chat will be deleted for all members - this cannot be undone! + Chat will be deleted for all members - this cannot be undone! + No comment provided by engineer. + + + Chat will be deleted for you - this cannot be undone! + Chat will be deleted for you - this cannot be undone! + No comment provided by engineer. + Chats Chats @@ -2231,6 +2251,11 @@ This is your own one-time link! Delete and notify contact No comment provided by engineer. + + Delete chat + Delete chat + No comment provided by engineer. + Delete chat profile Delete chat profile @@ -2241,6 +2266,11 @@ This is your own one-time link! Delete chat profile? No comment provided by engineer. + + Delete chat? + Delete chat? + No comment provided by engineer. + Delete connection Delete connection @@ -2506,6 +2536,11 @@ This is your own one-time link! Direct messages chat feature + + Direct messages between members are prohibited in this chat. + Direct messages between members are prohibited in this chat. + No comment provided by engineer. + Direct messages between members are prohibited. Direct messages between members are prohibited. @@ -3678,41 +3713,6 @@ Error: %2$@ Group links No comment provided by engineer. - - Members can add message reactions. - Members can add message reactions. - No comment provided by engineer. - - - Members can irreversibly delete sent messages. (24 hours) - Members can irreversibly delete sent messages. (24 hours) - No comment provided by engineer. - - - Members can send SimpleX links. - Members can send SimpleX links. - No comment provided by engineer. - - - Members can send direct messages. - Members can send direct messages. - No comment provided by engineer. - - - Members can send disappearing messages. - Members can send disappearing messages. - No comment provided by engineer. - - - Members can send files and media. - Members can send files and media. - No comment provided by engineer. - - - Members can send voice messages. - Members can send voice messages. - No comment provided by engineer. - Group message: Group message: @@ -4112,6 +4112,11 @@ More improvements are coming soon! Invite members No comment provided by engineer. + + Invite to chat + Invite to chat + No comment provided by engineer. + Invite to group Invite to group @@ -4270,6 +4275,16 @@ This is your link for group %@! Leave swipe action + + Leave chat + Leave chat + No comment provided by engineer. + + + Leave chat? + Leave chat? + No comment provided by engineer. + Leave group Leave group @@ -4400,6 +4415,11 @@ This is your link for group %@! Member inactive item status text + + Member role will be changed to "%@". All chat members will be notified. + Member role will be changed to "%@". All chat members will be notified. + No comment provided by engineer. + Member role will be changed to "%@". All group members will be notified. Member role will be changed to "%@". All group members will be notified. @@ -4410,11 +4430,51 @@ This is your link for group %@! Member role will be changed to "%@". The member will receive a new invitation. No comment provided by engineer. + + Member will be removed from chat - this cannot be undone! + Member will be removed from chat - this cannot be undone! + No comment provided by engineer. + Member will be removed from group - this cannot be undone! Member will be removed from group - this cannot be undone! No comment provided by engineer. + + Members can add message reactions. + Members can add message reactions. + No comment provided by engineer. + + + Members can irreversibly delete sent messages. (24 hours) + Members can irreversibly delete sent messages. (24 hours) + No comment provided by engineer. + + + Members can send SimpleX links. + Members can send SimpleX links. + No comment provided by engineer. + + + Members can send direct messages. + Members can send direct messages. + No comment provided by engineer. + + + Members can send disappearing messages. + Members can send disappearing messages. + No comment provided by engineer. + + + Members can send files and media. + Members can send files and media. + No comment provided by engineer. + + + Members can send voice messages. + Members can send voice messages. + No comment provided by engineer. + Menus Menus @@ -4988,6 +5048,11 @@ Requires compatible VPN. Onion hosts will not be used. No comment provided by engineer. + + Only chat owners can change preferences. + Only chat owners can change preferences. + No comment provided by engineer. + Only client devices store user profiles, contacts, groups, and messages. Only client devices store user profiles, contacts, groups, and messages. @@ -5384,6 +5449,11 @@ Error: %@ Privacy & security No comment provided by engineer. + + Privacy for your customers. + Privacy for your customers. + No comment provided by engineer. + Privacy redefined Privacy redefined @@ -8286,6 +8356,11 @@ Repeat connection request? You will still receive calls and notifications from muted profiles when they are active. No comment provided by engineer. + + You will stop receiving messages from this chat. Chat history will be preserved. + You will stop receiving messages from this chat. Chat history will be preserved. + No comment provided by engineer. + You will stop receiving messages from this group. Chat history will be preserved. You will stop receiving messages from this group. Chat history will be preserved. diff --git a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff index 647d650698..147ad6128f 100644 --- a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff +++ b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff @@ -1246,6 +1246,10 @@ Business address No comment provided by engineer. + + Business chats + 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). Mediante perfil (predeterminado) o [por conexión](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA). @@ -1388,6 +1392,10 @@ Cambiar perfil de usuario authentication reason + + Chat + No comment provided by engineer. + Chat already exists No comment provided by engineer. @@ -1471,6 +1479,14 @@ Tema de chat No comment provided by engineer. + + Chat will be deleted for all members - this cannot be undone! + No comment provided by engineer. + + + Chat will be deleted for you - this cannot be undone! + No comment provided by engineer. + Chats Chats @@ -2225,6 +2241,10 @@ This is your own one-time link! Eliminar y notificar contacto No comment provided by engineer. + + Delete chat + No comment provided by engineer. + Delete chat profile Eliminar perfil @@ -2235,6 +2255,10 @@ This is your own one-time link! ¿Eliminar perfil? No comment provided by engineer. + + Delete chat? + No comment provided by engineer. + Delete connection Eliminar conexión @@ -2500,6 +2524,10 @@ This is your own one-time link! Mensajes directos chat feature + + Direct messages between members are prohibited in this chat. + No comment provided by engineer. + Direct messages between members are prohibited. Los mensajes directos entre miembros del grupo no están permitidos. @@ -3672,41 +3700,6 @@ Error: %2$@ Enlaces de grupo No comment provided by engineer. - - Members can add message reactions. - Los miembros pueden añadir reacciones a los mensajes. - No comment provided by engineer. - - - Members can irreversibly delete sent messages. (24 hours) - Los miembros del grupo pueden eliminar mensajes de forma irreversible. (24 horas) - No comment provided by engineer. - - - Members can send SimpleX links. - Los miembros del grupo pueden enviar enlaces SimpleX. - No comment provided by engineer. - - - Members can send direct messages. - Los miembros del grupo pueden enviar mensajes directos. - No comment provided by engineer. - - - Members can send disappearing messages. - Los miembros del grupo pueden enviar mensajes temporales. - No comment provided by engineer. - - - Members can send files and media. - Los miembros del grupo pueden enviar archivos y multimedia. - No comment provided by engineer. - - - Members can send voice messages. - Los miembros del grupo pueden enviar mensajes de voz. - No comment provided by engineer. - Group message: Mensaje de grupo: @@ -4106,6 +4099,10 @@ More improvements are coming soon! Invitar miembros No comment provided by engineer. + + Invite to chat + No comment provided by engineer. + Invite to group Invitar al grupo @@ -4264,6 +4261,14 @@ This is your link for group %@! Salir swipe action + + Leave chat + No comment provided by engineer. + + + Leave chat? + No comment provided by engineer. + Leave group Salir del grupo @@ -4394,6 +4399,10 @@ This is your link for group %@! Miembro inactivo item status text + + Member role will be changed to "%@". All chat members will be notified. + No comment provided by engineer. + Member role will be changed to "%@". All group members will be notified. El rol del miembro cambiará a "%@" y se notificará al grupo. @@ -4404,11 +4413,50 @@ This is your link for group %@! El rol del miembro cambiará a "%@" y recibirá una invitación nueva. No comment provided by engineer. + + Member will be removed from chat - this cannot be undone! + No comment provided by engineer. + Member will be removed from group - this cannot be undone! El miembro será expulsado del grupo. ¡No podrá deshacerse! No comment provided by engineer. + + Members can add message reactions. + Los miembros pueden añadir reacciones a los mensajes. + No comment provided by engineer. + + + Members can irreversibly delete sent messages. (24 hours) + Los miembros del grupo pueden eliminar mensajes de forma irreversible. (24 horas) + No comment provided by engineer. + + + Members can send SimpleX links. + Los miembros del grupo pueden enviar enlaces SimpleX. + No comment provided by engineer. + + + Members can send direct messages. + Los miembros del grupo pueden enviar mensajes directos. + No comment provided by engineer. + + + Members can send disappearing messages. + Los miembros del grupo pueden enviar mensajes temporales. + No comment provided by engineer. + + + Members can send files and media. + Los miembros del grupo pueden enviar archivos y multimedia. + No comment provided by engineer. + + + Members can send voice messages. + Los miembros del grupo pueden enviar mensajes de voz. + No comment provided by engineer. + Menus Menus @@ -4982,6 +5030,10 @@ Requiere activación de la VPN. No se usarán hosts .onion. No comment provided by engineer. + + Only chat owners can change preferences. + No comment provided by engineer. + Only client devices store user profiles, contacts, groups, and messages. Sólo los dispositivos cliente almacenan perfiles de usuario, contactos, grupos y mensajes enviados con **cifrado de extremo a extremo de 2 capas**. @@ -5377,6 +5429,10 @@ Error: %@ Seguridad y Privacidad No comment provided by engineer. + + Privacy for your customers. + No comment provided by engineer. + Privacy redefined Privacidad redefinida @@ -8277,6 +8333,10 @@ Repeat connection request? Seguirás recibiendo llamadas y notificaciones de los perfiles silenciados cuando estén activos. No comment provided by engineer. + + You will stop receiving messages from this chat. Chat history will be preserved. + No comment provided by engineer. + You will stop receiving messages from this group. Chat history will be preserved. Dejarás de recibir mensajes de este grupo. El historial del chat se conservará. diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff index 00996b4a4f..39277bbcce 100644 --- a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff @@ -1154,6 +1154,10 @@ Business address No comment provided by engineer. + + Business chats + 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). Chat-profiilin mukaan (oletus) tai [yhteyden mukaan](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA). @@ -1286,6 +1290,10 @@ Change user profiles authentication reason + + Chat + No comment provided by engineer. + Chat already exists No comment provided by engineer. @@ -1362,6 +1370,14 @@ Chat theme No comment provided by engineer. + + Chat will be deleted for all members - this cannot be undone! + No comment provided by engineer. + + + Chat will be deleted for you - this cannot be undone! + No comment provided by engineer. + Chats Keskustelut @@ -2044,6 +2060,10 @@ This is your own one-time link! Delete and notify contact No comment provided by engineer. + + Delete chat + No comment provided by engineer. + Delete chat profile Poista keskusteluprofiili @@ -2054,6 +2074,10 @@ This is your own one-time link! Poista keskusteluprofiili? No comment provided by engineer. + + Delete chat? + No comment provided by engineer. + Delete connection Poista yhteys @@ -2302,6 +2326,10 @@ This is your own one-time link! Yksityisviestit chat feature + + Direct messages between members are prohibited in this chat. + No comment provided by engineer. + Direct messages between members are prohibited. Yksityisviestit jäsenten välillä ovat kiellettyjä tässä ryhmässä. @@ -3379,40 +3407,6 @@ Error: %2$@ Ryhmälinkit No comment provided by engineer. - - Members can add message reactions. - Ryhmän jäsenet voivat lisätä viestireaktioita. - No comment provided by engineer. - - - Members can irreversibly delete sent messages. (24 hours) - Ryhmän jäsenet voivat poistaa lähetetyt viestit peruuttamattomasti. (24 tuntia) - No comment provided by engineer. - - - Members can send SimpleX links. - No comment provided by engineer. - - - Members can send direct messages. - Ryhmän jäsenet voivat lähettää suoraviestejä. - No comment provided by engineer. - - - Members can send disappearing messages. - Ryhmän jäsenet voivat lähettää katoavia viestejä. - No comment provided by engineer. - - - Members can send files and media. - Ryhmän jäsenet voivat lähettää tiedostoja ja mediaa. - No comment provided by engineer. - - - Members can send voice messages. - Ryhmän jäsenet voivat lähettää ääniviestejä. - No comment provided by engineer. - Group message: Ryhmäviesti: @@ -3790,6 +3784,10 @@ More improvements are coming soon! Kutsu jäseniä No comment provided by engineer. + + Invite to chat + No comment provided by engineer. + Invite to group Kutsu ryhmään @@ -3938,6 +3936,14 @@ This is your link for group %@! Poistu swipe action + + Leave chat + No comment provided by engineer. + + + Leave chat? + No comment provided by engineer. + Leave group Poistu ryhmästä @@ -4062,6 +4068,10 @@ This is your link for group %@! Member inactive item status text + + Member role will be changed to "%@". All chat members will be notified. + No comment provided by engineer. + Member role will be changed to "%@". All group members will be notified. Jäsenen rooli muuttuu muotoon "%@". Kaikille ryhmän jäsenille ilmoitetaan asiasta. @@ -4072,11 +4082,49 @@ This is your link for group %@! Jäsenen rooli muutetaan muotoon "%@". Jäsen saa uuden kutsun. No comment provided by engineer. + + Member will be removed from chat - this cannot be undone! + No comment provided by engineer. + Member will be removed from group - this cannot be undone! Jäsen poistetaan ryhmästä - tätä ei voi perua! No comment provided by engineer. + + Members can add message reactions. + Ryhmän jäsenet voivat lisätä viestireaktioita. + No comment provided by engineer. + + + Members can irreversibly delete sent messages. (24 hours) + Ryhmän jäsenet voivat poistaa lähetetyt viestit peruuttamattomasti. (24 tuntia) + No comment provided by engineer. + + + Members can send SimpleX links. + No comment provided by engineer. + + + Members can send direct messages. + Ryhmän jäsenet voivat lähettää suoraviestejä. + No comment provided by engineer. + + + Members can send disappearing messages. + Ryhmän jäsenet voivat lähettää katoavia viestejä. + No comment provided by engineer. + + + Members can send files and media. + Ryhmän jäsenet voivat lähettää tiedostoja ja mediaa. + No comment provided by engineer. + + + Members can send voice messages. + Ryhmän jäsenet voivat lähettää ääniviestejä. + No comment provided by engineer. + Menus No comment provided by engineer. @@ -4594,6 +4642,10 @@ Edellyttää VPN:n sallimista. Onion-isäntiä ei käytetä. No comment provided by engineer. + + Only chat owners can change preferences. + No comment provided by engineer. + Only client devices store user profiles, contacts, groups, and messages. Vain asiakaslaitteet tallentavat käyttäjäprofiileja, yhteystietoja, ryhmiä ja viestejä, jotka on lähetetty **kaksinkertaisella päästä päähän -salauksella**. @@ -4955,6 +5007,10 @@ Error: %@ Yksityisyys ja turvallisuus No comment provided by engineer. + + Privacy for your customers. + No comment provided by engineer. + Privacy redefined Yksityisyys uudelleen määritettynä @@ -7599,6 +7655,10 @@ Repeat connection request? Saat edelleen puheluita ja ilmoituksia mykistetyiltä profiileilta, kun ne ovat aktiivisia. No comment provided by engineer. + + You will stop receiving messages from this chat. Chat history will be preserved. + No comment provided by engineer. + You will stop receiving messages from this group. Chat history will be preserved. Et enää saa viestejä tästä ryhmästä. Keskusteluhistoria säilytetään. diff --git a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff index 9498c255c8..c2030ab657 100644 --- a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff @@ -1234,6 +1234,10 @@ Business address No comment provided by engineer. + + Business chats + 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). Par profil de chat (par défaut) ou [par connexion](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA). @@ -1375,6 +1379,10 @@ Change user profiles authentication reason + + Chat + No comment provided by engineer. + Chat already exists No comment provided by engineer. @@ -1458,6 +1466,14 @@ Thème de chat No comment provided by engineer. + + Chat will be deleted for all members - this cannot be undone! + No comment provided by engineer. + + + Chat will be deleted for you - this cannot be undone! + No comment provided by engineer. + Chats Discussions @@ -2198,6 +2214,10 @@ Il s'agit de votre propre lien unique ! Supprimer et en informer le contact No comment provided by engineer. + + Delete chat + No comment provided by engineer. + Delete chat profile Supprimer le profil de chat @@ -2208,6 +2228,10 @@ Il s'agit de votre propre lien unique ! Supprimer le profil du chat ? No comment provided by engineer. + + Delete chat? + No comment provided by engineer. + Delete connection Supprimer la connexion @@ -2472,6 +2496,10 @@ Il s'agit de votre propre lien unique ! Messages directs chat feature + + Direct messages between members are prohibited in this chat. + No comment provided by engineer. + Direct messages between members are prohibited. Les messages directs entre membres sont interdits dans ce groupe. @@ -3632,41 +3660,6 @@ Erreur : %2$@ Liens de groupe No comment provided by engineer. - - Members can add message reactions. - Les membres du groupe peuvent ajouter des réactions aux messages. - No comment provided by engineer. - - - Members can irreversibly delete sent messages. (24 hours) - Les membres du groupe peuvent supprimer de manière irréversible les messages envoyés. (24 heures) - No comment provided by engineer. - - - Members can send SimpleX links. - Les membres du groupe peuvent envoyer des liens SimpleX. - No comment provided by engineer. - - - Members can send direct messages. - Les membres du groupe peuvent envoyer des messages directs. - No comment provided by engineer. - - - Members can send disappearing messages. - Les membres du groupes peuvent envoyer des messages éphémères. - No comment provided by engineer. - - - Members can send files and media. - Les membres du groupe peuvent envoyer des fichiers et des médias. - No comment provided by engineer. - - - Members can send voice messages. - Les membres du groupe peuvent envoyer des messages vocaux. - No comment provided by engineer. - Group message: Message du groupe : @@ -4064,6 +4057,10 @@ D'autres améliorations sont à venir ! Inviter des membres No comment provided by engineer. + + Invite to chat + No comment provided by engineer. + Invite to group Inviter au groupe @@ -4222,6 +4219,14 @@ Voici votre lien pour le groupe %@ ! Quitter swipe action + + Leave chat + No comment provided by engineer. + + + Leave chat? + No comment provided by engineer. + Leave group Quitter le groupe @@ -4352,6 +4357,10 @@ Voici votre lien pour le groupe %@ ! Membre inactif item status text + + Member role will be changed to "%@". All chat members will be notified. + No comment provided by engineer. + Member role will be changed to "%@". All group members will be notified. Le rôle du membre sera changé pour "%@". Tous les membres du groupe en seront informés. @@ -4362,11 +4371,50 @@ Voici votre lien pour le groupe %@ ! Le rôle du membre sera changé pour "%@". Ce membre recevra une nouvelle invitation. No comment provided by engineer. + + Member will be removed from chat - this cannot be undone! + No comment provided by engineer. + Member will be removed from group - this cannot be undone! Ce membre sera retiré du groupe - impossible de revenir en arrière ! No comment provided by engineer. + + Members can add message reactions. + Les membres du groupe peuvent ajouter des réactions aux messages. + No comment provided by engineer. + + + Members can irreversibly delete sent messages. (24 hours) + Les membres du groupe peuvent supprimer de manière irréversible les messages envoyés. (24 heures) + No comment provided by engineer. + + + Members can send SimpleX links. + Les membres du groupe peuvent envoyer des liens SimpleX. + No comment provided by engineer. + + + Members can send direct messages. + Les membres du groupe peuvent envoyer des messages directs. + No comment provided by engineer. + + + Members can send disappearing messages. + Les membres du groupes peuvent envoyer des messages éphémères. + No comment provided by engineer. + + + Members can send files and media. + Les membres du groupe peuvent envoyer des fichiers et des médias. + No comment provided by engineer. + + + Members can send voice messages. + Les membres du groupe peuvent envoyer des messages vocaux. + No comment provided by engineer. + Menus Menus @@ -4928,6 +4976,10 @@ Nécessite l'activation d'un VPN. Les hôtes .onion ne seront pas utilisés. No comment provided by engineer. + + Only chat owners can change preferences. + No comment provided by engineer. + Only client devices store user profiles, contacts, groups, and messages. Seuls les appareils clients stockent les profils des utilisateurs, les contacts, les groupes et les messages envoyés avec un **chiffrement de bout en bout à deux couches**. @@ -5317,6 +5369,10 @@ Erreur : %@ Vie privée et sécurité No comment provided by engineer. + + Privacy for your customers. + No comment provided by engineer. + Privacy redefined La vie privée redéfinie @@ -8181,6 +8237,10 @@ Répéter la demande de connexion ? Vous continuerez à recevoir des appels et des notifications des profils mis en sourdine lorsqu'ils sont actifs. No comment provided by engineer. + + You will stop receiving messages from this chat. Chat history will be preserved. + No comment provided by engineer. + You will stop receiving messages from this group. Chat history will be preserved. Vous ne recevrez plus de messages de ce groupe. L'historique du chat sera conservé. diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff index a17a6430d1..437d97274d 100644 --- a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff @@ -1246,6 +1246,10 @@ Business address No comment provided by engineer. + + Business chats + 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). A csevegési profillal (alapértelmezett), vagy a [kapcsolattal] (https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BÉTA). @@ -1388,6 +1392,10 @@ Felhasználói profilok megváltoztatása authentication reason + + Chat + No comment provided by engineer. + Chat already exists No comment provided by engineer. @@ -1471,6 +1479,14 @@ Csevegés témája No comment provided by engineer. + + Chat will be deleted for all members - this cannot be undone! + No comment provided by engineer. + + + Chat will be deleted for you - this cannot be undone! + No comment provided by engineer. + Chats Csevegések @@ -2225,6 +2241,10 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Törlés, és az ismerős értesítése No comment provided by engineer. + + Delete chat + No comment provided by engineer. + Delete chat profile Csevegési profil törlése @@ -2235,6 +2255,10 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Csevegési profil törlése? No comment provided by engineer. + + Delete chat? + No comment provided by engineer. + Delete connection Kapcsolat törlése @@ -2500,6 +2524,10 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Közvetlen üzenetek chat feature + + Direct messages between members are prohibited in this chat. + No comment provided by engineer. + Direct messages between members are prohibited. A közvetlen üzenetek küldése a tagok között le van tiltva ebben a csoportban. @@ -3672,41 +3700,6 @@ Hiba: %2$@ Csoporthivatkozások No comment provided by engineer. - - Members can add message reactions. - Csoporttagok üzenetreakciókat adhatnak hozzá. - No comment provided by engineer. - - - Members can irreversibly delete sent messages. (24 hours) - A csoport tagjai véglegesen törölhetik az elküldött üzeneteiket. (24 óra) - No comment provided by engineer. - - - Members can send SimpleX links. - A csoport tagjai küldhetnek SimpleX-hivatkozásokat. - No comment provided by engineer. - - - Members can send direct messages. - A csoport tagjai küldhetnek egymásnak közvetlen üzeneteket. - No comment provided by engineer. - - - Members can send disappearing messages. - A csoport tagjai küldhetnek eltűnő üzeneteket. - No comment provided by engineer. - - - Members can send files and media. - A csoport tagjai küldhetnek fájlokat és médiatartalmakat. - No comment provided by engineer. - - - Members can send voice messages. - A csoport tagjai küldhetnek hangüzeneteket. - No comment provided by engineer. - Group message: Csoport üzenet: @@ -4106,6 +4099,10 @@ További fejlesztések hamarosan! Tagok meghívása No comment provided by engineer. + + Invite to chat + No comment provided by engineer. + Invite to group Meghívás a csoportba @@ -4264,6 +4261,14 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! Elhagyás swipe action + + Leave chat + No comment provided by engineer. + + + Leave chat? + No comment provided by engineer. + Leave group Csoport elhagyása @@ -4394,6 +4399,10 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! Inaktív tag item status text + + Member role will be changed to "%@". All chat members will be notified. + No comment provided by engineer. + Member role will be changed to "%@". All group members will be notified. A tag szerepköre meg fog változni erre: „%@”. A csoportban az összes tag értesítve lesz. @@ -4404,11 +4413,50 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! A tag szerepköre meg fog változni erre: „%@”. A tag új meghívást fog kapni. No comment provided by engineer. + + Member will be removed from chat - this cannot be undone! + No comment provided by engineer. + Member will be removed from group - this cannot be undone! A tag eltávolítása a csoportból - ez a művelet nem vonható vissza! No comment provided by engineer. + + Members can add message reactions. + Csoporttagok üzenetreakciókat adhatnak hozzá. + No comment provided by engineer. + + + Members can irreversibly delete sent messages. (24 hours) + A csoport tagjai véglegesen törölhetik az elküldött üzeneteiket. (24 óra) + No comment provided by engineer. + + + Members can send SimpleX links. + A csoport tagjai küldhetnek SimpleX-hivatkozásokat. + No comment provided by engineer. + + + Members can send direct messages. + A csoport tagjai küldhetnek egymásnak közvetlen üzeneteket. + No comment provided by engineer. + + + Members can send disappearing messages. + A csoport tagjai küldhetnek eltűnő üzeneteket. + No comment provided by engineer. + + + Members can send files and media. + A csoport tagjai küldhetnek fájlokat és médiatartalmakat. + No comment provided by engineer. + + + Members can send voice messages. + A csoport tagjai küldhetnek hangüzeneteket. + No comment provided by engineer. + Menus Menük @@ -4982,6 +5030,10 @@ VPN engedélyezése szükséges. Onion-kiszolgálók nem lesznek használva. No comment provided by engineer. + + Only chat owners can change preferences. + No comment provided by engineer. + Only client devices store user profiles, contacts, groups, and messages. Csak az eszközök alkalmazásai tárolják a felhasználó-profilokat, névjegyeket, csoportokat és a **2 rétegű végpontok közötti titkosítással** küldött üzeneteket. @@ -5377,6 +5429,10 @@ Hiba: %@ Adatvédelem és biztonság No comment provided by engineer. + + Privacy for your customers. + No comment provided by engineer. + Privacy redefined Adatvédelem újraértelmezve @@ -8277,6 +8333,10 @@ Kapcsolatkérés megismétlése? Továbbra is kap hívásokat és értesítéseket a némított profiloktól, ha azok aktívak. No comment provided by engineer. + + You will stop receiving messages from this chat. Chat history will be preserved. + No comment provided by engineer. + You will stop receiving messages from this group. Chat history will be preserved. Ettől a csoporttól nem fog értesítéseket kapni. A csevegési előzmények megmaradnak. diff --git a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff index 0992a1f6bc..f20b0515db 100644 --- a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff +++ b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff @@ -1246,6 +1246,10 @@ Business address No comment provided by engineer. + + Business chats + 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). Per profilo di chat (predefinito) o [per connessione](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA). @@ -1388,6 +1392,10 @@ Modifica profili utente authentication reason + + Chat + No comment provided by engineer. + Chat already exists No comment provided by engineer. @@ -1471,6 +1479,14 @@ Tema della chat No comment provided by engineer. + + Chat will be deleted for all members - this cannot be undone! + No comment provided by engineer. + + + Chat will be deleted for you - this cannot be undone! + No comment provided by engineer. + Chats Chat @@ -2225,6 +2241,10 @@ Questo è il tuo link una tantum! Elimina e avvisa il contatto No comment provided by engineer. + + Delete chat + No comment provided by engineer. + Delete chat profile Elimina il profilo di chat @@ -2235,6 +2255,10 @@ Questo è il tuo link una tantum! Eliminare il profilo di chat? No comment provided by engineer. + + Delete chat? + No comment provided by engineer. + Delete connection Elimina connessione @@ -2500,6 +2524,10 @@ Questo è il tuo link una tantum! Messaggi diretti chat feature + + Direct messages between members are prohibited in this chat. + No comment provided by engineer. + Direct messages between members are prohibited. I messaggi diretti tra i membri sono vietati in questo gruppo. @@ -3672,41 +3700,6 @@ Errore: %2$@ Link del gruppo No comment provided by engineer. - - Members can add message reactions. - I membri del gruppo possono aggiungere reazioni ai messaggi. - No comment provided by engineer. - - - Members can irreversibly delete sent messages. (24 hours) - I membri del gruppo possono eliminare irreversibilmente i messaggi inviati. (24 ore) - No comment provided by engineer. - - - Members can send SimpleX links. - I membri del gruppo possono inviare link di Simplex. - No comment provided by engineer. - - - Members can send direct messages. - I membri del gruppo possono inviare messaggi diretti. - No comment provided by engineer. - - - Members can send disappearing messages. - I membri del gruppo possono inviare messaggi a tempo. - No comment provided by engineer. - - - Members can send files and media. - I membri del gruppo possono inviare file e contenuti multimediali. - No comment provided by engineer. - - - Members can send voice messages. - I membri del gruppo possono inviare messaggi vocali. - No comment provided by engineer. - Group message: Messaggio del gruppo: @@ -4106,6 +4099,10 @@ Altri miglioramenti sono in arrivo! Invita membri No comment provided by engineer. + + Invite to chat + No comment provided by engineer. + Invite to group Invita al gruppo @@ -4264,6 +4261,14 @@ Questo è il tuo link per il gruppo %@! Esci swipe action + + Leave chat + No comment provided by engineer. + + + Leave chat? + No comment provided by engineer. + Leave group Esci dal gruppo @@ -4394,6 +4399,10 @@ Questo è il tuo link per il gruppo %@! Membro inattivo item status text + + Member role will be changed to "%@". All chat members will be notified. + No comment provided by engineer. + Member role will be changed to "%@". All group members will be notified. Il ruolo del membro verrà cambiato in "%@". Tutti i membri del gruppo verranno avvisati. @@ -4404,11 +4413,50 @@ Questo è il tuo link per il gruppo %@! Il ruolo del membro verrà cambiato in "%@". Il membro riceverà un invito nuovo. No comment provided by engineer. + + Member will be removed from chat - this cannot be undone! + No comment provided by engineer. + Member will be removed from group - this cannot be undone! Il membro verrà rimosso dal gruppo, non è reversibile! No comment provided by engineer. + + Members can add message reactions. + I membri del gruppo possono aggiungere reazioni ai messaggi. + No comment provided by engineer. + + + Members can irreversibly delete sent messages. (24 hours) + I membri del gruppo possono eliminare irreversibilmente i messaggi inviati. (24 ore) + No comment provided by engineer. + + + Members can send SimpleX links. + I membri del gruppo possono inviare link di Simplex. + No comment provided by engineer. + + + Members can send direct messages. + I membri del gruppo possono inviare messaggi diretti. + No comment provided by engineer. + + + Members can send disappearing messages. + I membri del gruppo possono inviare messaggi a tempo. + No comment provided by engineer. + + + Members can send files and media. + I membri del gruppo possono inviare file e contenuti multimediali. + No comment provided by engineer. + + + Members can send voice messages. + I membri del gruppo possono inviare messaggi vocali. + No comment provided by engineer. + Menus Menu @@ -4982,6 +5030,10 @@ Richiede l'attivazione della VPN. Gli host Onion non verranno usati. No comment provided by engineer. + + Only chat owners can change preferences. + No comment provided by engineer. + Only client devices store user profiles, contacts, groups, and messages. Solo i dispositivi client archiviano profili utente, i contatti, i gruppi e i messaggi inviati con la **crittografia end-to-end a 2 livelli**. @@ -5377,6 +5429,10 @@ Errore: %@ Privacy e sicurezza No comment provided by engineer. + + Privacy for your customers. + No comment provided by engineer. + Privacy redefined Privacy ridefinita @@ -8277,6 +8333,10 @@ Ripetere la richiesta di connessione? Continuerai a ricevere chiamate e notifiche da profili silenziati quando sono attivi. No comment provided by engineer. + + You will stop receiving messages from this chat. Chat history will be preserved. + No comment provided by engineer. + You will stop receiving messages from this group. Chat history will be preserved. Non riceverai più messaggi da questo gruppo. La cronologia della chat verrà conservata. diff --git a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff index 32d2db371b..288276124c 100644 --- a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff +++ b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff @@ -1178,6 +1178,10 @@ Business address No comment provided by engineer. + + Business chats + 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). チャット プロファイル経由 (デフォルト) または [接続経由](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA). @@ -1310,6 +1314,10 @@ Change user profiles authentication reason + + Chat + No comment provided by engineer. + Chat already exists No comment provided by engineer. @@ -1387,6 +1395,14 @@ チャットテーマ No comment provided by engineer. + + Chat will be deleted for all members - this cannot be undone! + No comment provided by engineer. + + + Chat will be deleted for you - this cannot be undone! + No comment provided by engineer. + Chats チャット @@ -2088,6 +2104,10 @@ This is your own one-time link! Delete and notify contact No comment provided by engineer. + + Delete chat + No comment provided by engineer. + Delete chat profile チャットのプロフィールを削除する @@ -2098,6 +2118,10 @@ This is your own one-time link! チャットのプロフィールを削除しますか? No comment provided by engineer. + + Delete chat? + No comment provided by engineer. + Delete connection 接続を削除する @@ -2348,6 +2372,10 @@ This is your own one-time link! ダイレクトメッセージ chat feature + + Direct messages between members are prohibited in this chat. + No comment provided by engineer. + Direct messages between members are prohibited. このグループではメンバー間のダイレクトメッセージが使用禁止です。 @@ -3426,40 +3454,6 @@ Error: %2$@ グループのリンク No comment provided by engineer. - - Members can add message reactions. - グループメンバーはメッセージへのリアクションを追加できます。 - No comment provided by engineer. - - - Members can irreversibly delete sent messages. (24 hours) - グループのメンバーがメッセージを完全削除することができます。(24時間) - No comment provided by engineer. - - - Members can send SimpleX links. - No comment provided by engineer. - - - Members can send direct messages. - グループのメンバーがダイレクトメッセージを送信できます。 - No comment provided by engineer. - - - Members can send disappearing messages. - グループのメンバーが消えるメッセージを送信できます。 - No comment provided by engineer. - - - Members can send files and media. - グループメンバーはファイルやメディアを送信できます。 - No comment provided by engineer. - - - Members can send voice messages. - グループのメンバーが音声メッセージを送信できます。 - No comment provided by engineer. - Group message: グループメッセージ: @@ -3837,6 +3831,10 @@ More improvements are coming soon! メンバーを招待する No comment provided by engineer. + + Invite to chat + No comment provided by engineer. + Invite to group グループに招待する @@ -3985,6 +3983,14 @@ This is your link for group %@! 脱退 swipe action + + Leave chat + No comment provided by engineer. + + + Leave chat? + No comment provided by engineer. + Leave group グループを脱退 @@ -4109,6 +4115,10 @@ This is your link for group %@! Member inactive item status text + + Member role will be changed to "%@". All chat members will be notified. + No comment provided by engineer. + Member role will be changed to "%@". All group members will be notified. メンバーの役割が "%@" に変更されます。 グループメンバー全員に通知されます。 @@ -4119,11 +4129,49 @@ This is your link for group %@! メンバーの役割が "%@" に変更されます。 メンバーは新たな招待を受け取ります。 No comment provided by engineer. + + Member will be removed from chat - this cannot be undone! + No comment provided by engineer. + Member will be removed from group - this cannot be undone! メンバーをグループから除名する (※元に戻せません※)! No comment provided by engineer. + + Members can add message reactions. + グループメンバーはメッセージへのリアクションを追加できます。 + No comment provided by engineer. + + + Members can irreversibly delete sent messages. (24 hours) + グループのメンバーがメッセージを完全削除することができます。(24時間) + No comment provided by engineer. + + + Members can send SimpleX links. + No comment provided by engineer. + + + Members can send direct messages. + グループのメンバーがダイレクトメッセージを送信できます。 + No comment provided by engineer. + + + Members can send disappearing messages. + グループのメンバーが消えるメッセージを送信できます。 + No comment provided by engineer. + + + Members can send files and media. + グループメンバーはファイルやメディアを送信できます。 + No comment provided by engineer. + + + Members can send voice messages. + グループのメンバーが音声メッセージを送信できます。 + No comment provided by engineer. + Menus No comment provided by engineer. @@ -4643,6 +4691,10 @@ VPN を有効にする必要があります。 オニオンのホストが使われません。 No comment provided by engineer. + + Only chat owners can change preferences. + No comment provided by engineer. + Only client devices store user profiles, contacts, groups, and messages. **2 レイヤーのエンドツーエンド暗号化**を使用して送信されたユーザー プロファイル、連絡先、グループ、メッセージを保存できるのはクライアント デバイスのみです。 @@ -5005,6 +5057,10 @@ Error: %@ プライバシーとセキュリティ No comment provided by engineer. + + Privacy for your customers. + No comment provided by engineer. + Privacy redefined プライバシーの基準を新境地に @@ -7641,6 +7697,10 @@ Repeat connection request? ミュートされたプロフィールがアクティブな場合でも、そのプロフィールからの通話や通知は引き続き受信します。 No comment provided by engineer. + + You will stop receiving messages from this chat. Chat history will be preserved. + No comment provided by engineer. + You will stop receiving messages from this group. Chat history will be preserved. このグループからのメッセージが届かなくなります。チャットの履歴が残ります。 diff --git a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff index b9cba70c3a..6bf4808025 100644 --- a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff @@ -1246,6 +1246,10 @@ Business address No comment provided by engineer. + + Business chats + 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). Via chatprofiel (standaard) of [via verbinding](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA). @@ -1388,6 +1392,10 @@ Gebruikersprofielen wijzigen authentication reason + + Chat + No comment provided by engineer. + Chat already exists No comment provided by engineer. @@ -1471,6 +1479,14 @@ Chat thema No comment provided by engineer. + + Chat will be deleted for all members - this cannot be undone! + No comment provided by engineer. + + + Chat will be deleted for you - this cannot be undone! + No comment provided by engineer. + Chats Chats @@ -2225,6 +2241,10 @@ Dit is uw eigen eenmalige link! Verwijderen en contact op de hoogte stellen No comment provided by engineer. + + Delete chat + No comment provided by engineer. + Delete chat profile Chatprofiel verwijderen @@ -2235,6 +2255,10 @@ Dit is uw eigen eenmalige link! Chatprofiel verwijderen? No comment provided by engineer. + + Delete chat? + No comment provided by engineer. + Delete connection Verbinding verwijderen @@ -2500,6 +2524,10 @@ Dit is uw eigen eenmalige link! Directe berichten chat feature + + Direct messages between members are prohibited in this chat. + No comment provided by engineer. + Direct messages between members are prohibited. Directe berichten tussen leden zijn verboden in deze groep. @@ -3672,41 +3700,6 @@ Fout: %2$@ Groep links No comment provided by engineer. - - Members can add message reactions. - Groepsleden kunnen bericht reacties toevoegen. - No comment provided by engineer. - - - Members can irreversibly delete sent messages. (24 hours) - Groepsleden kunnen verzonden berichten onherroepelijk verwijderen. (24 uur) - No comment provided by engineer. - - - Members can send SimpleX links. - Groepsleden kunnen SimpleX-links verzenden. - No comment provided by engineer. - - - Members can send direct messages. - Groepsleden kunnen directe berichten sturen. - No comment provided by engineer. - - - Members can send disappearing messages. - Groepsleden kunnen verdwijnende berichten sturen. - No comment provided by engineer. - - - Members can send files and media. - Groepsleden kunnen bestanden en media verzenden. - No comment provided by engineer. - - - Members can send voice messages. - Groepsleden kunnen spraak berichten verzenden. - No comment provided by engineer. - Group message: Groep bericht: @@ -4106,6 +4099,10 @@ Binnenkort meer verbeteringen! Nodig leden uit No comment provided by engineer. + + Invite to chat + No comment provided by engineer. + Invite to group Uitnodigen voor groep @@ -4264,6 +4261,14 @@ Dit is jouw link voor groep %@! Verlaten swipe action + + Leave chat + No comment provided by engineer. + + + Leave chat? + No comment provided by engineer. + Leave group Groep verlaten @@ -4394,6 +4399,10 @@ Dit is jouw link voor groep %@! Lid inactief item status text + + Member role will be changed to "%@". All chat members will be notified. + No comment provided by engineer. + Member role will be changed to "%@". All group members will be notified. De rol van lid wordt gewijzigd in "%@". Alle groepsleden worden op de hoogte gebracht. @@ -4404,11 +4413,50 @@ Dit is jouw link voor groep %@! De rol van lid wordt gewijzigd in "%@". Het lid ontvangt een nieuwe uitnodiging. No comment provided by engineer. + + Member will be removed from chat - this cannot be undone! + No comment provided by engineer. + Member will be removed from group - this cannot be undone! Lid wordt uit de groep verwijderd, dit kan niet ongedaan worden gemaakt! No comment provided by engineer. + + Members can add message reactions. + Groepsleden kunnen bericht reacties toevoegen. + No comment provided by engineer. + + + Members can irreversibly delete sent messages. (24 hours) + Groepsleden kunnen verzonden berichten onherroepelijk verwijderen. (24 uur) + No comment provided by engineer. + + + Members can send SimpleX links. + Groepsleden kunnen SimpleX-links verzenden. + No comment provided by engineer. + + + Members can send direct messages. + Groepsleden kunnen directe berichten sturen. + No comment provided by engineer. + + + Members can send disappearing messages. + Groepsleden kunnen verdwijnende berichten sturen. + No comment provided by engineer. + + + Members can send files and media. + Groepsleden kunnen bestanden en media verzenden. + No comment provided by engineer. + + + Members can send voice messages. + Groepsleden kunnen spraak berichten verzenden. + No comment provided by engineer. + Menus Menu's @@ -4982,6 +5030,10 @@ Vereist het inschakelen van VPN. Onion hosts worden niet gebruikt. No comment provided by engineer. + + Only chat owners can change preferences. + No comment provided by engineer. + Only client devices store user profiles, contacts, groups, and messages. Alleen client apparaten slaan gebruikers profielen, contacten, groepen en berichten op die zijn verzonden met **2-laags end-to-end-codering**. @@ -5377,6 +5429,10 @@ Fout: %@ Privacy en beveiliging No comment provided by engineer. + + Privacy for your customers. + No comment provided by engineer. + Privacy redefined Privacy opnieuw gedefinieerd @@ -8277,6 +8333,10 @@ Verbindingsverzoek herhalen? U ontvangt nog steeds oproepen en meldingen van gedempte profielen wanneer deze actief zijn. No comment provided by engineer. + + You will stop receiving messages from this chat. Chat history will be preserved. + No comment provided by engineer. + You will stop receiving messages from this group. Chat history will be preserved. Je ontvangt geen berichten meer van deze groep. Je gesprek geschiedenis blijft behouden. diff --git a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff index bdfe502952..ea40b78582 100644 --- a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff @@ -1229,6 +1229,10 @@ Business address No comment provided by engineer. + + Business chats + 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). @@ -1370,6 +1374,10 @@ Change user profiles authentication reason + + Chat + No comment provided by engineer. + Chat already exists No comment provided by engineer. @@ -1453,6 +1461,14 @@ Motyw czatu No comment provided by engineer. + + Chat will be deleted for all members - this cannot be undone! + No comment provided by engineer. + + + Chat will be deleted for you - this cannot be undone! + No comment provided by engineer. + Chats Czaty @@ -2192,6 +2208,10 @@ To jest twój jednorazowy link! Usuń i powiadom kontakt No comment provided by engineer. + + Delete chat + No comment provided by engineer. + Delete chat profile Usuń profil czatu @@ -2202,6 +2222,10 @@ To jest twój jednorazowy link! Usunąć profil czatu? No comment provided by engineer. + + Delete chat? + No comment provided by engineer. + Delete connection Usuń połączenie @@ -2465,6 +2489,10 @@ To jest twój jednorazowy link! Bezpośrednie wiadomości chat feature + + Direct messages between members are prohibited in this chat. + No comment provided by engineer. + Direct messages between members are prohibited. Bezpośrednie wiadomości między członkami są zabronione w tej grupie. @@ -3624,41 +3652,6 @@ Błąd: %2$@ Linki grupowe No comment provided by engineer. - - Members can add message reactions. - Członkowie grupy mogą dodawać reakcje wiadomości. - No comment provided by engineer. - - - Members can irreversibly delete sent messages. (24 hours) - Członkowie grupy mogą nieodwracalnie usuwać wysłane wiadomości. (24 godziny) - No comment provided by engineer. - - - Members can send SimpleX links. - Członkowie grupy mogą wysyłać linki SimpleX. - No comment provided by engineer. - - - Members can send direct messages. - Członkowie grupy mogą wysyłać bezpośrednie wiadomości. - No comment provided by engineer. - - - Members can send disappearing messages. - Członkowie grupy mogą wysyłać znikające wiadomości. - No comment provided by engineer. - - - Members can send files and media. - Członkowie grupy mogą wysyłać pliki i media. - No comment provided by engineer. - - - Members can send voice messages. - Członkowie grupy mogą wysyłać wiadomości głosowe. - No comment provided by engineer. - Group message: Wiadomość grupowa: @@ -4054,6 +4047,10 @@ More improvements are coming soon! Zaproś członków No comment provided by engineer. + + Invite to chat + No comment provided by engineer. + Invite to group Zaproś do grupy @@ -4212,6 +4209,14 @@ To jest twój link do grupy %@! Opuść swipe action + + Leave chat + No comment provided by engineer. + + + Leave chat? + No comment provided by engineer. + Leave group Opuść grupę @@ -4342,6 +4347,10 @@ To jest twój link do grupy %@! Członek nieaktywny item status text + + Member role will be changed to "%@". All chat members will be notified. + No comment provided by engineer. + Member role will be changed to "%@". All group members will be notified. Rola członka grupy zostanie zmieniona na "%@". Wszyscy członkowie grupy zostaną powiadomieni. @@ -4352,11 +4361,50 @@ To jest twój link do grupy %@! Rola członka zostanie zmieniona na "%@". Członek otrzyma nowe zaproszenie. No comment provided by engineer. + + Member will be removed from chat - this cannot be undone! + No comment provided by engineer. + Member will be removed from group - this cannot be undone! Członek zostanie usunięty z grupy - nie można tego cofnąć! No comment provided by engineer. + + Members can add message reactions. + Członkowie grupy mogą dodawać reakcje wiadomości. + No comment provided by engineer. + + + Members can irreversibly delete sent messages. (24 hours) + Członkowie grupy mogą nieodwracalnie usuwać wysłane wiadomości. (24 godziny) + No comment provided by engineer. + + + Members can send SimpleX links. + Członkowie grupy mogą wysyłać linki SimpleX. + No comment provided by engineer. + + + Members can send direct messages. + Członkowie grupy mogą wysyłać bezpośrednie wiadomości. + No comment provided by engineer. + + + Members can send disappearing messages. + Członkowie grupy mogą wysyłać znikające wiadomości. + No comment provided by engineer. + + + Members can send files and media. + Członkowie grupy mogą wysyłać pliki i media. + No comment provided by engineer. + + + Members can send voice messages. + Członkowie grupy mogą wysyłać wiadomości głosowe. + No comment provided by engineer. + Menus Menu @@ -4918,6 +4966,10 @@ Wymaga włączenia VPN. Hosty onion nie będą używane. No comment provided by engineer. + + Only chat owners can change preferences. + No comment provided by engineer. + Only client devices store user profiles, contacts, groups, and messages. Tylko urządzenia klienckie przechowują profile użytkowników, kontakty, grupy i wiadomości wysyłane za pomocą **2-warstwowego szyfrowania end-to-end**. @@ -5307,6 +5359,10 @@ Błąd: %@ Prywatność i bezpieczeństwo No comment provided by engineer. + + Privacy for your customers. + No comment provided by engineer. + Privacy redefined Redefinicja prywatności @@ -8168,6 +8224,10 @@ Powtórzyć prośbę połączenia? Nadal będziesz otrzymywać połączenia i powiadomienia z wyciszonych profili, gdy są one aktywne. No comment provided by engineer. + + You will stop receiving messages from this chat. Chat history will be preserved. + No comment provided by engineer. + You will stop receiving messages from this group. Chat history will be preserved. Przestaniesz otrzymywać wiadomości od tej grupy. Historia czatu zostanie zachowana. diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff index 6d57734259..3fd5d194de 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff @@ -1235,6 +1235,10 @@ Business address No comment provided by engineer. + + Business chats + 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). По профилю чата или [по соединению](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (БЕТА). @@ -1376,6 +1380,10 @@ Change user profiles authentication reason + + Chat + No comment provided by engineer. + Chat already exists No comment provided by engineer. @@ -1459,6 +1467,14 @@ Тема чата No comment provided by engineer. + + Chat will be deleted for all members - this cannot be undone! + No comment provided by engineer. + + + Chat will be deleted for you - this cannot be undone! + No comment provided by engineer. + Chats Чаты @@ -2199,6 +2215,10 @@ This is your own one-time link! Удалить и уведомить контакт No comment provided by engineer. + + Delete chat + No comment provided by engineer. + Delete chat profile Удалить профиль чата @@ -2209,6 +2229,10 @@ This is your own one-time link! Удалить профиль? No comment provided by engineer. + + Delete chat? + No comment provided by engineer. + Delete connection Удалить соединение @@ -2473,6 +2497,10 @@ This is your own one-time link! Прямые сообщения chat feature + + Direct messages between members are prohibited in this chat. + No comment provided by engineer. + Direct messages between members are prohibited. Прямые сообщения между членами группы запрещены. @@ -3633,41 +3661,6 @@ Error: %2$@ Ссылки групп No comment provided by engineer. - - Members can add message reactions. - Члены группы могут добавлять реакции на сообщения. - No comment provided by engineer. - - - Members can irreversibly delete sent messages. (24 hours) - Члены группы могут необратимо удалять отправленные сообщения. (24 часа) - No comment provided by engineer. - - - Members can send SimpleX links. - Члены группы могут отправлять ссылки SimpleX. - No comment provided by engineer. - - - Members can send direct messages. - Члены группы могут посылать прямые сообщения. - No comment provided by engineer. - - - Members can send disappearing messages. - Члены группы могут посылать исчезающие сообщения. - No comment provided by engineer. - - - Members can send files and media. - Члены группы могут слать файлы и медиа. - No comment provided by engineer. - - - Members can send voice messages. - Члены группы могут отправлять голосовые сообщения. - No comment provided by engineer. - Group message: Групповое сообщение: @@ -4064,6 +4057,10 @@ More improvements are coming soon! Пригласить членов группы No comment provided by engineer. + + Invite to chat + No comment provided by engineer. + Invite to group Пригласить в группу @@ -4222,6 +4219,14 @@ This is your link for group %@! Выйти swipe action + + Leave chat + No comment provided by engineer. + + + Leave chat? + No comment provided by engineer. + Leave group Выйти из группы @@ -4352,6 +4357,10 @@ This is your link for group %@! Член неактивен item status text + + Member role will be changed to "%@". All chat members will be notified. + No comment provided by engineer. + Member role will be changed to "%@". All group members will be notified. Роль члена группы будет изменена на "%@". Все члены группы получат сообщение. @@ -4362,11 +4371,50 @@ This is your link for group %@! Роль члена группы будет изменена на "%@". Будет отправлено новое приглашение. No comment provided by engineer. + + Member will be removed from chat - this cannot be undone! + No comment provided by engineer. + Member will be removed from group - this cannot be undone! Член группы будет удален - это действие нельзя отменить! No comment provided by engineer. + + Members can add message reactions. + Члены группы могут добавлять реакции на сообщения. + No comment provided by engineer. + + + Members can irreversibly delete sent messages. (24 hours) + Члены группы могут необратимо удалять отправленные сообщения. (24 часа) + No comment provided by engineer. + + + Members can send SimpleX links. + Члены группы могут отправлять ссылки SimpleX. + No comment provided by engineer. + + + Members can send direct messages. + Члены группы могут посылать прямые сообщения. + No comment provided by engineer. + + + Members can send disappearing messages. + Члены группы могут посылать исчезающие сообщения. + No comment provided by engineer. + + + Members can send files and media. + Члены группы могут слать файлы и медиа. + No comment provided by engineer. + + + Members can send voice messages. + Члены группы могут отправлять голосовые сообщения. + No comment provided by engineer. + Menus Меню @@ -4928,6 +4976,10 @@ Requires compatible VPN. Onion хосты не используются. No comment provided by engineer. + + Only chat owners can change preferences. + No comment provided by engineer. + Only client devices store user profiles, contacts, groups, and messages. Только пользовательские устройства хранят контакты, группы и сообщения. @@ -5317,6 +5369,10 @@ Error: %@ Конфиденциальность No comment provided by engineer. + + Privacy for your customers. + No comment provided by engineer. + Privacy redefined Более конфиденциальный @@ -8181,6 +8237,10 @@ Repeat connection request? Вы все равно получите звонки и уведомления в профилях без звука, когда они активные. No comment provided by engineer. + + You will stop receiving messages from this chat. Chat history will be preserved. + No comment provided by engineer. + You will stop receiving messages from this group. Chat history will be preserved. Вы перестанете получать сообщения от этой группы. История чата будет сохранена. diff --git a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff index cec1637438..edf32b06f2 100644 --- a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff +++ b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff @@ -1146,6 +1146,10 @@ Business address No comment provided by engineer. + + Business chats + 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). ตามโปรไฟล์แชท (ค่าเริ่มต้น) หรือ [โดยการเชื่อมต่อ](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (เบต้า) @@ -1278,6 +1282,10 @@ Change user profiles authentication reason + + Chat + No comment provided by engineer. + Chat already exists No comment provided by engineer. @@ -1354,6 +1362,14 @@ Chat theme No comment provided by engineer. + + Chat will be deleted for all members - this cannot be undone! + No comment provided by engineer. + + + Chat will be deleted for you - this cannot be undone! + No comment provided by engineer. + Chats แชท @@ -2033,6 +2049,10 @@ This is your own one-time link! Delete and notify contact No comment provided by engineer. + + Delete chat + No comment provided by engineer. + Delete chat profile ลบโปรไฟล์แชท @@ -2043,6 +2063,10 @@ This is your own one-time link! ลบโปรไฟล์แชทไหม? No comment provided by engineer. + + Delete chat? + No comment provided by engineer. + Delete connection ลบการเชื่อมต่อ @@ -2290,6 +2314,10 @@ This is your own one-time link! ข้อความโดยตรง chat feature + + Direct messages between members are prohibited in this chat. + No comment provided by engineer. + Direct messages between members are prohibited. ข้อความโดยตรงระหว่างสมาชิกเป็นสิ่งต้องห้ามในกลุ่มนี้ @@ -3364,40 +3392,6 @@ Error: %2$@ ลิงค์กลุ่ม No comment provided by engineer. - - Members can add message reactions. - สมาชิกกลุ่มสามารถเพิ่มการแสดงปฏิกิริยาต่อข้อความได้ - No comment provided by engineer. - - - Members can irreversibly delete sent messages. (24 hours) - สมาชิกกลุ่มสามารถลบข้อความที่ส่งแล้วอย่างถาวร - No comment provided by engineer. - - - Members can send SimpleX links. - No comment provided by engineer. - - - Members can send direct messages. - สมาชิกกลุ่มสามารถส่งข้อความโดยตรงได้ - No comment provided by engineer. - - - Members can send disappearing messages. - สมาชิกกลุ่มสามารถส่งข้อความที่จะหายไปหลังจากเวลาที่กำหนดหลังการอ่าน (disappearing messages) ได้ - No comment provided by engineer. - - - Members can send files and media. - สมาชิกกลุ่มสามารถส่งไฟล์และสื่อ - No comment provided by engineer. - - - Members can send voice messages. - สมาชิกกลุ่มสามารถส่งข้อความเสียง - No comment provided by engineer. - Group message: ข้อความกลุ่ม: @@ -3773,6 +3767,10 @@ More improvements are coming soon! เชิญสมาชิก No comment provided by engineer. + + Invite to chat + No comment provided by engineer. + Invite to group เชิญเข้าร่วมกลุ่ม @@ -3921,6 +3919,14 @@ This is your link for group %@! ออกจาก swipe action + + Leave chat + No comment provided by engineer. + + + Leave chat? + No comment provided by engineer. + Leave group ออกจากกลุ่ม @@ -4045,6 +4051,10 @@ This is your link for group %@! Member inactive item status text + + Member role will be changed to "%@". All chat members will be notified. + No comment provided by engineer. + Member role will be changed to "%@". All group members will be notified. บทบาทของสมาชิกจะถูกเปลี่ยนเป็น "%@" สมาชิกกลุ่มทั้งหมดจะได้รับแจ้ง @@ -4055,11 +4065,49 @@ This is your link for group %@! บทบาทของสมาชิกจะถูกเปลี่ยนเป็น "%@" สมาชิกจะได้รับคำเชิญใหม่ No comment provided by engineer. + + Member will be removed from chat - this cannot be undone! + No comment provided by engineer. + Member will be removed from group - this cannot be undone! สมาชิกจะถูกลบออกจากกลุ่ม - ไม่สามารถยกเลิกได้! No comment provided by engineer. + + Members can add message reactions. + สมาชิกกลุ่มสามารถเพิ่มการแสดงปฏิกิริยาต่อข้อความได้ + No comment provided by engineer. + + + Members can irreversibly delete sent messages. (24 hours) + สมาชิกกลุ่มสามารถลบข้อความที่ส่งแล้วอย่างถาวร + No comment provided by engineer. + + + Members can send SimpleX links. + No comment provided by engineer. + + + Members can send direct messages. + สมาชิกกลุ่มสามารถส่งข้อความโดยตรงได้ + No comment provided by engineer. + + + Members can send disappearing messages. + สมาชิกกลุ่มสามารถส่งข้อความที่จะหายไปหลังจากเวลาที่กำหนดหลังการอ่าน (disappearing messages) ได้ + No comment provided by engineer. + + + Members can send files and media. + สมาชิกกลุ่มสามารถส่งไฟล์และสื่อ + No comment provided by engineer. + + + Members can send voice messages. + สมาชิกกลุ่มสามารถส่งข้อความเสียง + No comment provided by engineer. + Menus No comment provided by engineer. @@ -4573,6 +4621,10 @@ Requires compatible VPN. โฮสต์หัวหอมจะไม่ถูกใช้ No comment provided by engineer. + + Only chat owners can change preferences. + No comment provided by engineer. + Only client devices store user profiles, contacts, groups, and messages. เฉพาะอุปกรณ์ไคลเอนต์เท่านั้นที่จัดเก็บโปรไฟล์ผู้ใช้ ผู้ติดต่อ กลุ่ม และข้อความที่ส่งด้วย **การเข้ารหัส encrypt แบบ 2 ชั้น** @@ -4934,6 +4986,10 @@ Error: %@ ความเป็นส่วนตัวและความปลอดภัย No comment provided by engineer. + + Privacy for your customers. + No comment provided by engineer. + Privacy redefined นิยามความเป็นส่วนตัวใหม่ @@ -7568,6 +7624,10 @@ Repeat connection request? คุณจะยังได้รับสายเรียกเข้าและการแจ้งเตือนจากโปรไฟล์ที่ปิดเสียงเมื่อโปรไฟล์ของเขามีการใช้งาน No comment provided by engineer. + + You will stop receiving messages from this chat. Chat history will be preserved. + No comment provided by engineer. + You will stop receiving messages from this group. Chat history will be preserved. คุณจะหยุดได้รับข้อความจากกลุ่มนี้ ประวัติการแชทจะถูกรักษาไว้ diff --git a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff index 8ab4b2419d..6d69920454 100644 --- a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff @@ -1234,6 +1234,10 @@ Business address No comment provided by engineer. + + Business chats + 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). Sohbet profiline göre (varsayılan) veya [bağlantıya göre](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA). @@ -1375,6 +1379,10 @@ Change user profiles authentication reason + + Chat + No comment provided by engineer. + Chat already exists No comment provided by engineer. @@ -1458,6 +1466,14 @@ Sohbet teması No comment provided by engineer. + + Chat will be deleted for all members - this cannot be undone! + No comment provided by engineer. + + + Chat will be deleted for you - this cannot be undone! + No comment provided by engineer. + Chats Sohbetler @@ -2198,6 +2214,10 @@ Bu senin kendi tek kullanımlık bağlantın! Sil ve kişiye bildir No comment provided by engineer. + + Delete chat + No comment provided by engineer. + Delete chat profile Sohbet profilini sil @@ -2208,6 +2228,10 @@ Bu senin kendi tek kullanımlık bağlantın! Sohbet profili silinsin mi? No comment provided by engineer. + + Delete chat? + No comment provided by engineer. + Delete connection Bağlantıyı sil @@ -2472,6 +2496,10 @@ Bu senin kendi tek kullanımlık bağlantın! Doğrudan mesajlar chat feature + + Direct messages between members are prohibited in this chat. + No comment provided by engineer. + Direct messages between members are prohibited. Bu grupta üyeler arasında direkt mesajlaşma yasaktır. @@ -3632,41 +3660,6 @@ Hata: %2$@ Grup bağlantıları No comment provided by engineer. - - Members can add message reactions. - Grup üyeleri mesaj tepkileri ekleyebilir. - No comment provided by engineer. - - - Members can irreversibly delete sent messages. (24 hours) - Grup üyeleri, gönderilen mesajları kalıcı olarak silebilir. (24 saat içinde) - No comment provided by engineer. - - - Members can send SimpleX links. - Grup üyeleri SimpleX bağlantıları gönderebilir. - No comment provided by engineer. - - - Members can send direct messages. - Grup üyeleri doğrudan mesajlar gönderebilir. - No comment provided by engineer. - - - Members can send disappearing messages. - Grup üyeleri kaybolan mesajlar gönderebilir. - No comment provided by engineer. - - - Members can send files and media. - Grup üyeleri dosyalar ve medya gönderebilir. - No comment provided by engineer. - - - Members can send voice messages. - Grup üyeleri sesli mesajlar gönderebilir. - No comment provided by engineer. - Group message: Grup mesajı: @@ -4064,6 +4057,10 @@ Daha fazla iyileştirme yakında geliyor! Üyeleri davet et No comment provided by engineer. + + Invite to chat + No comment provided by engineer. + Invite to group Gruba davet et @@ -4222,6 +4219,14 @@ Bu senin grup için bağlantın %@! Ayrıl swipe action + + Leave chat + No comment provided by engineer. + + + Leave chat? + No comment provided by engineer. + Leave group Gruptan ayrıl @@ -4352,6 +4357,10 @@ Bu senin grup için bağlantın %@! Üye inaktif item status text + + Member role will be changed to "%@". All chat members will be notified. + No comment provided by engineer. + Member role will be changed to "%@". All group members will be notified. Üye rolü "%@" olarak değiştirilecektir. Ve tüm grup üyeleri bilgilendirilecektir. @@ -4362,11 +4371,50 @@ Bu senin grup için bağlantın %@! Üye rolü "%@" olarak değiştirilecektir. Ve üye yeni bir davetiye alacaktır. No comment provided by engineer. + + Member will be removed from chat - this cannot be undone! + No comment provided by engineer. + Member will be removed from group - this cannot be undone! Üye gruptan çıkarılacaktır - bu geri alınamaz! No comment provided by engineer. + + Members can add message reactions. + Grup üyeleri mesaj tepkileri ekleyebilir. + No comment provided by engineer. + + + Members can irreversibly delete sent messages. (24 hours) + Grup üyeleri, gönderilen mesajları kalıcı olarak silebilir. (24 saat içinde) + No comment provided by engineer. + + + Members can send SimpleX links. + Grup üyeleri SimpleX bağlantıları gönderebilir. + No comment provided by engineer. + + + Members can send direct messages. + Grup üyeleri doğrudan mesajlar gönderebilir. + No comment provided by engineer. + + + Members can send disappearing messages. + Grup üyeleri kaybolan mesajlar gönderebilir. + No comment provided by engineer. + + + Members can send files and media. + Grup üyeleri dosyalar ve medya gönderebilir. + No comment provided by engineer. + + + Members can send voice messages. + Grup üyeleri sesli mesajlar gönderebilir. + No comment provided by engineer. + Menus Menüler @@ -4928,6 +4976,10 @@ VPN'nin etkinleştirilmesi gerekir. Onion ana bilgisayarları kullanılmayacaktır. No comment provided by engineer. + + Only chat owners can change preferences. + No comment provided by engineer. + Only client devices store user profiles, contacts, groups, and messages. Yalnızca istemci cihazlar kullanıcı profillerini, kişileri, grupları ve **2 katmanlı uçtan uca şifreleme** ile gönderilen mesajları depolar. @@ -5317,6 +5369,10 @@ Hata: %@ Gizlilik & güvenlik No comment provided by engineer. + + Privacy for your customers. + No comment provided by engineer. + Privacy redefined Gizlilik yeniden tanımlandı @@ -8181,6 +8237,10 @@ Bağlantı isteği tekrarlansın mı? Aktif olduklarında sessize alınmış profillerden arama ve bildirim almaya devam edersiniz. No comment provided by engineer. + + You will stop receiving messages from this chat. Chat history will be preserved. + No comment provided by engineer. + You will stop receiving messages from this group. Chat history will be preserved. Bu gruptan artık mesaj almayacaksınız. Sohbet geçmişi korunacaktır. diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff index 801b3d0c79..2f1e5d96a6 100644 --- a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff @@ -1246,6 +1246,10 @@ Business address No comment provided by engineer. + + Business chats + 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). Через профіль чату (за замовчуванням) або [за з'єднанням](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA). @@ -1388,6 +1392,10 @@ Зміна профілів користувачів authentication reason + + Chat + No comment provided by engineer. + Chat already exists No comment provided by engineer. @@ -1471,6 +1479,14 @@ Тема чату No comment provided by engineer. + + Chat will be deleted for all members - this cannot be undone! + No comment provided by engineer. + + + Chat will be deleted for you - this cannot be undone! + No comment provided by engineer. + Chats Чати @@ -2225,6 +2241,10 @@ This is your own one-time link! Видалити та повідомити контакт No comment provided by engineer. + + Delete chat + No comment provided by engineer. + Delete chat profile Видалити профіль чату @@ -2235,6 +2255,10 @@ This is your own one-time link! Видалити профіль чату? No comment provided by engineer. + + Delete chat? + No comment provided by engineer. + Delete connection Видалити підключення @@ -2500,6 +2524,10 @@ This is your own one-time link! Прямі повідомлення chat feature + + Direct messages between members are prohibited in this chat. + No comment provided by engineer. + Direct messages between members are prohibited. У цій групі заборонені прямі повідомлення між учасниками. @@ -3672,41 +3700,6 @@ Error: %2$@ Групові посилання No comment provided by engineer. - - Members can add message reactions. - Учасники групи можуть додавати реакції на повідомлення. - No comment provided by engineer. - - - Members can irreversibly delete sent messages. (24 hours) - Учасники групи можуть безповоротно видаляти надіслані повідомлення. (24 години) - No comment provided by engineer. - - - Members can send SimpleX links. - Учасники групи можуть надсилати посилання SimpleX. - No comment provided by engineer. - - - Members can send direct messages. - Учасники групи можуть надсилати прямі повідомлення. - No comment provided by engineer. - - - Members can send disappearing messages. - Учасники групи можуть надсилати зникаючі повідомлення. - No comment provided by engineer. - - - Members can send files and media. - Учасники групи можуть надсилати файли та медіа. - No comment provided by engineer. - - - Members can send voice messages. - Учасники групи можуть надсилати голосові повідомлення. - No comment provided by engineer. - Group message: Групове повідомлення: @@ -4106,6 +4099,10 @@ More improvements are coming soon! Запросити учасників No comment provided by engineer. + + Invite to chat + No comment provided by engineer. + Invite to group Запросити до групи @@ -4264,6 +4261,14 @@ This is your link for group %@! Залишити swipe action + + Leave chat + No comment provided by engineer. + + + Leave chat? + No comment provided by engineer. + Leave group Покинути групу @@ -4394,6 +4399,10 @@ This is your link for group %@! Користувач неактивний item status text + + Member role will be changed to "%@". All chat members will be notified. + No comment provided by engineer. + Member role will be changed to "%@". All group members will be notified. Роль учасника буде змінено на "%@". Всі учасники групи будуть повідомлені про це. @@ -4404,11 +4413,50 @@ This is your link for group %@! Роль учасника буде змінено на "%@". Учасник отримає нове запрошення. No comment provided by engineer. + + Member will be removed from chat - this cannot be undone! + No comment provided by engineer. + Member will be removed from group - this cannot be undone! Учасник буде видалений з групи - це неможливо скасувати! No comment provided by engineer. + + Members can add message reactions. + Учасники групи можуть додавати реакції на повідомлення. + No comment provided by engineer. + + + Members can irreversibly delete sent messages. (24 hours) + Учасники групи можуть безповоротно видаляти надіслані повідомлення. (24 години) + No comment provided by engineer. + + + Members can send SimpleX links. + Учасники групи можуть надсилати посилання SimpleX. + No comment provided by engineer. + + + Members can send direct messages. + Учасники групи можуть надсилати прямі повідомлення. + No comment provided by engineer. + + + Members can send disappearing messages. + Учасники групи можуть надсилати зникаючі повідомлення. + No comment provided by engineer. + + + Members can send files and media. + Учасники групи можуть надсилати файли та медіа. + No comment provided by engineer. + + + Members can send voice messages. + Учасники групи можуть надсилати голосові повідомлення. + No comment provided by engineer. + Menus Меню @@ -4982,6 +5030,10 @@ Requires compatible VPN. Onion хости не будуть використовуватися. No comment provided by engineer. + + Only chat owners can change preferences. + No comment provided by engineer. + Only client devices store user profiles, contacts, groups, and messages. Тільки клієнтські пристрої зберігають профілі користувачів, контакти, групи та повідомлення, надіслані за допомогою **2-шарового наскрізного шифрування**. @@ -5377,6 +5429,10 @@ Error: %@ Конфіденційність і безпека No comment provided by engineer. + + Privacy for your customers. + No comment provided by engineer. + Privacy redefined Конфіденційність переглянута @@ -8277,6 +8333,10 @@ Repeat connection request? Ви все одно отримуватимете дзвінки та сповіщення від вимкнених профілів, якщо вони активні. No comment provided by engineer. + + You will stop receiving messages from this chat. Chat history will be preserved. + No comment provided by engineer. + You will stop receiving messages from this group. Chat history will be preserved. Ви перестанете отримувати повідомлення від цієї групи. Історія чату буде збережена. diff --git a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff index d9cb36f971..a113c75603 100644 --- a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff +++ b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff @@ -1221,6 +1221,10 @@ Business address No comment provided by engineer. + + Business chats + 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). 通过聊天资料(默认)或者[通过连接](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)。 @@ -1362,6 +1366,10 @@ Change user profiles authentication reason + + Chat + No comment provided by engineer. + Chat already exists No comment provided by engineer. @@ -1444,6 +1452,14 @@ 聊天主题 No comment provided by engineer. + + Chat will be deleted for all members - this cannot be undone! + No comment provided by engineer. + + + Chat will be deleted for you - this cannot be undone! + No comment provided by engineer. + Chats 聊天 @@ -2182,6 +2198,10 @@ This is your own one-time link! 删除并通知联系人 No comment provided by engineer. + + Delete chat + No comment provided by engineer. + Delete chat profile 删除聊天资料 @@ -2192,6 +2212,10 @@ This is your own one-time link! 删除聊天资料? No comment provided by engineer. + + Delete chat? + No comment provided by engineer. + Delete connection 删除连接 @@ -2455,6 +2479,10 @@ This is your own one-time link! 私信 chat feature + + Direct messages between members are prohibited in this chat. + No comment provided by engineer. + Direct messages between members are prohibited. 此群中禁止成员之间私信。 @@ -3602,41 +3630,6 @@ Error: %2$@ 群组链接 No comment provided by engineer. - - Members can add message reactions. - 群组成员可以添加信息回应。 - No comment provided by engineer. - - - Members can irreversibly delete sent messages. (24 hours) - 群组成员可以不可撤回地删除已发送的消息 - No comment provided by engineer. - - - Members can send SimpleX links. - 群成员可发送 SimpleX 链接。 - No comment provided by engineer. - - - Members can send direct messages. - 群组成员可以私信。 - No comment provided by engineer. - - - Members can send disappearing messages. - 群组成员可以发送限时消息。 - No comment provided by engineer. - - - Members can send files and media. - 群组成员可以发送文件和媒体。 - No comment provided by engineer. - - - Members can send voice messages. - 群组成员可以发送语音消息。 - No comment provided by engineer. - Group message: 群组消息: @@ -4031,6 +4024,10 @@ More improvements are coming soon! 邀请成员 No comment provided by engineer. + + Invite to chat + No comment provided by engineer. + Invite to group 邀请加入群组 @@ -4189,6 +4186,14 @@ This is your link for group %@! 离开 swipe action + + Leave chat + No comment provided by engineer. + + + Leave chat? + No comment provided by engineer. + Leave group 离开群组 @@ -4319,6 +4324,10 @@ This is your link for group %@! 成员不活跃 item status text + + Member role will be changed to "%@". All chat members will be notified. + No comment provided by engineer. + Member role will be changed to "%@". All group members will be notified. 成员角色将更改为 "%@"。所有群成员将收到通知。 @@ -4329,11 +4338,50 @@ This is your link for group %@! 成员角色将更改为 "%@"。该成员将收到一份新的邀请。 No comment provided by engineer. + + Member will be removed from chat - this cannot be undone! + No comment provided by engineer. + Member will be removed from group - this cannot be undone! 成员将被移出群组——此操作无法撤消! No comment provided by engineer. + + Members can add message reactions. + 群组成员可以添加信息回应。 + No comment provided by engineer. + + + Members can irreversibly delete sent messages. (24 hours) + 群组成员可以不可撤回地删除已发送的消息 + No comment provided by engineer. + + + Members can send SimpleX links. + 群成员可发送 SimpleX 链接。 + No comment provided by engineer. + + + Members can send direct messages. + 群组成员可以私信。 + No comment provided by engineer. + + + Members can send disappearing messages. + 群组成员可以发送限时消息。 + No comment provided by engineer. + + + Members can send files and media. + 群组成员可以发送文件和媒体。 + No comment provided by engineer. + + + Members can send voice messages. + 群组成员可以发送语音消息。 + No comment provided by engineer. + Menus 菜单 @@ -4888,6 +4936,10 @@ Requires compatible VPN. 将不会使用 Onion 主机。 No comment provided by engineer. + + Only chat owners can change preferences. + No comment provided by engineer. + Only client devices store user profiles, contacts, groups, and messages. 只有客户端设备存储用户资料、联系人、群组和**双层端到端加密**发送的消息。 @@ -5273,6 +5325,10 @@ Error: %@ 隐私和安全 No comment provided by engineer. + + Privacy for your customers. + No comment provided by engineer. + Privacy redefined 重新定义隐私 @@ -8118,6 +8174,10 @@ Repeat connection request? 当静音配置文件处于活动状态时,您仍会收到来自静音配置文件的电话和通知。 No comment provided by engineer. + + You will stop receiving messages from this chat. Chat history will be preserved. + No comment provided by engineer. + You will stop receiving messages from this group. Chat history will be preserved. 您将停止接收来自该群组的消息。聊天记录将被保留。 From b9777c92a519c4e18aae8274bda989ce14577bc3 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Tue, 3 Dec 2024 18:52:06 +0000 Subject: [PATCH 119/167] core: 6.2.0.4 (simplexmq: 6.2.0.5) --- ...0-simplex-network-v6-2-servers-by-flux-business-chats.md | 6 ------ cabal.project | 2 +- package.yaml | 2 +- scripts/nix/sha256map.nix | 2 +- simplex-chat.cabal | 2 +- src/Simplex/Chat/Remote.hs | 4 ++-- 6 files changed, 6 insertions(+), 12 deletions(-) diff --git a/blog/20241210-simplex-network-v6-2-servers-by-flux-business-chats.md b/blog/20241210-simplex-network-v6-2-servers-by-flux-business-chats.md index d59d8e6003..55de82df47 100644 --- a/blog/20241210-simplex-network-v6-2-servers-by-flux-business-chats.md +++ b/blog/20241210-simplex-network-v6-2-servers-by-flux-business-chats.md @@ -52,9 +52,3 @@ Thank you, Evgeny SimpleX Chat founder - -[1] You can also to self-host your own SimpleX servers on [Flux decentralized cloud](https://home.runonflux.io/apps/marketplace?q=simplex). - -[2] The probability of connection being de-anonymized and the number of random server choices follow this equation: `(1 - s ^ 2) ^ n = 1 - p`, where `s` is the share of attacker-controlled servers in the network, `n` is the number of random choices of entry and exit nodes for the circuit, and `p` is the probability of both entry and exit nodes, and the connection privacy being compromised. Substituting `0.02` (2%) for `s`, `0.5` (50%) for `p`, and solving this equation for `n` we obtain that `1733` random circuits have 50% probability of privacy being compromised. - -Also see [this presentation about Tor](https://ritter.vg/p/tor-v1.6.pdf), specifically the approximate calculations on page 76, and also [Tor project post](https://blog.torproject.org/announcing-vanguards-add-onion-services/) about the changes that made attack on hidden service anonymity harder, but still viable in case the it is used for a long time. diff --git a/cabal.project b/cabal.project index b89dc764cb..414bae94dd 100644 --- a/cabal.project +++ b/cabal.project @@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 38ad3c046e1bd5eb1ffe696dd24b10dd69001ba2 + tag: 4b43cb805437dc0822eb81c57b2c85e77ad333ca source-repository-package type: git diff --git a/package.yaml b/package.yaml index 98571e1342..ca29dc549b 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: simplex-chat -version: 6.2.0.3 +version: 6.2.0.4 #synopsis: #description: homepage: https://github.com/simplex-chat/simplex-chat#readme diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 7a812dbc6e..963ff27092 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."38ad3c046e1bd5eb1ffe696dd24b10dd69001ba2" = "0nq2a2lklbxpc049zjxa5w8c63l9l9nf08jb7pny42nmah0mlc20"; + "https://github.com/simplex-chat/simplexmq.git"."4b43cb805437dc0822eb81c57b2c85e77ad333ca" = "143cskyh3z6yxn6fnaw8biskbspa2cndc65rzziajlw13yd0wggg"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 4e339fc5da..7d8920e3e7 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplex-chat -version: 6.2.0.3 +version: 6.2.0.4 category: Web, System, Services, Cryptography homepage: https://github.com/simplex-chat/simplex-chat#readme author: simplex.chat diff --git a/src/Simplex/Chat/Remote.hs b/src/Simplex/Chat/Remote.hs index 3a7d450691..ba713420fc 100644 --- a/src/Simplex/Chat/Remote.hs +++ b/src/Simplex/Chat/Remote.hs @@ -73,11 +73,11 @@ import UnliftIO.Directory (copyFile, createDirectoryIfMissing, doesDirectoryExis -- when acting as host minRemoteCtrlVersion :: AppVersion -minRemoteCtrlVersion = AppVersion [6, 2, 0, 3] +minRemoteCtrlVersion = AppVersion [6, 2, 0, 4] -- when acting as controller minRemoteHostVersion :: AppVersion -minRemoteHostVersion = AppVersion [6, 2, 0, 3] +minRemoteHostVersion = AppVersion [6, 2, 0, 4] currentAppVersion :: AppVersion currentAppVersion = AppVersion SC.version From 3acc69c6d88544f3bf184c3b9aa2d1ee2e6c037c Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Tue, 3 Dec 2024 20:13:09 +0000 Subject: [PATCH 120/167] 6.2-beta.4: ios 250, android 255, desktop 79 --- apps/ios/SimpleX.xcodeproj/project.pbxproj | 36 +++++++++++----------- apps/multiplatform/gradle.properties | 8 ++--- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 8d4e4fe5c4..24984e5efc 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -167,9 +167,9 @@ 648010AB281ADD15009009B9 /* CIFileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648010AA281ADD15009009B9 /* CIFileView.swift */; }; 648679AB2BC96A74006456E7 /* ChatItemForwardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648679AA2BC96A74006456E7 /* ChatItemForwardingView.swift */; }; 649B28DD2CFE07CF00536B68 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 649B28D82CFE07CF00536B68 /* libffi.a */; }; - 649B28DE2CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.3-ELfYrsBTXJJ5vBEIgQ1y2B-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 649B28D92CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.3-ELfYrsBTXJJ5vBEIgQ1y2B-ghc9.6.3.a */; }; + 649B28DE2CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.4-3HGUY7rfU7IFvhj8CPEbqy-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 649B28D92CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.4-3HGUY7rfU7IFvhj8CPEbqy-ghc9.6.3.a */; }; 649B28DF2CFE07CF00536B68 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 649B28DA2CFE07CF00536B68 /* libgmpxx.a */; }; - 649B28E02CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.3-ELfYrsBTXJJ5vBEIgQ1y2B.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 649B28DB2CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.3-ELfYrsBTXJJ5vBEIgQ1y2B.a */; }; + 649B28E02CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.4-3HGUY7rfU7IFvhj8CPEbqy.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 649B28DB2CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.4-3HGUY7rfU7IFvhj8CPEbqy.a */; }; 649B28E12CFE07CF00536B68 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 649B28DC2CFE07CF00536B68 /* libgmp.a */; }; 649BCDA0280460FD00C3A862 /* ComposeImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649BCD9F280460FD00C3A862 /* ComposeImageView.swift */; }; 649BCDA22805D6EF00C3A862 /* CIImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649BCDA12805D6EF00C3A862 /* CIImageView.swift */; }; @@ -516,9 +516,9 @@ 648679AA2BC96A74006456E7 /* ChatItemForwardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemForwardingView.swift; sourceTree = ""; }; 6493D667280ED77F007A76FB /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; 649B28D82CFE07CF00536B68 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 649B28D92CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.3-ELfYrsBTXJJ5vBEIgQ1y2B-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.2.0.3-ELfYrsBTXJJ5vBEIgQ1y2B-ghc9.6.3.a"; sourceTree = ""; }; + 649B28D92CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.4-3HGUY7rfU7IFvhj8CPEbqy-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.2.0.4-3HGUY7rfU7IFvhj8CPEbqy-ghc9.6.3.a"; sourceTree = ""; }; 649B28DA2CFE07CF00536B68 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; - 649B28DB2CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.3-ELfYrsBTXJJ5vBEIgQ1y2B.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.2.0.3-ELfYrsBTXJJ5vBEIgQ1y2B.a"; sourceTree = ""; }; + 649B28DB2CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.4-3HGUY7rfU7IFvhj8CPEbqy.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.2.0.4-3HGUY7rfU7IFvhj8CPEbqy.a"; sourceTree = ""; }; 649B28DC2CFE07CF00536B68 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; 649BCD9F280460FD00C3A862 /* ComposeImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeImageView.swift; sourceTree = ""; }; 649BCDA12805D6EF00C3A862 /* CIImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIImageView.swift; sourceTree = ""; }; @@ -671,9 +671,9 @@ 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, 649B28E12CFE07CF00536B68 /* libgmp.a in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, - 649B28E02CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.3-ELfYrsBTXJJ5vBEIgQ1y2B.a in Frameworks */, + 649B28E02CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.4-3HGUY7rfU7IFvhj8CPEbqy.a in Frameworks */, CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */, - 649B28DE2CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.3-ELfYrsBTXJJ5vBEIgQ1y2B-ghc9.6.3.a in Frameworks */, + 649B28DE2CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.4-3HGUY7rfU7IFvhj8CPEbqy-ghc9.6.3.a in Frameworks */, 649B28DD2CFE07CF00536B68 /* libffi.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -754,8 +754,8 @@ 649B28D82CFE07CF00536B68 /* libffi.a */, 649B28DC2CFE07CF00536B68 /* libgmp.a */, 649B28DA2CFE07CF00536B68 /* libgmpxx.a */, - 649B28D92CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.3-ELfYrsBTXJJ5vBEIgQ1y2B-ghc9.6.3.a */, - 649B28DB2CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.3-ELfYrsBTXJJ5vBEIgQ1y2B.a */, + 649B28D92CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.4-3HGUY7rfU7IFvhj8CPEbqy-ghc9.6.3.a */, + 649B28DB2CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.4-3HGUY7rfU7IFvhj8CPEbqy.a */, ); path = Libraries; sourceTree = ""; @@ -1931,7 +1931,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 249; + CURRENT_PROJECT_VERSION = 250; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -1980,7 +1980,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 249; + CURRENT_PROJECT_VERSION = 250; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -2021,7 +2021,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 249; + CURRENT_PROJECT_VERSION = 250; DEVELOPMENT_TEAM = 5NN7GUYB6T; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; @@ -2041,7 +2041,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 249; + CURRENT_PROJECT_VERSION = 250; DEVELOPMENT_TEAM = 5NN7GUYB6T; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; @@ -2066,7 +2066,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 249; + CURRENT_PROJECT_VERSION = 250; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GCC_OPTIMIZATION_LEVEL = s; @@ -2103,7 +2103,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 249; + CURRENT_PROJECT_VERSION = 250; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_CODE_COVERAGE = NO; @@ -2140,7 +2140,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 249; + CURRENT_PROJECT_VERSION = 250; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -2191,7 +2191,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 249; + CURRENT_PROJECT_VERSION = 250; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -2242,7 +2242,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 249; + CURRENT_PROJECT_VERSION = 250; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -2276,7 +2276,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 249; + CURRENT_PROJECT_VERSION = 250; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; diff --git a/apps/multiplatform/gradle.properties b/apps/multiplatform/gradle.properties index 0893c75520..e940f077ff 100644 --- a/apps/multiplatform/gradle.properties +++ b/apps/multiplatform/gradle.properties @@ -24,11 +24,11 @@ android.nonTransitiveRClass=true kotlin.mpp.androidSourceSetLayoutVersion=2 kotlin.jvm.target=11 -android.version_name=6.2-beta.3 -android.version_code=254 +android.version_name=6.2-beta.4 +android.version_code=255 -desktop.version_name=6.2-beta.3 -desktop.version_code=78 +desktop.version_name=6.2-beta.4 +desktop.version_code=79 kotlin.version=1.9.23 gradle.plugin.version=8.2.0 From 6ff7d4a73c07a1d9dc8c58bdaa0977beb80f0649 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Wed, 4 Dec 2024 12:12:30 +0400 Subject: [PATCH 121/167] core: fix business chat state on accept (fixes icon) (#5312) --- src/Simplex/Chat/Store/Groups.hs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index 2c938ecc44..dec4b6847e 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -919,9 +919,9 @@ createBusinessRequestGroup currentTs <- liftIO getCurrentTime groupInfo <- insertGroup_ currentTs (groupMemberId, memberId) <- insertClientMember_ currentTs groupInfo - liftIO $ setBusinessMemberId groupInfo memberId + groupInfo' <- liftIO $ setBusinessMemberId groupInfo memberId clientMember <- getGroupMemberById db vr user groupMemberId - pure (groupInfo, clientMember) + pure (groupInfo', clientMember) where insertGroup_ currentTs = ExceptT $ withLocalDisplayName db userId displayName $ \localDisplayName -> runExceptT $ do @@ -969,8 +969,9 @@ createBusinessRequestGroup ) groupMemberId <- liftIO $ insertedRowId db pure (groupMemberId, MemberId memId) - setBusinessMemberId GroupInfo {groupId} businessMemberId = do - DB.execute db "UPDATE groups SET business_member_id = ? WHERE group_id = ?" (businessMemberId, groupId) + setBusinessMemberId groupInfo@GroupInfo {groupId} memberId = do + DB.execute db "UPDATE groups SET business_member_id = ? WHERE group_id = ?" (memberId, groupId) + pure (groupInfo {businessChat = Just BusinessChatInfo {memberId, chatType = BCCustomer}} :: GroupInfo) getContactViaMember :: DB.Connection -> VersionRangeChat -> User -> GroupMember -> ExceptT StoreError IO Contact getContactViaMember db vr user@User {userId} GroupMember {groupMemberId} = do From 4f1cf6e79f5ce3c18274a9c34d3573e176b5078c Mon Sep 17 00:00:00 2001 From: sh <37271604+shumvgolove@users.noreply.github.com> Date: Wed, 4 Dec 2024 10:24:31 +0000 Subject: [PATCH 122/167] docs: business page, technical advice (#5314) * docs/business: populate technical advice sections * dev tools * update --------- Co-authored-by: Evgeny Poberezkin --- docs/BUSINESS.md | 67 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/docs/BUSINESS.md b/docs/BUSINESS.md index aed5fd877c..8fd5df5c36 100755 --- a/docs/BUSINESS.md +++ b/docs/BUSINESS.md @@ -53,8 +53,75 @@ With all advantages in privacy and security of e2e encryption in SimpleX Chat, t ### Running SimpleX Chat in the cloud +To install SimpleX Chat CLI in the cloud, follow this: + +1. Create dedicated user for CLI: + + ```sh + useradd -m -s /bin/bash simplex-cli + ``` + +2. Create new tmux session + + ```sh + tmux new -s simplex-cli + ``` + +3. Login to dedicated user: + + ```sh + su - simplex-cli + ``` + +4. Install CLI: + + ```sh + curl -o- https://raw.githubusercontent.com/simplex-chat/simplex-chat/stable/install.sh | bash + ``` + +5. Run the CLI: + + ```sh + simplex-chat + ``` + +To deattach from running CLI simply press `Ctrl+B` and then `D`. + +To reattach back to CLI, run: `tmux attach -t simplex-cli`. + ### Using remote profiles via Desktop app +To use CLI from Desktop app, follow this: + +1. Enable Developer tools in desktop app. + +2. In the Desktop app, click - `Linked mobile` -> `+ Link a mobile`, choose local address `127.0.0.1`, enter some fixed port (can be any free port, e.g. 12345), and copy the link. + +3. In the same machine where Desktop app is running, execute: + + Change `PORT` to port, chosen in the previous step in Desktop app and `SERVER_IP` to your server. + + ```sh + ssh -R PORT:127.0.0.1:PORT -N root@SERVER_IP + ``` + +4. In the CLI on the server: + + Change `LINK` to link, copied in the first step and enter the following: + + ```sh + /crc LINK + ``` + + CLI will print verification code: + + ```sh + Compare session code with controller and use: + /verify remote ctrl ... + ``` + + Simply copy the whole line starting with `/verify ...` from the terminal and paste it. Now you can control the CLI from your Desktop app. + ## Organizations using SimpleX Chat for customer service, support and sales Please let us know if you use SimpleX Chat to communicate with your customers and want to be included in this list. From 89f380400e9b973f6b83417b069fbef88984c0dc Mon Sep 17 00:00:00 2001 From: Evgeny Date: Wed, 4 Dec 2024 16:32:01 +0000 Subject: [PATCH 123/167] core: update business chat profile (#5313) * core: update business chat profile * fix, test * refactor * test changing non-title member profile * fix history --- src/Simplex/Chat.hs | 21 ++++---- src/Simplex/Chat/Store/Groups.hs | 40 ++++++++++++---- src/Simplex/Chat/Store/Messages.hs | 20 ++++---- tests/ChatTests/Profiles.hs | 77 ++++++++++++++++++++++++++++++ 4 files changed, 131 insertions(+), 27 deletions(-) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 31862253dd..e78d6de288 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -5110,7 +5110,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = void . sendGroupMessage user gInfo members . XGrpMemNew $ memberInfo m sendIntroductions members when (groupFeatureAllowed SGFHistory gInfo) sendHistory - when (connChatVersion < batchSend2Version) $ sendGroupAutoReply members + when (connChatVersion < batchSend2Version) sendGroupAutoReply where sendXGrpLinkMem = do let profileMode = ExistingIncognito <$> incognitoMembershipProfile gInfo @@ -5145,7 +5145,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = withStore' $ \db -> updateIntroStatus db introId GMIntroSent sendHistory = when (m `supportsVersion` batchSendVersion) $ do - (errs, items) <- partitionEithers <$> withStore' (\db -> getGroupHistoryItems db user gInfo 100) + (errs, items) <- partitionEithers <$> withStore' (\db -> getGroupHistoryItems db user gInfo m 100) (errs', events) <- partitionEithers <$> mapM (tryChatError . itemForwardEvents) items let errors = map ChatErrorStore errs <> errs' unless (null errors) $ toView $ CRChatErrors (Just user) errors @@ -5376,9 +5376,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = JOINED sqSecured -> -- [async agent commands] continuation on receiving JOINED when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> - when sqSecured $ do - members <- withStore' $ \db -> getGroupMembers db vr user gInfo - when (connChatVersion >= batchSend2Version) $ sendGroupAutoReply members + when (sqSecured && connChatVersion >= batchSend2Version) sendGroupAutoReply QCONT -> do continued <- continueSending connEntity conn when continued $ sendPendingGroupMessages user m conn @@ -5406,7 +5404,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = updateGroupItemsErrorStatus db msgId groupMemberId newStatus = do itemIds <- getChatItemIdsByAgentMsgId db connId msgId forM_ itemIds $ \itemId -> updateGroupMemSndStatus' db itemId groupMemberId newStatus - sendGroupAutoReply members = autoReplyMC >>= mapM_ send + sendGroupAutoReply = autoReplyMC >>= mapM_ send where autoReplyMC = do let GroupInfo {businessChat} = gInfo @@ -5420,8 +5418,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = _ -> Nothing _ -> pure Nothing send mc = do - msg <- sendGroupMessage' user gInfo members (XMsgNew $ MCSimple (extMsgContent mc Nothing)) + msg <- sendGroupMessage' user gInfo [m] (XMsgNew $ MCSimple (extMsgContent mc Nothing)) ci <- saveSndChatItem user (CDGroupSnd gInfo) msg (CISndMsgContent mc) + withStore' $ \db -> createGroupSndStatus db (chatItemId' ci) (groupMemberId' m) GSSNew toView $ CRNewChatItems user [AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci] agentMsgDecryptError :: AgentCryptoError -> (MsgDecryptError, Word32) @@ -6459,7 +6458,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = processMemberProfileUpdate :: GroupInfo -> GroupMember -> Profile -> Bool -> Maybe UTCTime -> CM GroupMember processMemberProfileUpdate gInfo m@GroupMember {memberProfile = p, memberContactId} p' createItems itemTs_ - | redactedMemberProfile (fromLocalProfile p) /= redactedMemberProfile p' = + | redactedMemberProfile (fromLocalProfile p) /= redactedMemberProfile p' = do + updateBusinessChatProfile gInfo m case memberContactId of Nothing -> do m' <- withStore $ \db -> updateMemberProfile db user m p' @@ -6485,6 +6485,11 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = | otherwise = pure m where + updateBusinessChatProfile g@GroupInfo {businessChat} GroupMember {memberId} = case businessChat of + Just BusinessChatInfo {memberId = mId} | mId == memberId -> do + g' <- withStore $ \db -> updateGroupProfileFromMember db user g p' + toView $ CRGroupUpdated user g g' (Just m) + _ -> pure () createProfileUpdatedItem m' = when createItems $ do let ciContent = CIRcvGroupEvent $ RGEMemberProfileUpdated (fromLocalProfile p) p' diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index dec4b6847e..895acf0a7d 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -39,6 +39,7 @@ module Simplex.Chat.Store.Groups getGroupInfoByUserContactLinkConnReq, getGroupInfoByGroupLinkHash, updateGroupProfile, + updateGroupProfileFromMember, getGroupIdByName, getGroupMemberIdByName, getActiveMembersByName, @@ -917,11 +918,12 @@ createBusinessRequestGroup UserContactRequest {cReqChatVRange, xContactId, profile = Profile {displayName, fullName, image, contactLink, preferences}} groupPreferences = do currentTs <- liftIO getCurrentTime - groupInfo <- insertGroup_ currentTs - (groupMemberId, memberId) <- insertClientMember_ currentTs groupInfo - groupInfo' <- liftIO $ setBusinessMemberId groupInfo memberId + (groupId, membership) <- insertGroup_ currentTs + (groupMemberId, memberId) <- insertClientMember_ currentTs groupId membership + liftIO $ DB.execute db "UPDATE groups SET business_member_id = ? WHERE group_id = ?" (memberId, groupId) + groupInfo <- getGroupInfo db vr user groupId clientMember <- getGroupMemberById db vr user groupMemberId - pure (groupInfo', clientMember) + pure (groupInfo, clientMember) where insertGroup_ currentTs = ExceptT $ withLocalDisplayName db userId displayName $ \localDisplayName -> runExceptT $ do @@ -942,10 +944,10 @@ createBusinessRequestGroup (profileId, localDisplayName, userId, True, currentTs, currentTs, currentTs, currentTs, BCCustomer, xContactId) insertedRowId db memberId <- liftIO $ encodedRandomBytes gVar 12 - void $ createContactMemberInv_ db user groupId Nothing user (MemberIdRole (MemberId memberId) GROwner) GCUserMember GSMemCreator IBUser Nothing currentTs vr - getGroupInfo db vr user groupId + membership <- createContactMemberInv_ db user groupId Nothing user (MemberIdRole (MemberId memberId) GROwner) GCUserMember GSMemCreator IBUser Nothing currentTs vr + pure (groupId, membership) VersionRange minV maxV = cReqChatVRange - insertClientMember_ currentTs GroupInfo {groupId, membership} = ExceptT $ do + insertClientMember_ currentTs groupId membership = ExceptT $ do withLocalDisplayName db userId displayName $ \localDisplayName -> runExceptT $ do liftIO $ DB.execute @@ -969,9 +971,6 @@ createBusinessRequestGroup ) groupMemberId <- liftIO $ insertedRowId db pure (groupMemberId, MemberId memId) - setBusinessMemberId groupInfo@GroupInfo {groupId} memberId = do - DB.execute db "UPDATE groups SET business_member_id = ? WHERE group_id = ?" (memberId, groupId) - pure (groupInfo {businessChat = Just BusinessChatInfo {memberId, chatType = BCCustomer}} :: GroupInfo) getContactViaMember :: DB.Connection -> VersionRangeChat -> User -> GroupMember -> ExceptT StoreError IO Contact getContactViaMember db vr user@User {userId} GroupMember {groupMemberId} = do @@ -1457,6 +1456,27 @@ updateGroupProfile db user@User {userId} g@GroupInfo {groupId, localDisplayName, (ldn, currentTs, userId, groupId) safeDeleteLDN db user localDisplayName +updateGroupProfileFromMember :: DB.Connection -> User -> GroupInfo -> Profile -> ExceptT StoreError IO GroupInfo +updateGroupProfileFromMember db user g@GroupInfo {groupId} Profile {displayName = n, fullName = fn, image = img} = do + p <- getGroupProfile -- to avoid any race conditions with UI + let g' = g {groupProfile = p} :: GroupInfo + p' = p {displayName = n, fullName = fn, image = img} :: GroupProfile + updateGroupProfile db user g' p' + where + getGroupProfile = + ExceptT $ firstRow toGroupProfile (SEGroupNotFound groupId) $ + DB.query + db + [sql| + SELECT gp.display_name, gp.full_name, gp.description, gp.image, gp.preferences + FROM group_profiles gp + JOIN groups g ON gp.group_profile_id = g.group_profile_id + WHERE g.group_id = ? + |] + (Only groupId) + toGroupProfile (displayName, fullName, description, image, groupPreferences) = + GroupProfile {displayName, fullName, description, image, groupPreferences} + getGroupInfo :: DB.Connection -> VersionRangeChat -> User -> Int64 -> ExceptT StoreError IO GroupInfo getGroupInfo db vr User {userId, userContactId} groupId = ExceptT . firstRow (toGroupInfo vr userContactId) (SEGroupNotFound groupId) $ diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs index cff7f6b785..13cadcca77 100644 --- a/src/Simplex/Chat/Store/Messages.hs +++ b/src/Simplex/Chat/Store/Messages.hs @@ -3034,8 +3034,8 @@ getGroupSndStatusCounts db itemId = |] (Only itemId) -getGroupHistoryItems :: DB.Connection -> User -> GroupInfo -> Int -> IO [Either StoreError (CChatItem 'CTGroup)] -getGroupHistoryItems db user@User {userId} GroupInfo {groupId} count = do +getGroupHistoryItems :: DB.Connection -> User -> GroupInfo -> GroupMember -> Int -> IO [Either StoreError (CChatItem 'CTGroup)] +getGroupHistoryItems db user@User {userId} GroupInfo {groupId} m count = do ciIds <- getLastItemIds_ -- use getGroupCIWithReactions to read reactions data reverse <$> mapM (runExceptT . getGroupChatItem db user groupId) ciIds @@ -3046,12 +3046,14 @@ getGroupHistoryItems db user@User {userId} GroupInfo {groupId} count = do <$> DB.query db [sql| - SELECT chat_item_id - FROM chat_items - WHERE user_id = ? AND group_id = ? - AND item_content_tag IN (?,?) - AND item_deleted = 0 - ORDER BY item_ts DESC, chat_item_id DESC + SELECT i.chat_item_id + FROM chat_items i + LEFT JOIN group_snd_item_statuses s ON s.chat_item_id = i.chat_item_id AND s.group_member_id = ? + WHERE i.user_id = ? AND i.group_id = ? + AND i.item_content_tag IN (?,?) + AND i.item_deleted = 0 + AND s.group_snd_item_status_id IS NULL + ORDER BY i.item_ts DESC, i.chat_item_id DESC LIMIT ? |] - (userId, groupId, rcvMsgContentTag, sndMsgContentTag, count) + (groupMemberId' m, userId, groupId, rcvMsgContentTag, sndMsgContentTag, count) diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index c5a464d969..ebfeecdbca 100644 --- a/tests/ChatTests/Profiles.hs +++ b/tests/ChatTests/Profiles.hs @@ -49,6 +49,7 @@ chatProfileTests = do it "auto-reply message in incognito" testAutoReplyMessageInIncognito describe "business address" $ do it "create and connect via business address" testBusinessAddress + it "update profiles with business address" testBusinessUpdateProfiles describe "contact address connection plan" $ do it "contact address ok to connect; known contact" testPlanAddressOkKnown it "own contact address" testPlanAddressOwn @@ -732,6 +733,82 @@ testBusinessAddress = testChat3 businessProfile aliceProfile {fullName = "Alice (alice <# "#bob bob_1> hey there") (biz <# "#bob bob_1> hey there") +testBusinessUpdateProfiles :: HasCallStack => FilePath -> IO () +testBusinessUpdateProfiles = testChat3 businessProfile aliceProfile bobProfile $ + \biz alice bob -> do + biz ##> "/ad" + cLink <- getContactLink biz True + biz ##> "/auto_accept on business text Welcome" + biz <## "auto_accept on, business" + biz <## "auto reply:" + biz <## "Welcome" + alice ##> ("/c " <> cLink) + alice <## "connection request sent!" + biz <## "#alice (Alice): accepting business address request..." + alice <## "#biz: joining the group..." + biz <# "#alice Welcome" -- auto reply + biz <## "#alice: alice_1 joined the group" + alice <# "#biz biz_1> Welcome" + alice <## "#biz: you joined the group" + biz #> "#alice hi" + alice <# "#biz biz_1> hi" + alice #> "#biz hello" + biz <# "#alice alice_1> hello" + alice ##> "/p alisa" + alice <## "user profile is changed to alisa (your 0 contacts are notified)" + alice #> "#biz hello again" -- profile update is sent with message + biz <## "alice_1 updated group #alice:" + biz <## "changed to #alisa" + biz <# "#alisa alisa_1> hello again" + -- customer can invite members too, if business allows + biz ##> "/mr alisa alisa_1 admin" + biz <## "#alisa: you changed the role of alisa_1 from member to admin" + alice <## "#biz: biz_1 changed your role from member to admin" + connectUsers alice bob + alice ##> "/a #biz bob" + alice <## "invitation to join the group #biz sent to bob" + bob <## "#biz (Biz Inc): alisa invites you to join the group as member" + bob <## "use /j biz to accept" + bob ##> "/j biz" + concurrentlyN_ + [ do + bob <## "#biz: you joined the group" + bob + <### + [ WithTime "#biz biz_1> Welcome [>>]", + WithTime "#biz biz_1> hi [>>]", + WithTime "#biz alisa> hello [>>]", + WithTime "#biz alisa> hello again [>>]" + ] + bob <## "#biz: member biz_1 (Biz Inc) is connected", + alice <## "#biz: bob joined the group", + do + biz <## "#alisa: alisa_1 added bob (Bob) to the group (connecting...)" + biz <## "#alisa: new member bob is connected" + ] + -- changing other member profiles does not change group profile + bob ##> "/p robert" + bob <## "user profile is changed to robert (your 1 contacts are notified)" + alice <## "contact bob changed to robert" -- only alice receives profile update + alice <## "use @robert to send messages" + bob #> "#biz hi there" -- profile update is sent to group with message + alice <# "#biz robert> hi there" + biz <# "#alisa robert> hi there" + -- both customers receive business profile change + biz ##> "/p business" + biz <## "user profile is changed to business (your 0 contacts are notified)" + biz #> "#alisa hey" + concurrentlyN_ + [ do + alice <## "biz_1 updated group #biz:" + alice <## "changed to #business" + alice <# "#business business_1> hey", + do + bob <## "biz_1 updated group #biz:" + bob <## "changed to #business" + bob <# "#business business_1> hey" + ] + testPlanAddressOkKnown :: HasCallStack => FilePath -> IO () testPlanAddressOkKnown = testChat2 aliceProfile bobProfile $ From 219381f9411a6aad72533ca0e4d713d03839cdbf Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Wed, 4 Dec 2024 23:33:14 +0700 Subject: [PATCH 124/167] android, desktop: don't load new messages when pressing quote while searching (#5315) --- .../kotlin/chat/simplex/common/views/chat/ChatView.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index 5db413a17a..30f76bb878 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -1001,7 +1001,7 @@ fun BoxScope.ChatItemsList( val chatInfoUpdated = rememberUpdatedState(chatInfo) val highlightedItems = remember { mutableStateOf(setOf()) } val scope = rememberCoroutineScope() - val scrollToItem: (Long) -> Unit = remember { scrollToItem(loadingMoreItems, highlightedItems, chatInfoUpdated, maxHeight, scope, reversedChatItems, mergedItems, listState, loadMessages) } + val scrollToItem: (Long) -> Unit = remember { scrollToItem(searchValue, loadingMoreItems, highlightedItems, chatInfoUpdated, maxHeight, scope, reversedChatItems, mergedItems, listState, loadMessages) } val scrollToQuotedItemFromItem: (Long) -> Unit = remember { findQuotedItemFromItem(remoteHostIdUpdated, chatInfoUpdated, scope, scrollToItem) } LoadLastItems(loadingMoreItems, remoteHostId, chatInfo) @@ -1834,6 +1834,7 @@ private fun lastFullyVisibleIemInListState(topPaddingToContentPx: State, de } private fun scrollToItem( + searchValue: State, loadingMoreItems: MutableState, highlightedItems: MutableState>, chatInfo: State, @@ -1847,6 +1848,8 @@ private fun scrollToItem( withApi { try { var index = mergedItems.value.indexInParentItems[itemId] ?: -1 + // Don't try to load messages while in search + if (index == -1 && searchValue.value.isNotBlank()) return@withApi // setting it to 'loading' even if the item is loaded because in rare cases when the resulting item is near the top, scrolling to // it will trigger loading more items and will scroll to incorrect position (because of trimming) loadingMoreItems.value = true From ee146cdc7b4be10f5d6e6396d144d8830fff6402 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Wed, 4 Dec 2024 23:49:45 +0700 Subject: [PATCH 125/167] android, desktop, core: option to show toolbar in chat at the top in reachable UI (#5316) * android, desktop: ability to show toolbar in chat at the top in reachable UI * rename * core AppSettings * ios AppSettings * rename * strings, enable reachable chat toolbar when enabled reachable app toolbars --------- Co-authored-by: Evgeny Poberezkin --- .../Views/UserSettings/AppSettings.swift | 2 + apps/ios/SimpleXChat/APITypes.swift | 5 ++- apps/ios/SimpleXChat/AppGroup.swift | 4 +- .../platform/ScrollableColumn.android.kt | 2 + .../views/usersettings/Appearance.android.kt | 7 ++- .../chat/simplex/common/model/SimpleXAPI.kt | 14 ++++-- .../common/platform/ScrollableColumn.kt | 2 + .../chat/simplex/common/views/TerminalView.kt | 2 +- .../simplex/common/views/chat/ChatView.kt | 45 ++++++++++--------- .../views/chat/group/GroupChatInfoView.kt | 2 +- .../common/views/chatlist/ChatListView.kt | 2 +- .../common/views/chatlist/ShareListView.kt | 2 +- .../common/views/newchat/NewChatSheet.kt | 4 +- .../common/views/newchat/NewChatView.kt | 2 +- .../commonMain/resources/MR/base/strings.xml | 3 +- .../platform/ScrollableColumn.desktop.kt | 13 +++--- .../views/usersettings/Appearance.desktop.kt | 3 ++ src/Simplex/Chat/AppSettings.hs | 16 ++++--- 18 files changed, 83 insertions(+), 47 deletions(-) diff --git a/apps/ios/Shared/Views/UserSettings/AppSettings.swift b/apps/ios/Shared/Views/UserSettings/AppSettings.swift index aa7f885ac6..44e0b20958 100644 --- a/apps/ios/Shared/Views/UserSettings/AppSettings.swift +++ b/apps/ios/Shared/Views/UserSettings/AppSettings.swift @@ -65,6 +65,7 @@ extension AppSettings { 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) } + if let val = chatBottomBar { groupDefaults.setValue(val, forKey: GROUP_DEFAULT_CHAT_BOTTOM_BAR) } } public static var current: AppSettings { @@ -100,6 +101,7 @@ extension AppSettings { c.uiCurrentThemeIds = currentThemeIdsDefault.get() c.uiThemes = themeOverridesDefault.get() c.oneHandUI = groupDefaults.bool(forKey: GROUP_DEFAULT_ONE_HAND_UI) + c.chatBottomBar = groupDefaults.bool(forKey: GROUP_DEFAULT_CHAT_BOTTOM_BAR) return c } } diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index ca9bb70ea4..2bd76dea63 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -2655,6 +2655,7 @@ public struct AppSettings: Codable, Equatable { public var uiCurrentThemeIds: [String: String]? = nil public var uiThemes: [ThemeOverrides]? = nil public var oneHandUI: Bool? = nil + public var chatBottomBar: Bool? = nil public func prepareForExport() -> AppSettings { var empty = AppSettings() @@ -2689,6 +2690,7 @@ public struct AppSettings: Codable, Equatable { if uiCurrentThemeIds != def.uiCurrentThemeIds { empty.uiCurrentThemeIds = uiCurrentThemeIds } if uiThemes != def.uiThemes { empty.uiThemes = uiThemes } if oneHandUI != def.oneHandUI { empty.oneHandUI = oneHandUI } + if chatBottomBar != def.chatBottomBar { empty.chatBottomBar = chatBottomBar } return empty } @@ -2723,7 +2725,8 @@ public struct AppSettings: Codable, Equatable { uiDarkColorScheme: DefaultTheme.SIMPLEX.themeName, uiCurrentThemeIds: nil as [String: String]?, uiThemes: nil as [ThemeOverrides]?, - oneHandUI: false + oneHandUI: false, + chatBottomBar: true ) } } diff --git a/apps/ios/SimpleXChat/AppGroup.swift b/apps/ios/SimpleXChat/AppGroup.swift index 5ae3c9b901..c754f0740d 100644 --- a/apps/ios/SimpleXChat/AppGroup.swift +++ b/apps/ios/SimpleXChat/AppGroup.swift @@ -57,6 +57,7 @@ public let GROUP_DEFAULT_CONFIRM_DB_UPGRADES = "confirmDBUpgrades" public let GROUP_DEFAULT_CALL_KIT_ENABLED = "callKitEnabled" public let GROUP_DEFAULT_PQ_EXPERIMENTAL_ENABLED = "pqExperimentalEnabled" // no longer used public let GROUP_DEFAULT_ONE_HAND_UI = "oneHandUI" +public let GROUP_DEFAULT_CHAT_BOTTOM_BAR = "chatBottomBar" public let APP_GROUP_NAME = "group.chat.simplex.app" @@ -94,7 +95,8 @@ public func registerGroupDefaults() { GROUP_DEFAULT_CONFIRM_DB_UPGRADES: false, GROUP_DEFAULT_CALL_KIT_ENABLED: true, GROUP_DEFAULT_PQ_EXPERIMENTAL_ENABLED: false, - GROUP_DEFAULT_ONE_HAND_UI: true + GROUP_DEFAULT_ONE_HAND_UI: true, + GROUP_DEFAULT_CHAT_BOTTOM_BAR: true ]) } diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/ScrollableColumn.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/ScrollableColumn.android.kt index cf95604504..60197f3851 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/ScrollableColumn.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/ScrollableColumn.android.kt @@ -29,6 +29,7 @@ actual fun LazyColumnWithScrollBar( flingBehavior: FlingBehavior, userScrollEnabled: Boolean, additionalBarOffset: State?, + chatBottomBar: State, fillMaxSize: Boolean, content: LazyListScope.() -> Unit ) { @@ -91,6 +92,7 @@ actual fun LazyColumnWithScrollBarNoAppBar( flingBehavior: FlingBehavior, userScrollEnabled: Boolean, additionalBarOffset: State?, + chatBottomBar: State, content: LazyListScope.() -> Unit ) { val state = state ?: rememberLazyListState() diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/Appearance.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/Appearance.android.kt index e5450e8e49..320a8e876a 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/Appearance.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/Appearance.android.kt @@ -106,7 +106,12 @@ fun AppearanceScope.AppearanceLayout( } // } - SettingsPreferenceItem(icon = null, stringResource(MR.strings.one_hand_ui), ChatModel.controller.appPrefs.oneHandUI) + SettingsPreferenceItem(icon = null, stringResource(MR.strings.one_hand_ui), ChatModel.controller.appPrefs.oneHandUI) { enabled -> + if (enabled) appPrefs.chatBottomBar.set(true) + } + if (remember { appPrefs.oneHandUI.state }.value) { + SettingsPreferenceItem(icon = null, stringResource(MR.strings.chat_bottom_bar), ChatModel.controller.appPrefs.chatBottomBar) + } } SectionDividerSpaced() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index 9531712554..701b32f6f6 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -257,6 +257,7 @@ class AppPreferences { val iosCallKitCallsInRecents = mkBoolPreference(SHARED_PREFS_IOS_CALL_KIT_CALLS_IN_RECENTS, false) val oneHandUI = mkBoolPreference(SHARED_PREFS_ONE_HAND_UI, true) + val chatBottomBar = mkBoolPreference(SHARED_PREFS_CHAT_BOTTOM_BAR, true) val hintPreferences: List, Boolean>> = listOf( laNoticeShown to false, @@ -431,6 +432,7 @@ class AppPreferences { private const val SHARED_PREFS_SHOULD_IMPORT_APP_SETTINGS = "ShouldImportAppSettings" private const val SHARED_PREFS_CONFIRM_DB_UPGRADES = "ConfirmDBUpgrades" private const val SHARED_PREFS_ONE_HAND_UI = "OneHandUI" + private const val SHARED_PREFS_CHAT_BOTTOM_BAR = "ChatBottomBar" private const val SHARED_PREFS_SELF_DESTRUCT = "LocalAuthenticationSelfDestruct" private const val SHARED_PREFS_SELF_DESTRUCT_DISPLAY_NAME = "LocalAuthenticationSelfDestructDisplayName" private const val SHARED_PREFS_PQ_EXPERIMENTAL_ENABLED = "PQExperimentalEnabled" // no longer used @@ -438,7 +440,6 @@ class AppPreferences { private const val SHARED_PREFS_CURRENT_THEME_IDs = "CurrentThemeIds" private const val SHARED_PREFS_SYSTEM_DARK_THEME = "SystemDarkTheme" private const val SHARED_PREFS_THEMES_OLD = "Themes" - private const val SHARED_PREFS_THEME_OVERRIDES = "ThemeOverrides" private const val SHARED_PREFS_PROFILE_IMAGE_CORNER_RADIUS = "ProfileImageCornerRadius" private const val SHARED_PREFS_CHAT_ITEM_ROUNDNESS = "ChatItemRoundness" private const val SHARED_PREFS_CHAT_ITEM_TAIL = "ChatItemTail" @@ -6882,7 +6883,8 @@ data class AppSettings( var uiDarkColorScheme: String? = null, var uiCurrentThemeIds: Map? = null, var uiThemes: List? = null, - var oneHandUI: Boolean? = null + var oneHandUI: Boolean? = null, + var chatBottomBar: Boolean? = null ) { fun prepareForExport(): AppSettings { val empty = AppSettings() @@ -6917,6 +6919,7 @@ data class AppSettings( if (uiCurrentThemeIds != def.uiCurrentThemeIds) { empty.uiCurrentThemeIds = uiCurrentThemeIds } if (uiThemes != def.uiThemes) { empty.uiThemes = uiThemes } if (oneHandUI != def.oneHandUI) { empty.oneHandUI = oneHandUI } + if (chatBottomBar != def.chatBottomBar) { empty.chatBottomBar = chatBottomBar } return empty } @@ -6962,6 +6965,7 @@ data class AppSettings( uiCurrentThemeIds?.let { def.currentThemeIds.set(it) } uiThemes?.let { def.themeOverrides.set(it.skipDuplicates()) } oneHandUI?.let { def.oneHandUI.set(it) } + chatBottomBar?.let { if (appPlatform.isAndroid) def.chatBottomBar.set(it) else def.chatBottomBar.set(true) } } companion object { @@ -6996,7 +7000,8 @@ data class AppSettings( uiDarkColorScheme = DefaultTheme.SIMPLEX.themeName, uiCurrentThemeIds = null, uiThemes = null, - oneHandUI = true + oneHandUI = true, + chatBottomBar = true, ) val current: AppSettings @@ -7032,7 +7037,8 @@ data class AppSettings( uiDarkColorScheme = def.systemDarkTheme.get() ?: DefaultTheme.SIMPLEX.themeName, uiCurrentThemeIds = def.currentThemeIds.get(), uiThemes = def.themeOverrides.get(), - oneHandUI = def.oneHandUI.get() + oneHandUI = def.oneHandUI.get(), + chatBottomBar = def.chatBottomBar.get() ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/ScrollableColumn.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/ScrollableColumn.kt index b0be547a31..b4e823bd45 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/ScrollableColumn.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/ScrollableColumn.kt @@ -23,6 +23,7 @@ expect fun LazyColumnWithScrollBar( flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(), userScrollEnabled: Boolean = true, additionalBarOffset: State? = null, + chatBottomBar: State = remember { mutableStateOf(true) }, // by default, this function will include .fillMaxSize() without you doing anything. If you don't need it, pass `false` here // maxSize (at least maxHeight) is needed for blur on appBars to work correctly fillMaxSize: Boolean = true, @@ -41,6 +42,7 @@ expect fun LazyColumnWithScrollBarNoAppBar( flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(), userScrollEnabled: Boolean = true, additionalBarOffset: State? = null, + chatBottomBar: State = remember { mutableStateOf(true) }, content: LazyListScope.() -> Unit ) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt index b6eb4c8996..6a6db0da85 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt @@ -156,7 +156,7 @@ fun TerminalLog(floating: Boolean, composeViewHeight: State) { LazyColumnWithScrollBar ( reverseLayout = true, contentPadding = PaddingValues( - top = topPaddingToContent(), + top = topPaddingToContent(false), bottom = composeViewHeight.value ), state = listState, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index 30f76bb878..ddf25a6e3b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -658,6 +658,8 @@ fun ChatLayout( Box(Modifier.fillMaxSize().chatViewBackgroundModifier(MaterialTheme.colors, MaterialTheme.wallpaper, LocalAppBarHandler.current?.backgroundGraphicsLayerSize, LocalAppBarHandler.current?.backgroundGraphicsLayer)) { val remoteHostId = remember { remoteHostId }.value val chatInfo = remember { chatInfo }.value + val oneHandUI = remember { appPrefs.oneHandUI.state } + val chatBottomBar = remember { appPrefs.chatBottomBar.state } AdaptingBottomPaddingLayout(Modifier, CHAT_COMPOSE_LAYOUT_ID, composeViewHeight) { if (chatInfo != null) { Box(Modifier.fillMaxSize()) { @@ -670,25 +672,23 @@ fun ChatLayout( ) } } - val oneHandUI = remember { appPrefs.oneHandUI.state } Box( Modifier .layoutId(CHAT_COMPOSE_LAYOUT_ID) .align(Alignment.BottomCenter) .imePadding() .navigationBarsPadding() - .then(if (oneHandUI.value) Modifier.padding(bottom = AppBarHeight * fontSizeSqrtMultiplier) else Modifier) + .then(if (oneHandUI.value && chatBottomBar.value) Modifier.padding(bottom = AppBarHeight * fontSizeSqrtMultiplier) else Modifier) ) { composeView() } } - val oneHandUI = remember { appPrefs.oneHandUI.state } - if (oneHandUI.value) { + if (oneHandUI.value && chatBottomBar.value) { StatusBarBackground() } else { NavigationBarBackground(true, oneHandUI.value, noAlpha = true) } - Box(if (oneHandUI.value) Modifier.align(Alignment.BottomStart).imePadding() else Modifier) { + Box(if (oneHandUI.value && chatBottomBar.value) Modifier.align(Alignment.BottomStart).imePadding() else Modifier) { if (selectedChatItems.value == null) { if (chatInfo != null) { ChatInfoToolbar(chatInfo, back, info, startCall, endCall, addMembers, openGroupLink, changeNtfsState, onSearchValueChanged, showSearch) @@ -856,12 +856,13 @@ fun BoxScope.ChatInfoToolbar( } } val oneHandUI = remember { appPrefs.oneHandUI.state } + val chatBottomBar = remember { appPrefs.chatBottomBar.state } DefaultAppBar( navigationButton = { if (appPlatform.isAndroid || showSearch.value) { NavigationButtonBack(onBackClicked) } }, title = { ChatInfoToolbarTitle(chatInfo) }, onTitleClick = if (chatInfo is ChatInfo.Local) null else info, showSearch = showSearch.value, - onTop = !oneHandUI.value, + onTop = !oneHandUI.value || !chatBottomBar.value, onSearchValueChanged = onSearchValueChanged, buttons = { barButtons.forEach { it() } } ) @@ -873,11 +874,11 @@ fun BoxScope.ChatInfoToolbar( showMenu, modifier = Modifier.onSizeChanged { with(density) { width.value = it.width.toDp().coerceAtLeast(250.dp) - if (oneHandUI.value && (appPlatform.isDesktop || (platform.androidApiLevel ?: 0) >= 30)) height.value = it.height.toDp() + if (oneHandUI.value && chatBottomBar.value && (appPlatform.isDesktop || (platform.androidApiLevel ?: 0) >= 30)) height.value = it.height.toDp() } }, - offset = DpOffset(-width.value, if (oneHandUI.value) -height.value else AppBarHeight) + offset = DpOffset(-width.value, if (oneHandUI.value && chatBottomBar.value) -height.value else AppBarHeight) ) { - if (oneHandUI.value) { + if (oneHandUI.value && chatBottomBar.value) { menuItems.asReversed().forEach { it() } } else { menuItems.forEach { it() } @@ -964,7 +965,7 @@ fun BoxScope.ChatItemsList( val reversedChatItems = remember { derivedStateOf { chatModel.chatItems.asReversed() } } val revealedItems = rememberSaveable(stateSaver = serializableSaver()) { mutableStateOf(setOf()) } val mergedItems = remember { derivedStateOf { MergedItems.create(reversedChatItems.value, unreadCount, revealedItems.value, chatModel.chatState) } } - val topPaddingToContentPx = rememberUpdatedState(with(LocalDensity.current) { topPaddingToContent().roundToPx() }) + val topPaddingToContentPx = rememberUpdatedState(with(LocalDensity.current) { topPaddingToContent(true).roundToPx() }) /** determines height based on window info and static height of two AppBars. It's needed because in the first graphic frame height of * [composeViewHeight] is unknown, but we need to set scroll position for unread messages already so it will be correct before the first frame appears * */ @@ -1264,10 +1265,11 @@ fun BoxScope.ChatItemsList( state = listState.value, reverseLayout = true, contentPadding = PaddingValues( - top = topPaddingToContent(), + top = topPaddingToContent(true), bottom = composeViewHeight.value ), - additionalBarOffset = composeViewHeight + additionalBarOffset = composeViewHeight, + chatBottomBar = remember { appPrefs.chatBottomBar.state } ) { val mergedItemsValue = mergedItems.value itemsIndexed(mergedItemsValue.items, key = { _, merged -> keyForItem(merged.newest().item) }) { index, merged -> @@ -1310,8 +1312,8 @@ fun BoxScope.ChatItemsList( } } } - FloatingButtons(loadingMoreItems, mergedItems, unreadCount, maxHeight, composeViewHeight, remoteHostId, chatInfo, searchValue, markChatRead, listState) - FloatingDate(Modifier.padding(top = 10.dp + topPaddingToContent()).align(Alignment.TopCenter), mergedItems, listState) + FloatingButtons(loadingMoreItems, mergedItems, unreadCount, maxHeight, composeViewHeight, searchValue, markChatRead, listState) + FloatingDate(Modifier.padding(top = 10.dp + topPaddingToContent(true)).align(Alignment.TopCenter), mergedItems, listState) LaunchedEffect(Unit) { snapshotFlow { listState.value.isScrollInProgress } @@ -1400,14 +1402,12 @@ fun BoxScope.FloatingButtons( unreadCount: State, maxHeight: State, composeViewHeight: State, - remoteHostId: Long?, - chatInfo: ChatInfo, searchValue: State, markChatRead: () -> Unit, listState: State ) { val scope = rememberCoroutineScope() - val topPaddingToContentPx = rememberUpdatedState(with(LocalDensity.current) { topPaddingToContent().roundToPx() }) + val topPaddingToContentPx = rememberUpdatedState(with(LocalDensity.current) { topPaddingToContent(true).roundToPx() }) val bottomUnreadCount = remember { derivedStateOf { if (unreadCount.value == 0) return@derivedStateOf 0 @@ -1447,7 +1447,7 @@ fun BoxScope.FloatingButtons( val showDropDown = remember { mutableStateOf(false) } TopEndFloatingButton( - Modifier.padding(end = DEFAULT_PADDING, top = 24.dp + topPaddingToContent()).align(Alignment.TopEnd), + Modifier.padding(end = DEFAULT_PADDING, top = 24.dp + topPaddingToContent(true)).align(Alignment.TopEnd), topUnreadCount, onClick = { val index = mergedItems.value.items.indexOfLast { it.hasUnread() } @@ -1465,7 +1465,7 @@ fun BoxScope.FloatingButtons( DefaultDropdownMenu( showDropDown, modifier = Modifier.onSizeChanged { with(density) { width.value = it.width.toDp().coerceAtLeast(250.dp) } }, - offset = DpOffset(-DEFAULT_PADDING - width.value, 24.dp + fabSize + topPaddingToContent()) + offset = DpOffset(-DEFAULT_PADDING - width.value, 24.dp + fabSize + topPaddingToContent(true)) ) { ItemAction( generalGetString(MR.strings.mark_read), @@ -1615,9 +1615,10 @@ private fun TopEndFloatingButton( } @Composable -fun topPaddingToContent(): Dp { +fun topPaddingToContent(chatView: Boolean): Dp { val oneHandUI = remember { appPrefs.oneHandUI.state } - return if (oneHandUI.value) { + val chatBottomBar = remember { appPrefs.chatBottomBar.state } + return if (oneHandUI.value && (!chatView || chatBottomBar.value)) { WindowInsets.statusBars.asPaddingValues().calculateTopPadding() } else { AppBarHeight * fontSizeSqrtMultiplier + WindowInsets.statusBars.asPaddingValues().calculateTopPadding() @@ -1634,7 +1635,7 @@ private fun FloatingDate( val nearBottomIndex = remember(chatModel.chatId) { mutableStateOf(if (isNearBottom.value) -1 else 0) } val showDate = remember(chatModel.chatId) { mutableStateOf(false) } val density = LocalDensity.current.density - val topPaddingToContentPx = rememberUpdatedState(with(LocalDensity.current) { topPaddingToContent().roundToPx() }) + val topPaddingToContentPx = rememberUpdatedState(with(LocalDensity.current) { topPaddingToContent(true).roundToPx() }) val fontSizeSqrtMultiplier = fontSizeSqrtMultiplier val lastVisibleItemDate = remember { derivedStateOf { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt index 804222f264..5ee6e40e6e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt @@ -306,7 +306,7 @@ fun ModalData.GroupChatInfoLayout( contentPadding = if (oneHandUI.value) { PaddingValues(top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + DEFAULT_PADDING + 5.dp, bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()) } else { - PaddingValues(top = topPaddingToContent()) + PaddingValues(top = topPaddingToContent(false)) }, state = listState ) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt index 20bb65ec7d..ff776bc8ca 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt @@ -761,7 +761,7 @@ private fun BoxScope.ChatList(searchText: MutableState, listStat val searchShowingSimplexLink = remember { mutableStateOf(false) } val searchChatFilteredBySimplexLink = remember { mutableStateOf(null) } val chats = filteredChats(showUnreadAndFavorites, searchShowingSimplexLink, searchChatFilteredBySimplexLink, searchText.value.text, allChats.value.toList()) - val topPaddingToContent = topPaddingToContent() + val topPaddingToContent = topPaddingToContent(false) val blankSpaceSize = if (oneHandUI.value) WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + AppBarHeight * fontSizeSqrtMultiplier else topPaddingToContent LazyColumnWithScrollBar( if (!oneHandUI.value) Modifier.imePadding() else Modifier, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt index 9ca2c1e2cd..e048c39fe7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt @@ -194,7 +194,7 @@ private fun ShareList( filteredChats(false, mutableStateOf(false), mutableStateOf(null), search, sorted) } } - val topPaddingToContent = topPaddingToContent() + val topPaddingToContent = topPaddingToContent(false) LazyColumnWithScrollBar( modifier = Modifier.then(if (oneHandUI.value) Modifier.consumeWindowInsets(WindowInsets.navigationBars.only(WindowInsetsSides.Vertical)) else Modifier).imePadding(), contentPadding = PaddingValues( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt index 72118224e6..cb4991c99f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt @@ -334,7 +334,7 @@ private fun ModalData.NewChatSheetLayout( @Composable fun NonOneHandLazyColumn() { - val blankSpaceSize = topPaddingToContent() + val blankSpaceSize = topPaddingToContent(false) LazyColumnWithScrollBar( Modifier.imePadding(), state = listState, @@ -646,7 +646,7 @@ private fun ModalData.DeletedContactsView(rh: RemoteHostInfo?, closeDeletedChats ) Box { - val topPaddingToContent = topPaddingToContent() + val topPaddingToContent = topPaddingToContent(false) LazyColumnWithScrollBar( if (!oneHandUI.value) Modifier.imePadding() else Modifier, contentPadding = PaddingValues( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt index e08d46d880..058c82c2fe 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt @@ -400,7 +400,7 @@ fun ActiveProfilePicker( .fillMaxSize() .alpha(if (progressByTimeout) 0.6f else 1f) ) { - LazyColumnWithScrollBar(Modifier.padding(top = topPaddingToContent()), userScrollEnabled = !switchingProfile.value) { + LazyColumnWithScrollBar(Modifier.padding(top = topPaddingToContent(false)), userScrollEnabled = !switchingProfile.value) { item { val oneHandUI = remember { appPrefs.oneHandUI.state } if (oneHandUI.value) { diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index ddf8805e8a..df0b3d5cd7 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -1396,7 +1396,8 @@ Database downgrade Incompatible database version Confirm database upgrades - Reachable chat toolbar + Reachable app toolbars + Reachable chat toolbar Toggle chat list: You can change it in Appearance settings. Show console in new window diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/ScrollableColumn.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/ScrollableColumn.desktop.kt index fc806feb2b..785c3b40fa 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/ScrollableColumn.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/ScrollableColumn.desktop.kt @@ -36,6 +36,7 @@ actual fun LazyColumnWithScrollBar( flingBehavior: FlingBehavior, userScrollEnabled: Boolean, additionalBarOffset: State?, + chatBottomBar: State, fillMaxSize: Boolean, content: LazyListScope.() -> Unit ) { @@ -92,7 +93,7 @@ actual fun LazyColumnWithScrollBar( val modifier = if (fillMaxSize) Modifier.fillMaxSize().then(modifier) else modifier Box(Modifier.copyViewToAppBar(remember { appPrefs.appearanceBarsBlurRadius.state }.value, LocalAppBarHandler.current?.graphicsLayer).nestedScroll(connection)) { LazyColumn(modifier.then(scrollModifier), state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled, content) - ScrollBar(reverseLayout, state, scrollBarAlpha, scrollJob, scrollBarDraggingState, additionalBarOffset) + ScrollBar(reverseLayout, state, scrollBarAlpha, scrollJob, scrollBarDraggingState, additionalBarOffset, chatBottomBar) } } @@ -107,6 +108,7 @@ actual fun LazyColumnWithScrollBarNoAppBar( flingBehavior: FlingBehavior, userScrollEnabled: Boolean, additionalBarOffset: State?, + chatBottomBar: State, content: LazyListScope.() -> Unit ) { val scope = rememberCoroutineScope() @@ -133,7 +135,7 @@ actual fun LazyColumnWithScrollBarNoAppBar( val scrollBarDraggingState = remember { mutableStateOf(false) } Box { LazyColumn(modifier.then(scrollModifier), state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled, content) - ScrollBar(reverseLayout, state, scrollBarAlpha, scrollJob, scrollBarDraggingState, additionalBarOffset) + ScrollBar(reverseLayout, state, scrollBarAlpha, scrollJob, scrollBarDraggingState, additionalBarOffset, chatBottomBar) } } @@ -144,15 +146,16 @@ private fun ScrollBar( scrollBarAlpha: Animatable, scrollJob: MutableState, scrollBarDraggingState: MutableState, - additionalBarHeight: State? + additionalBarHeight: State?, + chatBottomBar: State, ) { val oneHandUI = remember { appPrefs.oneHandUI.state } val padding = if (additionalBarHeight != null) { - PaddingValues(top = if (oneHandUI.value) 0.dp else AppBarHeight * fontSizeSqrtMultiplier, bottom = additionalBarHeight.value) + PaddingValues(top = if (oneHandUI.value && chatBottomBar.value) 0.dp else AppBarHeight * fontSizeSqrtMultiplier, bottom = additionalBarHeight.value) } else if (reverseLayout) { PaddingValues(bottom = AppBarHeight * fontSizeSqrtMultiplier) } else { - PaddingValues(top = if (oneHandUI.value) 0.dp else AppBarHeight * fontSizeSqrtMultiplier) + PaddingValues(top = if (oneHandUI.value && chatBottomBar.value) 0.dp else AppBarHeight * fontSizeSqrtMultiplier) } Box(Modifier.fillMaxSize().padding(padding), contentAlignment = Alignment.CenterEnd) { DesktopScrollBar(rememberScrollbarAdapter(state), Modifier.fillMaxHeight(), scrollBarAlpha, scrollJob, reverseLayout, scrollBarDraggingState) diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/Appearance.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/Appearance.desktop.kt index 91ff8831ce..c270bddb73 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/Appearance.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/Appearance.desktop.kt @@ -58,6 +58,9 @@ fun AppearanceScope.AppearanceLayout( } } SettingsPreferenceItem(icon = null, stringResource(MR.strings.one_hand_ui), ChatModel.controller.appPrefs.oneHandUI) + if (remember { appPrefs.oneHandUI.state }.value && !remember { appPrefs.chatBottomBar.state }.value) { + SettingsPreferenceItem(icon = null, stringResource(MR.strings.chat_bottom_bar), ChatModel.controller.appPrefs.chatBottomBar) + } } SectionDividerSpaced() ThemesSection(systemDarkTheme) diff --git a/src/Simplex/Chat/AppSettings.hs b/src/Simplex/Chat/AppSettings.hs index e75546d206..1efa69fad4 100644 --- a/src/Simplex/Chat/AppSettings.hs +++ b/src/Simplex/Chat/AppSettings.hs @@ -56,7 +56,8 @@ data AppSettings = AppSettings uiDarkColorScheme :: Maybe DarkColorScheme, uiCurrentThemeIds :: Maybe (Map ThemeColorScheme Text), uiThemes :: Maybe [UITheme], - oneHandUI :: Maybe Bool + oneHandUI :: Maybe Bool, + chatBottomBar :: Maybe Bool } deriving (Show) @@ -105,7 +106,8 @@ defaultAppSettings = uiDarkColorScheme = Just DCSSimplex, uiCurrentThemeIds = Nothing, uiThemes = Nothing, - oneHandUI = Just True + oneHandUI = Just True, + chatBottomBar = Just True } defaultParseAppSettings :: AppSettings @@ -141,7 +143,8 @@ defaultParseAppSettings = uiDarkColorScheme = Nothing, uiCurrentThemeIds = Nothing, uiThemes = Nothing, - oneHandUI = Nothing + oneHandUI = Nothing, + chatBottomBar = Nothing } combineAppSettings :: AppSettings -> AppSettings -> AppSettings @@ -177,7 +180,8 @@ combineAppSettings platformDefaults storedSettings = uiDarkColorScheme = p uiDarkColorScheme, uiCurrentThemeIds = p uiCurrentThemeIds, uiThemes = p uiThemes, - oneHandUI = p oneHandUI + oneHandUI = p oneHandUI, + chatBottomBar = p chatBottomBar } where p :: (AppSettings -> Maybe a) -> Maybe a @@ -230,6 +234,7 @@ instance FromJSON AppSettings where uiCurrentThemeIds <- p "uiCurrentThemeIds" uiThemes <- p "uiThemes" oneHandUI <- p "oneHandUI" + chatBottomBar <- p "chatBottomBar" pure AppSettings { appPlatform, @@ -262,7 +267,8 @@ instance FromJSON AppSettings where uiDarkColorScheme, uiCurrentThemeIds, uiThemes, - oneHandUI + oneHandUI, + chatBottomBar } where p key = v .:? key <|> pure Nothing From 3fa1d7b07c7ee3f2f52fc887e96d3a141a05cc22 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Wed, 4 Dec 2024 20:50:19 +0400 Subject: [PATCH 126/167] core: fix cli servers override (#5317) --- src/Simplex/Chat.hs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index e78d6de288..516d033ebf 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -101,7 +101,7 @@ import qualified Simplex.FileTransfer.Transport as XFTP import Simplex.FileTransfer.Types (FileErrorType (..), RcvFileId, SndFileId) import Simplex.Messaging.Agent as Agent import Simplex.Messaging.Agent.Client (SubInfo (..), agentClientStore, getAgentQueuesInfo, getAgentWorkersDetails, getAgentWorkersSummary, getFastNetworkConfig, ipAddressProtected, withLockMap) -import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), ServerCfg (..), ServerRoles (..), allRoles, createAgentStore, defaultAgentConfig) +import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), ServerCfg (..), ServerRoles (..), allRoles, createAgentStore, defaultAgentConfig, presetServerCfg) import Simplex.Messaging.Agent.Lock (withLock) import Simplex.Messaging.Agent.Protocol import qualified Simplex.Messaging.Agent.Protocol as AP (AgentErrorType (..)) @@ -427,8 +427,12 @@ newChatController ops <- getUpdateServerOperators db presetOps (null users) let opDomains = operatorDomains $ mapMaybe snd ops (smp', xftp') <- unzip <$> mapM (getServers ops opDomains) users - pure InitialAgentServers {smp = M.fromList smp', xftp = M.fromList xftp', ntf, netCfg} + pure InitialAgentServers {smp = M.fromList (optServers smp' smpServers), xftp = M.fromList (optServers xftp' xftpServers), ntf, netCfg} where + optServers :: [(UserId, NonEmpty (ServerCfg p))] -> [ProtoServerWithAuth p] -> [(UserId, NonEmpty (ServerCfg p))] + optServers srvs overrides_ = case L.nonEmpty overrides_ of + Just overrides -> map (second $ const $ L.map (presetServerCfg True allRoles Nothing) overrides) srvs + Nothing -> srvs getServers :: [(Maybe PresetOperator, Maybe ServerOperator)] -> [(Text, ServerOperator)] -> User -> IO ((UserId, NonEmpty (ServerCfg 'PSMP)), (UserId, NonEmpty (ServerCfg 'PXFTP))) getServers ops opDomains user' = do smpSrvs <- getProtocolServers db SPSMP user' From 892f6498be3ebbab92402e4cf7d75b6d6b0e9399 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Wed, 4 Dec 2024 21:33:12 +0400 Subject: [PATCH 127/167] ios: fix contact cards opening empty page on connection 2 (#5319) --- apps/ios/Shared/Model/SimpleXAPI.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 5f29a848ef..e29748f3af 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -2014,11 +2014,16 @@ func processReceivedMsg(_ res: ChatResponse) async { m.removeChat(hostConn.id) } } - case let .businessLinkConnecting(user, groupInfo, hostMember, fromContact): + case let .businessLinkConnecting(user, groupInfo, _, fromContact): if !active(user) { return } await MainActor.run { m.updateGroup(groupInfo) + } + if m.chatId == fromContact.id { + ItemsModel.shared.loadOpenChat(groupInfo.id) + } + await MainActor.run { m.removeChat(fromContact.id) } case let .joinedGroupMemberConnecting(user, groupInfo, _, member): From 009d13210f217edb2dee2d36c540caa48d9ef4ce Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Wed, 4 Dec 2024 21:33:30 +0400 Subject: [PATCH 128/167] android: fix simplex chat team contact card opening empty page on connection (#5320) --- .../kotlin/chat/simplex/common/model/SimpleXAPI.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index 701b32f6f6..bd9c3ddc7b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -23,6 +23,7 @@ import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.call.* import chat.simplex.common.views.chat.item.showQuotedItemDoesNotExistAlert +import chat.simplex.common.views.chatlist.openGroupChat import chat.simplex.common.views.migration.MigrationFileLinkData import chat.simplex.common.views.onboarding.OnboardingStage import chat.simplex.common.views.usersettings.* @@ -2532,6 +2533,11 @@ object ChatController { withChats { updateGroup(rhId, r.groupInfo) + } + if (chatModel.chatId.value == r.fromContact.id) { + openGroupChat(rhId, r.groupInfo.groupId) + } + withChats { removeChat(rhId, r.fromContact.id) } } From c1c17d1f19aab9893874f60462ef0c4480cc964c Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Wed, 4 Dec 2024 19:59:42 +0000 Subject: [PATCH 129/167] core: 6.2.0.5 (simplexmq: 6.2.0.6) --- cabal.project | 2 +- package.yaml | 2 +- scripts/nix/sha256map.nix | 2 +- simplex-chat.cabal | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cabal.project b/cabal.project index 414bae94dd..da9c71587f 100644 --- a/cabal.project +++ b/cabal.project @@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 4b43cb805437dc0822eb81c57b2c85e77ad333ca + tag: 966b9990e0bf5fdb701f79b6efd722baddd1ee1d source-repository-package type: git diff --git a/package.yaml b/package.yaml index ca29dc549b..8752a141f7 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: simplex-chat -version: 6.2.0.4 +version: 6.2.0.5 #synopsis: #description: homepage: https://github.com/simplex-chat/simplex-chat#readme diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 963ff27092..3eec88a25b 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."4b43cb805437dc0822eb81c57b2c85e77ad333ca" = "143cskyh3z6yxn6fnaw8biskbspa2cndc65rzziajlw13yd0wggg"; + "https://github.com/simplex-chat/simplexmq.git"."966b9990e0bf5fdb701f79b6efd722baddd1ee1d" = "0gmycrmyrgy5wbhr3f7qy6hbpppsamfypq7y650dinpbqyrfs9fb"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 7d8920e3e7..92eea030c0 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplex-chat -version: 6.2.0.4 +version: 6.2.0.5 category: Web, System, Services, Cryptography homepage: https://github.com/simplex-chat/simplex-chat#readme author: simplex.chat From c1a0943448a767022bfd0644e39b3378c301c1ad Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Wed, 4 Dec 2024 21:56:09 +0000 Subject: [PATCH 130/167] 6.2-beta.5: ios 251, android 256, desktop 80 --- apps/ios/SimpleX.xcodeproj/project.pbxproj | 36 +++++++++++----------- apps/multiplatform/gradle.properties | 8 ++--- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 24984e5efc..690ba2579a 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -167,9 +167,9 @@ 648010AB281ADD15009009B9 /* CIFileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648010AA281ADD15009009B9 /* CIFileView.swift */; }; 648679AB2BC96A74006456E7 /* ChatItemForwardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648679AA2BC96A74006456E7 /* ChatItemForwardingView.swift */; }; 649B28DD2CFE07CF00536B68 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 649B28D82CFE07CF00536B68 /* libffi.a */; }; - 649B28DE2CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.4-3HGUY7rfU7IFvhj8CPEbqy-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 649B28D92CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.4-3HGUY7rfU7IFvhj8CPEbqy-ghc9.6.3.a */; }; + 649B28DE2CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.5-592uBhlQO6KIf70TJf5KpP-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 649B28D92CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.5-592uBhlQO6KIf70TJf5KpP-ghc9.6.3.a */; }; 649B28DF2CFE07CF00536B68 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 649B28DA2CFE07CF00536B68 /* libgmpxx.a */; }; - 649B28E02CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.4-3HGUY7rfU7IFvhj8CPEbqy.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 649B28DB2CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.4-3HGUY7rfU7IFvhj8CPEbqy.a */; }; + 649B28E02CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.5-592uBhlQO6KIf70TJf5KpP.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 649B28DB2CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.5-592uBhlQO6KIf70TJf5KpP.a */; }; 649B28E12CFE07CF00536B68 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 649B28DC2CFE07CF00536B68 /* libgmp.a */; }; 649BCDA0280460FD00C3A862 /* ComposeImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649BCD9F280460FD00C3A862 /* ComposeImageView.swift */; }; 649BCDA22805D6EF00C3A862 /* CIImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649BCDA12805D6EF00C3A862 /* CIImageView.swift */; }; @@ -516,9 +516,9 @@ 648679AA2BC96A74006456E7 /* ChatItemForwardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemForwardingView.swift; sourceTree = ""; }; 6493D667280ED77F007A76FB /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; 649B28D82CFE07CF00536B68 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 649B28D92CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.4-3HGUY7rfU7IFvhj8CPEbqy-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.2.0.4-3HGUY7rfU7IFvhj8CPEbqy-ghc9.6.3.a"; sourceTree = ""; }; + 649B28D92CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.5-592uBhlQO6KIf70TJf5KpP-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.2.0.5-592uBhlQO6KIf70TJf5KpP-ghc9.6.3.a"; sourceTree = ""; }; 649B28DA2CFE07CF00536B68 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; - 649B28DB2CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.4-3HGUY7rfU7IFvhj8CPEbqy.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.2.0.4-3HGUY7rfU7IFvhj8CPEbqy.a"; sourceTree = ""; }; + 649B28DB2CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.5-592uBhlQO6KIf70TJf5KpP.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.2.0.5-592uBhlQO6KIf70TJf5KpP.a"; sourceTree = ""; }; 649B28DC2CFE07CF00536B68 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; 649BCD9F280460FD00C3A862 /* ComposeImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeImageView.swift; sourceTree = ""; }; 649BCDA12805D6EF00C3A862 /* CIImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIImageView.swift; sourceTree = ""; }; @@ -671,9 +671,9 @@ 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, 649B28E12CFE07CF00536B68 /* libgmp.a in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, - 649B28E02CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.4-3HGUY7rfU7IFvhj8CPEbqy.a in Frameworks */, + 649B28E02CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.5-592uBhlQO6KIf70TJf5KpP.a in Frameworks */, CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */, - 649B28DE2CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.4-3HGUY7rfU7IFvhj8CPEbqy-ghc9.6.3.a in Frameworks */, + 649B28DE2CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.5-592uBhlQO6KIf70TJf5KpP-ghc9.6.3.a in Frameworks */, 649B28DD2CFE07CF00536B68 /* libffi.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -754,8 +754,8 @@ 649B28D82CFE07CF00536B68 /* libffi.a */, 649B28DC2CFE07CF00536B68 /* libgmp.a */, 649B28DA2CFE07CF00536B68 /* libgmpxx.a */, - 649B28D92CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.4-3HGUY7rfU7IFvhj8CPEbqy-ghc9.6.3.a */, - 649B28DB2CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.4-3HGUY7rfU7IFvhj8CPEbqy.a */, + 649B28D92CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.5-592uBhlQO6KIf70TJf5KpP-ghc9.6.3.a */, + 649B28DB2CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.5-592uBhlQO6KIf70TJf5KpP.a */, ); path = Libraries; sourceTree = ""; @@ -1931,7 +1931,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 250; + CURRENT_PROJECT_VERSION = 251; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -1980,7 +1980,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 250; + CURRENT_PROJECT_VERSION = 251; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -2021,7 +2021,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 250; + CURRENT_PROJECT_VERSION = 251; DEVELOPMENT_TEAM = 5NN7GUYB6T; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; @@ -2041,7 +2041,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 250; + CURRENT_PROJECT_VERSION = 251; DEVELOPMENT_TEAM = 5NN7GUYB6T; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; @@ -2066,7 +2066,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 250; + CURRENT_PROJECT_VERSION = 251; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GCC_OPTIMIZATION_LEVEL = s; @@ -2103,7 +2103,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 250; + CURRENT_PROJECT_VERSION = 251; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_CODE_COVERAGE = NO; @@ -2140,7 +2140,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 250; + CURRENT_PROJECT_VERSION = 251; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -2191,7 +2191,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 250; + CURRENT_PROJECT_VERSION = 251; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -2242,7 +2242,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 250; + CURRENT_PROJECT_VERSION = 251; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -2276,7 +2276,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 250; + CURRENT_PROJECT_VERSION = 251; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; diff --git a/apps/multiplatform/gradle.properties b/apps/multiplatform/gradle.properties index e940f077ff..b490c26f87 100644 --- a/apps/multiplatform/gradle.properties +++ b/apps/multiplatform/gradle.properties @@ -24,11 +24,11 @@ android.nonTransitiveRClass=true kotlin.mpp.androidSourceSetLayoutVersion=2 kotlin.jvm.target=11 -android.version_name=6.2-beta.4 -android.version_code=255 +android.version_name=6.2-beta.5 +android.version_code=256 -desktop.version_name=6.2-beta.4 -desktop.version_code=79 +desktop.version_name=6.2-beta.5 +desktop.version_code=80 kotlin.version=1.9.23 gradle.plugin.version=8.2.0 From 4f8a70a6c1d8c5a4df4b2c12d792030067de62d4 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Thu, 5 Dec 2024 22:52:45 +0700 Subject: [PATCH 131/167] android, desktop: allow to scan QR multiple times after fail (#5323) --- .../views/newchat/QRCodeScanner.android.kt | 42 ++++++----- .../simplex/common/views/chat/ScanCodeView.kt | 18 ++--- .../common/views/chat/VerifyCodeView.kt | 23 +++--- .../common/views/migration/MigrateToDevice.kt | 8 +- .../common/views/newchat/ConnectPlan.kt | 19 ++++- .../common/views/newchat/NewChatView.kt | 32 ++++---- .../common/views/newchat/QRCodeScanner.kt | 2 +- .../common/views/remote/ConnectDesktopView.kt | 75 ++++++++++--------- .../networkAndServers/ScanProtocolServer.kt | 1 + .../views/newchat/QRCodeScanner.desktop.kt | 2 +- 10 files changed, 123 insertions(+), 99 deletions(-) diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/newchat/QRCodeScanner.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/newchat/QRCodeScanner.android.kt index df38295787..6cf432a15f 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/newchat/QRCodeScanner.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/newchat/QRCodeScanner.android.kt @@ -33,6 +33,7 @@ import com.google.accompanist.permissions.rememberPermissionState import com.google.common.util.concurrent.ListenableFuture import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource +import kotlinx.coroutines.delay import java.util.concurrent.* // Adapted from learntodroid - https://gist.github.com/learntodroid/8f839be0b29d0378f843af70607bd7f5 @@ -41,13 +42,13 @@ import java.util.concurrent.* actual fun QRCodeScanner( showQRCodeScanner: MutableState, padding: PaddingValues, - onBarcode: (String) -> Unit + onBarcode: suspend (String) -> Boolean ) { val context = LocalContext.current val lifecycleOwner = LocalLifecycleOwner.current - var preview by remember { mutableStateOf(null) } - var lastAnalyzedTimeStamp = 0L - var contactLink = "" + val preview = remember { mutableStateOf(null) } + val contactLink = remember { mutableStateOf("") } + val checkingLink = remember { mutableStateOf(false) } val cameraProviderFuture by produceState?>(initialValue = null) { value = ProcessCameraProvider.getInstance(context) @@ -86,28 +87,33 @@ actual fun QRCodeScanner( .build() val cameraExecutor: ExecutorService = Executors.newSingleThreadExecutor() cameraProviderFuture?.addListener({ - preview = Preview.Builder().build().also { + preview.value = Preview.Builder().build().also { it.setSurfaceProvider(previewView.surfaceProvider) } val detector: QrCodeDetector = FactoryFiducial.qrcode(null, GrayU8::class.java) - fun getQR(imageProxy: ImageProxy) { - val currentTimeStamp = System.currentTimeMillis() - if (currentTimeStamp - lastAnalyzedTimeStamp >= TimeUnit.SECONDS.toMillis(1)) { - detector.process(imageProxyToGrayU8(imageProxy)) - val found = detector.detections - val qr = found.firstOrNull() - if (qr != null) { - if (qr.message != contactLink) { - // Make sure link is new and not a repeat - contactLink = qr.message - onBarcode(contactLink) + suspend fun getQR(imageProxy: ImageProxy) { + if (checkingLink.value) return + checkingLink.value = true + + detector.process(imageProxyToGrayU8(imageProxy)) + val found = detector.detections + val qr = found.firstOrNull() + if (qr != null) { + if (qr.message != contactLink.value) { + // Make sure link is new and not a repeat if that link was handled successfully + if (onBarcode(qr.message)) { + contactLink.value = qr.message } + // just some delay to not spam endlessly with alert in case the user scan something wrong, and it fails fast + // (for example, scan user's address while verifying contact code - it prevents alert spam) + delay(1000) } } + checkingLink.value = false imageProxy.close() } - val imageAnalyzer = ImageAnalysis.Analyzer { proxy -> getQR(proxy) } + val imageAnalyzer = ImageAnalysis.Analyzer { proxy -> withApi { getQR(proxy) } } val imageAnalysis: ImageAnalysis = ImageAnalysis.Builder() .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .setImageQueueDepth(1) @@ -115,7 +121,7 @@ actual fun QRCodeScanner( .also { it.setAnalyzer(cameraExecutor, imageAnalyzer) } try { cameraProviderFuture?.get()?.unbindAll() - cameraProviderFuture?.get()?.bindToLifecycle(lifecycleOwner, cameraSelector, preview, imageAnalysis) + cameraProviderFuture?.get()?.bindToLifecycle(lifecycleOwner, cameraSelector, preview.value, imageAnalysis) } catch (e: Exception) { Log.d(TAG, "CameraPreview: ${e.localizedMessage}") } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ScanCodeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ScanCodeView.kt index a12a75b747..428d4b1b8f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ScanCodeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ScanCodeView.kt @@ -13,19 +13,19 @@ import chat.simplex.res.MR import dev.icerock.moko.resources.compose.stringResource @Composable -fun ScanCodeView(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, close: () -> Unit) { +fun ScanCodeView(verifyCode: suspend (String?) -> Boolean, close: () -> Unit) { ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.scan_code)) QRCodeScanner { text -> - verifyCode(text) { - if (it) { - close() - } else { - AlertManager.shared.showAlertMsg( - title = generalGetString(MR.strings.incorrect_code) - ) - } + val success = verifyCode(text) + if (success) { + close() + } else { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.incorrect_code) + ) } + success } Text(stringResource(MR.strings.scan_code_from_contacts_app), Modifier.padding(horizontal = DEFAULT_PADDING)) SectionBottomSpacer() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/VerifyCodeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/VerifyCodeView.kt index 69087ecd60..e670fae5ef 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/VerifyCodeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/VerifyCodeView.kt @@ -35,14 +35,14 @@ fun VerifyCodeView( displayName, connectionCode, connectionVerified, - verifyCode = { newCode, cb -> - withBGApi { - val res = verify(newCode) - if (res != null) { - val (verified) = res - cb(verified) - if (verified) close() - } + verifyCode = { newCode -> + val res = verify(newCode) + if (res != null) { + val (verified) = res + if (verified) close() + verified + } else { + false } } ) @@ -54,7 +54,7 @@ private fun VerifyCodeLayout( displayName: String, connectionCode: String, connectionVerified: Boolean, - verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, + verifyCode: suspend (String?) -> Boolean, ) { ColumnWithScrollBar(Modifier.padding(horizontal = DEFAULT_PADDING)) { AppBarTitle(stringResource(MR.strings.security_code), withPadding = false) @@ -100,7 +100,7 @@ private fun VerifyCodeLayout( ) { if (connectionVerified) { SimpleButton(generalGetString(MR.strings.clear_verification), painterResource(MR.images.ic_shield)) { - verifyCode(null) {} + withApi { verifyCode(null) } } } else { if (appPlatform.isAndroid) { @@ -111,7 +111,8 @@ private fun VerifyCodeLayout( } } SimpleButton(generalGetString(MR.strings.mark_code_verified), painterResource(MR.images.ic_verified_user)) { - verifyCode(connectionCode) { verified -> + withApi { + val verified = verifyCode(connectionCode) if (!verified) { AlertManager.shared.showAlertMsg( title = generalGetString(MR.strings.incorrect_code) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateToDevice.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateToDevice.kt index f4f537aab7..788c07a9d2 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateToDevice.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateToDevice.kt @@ -203,7 +203,7 @@ private fun MutableState.PasteOrScanLinkView(close: () -> Uni if (appPlatform.isAndroid) { SectionView(stringResource(MR.strings.scan_QR_code).replace('\n', ' ').uppercase()) { QRCodeScanner(showQRCodeScanner = remember { mutableStateOf(true) }) { text -> - withBGApi { checkUserLink(text) } + checkUserLink(text) } } SectionSpacer() @@ -518,8 +518,8 @@ private fun ProgressView() { DefaultProgressView(null) } -private suspend fun MutableState.checkUserLink(link: String) { - if (strHasSimplexFileLink(link.trim())) { +private suspend fun MutableState.checkUserLink(link: String): Boolean { + return if (strHasSimplexFileLink(link.trim())) { val data = MigrationFileLinkData.readFromLink(link) val hasProxyConfigured = data?.networkConfig?.hasProxyConfigured() ?: false val networkConfig = data?.networkConfig?.transformToPlatformSupported() @@ -537,11 +537,13 @@ private suspend fun MutableState.checkUserLink(link: String) networkProxy = null ) } + true } else { AlertManager.shared.showAlertMsg( title = generalGetString(MR.strings.invalid_file_link), text = generalGetString(MR.strings.the_text_you_pasted_is_not_a_link) ) + false } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt index e8190e0767..1b5b475b35 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt @@ -12,6 +12,7 @@ import chat.simplex.common.platform.* import chat.simplex.common.views.chatlist.* import chat.simplex.common.views.helpers.* import chat.simplex.res.MR +import kotlinx.coroutines.* import java.net.URI enum class ConnectionLinkType { @@ -26,8 +27,18 @@ suspend fun planAndConnect( cleanup: (() -> Unit)? = null, filterKnownContact: ((Contact) -> Unit)? = null, filterKnownGroup: ((GroupInfo) -> Unit)? = null, -) { - val connectionPlan = chatModel.controller.apiConnectPlan(rhId, uri.toString()) +): CompletableDeferred { + val completable = CompletableDeferred() + val close: (() -> Unit)? = { + close?.invoke() + // if close was called, it means the connection was created + completable.complete(true) + } + val cleanup: (() -> Unit)? = { + cleanup?.invoke() + completable.complete(!completable.isActive) + } + val connectionPlan = chatModel.controller.apiConnectPlan(rhId, uri) if (connectionPlan != null) { val link = strHasSingleSimplexLink(uri.trim()) val linkText = if (link?.format is Format.SimplexLink) @@ -333,6 +344,7 @@ suspend fun planAndConnect( ) } } + return completable } suspend fun connectViaUri( @@ -343,7 +355,7 @@ suspend fun connectViaUri( connectionPlan: ConnectionPlan?, close: (() -> Unit)?, cleanup: (() -> Unit)?, -) { +): Boolean { val pcc = chatModel.controller.apiConnect(rhId, incognito, uri) val connLinkType = if (connectionPlan != null) planToConnectionLinkType(connectionPlan) else ConnectionLinkType.INVITATION if (pcc != null) { @@ -363,6 +375,7 @@ suspend fun connectViaUri( ) } cleanup?.invoke() + return pcc != null } fun planToConnectionLinkType(connectionPlan: ConnectionPlan): ConnectionLinkType { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt index 058c82c2fe..923c0256a8 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt @@ -38,8 +38,7 @@ import chat.simplex.common.views.chat.topPaddingToContent import chat.simplex.common.views.helpers.* import chat.simplex.common.views.usersettings.* import chat.simplex.res.MR -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch +import kotlinx.coroutines.* import java.net.URI enum class NewChatOption { @@ -559,15 +558,14 @@ private fun ConnectView(rhId: Long?, showQRCodeScanner: MutableState, p SectionView(stringResource(MR.strings.or_scan_qr_code).uppercase(), headerBottomPadding = 5.dp) { QRCodeScanner(showQRCodeScanner) { text -> - withBGApi { - val res = verify(rhId, text, close) - if (!res) { - AlertManager.shared.showAlertMsg( - title = generalGetString(MR.strings.invalid_qr_code), - text = generalGetString(MR.strings.code_you_scanned_is_not_simplex_link_qr_code) - ) - } + val linkVerified = verifyOnly(text) + if (!linkVerified) { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.invalid_qr_code), + text = generalGetString(MR.strings.code_you_scanned_is_not_simplex_link_qr_code) + ) } + verifyAndConnect(rhId, text, close) } } } @@ -656,23 +654,25 @@ private fun filteredProfiles(users: List, searchTextOrPassword: String): L } } -private suspend fun verify(rhId: Long?, text: String?, close: () -> Unit): Boolean { +private fun verifyOnly(text: String?): Boolean = text != null && strIsSimplexLink(text) + +private suspend fun verifyAndConnect(rhId: Long?, text: String?, close: () -> Unit): Boolean { if (text != null && strIsSimplexLink(text)) { - connect(rhId, text, close) - return true + return withContext(Dispatchers.Default) { + connect(rhId, text, close) + } } return false } -private suspend fun connect(rhId: Long?, link: String, close: () -> Unit, cleanup: (() -> Unit)? = null) { +private suspend fun connect(rhId: Long?, link: String, close: () -> Unit, cleanup: (() -> Unit)? = null): Boolean = planAndConnect( rhId, link, close = close, cleanup = cleanup, incognito = null - ) -} + ).await() private fun createInvitation( rhId: Long?, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/QRCodeScanner.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/QRCodeScanner.kt index 1e497e0581..f368edea1b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/QRCodeScanner.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/QRCodeScanner.kt @@ -10,5 +10,5 @@ import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF expect fun QRCodeScanner( showQRCodeScanner: MutableState = remember { mutableStateOf(true) }, padding: PaddingValues = PaddingValues(horizontal = DEFAULT_PADDING * 2f, vertical = DEFAULT_PADDING_HALF), - onBarcode: (String) -> Unit + onBarcode: suspend (String) -> Boolean ) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectDesktopView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectDesktopView.kt index c3eed3118e..3b6e176ca3 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectDesktopView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectDesktopView.kt @@ -40,6 +40,8 @@ import chat.simplex.res.MR import dev.icerock.moko.resources.ImageResource import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext @Composable fun ConnectDesktopView(close: () -> Unit) { @@ -233,7 +235,7 @@ private fun FoundDesktop( SectionSpacer() if (compatible) { - SectionItemView({ confirmKnownDesktop(sessionAddress, rc) }) { + SectionItemView({ withBGApi { confirmKnownDesktop(sessionAddress, rc) } }) { Icon(painterResource(MR.images.ic_check), generalGetString(MR.strings.connect_button), tint = MaterialTheme.colors.secondary) TextIconSpaced(false) Text(generalGetString(MR.strings.connect_button)) @@ -356,7 +358,7 @@ private fun ScanDesktopAddressView(sessionAddress: MutableState) { SectionView(stringResource(MR.strings.scan_qr_code_from_desktop).uppercase()) { QRCodeScanner { text -> sessionAddress.value = text - processDesktopQRCode(sessionAddress, text) + connectDesktopAddress(sessionAddress, text) } } } @@ -398,7 +400,7 @@ private fun DesktopAddressView(sessionAddress: MutableState) { stringResource(MR.strings.connect_to_desktop), disabled = sessionAddress.value.isEmpty(), click = { - connectDesktopAddress(sessionAddress, sessionAddress.value) + withBGApi { connectDesktopAddress(sessionAddress, sessionAddress.value) } }, ) } @@ -461,10 +463,6 @@ private suspend fun updateRemoteCtrls(remoteCtrls: SnapshotStateList, resp: String) { - connectDesktopAddress(sessionAddress, resp) -} - private fun findKnownDesktop(showConnectScreen: MutableState) { withBGApi { if (controller.findKnownRemoteCtrl()) { @@ -478,45 +476,48 @@ private fun findKnownDesktop(showConnectScreen: MutableState) { } } -private fun confirmKnownDesktop(sessionAddress: MutableState, rc: RemoteCtrlInfo) { - connectDesktop(sessionAddress) { - controller.confirmRemoteCtrl(rc.remoteCtrlId) +private suspend fun confirmKnownDesktop(sessionAddress: MutableState, rc: RemoteCtrlInfo): Boolean { + return withContext(Dispatchers.Default) { + connectDesktop(sessionAddress) { + controller.confirmRemoteCtrl(rc.remoteCtrlId) + } } } -private fun connectDesktopAddress(sessionAddress: MutableState, addr: String) { - connectDesktop(sessionAddress) { - controller.connectRemoteCtrl(addr) +private suspend fun connectDesktopAddress(sessionAddress: MutableState, addr: String): Boolean { + return withContext(Dispatchers.Default) { + connectDesktop(sessionAddress) { + controller.connectRemoteCtrl(addr) + } } } -private fun connectDesktop(sessionAddress: MutableState, connect: suspend () -> Pair) { - withBGApi { - val res = connect() - if (res.first != null) { - val (rc_, ctrlAppInfo, v) = res.first!! - sessionAddress.value = "" - chatModel.remoteCtrlSession.value = RemoteCtrlSession( - ctrlAppInfo = ctrlAppInfo, - appVersion = v, - sessionState = UIRemoteCtrlSessionState.Connecting(remoteCtrl_ = rc_) - ) - } else { - val e = res.second ?: return@withBGApi - when { - e.chatError is ChatError.ChatErrorRemoteCtrl && e.chatError.remoteCtrlError is RemoteCtrlError.BadInvitation -> showBadInvitationErrorAlert() - e.chatError is ChatError.ChatErrorChat && e.chatError.errorType is ChatErrorType.CommandError -> showBadInvitationErrorAlert() - e.chatError is ChatError.ChatErrorRemoteCtrl && e.chatError.remoteCtrlError is RemoteCtrlError.BadVersion -> showBadVersionAlert(v = e.chatError.remoteCtrlError.appVersion) - e.chatError is ChatError.ChatErrorAgent && e.chatError.agentError is AgentErrorType.RCP && e.chatError.agentError.rcpErr is RCErrorType.VERSION -> showBadVersionAlert(v = null) - e.chatError is ChatError.ChatErrorAgent && e.chatError.agentError is AgentErrorType.RCP && e.chatError.agentError.rcpErr is RCErrorType.CTRL_AUTH -> showDesktopDisconnectedErrorAlert() - else -> { - val errMsg = "${e.responseType}: ${e.details}" - Log.e(TAG, "bad response: $errMsg") - AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error), errMsg) - } +private suspend fun connectDesktop(sessionAddress: MutableState, connect: suspend () -> Pair): Boolean { + val res = connect() + if (res.first != null) { + val (rc_, ctrlAppInfo, v) = res.first!! + sessionAddress.value = "" + chatModel.remoteCtrlSession.value = RemoteCtrlSession( + ctrlAppInfo = ctrlAppInfo, + appVersion = v, + sessionState = UIRemoteCtrlSessionState.Connecting(remoteCtrl_ = rc_) + ) + } else { + val e = res.second ?: return false + when { + e.chatError is ChatError.ChatErrorRemoteCtrl && e.chatError.remoteCtrlError is RemoteCtrlError.BadInvitation -> showBadInvitationErrorAlert() + e.chatError is ChatError.ChatErrorChat && e.chatError.errorType is ChatErrorType.CommandError -> showBadInvitationErrorAlert() + e.chatError is ChatError.ChatErrorRemoteCtrl && e.chatError.remoteCtrlError is RemoteCtrlError.BadVersion -> showBadVersionAlert(v = e.chatError.remoteCtrlError.appVersion) + e.chatError is ChatError.ChatErrorAgent && e.chatError.agentError is AgentErrorType.RCP && e.chatError.agentError.rcpErr is RCErrorType.VERSION -> showBadVersionAlert(v = null) + e.chatError is ChatError.ChatErrorAgent && e.chatError.agentError is AgentErrorType.RCP && e.chatError.agentError.rcpErr is RCErrorType.CTRL_AUTH -> showDesktopDisconnectedErrorAlert() + else -> { + val errMsg = "${e.responseType}: ${e.details}" + Log.e(TAG, "bad response: $errMsg") + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error), errMsg) } } } + return res.first != null } private fun verifyDesktopSessionCode(remoteCtrls: SnapshotStateList, sessCode: String) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ScanProtocolServer.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ScanProtocolServer.kt index 56f16d4eb1..d280773976 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ScanProtocolServer.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ScanProtocolServer.kt @@ -26,6 +26,7 @@ fun ScanProtocolServerLayout(rhId: Long?, onNext: (UserServer) -> Unit) { text = generalGetString(MR.strings.smp_servers_check_address) ) } + res != null } } } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/newchat/QRCodeScanner.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/newchat/QRCodeScanner.desktop.kt index 0142afb4ac..6ae35e1a41 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/newchat/QRCodeScanner.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/newchat/QRCodeScanner.desktop.kt @@ -7,7 +7,7 @@ import androidx.compose.runtime.* actual fun QRCodeScanner( showQRCodeScanner: MutableState, padding: PaddingValues, - onBarcode: (String) -> Unit + onBarcode: suspend (String) -> Boolean ) { //LALAL } From 97cd2682d77980849f91a19ffea213952ca51111 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Thu, 5 Dec 2024 20:10:44 +0400 Subject: [PATCH 132/167] core: take address lock before reading contact request data (to prevent possible race condition if user quickly accepts request several times in a row); android, desktop: show error context in agent CMD errors (#5324) --- .../kotlin/chat/simplex/common/model/SimpleXAPI.kt | 4 ++-- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- src/Simplex/Chat.hs | 12 +++++++----- src/Simplex/Chat/Store/Direct.hs | 6 ++++++ 5 files changed, 17 insertions(+), 9 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index bd9c3ddc7b..757d80193c 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -6498,7 +6498,7 @@ sealed class SQLiteError { @Serializable sealed class AgentErrorType { val string: String get() = when (this) { - is CMD -> "CMD ${cmdErr.string}" + is CMD -> "CMD ${cmdErr.string} $errContext" is CONN -> "CONN ${connErr.string}" is SMP -> "SMP ${smpErr.string}" // is NTF -> "NTF ${ntfErr.string}" @@ -6511,7 +6511,7 @@ sealed class AgentErrorType { is CRITICAL -> "CRITICAL $offerRestart $criticalErr" is INACTIVE -> "INACTIVE" } - @Serializable @SerialName("CMD") class CMD(val cmdErr: CommandErrorType): AgentErrorType() + @Serializable @SerialName("CMD") class CMD(val cmdErr: CommandErrorType, val errContext: String): AgentErrorType() @Serializable @SerialName("CONN") class CONN(val connErr: ConnectionErrorType): AgentErrorType() @Serializable @SerialName("SMP") class SMP(val serverAddress: String, val smpErr: SMPErrorType): AgentErrorType() // @Serializable @SerialName("NTF") class NTF(val ntfErr: SMPErrorType): AgentErrorType() diff --git a/cabal.project b/cabal.project index da9c71587f..3212768506 100644 --- a/cabal.project +++ b/cabal.project @@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 966b9990e0bf5fdb701f79b6efd722baddd1ee1d + tag: 9893935e7c3cf8d102c85730a4e48d32f05c2ec7 source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 3eec88a25b..098851e9ff 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."966b9990e0bf5fdb701f79b6efd722baddd1ee1d" = "0gmycrmyrgy5wbhr3f7qy6hbpppsamfypq7y650dinpbqyrfs9fb"; + "https://github.com/simplex-chat/simplexmq.git"."9893935e7c3cf8d102c85730a4e48d32f05c2ec7" = "1bpgsdnmk8fml6ad9bjbvyichvd0kq0nqj562xyy5y1npymaxpyn"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 516d033ebf..af730e4c06 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -1423,8 +1423,9 @@ processChatCommand' vr = \case CTContactConnection -> pure $ chatCmdError (Just user) "not supported" CTContactRequest -> pure $ chatCmdError (Just user) "not supported" APIAcceptContact incognito connReqId -> withUser $ \_ -> do - (user@User {userId}, cReq@UserContactRequest {userContactLinkId}) <- withFastStore $ \db -> getContactRequest' db connReqId + userContactLinkId <- withFastStore $ \db -> getUserContactLinkIdByCReq db connReqId withUserContactLock "acceptContact" userContactLinkId $ do + (user@User {userId}, cReq) <- withFastStore $ \db -> getContactRequest' db connReqId (ct, conn@Connection {connId}, sqSecured) <- acceptContactRequest user cReq incognito ucl <- withFastStore $ \db -> getUserContactLinkById db userId userContactLinkId let contactUsed = (\(_, groupId_, _) -> isNothing groupId_) ucl @@ -1438,11 +1439,12 @@ processChatCommand' vr = \case pure ct {contactUsed, activeConn = Just conn'} pure $ CRAcceptingContactRequest user ct' APIRejectContact connReqId -> withUser $ \user -> do - cReq@UserContactRequest {userContactLinkId, agentContactConnId = AgentConnId connId, agentInvitationId = AgentInvId invId} <- - withFastStore $ \db -> - getContactRequest db user connReqId - `storeFinally` liftIO (deleteContactRequest db user connReqId) + userContactLinkId <- withFastStore $ \db -> getUserContactLinkIdByCReq db connReqId withUserContactLock "rejectContact" userContactLinkId $ do + cReq@UserContactRequest {agentContactConnId = AgentConnId connId, agentInvitationId = AgentInvId invId} <- + withFastStore $ \db -> + getContactRequest db user connReqId + `storeFinally` liftIO (deleteContactRequest db user connReqId) withAgent $ \a -> rejectContact a connId invId pure $ CRContactRequestRejected user cReq APISendCallInvitation contactId callType -> withUser $ \user -> do diff --git a/src/Simplex/Chat/Store/Direct.hs b/src/Simplex/Chat/Store/Direct.hs index b7759f0905..d5396a0fef 100644 --- a/src/Simplex/Chat/Store/Direct.hs +++ b/src/Simplex/Chat/Store/Direct.hs @@ -57,6 +57,7 @@ module Simplex.Chat.Store.Direct setQuotaErrCounter, getUserContacts, createOrUpdateContactRequest, + getUserContactLinkIdByCReq, getContactRequest', getContactRequest, getContactRequestIdByName, @@ -727,6 +728,11 @@ createOrUpdateContactRequest db vr user@User {userId, userContactId} userContact |] (displayName, fullName, image, contactLink, currentTs, userId, cReqId) +getUserContactLinkIdByCReq :: DB.Connection -> Int64 -> ExceptT StoreError IO Int64 +getUserContactLinkIdByCReq db contactRequestId = + ExceptT . firstRow fromOnly (SEContactRequestNotFound contactRequestId) $ + DB.query db "SELECT user_contact_link_id FROM contact_requests WHERE contact_request_id = ?" (Only contactRequestId) + getContactRequest' :: DB.Connection -> Int64 -> ExceptT StoreError IO (User, UserContactRequest) getContactRequest' db contactRequestId = do user <- getUserByContactRequestId db contactRequestId From 5f66c29dbdbde40fe2f36782fea24eb77576f978 Mon Sep 17 00:00:00 2001 From: Diogo Date: Thu, 5 Dec 2024 16:15:24 +0000 Subject: [PATCH 133/167] ios: fix open from notification and connected directly chat item chat loading (#5326) * ios: fix opening from notification and connected directly chat item chat loading * better fix --- apps/ios/Shared/Model/NtfManager.swift | 6 +++++- apps/ios/Shared/SimpleXApp.swift | 3 ++- .../Views/Chat/ChatItem/CIMemberCreatedContactView.swift | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/apps/ios/Shared/Model/NtfManager.swift b/apps/ios/Shared/Model/NtfManager.swift index b2fa6a0200..6c33031eeb 100644 --- a/apps/ios/Shared/Model/NtfManager.swift +++ b/apps/ios/Shared/Model/NtfManager.swift @@ -26,6 +26,7 @@ enum NtfCallAction { class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject { static let shared = NtfManager() + public var navigatingToChat = false private var granted = false private var prevNtfTime: Dictionary = [:] @@ -74,7 +75,10 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject { } } else { if let chatId = content.targetContentIdentifier { - ItemsModel.shared.loadOpenChat(chatId) + self.navigatingToChat = true + ItemsModel.shared.loadOpenChat(chatId) { + self.navigatingToChat = false + } } } } diff --git a/apps/ios/Shared/SimpleXApp.swift b/apps/ios/Shared/SimpleXApp.swift index 0dd54782b4..10120db185 100644 --- a/apps/ios/Shared/SimpleXApp.swift +++ b/apps/ios/Shared/SimpleXApp.swift @@ -143,7 +143,8 @@ struct SimpleXApp: App { let chats = try await apiGetChatsAsync() await MainActor.run { chatModel.updateChats(chats) } if let id = chatModel.chatId, - let chat = chatModel.getChat(id) { + let chat = chatModel.getChat(id), + !NtfManager.shared.navigatingToChat { Task { await loadChat(chat: chat, clearItems: false) } } if let ncr = chatModel.ntfContactRequest { diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIMemberCreatedContactView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIMemberCreatedContactView.swift index e9cd838234..d24c737907 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIMemberCreatedContactView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIMemberCreatedContactView.swift @@ -23,7 +23,7 @@ struct CIMemberCreatedContactView: View { .onTapGesture { dismissAllSheets(animated: true) DispatchQueue.main.async { - m.chatId = "@\(contactId)" + ItemsModel.shared.loadOpenChat("@\(contactId)") } } } else { From de76e271a85e9e358a36ca8ce602a5961addf237 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Fri, 6 Dec 2024 01:10:36 +0700 Subject: [PATCH 134/167] android, desktop: displaying deleted message with file in chat list (#5329) --- .../chat/simplex/common/views/chatlist/ChatPreviewView.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt index 036768c6e7..0e0c3e74f4 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt @@ -347,7 +347,7 @@ fun ChatPreviewView( chatItemContentPreview(chat, ci) } if (mc !is MsgContent.MCVoice || !showContentPreview || mc.text.isNotEmpty() || chatModelDraftChatId == chat.id) { - Box(Modifier.offset(x = if (mc is MsgContent.MCFile) -15.sp.toDp() else 0.dp)) { + Box(Modifier.offset(x = if (mc is MsgContent.MCFile && ci.meta.itemDeleted == null) -15.sp.toDp() else 0.dp)) { chatPreviewText() } } From 60e0e454e8b0dcf3fe43c926e160cc39059801d9 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Thu, 5 Dec 2024 18:32:00 +0000 Subject: [PATCH 135/167] core: only update business chat preferences (#5325) * core: only update business chat preferences * rework * migration * test, bump protocol version * fix tests --- simplex-chat.cabal | 1 + src/Simplex/Chat.hs | 71 +++++++++++++------ .../M20241205_business_chat_members.hs | 18 +++++ src/Simplex/Chat/Migrations/chat_schema.sql | 3 +- src/Simplex/Chat/Protocol.hs | 16 ++++- src/Simplex/Chat/Store/Connections.hs | 2 +- src/Simplex/Chat/Store/Groups.hs | 52 +++++++++----- src/Simplex/Chat/Store/Migrations.hs | 4 +- src/Simplex/Chat/Store/Shared.hs | 14 ++-- src/Simplex/Chat/Types.hs | 9 +-- tests/ChatTests/Profiles.hs | 59 +++++++++++++-- tests/ProtocolTests.hs | 12 ++-- 12 files changed, 201 insertions(+), 60 deletions(-) create mode 100644 src/Simplex/Chat/Migrations/M20241205_business_chat_members.hs diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 92eea030c0..e23305faf9 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -154,6 +154,7 @@ library Simplex.Chat.Migrations.M20241027_server_operators Simplex.Chat.Migrations.M20241125_indexes Simplex.Chat.Migrations.M20241128_business_chats + Simplex.Chat.Migrations.M20241205_business_chat_members Simplex.Chat.Mobile Simplex.Chat.Mobile.File Simplex.Chat.Mobile.Shared diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index af730e4c06..d5ad68079f 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -2929,11 +2929,22 @@ processChatCommand' vr = \case lift . when (directOrUsed ct') $ createSndFeatureItems user ct ct' pure $ CRContactPrefsUpdated user ct ct' runUpdateGroupProfile :: User -> Group -> GroupProfile -> CM ChatResponse - runUpdateGroupProfile user (Group g@GroupInfo {groupProfile = p@GroupProfile {displayName = n}} ms) p'@GroupProfile {displayName = n'} = do + runUpdateGroupProfile user (Group g@GroupInfo {businessChat, groupProfile = p@GroupProfile {displayName = n}} ms) p'@GroupProfile {displayName = n'} = do assertUserGroupRole g GROwner when (n /= n') $ checkValidName n' g' <- withStore $ \db -> updateGroupProfile db user g p' - msg <- sendGroupMessage user g' ms (XGrpInfo p') + msg <- case businessChat of + Just BusinessChatInfo {businessId} -> do + let (newMs, oldMs) = partition (\m -> maxVersion (memberChatVRange m) >= businessChatPrefsVersion) ms + -- this is a fallback to send the members with the old version correct profile of the business when preferences change + unless (null oldMs) $ do + GroupMember {memberProfile = LocalProfile {displayName, fullName, image}} <- + withStore $ \db -> getGroupMemberByMemberId db vr user g businessId + let p'' = p' {displayName, fullName, image} :: GroupProfile + void $ sendGroupMessage user g' oldMs (XGrpInfo p'') + let ps' = fromMaybe defaultBusinessGroupPrefs $ groupPreferences p' + sendGroupMessage user g' newMs $ XGrpPrefs ps' + Nothing -> sendGroupMessage user g' ms (XGrpInfo p') let cd = CDGroupSnd g' unless (sameGroupProfileInfo p p') $ do ci <- saveSndChatItem user cd msg (CISndGroupEvent $ SGEGroupUpdated p') @@ -3026,7 +3037,7 @@ processChatCommand' vr = \case invitedMember = MemberIdRole memberId memRole, connRequest = cReq, groupProfile, - businessChat, + business = businessChat, groupLinkId = Nothing, groupSize = Just currentMemCount } @@ -4003,7 +4014,7 @@ acceptGroupJoinRequestAsync fromMemberName = displayName, invitedMember = MemberIdRole memberId gLinkMemRole, groupProfile, - businessChat, + business = businessChat, groupSize = Just currentMemCount } subMode <- chatReadVar subscriptionMode @@ -4038,7 +4049,7 @@ acceptBusinessJoinRequestAsync -- This refers to the "title member" that defines the group name and profile. -- This coincides with fromMember to be current user when accepting the connecting user, -- but it will be different when inviting somebody else. - businessChat = Just $ BusinessChatInfo userMemberId BCBusiness, + business = Just $ BusinessChatInfo {chatType = BCBusiness, businessId = userMemberId, customerId = memberId}, groupSize = Just 1 } subMode <- chatReadVar subscriptionMode @@ -5042,7 +5053,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = invitedMember = MemberIdRole memberId memRole, connRequest = cReq, groupProfile, - businessChat = Nothing, + business = Nothing, groupLinkId = groupLinkId, groupSize = Just currentMemCount } @@ -5299,6 +5310,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = XGrpLeave -> xGrpLeave gInfo m' msg brokerTs XGrpDel -> xGrpDel gInfo m' msg brokerTs XGrpInfo p' -> xGrpInfo gInfo m' p' msg brokerTs + XGrpPrefs ps' -> xGrpPrefs gInfo m' ps' XGrpDirectInv connReq mContent_ -> memberCanSend m' $ xGrpDirectInv gInfo m' conn' connReq mContent_ msg brokerTs XGrpMsgForward memberId msg' msgTs -> xGrpMsgForward gInfo m' memberId msg' msgTs XInfoProbe probe -> xInfoProbe (COMGroupMember m') probe @@ -5416,8 +5428,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = let GroupInfo {businessChat} = gInfo GroupMember {memberId = joiningMemberId} = m case businessChat of - Just BusinessChatInfo {memberId, chatType = BCCustomer} - | joiningMemberId == memberId -> useReply <$> withStore (`getUserAddress` user) + Just BusinessChatInfo {customerId, chatType = BCCustomer} + | joiningMemberId == customerId -> useReply <$> withStore (`getUserAddress` user) where useReply UserContactLink {autoAccept} = case autoAccept of Just AutoAccept {businessAddress, autoReply} | businessAddress -> autoReply @@ -6465,7 +6477,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = processMemberProfileUpdate :: GroupInfo -> GroupMember -> Profile -> Bool -> Maybe UTCTime -> CM GroupMember processMemberProfileUpdate gInfo m@GroupMember {memberProfile = p, memberContactId} p' createItems itemTs_ | redactedMemberProfile (fromLocalProfile p) /= redactedMemberProfile p' = do - updateBusinessChatProfile gInfo m + updateBusinessChatProfile gInfo case memberContactId of Nothing -> do m' <- withStore $ \db -> updateMemberProfile db user m p' @@ -6491,11 +6503,14 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = | otherwise = pure m where - updateBusinessChatProfile g@GroupInfo {businessChat} GroupMember {memberId} = case businessChat of - Just BusinessChatInfo {memberId = mId} | mId == memberId -> do + updateBusinessChatProfile g@GroupInfo {businessChat} = case businessChat of + Just bc | isMainBusinessMember bc m -> do g' <- withStore $ \db -> updateGroupProfileFromMember db user g p' toView $ CRGroupUpdated user g g' (Just m) _ -> pure () + isMainBusinessMember BusinessChatInfo {chatType, businessId, customerId} GroupMember {memberId} = case chatType of + BCBusiness -> businessId == memberId + BCCustomer -> customerId == memberId createProfileUpdatedItem m' = when createItems $ do let ciContent = CIRcvGroupEvent $ RGEMemberProfileUpdated (fromLocalProfile p) p' @@ -7008,16 +7023,31 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = toView $ CRGroupDeleted user gInfo {membership = membership {memberStatus = GSMemGroupDeleted}} m xGrpInfo :: GroupInfo -> GroupMember -> GroupProfile -> RcvMessage -> UTCTime -> CM () - xGrpInfo g@GroupInfo {groupProfile = p} m@GroupMember {memberRole} p' msg brokerTs + xGrpInfo g@GroupInfo {groupProfile = p, businessChat} m@GroupMember {memberRole} p' msg brokerTs | memberRole < GROwner = messageError "x.grp.info with insufficient member permissions" - | otherwise = unless (p == p') $ do - g' <- withStore $ \db -> updateGroupProfile db user g p' - toView $ CRGroupUpdated user g g' (Just m) - let cd = CDGroupRcv g' m - unless (sameGroupProfileInfo p p') $ do - ci <- saveRcvChatItem user cd msg brokerTs (CIRcvGroupEvent $ RGEGroupUpdated p') - groupMsgToView g' ci - createGroupFeatureChangedItems user cd CIRcvGroupFeature g g' + | otherwise = case businessChat of + Nothing -> unless (p == p') $ do + g' <- withStore $ \db -> updateGroupProfile db user g p' + toView $ CRGroupUpdated user g g' (Just m) + let cd = CDGroupRcv g' m + unless (sameGroupProfileInfo p p') $ do + ci <- saveRcvChatItem user cd msg brokerTs (CIRcvGroupEvent $ RGEGroupUpdated p') + groupMsgToView g' ci + createGroupFeatureChangedItems user cd CIRcvGroupFeature g g' + Just _ -> updateGroupPrefs_ g m $ fromMaybe defaultBusinessGroupPrefs $ groupPreferences p' + + xGrpPrefs :: GroupInfo -> GroupMember -> GroupPreferences -> CM () + xGrpPrefs g m@GroupMember {memberRole} ps' + | memberRole < GROwner = messageError "x.grp.prefs with insufficient member permissions" + | otherwise = updateGroupPrefs_ g m ps' + + updateGroupPrefs_ :: GroupInfo -> GroupMember -> GroupPreferences -> CM () + updateGroupPrefs_ g@GroupInfo {groupProfile = p} m ps' = + unless (groupPreferences p == Just ps') $ do + g' <- withStore' $ \db -> updateGroupPreferences db user g ps' + toView $ CRGroupUpdated user g g' (Just m) + let cd = CDGroupRcv g' m + createGroupFeatureChangedItems user cd CIRcvGroupFeature g g' xGrpDirectInv :: GroupInfo -> GroupMember -> Connection -> ConnReqInvitation -> Maybe MsgContent -> RcvMessage -> UTCTime -> CM () xGrpDirectInv g m mConn connReq mContent_ msg brokerTs = do @@ -7098,6 +7128,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = XGrpLeave -> xGrpLeave gInfo author rcvMsg msgTs XGrpDel -> xGrpDel gInfo author rcvMsg msgTs XGrpInfo p' -> xGrpInfo gInfo author p' rcvMsg msgTs + XGrpPrefs ps' -> xGrpPrefs gInfo author ps' _ -> messageError $ "x.grp.msg.forward: unsupported forwarded event " <> T.pack (show $ toCMEventTag event) createUnknownMember :: GroupInfo -> MemberId -> CM GroupMember diff --git a/src/Simplex/Chat/Migrations/M20241205_business_chat_members.hs b/src/Simplex/Chat/Migrations/M20241205_business_chat_members.hs new file mode 100644 index 0000000000..5d019d73e1 --- /dev/null +++ b/src/Simplex/Chat/Migrations/M20241205_business_chat_members.hs @@ -0,0 +1,18 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Migrations.M20241205_business_chat_members where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20241205_business_chat_members :: Query +m20241205_business_chat_members = + [sql| +ALTER TABLE groups ADD COLUMN customer_member_id BLOB NULL; +|] + +down_m20241205_business_chat_members :: Query +down_m20241205_business_chat_members = + [sql| +ALTER TABLE groups DROP COLUMN customer_member_id; +|] diff --git a/src/Simplex/Chat/Migrations/chat_schema.sql b/src/Simplex/Chat/Migrations/chat_schema.sql index 621c28f91e..94ccc65b7f 100644 --- a/src/Simplex/Chat/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Migrations/chat_schema.sql @@ -130,7 +130,8 @@ CREATE TABLE groups( ui_themes TEXT, business_member_id BLOB NULL, business_chat TEXT NULL, - business_xcontact_id BLOB NULL, -- received + business_xcontact_id BLOB NULL, + customer_member_id BLOB NULL, -- received FOREIGN KEY(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name) ON DELETE CASCADE diff --git a/src/Simplex/Chat/Protocol.hs b/src/Simplex/Chat/Protocol.hs index 8afefdc850..934f23007d 100644 --- a/src/Simplex/Chat/Protocol.hs +++ b/src/Simplex/Chat/Protocol.hs @@ -46,6 +46,7 @@ import Database.SQLite.Simple.FromField (FromField (..)) import Database.SQLite.Simple.ToField (ToField (..)) import Simplex.Chat.Call import Simplex.Chat.Types +import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Shared import Simplex.Messaging.Agent.Protocol (VersionSMPA, pqdrSMPAgentVersion) import Simplex.Messaging.Compression (Compressed, compress1, decompress1) @@ -67,12 +68,13 @@ import Simplex.Messaging.Version hiding (version) -- 8 - compress messages and PQ e2e encryption (2024-03-08) -- 9 - batch sending in direct connections (2024-07-24) -- 10 - business chats (2024-11-29) +-- 11 - fix profile update in business chats (2024-12-05) -- This should not be used directly in code, instead use `maxVersion chatVRange` from ChatConfig. -- This indirection is needed for backward/forward compatibility testing. -- Testing with real app versions is still needed, as tests use the current code with different version ranges, not the old code. currentChatVersion :: VersionChat -currentChatVersion = VersionChat 10 +currentChatVersion = VersionChat 11 -- This should not be used directly in code, instead use `chatVRange` from ChatConfig (see comment above) supportedChatVRange :: VersionRangeChat @@ -115,6 +117,10 @@ batchSend2Version = VersionChat 9 businessChatsVersion :: VersionChat businessChatsVersion = VersionChat 10 +-- support updating preferences in business chats (XGrpPrefs message) +businessChatPrefsVersion :: VersionChat +businessChatPrefsVersion = VersionChat 11 + agentToChatVersion :: VersionSMPA -> VersionChat agentToChatVersion v | v < pqdrSMPAgentVersion = initialChatVersion @@ -299,6 +305,7 @@ data ChatMsgEvent (e :: MsgEncoding) where XGrpLeave :: ChatMsgEvent 'Json XGrpDel :: ChatMsgEvent 'Json XGrpInfo :: GroupProfile -> ChatMsgEvent 'Json + XGrpPrefs :: GroupPreferences -> ChatMsgEvent 'Json XGrpDirectInv :: ConnReqInvitation -> Maybe MsgContent -> ChatMsgEvent 'Json XGrpMsgForward :: MemberId -> ChatMessage 'Json -> UTCTime -> ChatMsgEvent 'Json XInfoProbe :: Probe -> ChatMsgEvent 'Json @@ -339,6 +346,7 @@ isForwardedGroupMsg ev = case ev of XGrpLeave -> True XGrpDel -> True -- TODO there should be a special logic - host should forward before deleting connections XGrpInfo _ -> True + XGrpPrefs _ -> True _ -> False forwardedGroupMsg :: forall e. MsgEncodingI e => ChatMessage e -> Maybe (ChatMessage 'Json) @@ -721,6 +729,7 @@ data CMEventTag (e :: MsgEncoding) where XGrpLeave_ :: CMEventTag 'Json XGrpDel_ :: CMEventTag 'Json XGrpInfo_ :: CMEventTag 'Json + XGrpPrefs_ :: CMEventTag 'Json XGrpDirectInv_ :: CMEventTag 'Json XGrpMsgForward_ :: CMEventTag 'Json XInfoProbe_ :: CMEventTag 'Json @@ -771,6 +780,7 @@ instance MsgEncodingI e => StrEncoding (CMEventTag e) where XGrpLeave_ -> "x.grp.leave" XGrpDel_ -> "x.grp.del" XGrpInfo_ -> "x.grp.info" + XGrpPrefs_ -> "x.grp.prefs" XGrpDirectInv_ -> "x.grp.direct.inv" XGrpMsgForward_ -> "x.grp.msg.forward" XInfoProbe_ -> "x.info.probe" @@ -822,6 +832,7 @@ instance StrEncoding ACMEventTag where "x.grp.leave" -> XGrpLeave_ "x.grp.del" -> XGrpDel_ "x.grp.info" -> XGrpInfo_ + "x.grp.prefs" -> XGrpPrefs_ "x.grp.direct.inv" -> XGrpDirectInv_ "x.grp.msg.forward" -> XGrpMsgForward_ "x.info.probe" -> XInfoProbe_ @@ -869,6 +880,7 @@ toCMEventTag msg = case msg of XGrpLeave -> XGrpLeave_ XGrpDel -> XGrpDel_ XGrpInfo _ -> XGrpInfo_ + XGrpPrefs _ -> XGrpPrefs_ XGrpDirectInv _ _ -> XGrpDirectInv_ XGrpMsgForward {} -> XGrpMsgForward_ XInfoProbe _ -> XInfoProbe_ @@ -969,6 +981,7 @@ appJsonToCM AppMessageJson {v, msgId, event, params} = do XGrpLeave_ -> pure XGrpLeave XGrpDel_ -> pure XGrpDel XGrpInfo_ -> XGrpInfo <$> p "groupProfile" + XGrpPrefs_ -> XGrpPrefs <$> p "groupPreferences" XGrpDirectInv_ -> XGrpDirectInv <$> p "connReq" <*> opt "content" XGrpMsgForward_ -> XGrpMsgForward <$> p "memberId" <*> p "msg" <*> p "msgTs" XInfoProbe_ -> XInfoProbe <$> p "probe" @@ -1030,6 +1043,7 @@ chatToAppMessage ChatMessage {chatVRange, msgId, chatMsgEvent} = case encoding @ XGrpLeave -> JM.empty XGrpDel -> JM.empty XGrpInfo p -> o ["groupProfile" .= p] + XGrpPrefs p -> o ["groupPreferences" .= p] XGrpDirectInv connReq content -> o $ ("content" .=? content) ["connReq" .= connReq] XGrpMsgForward memberId msg msgTs -> o ["memberId" .= memberId, "msg" .= msg, "msgTs" .= msgTs] XInfoProbe probe -> o ["probe" .= probe] diff --git a/src/Simplex/Chat/Store/Connections.hs b/src/Simplex/Chat/Store/Connections.hs index fe52c6d7b7..db787b0112 100644 --- a/src/Simplex/Chat/Store/Connections.hs +++ b/src/Simplex/Chat/Store/Connections.hs @@ -123,7 +123,7 @@ getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do -- GroupInfo g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, - g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_member_id, g.business_chat, g.ui_themes, g.custom_data, + g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, -- GroupInfo {membership} mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index 895acf0a7d..49158a60c9 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -39,6 +39,7 @@ module Simplex.Chat.Store.Groups getGroupInfoByUserContactLinkConnReq, getGroupInfoByGroupLinkHash, updateGroupProfile, + updateGroupPreferences, updateGroupProfileFromMember, getGroupIdByName, getGroupMemberIdByName, @@ -257,7 +258,7 @@ getGroupAndMember db User {userId, userContactId} groupMemberId vr = -- GroupInfo g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, - g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_member_id, g.business_chat, g.ui_themes, g.custom_data, + g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, -- GroupInfo {membership} mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, @@ -339,7 +340,7 @@ createNewGroup db vr gVar user@User {userId} groupProfile incognitoProfile = Exc -- | creates a new group record for the group the current user was invited to, or returns an existing one createGroupInvitation :: DB.Connection -> VersionRangeChat -> User -> Contact -> GroupInvitation -> Maybe ProfileId -> ExceptT StoreError IO (GroupInfo, GroupMemberId) createGroupInvitation _ _ _ Contact {localDisplayName, activeConn = Nothing} _ _ = throwError $ SEContactNotReady localDisplayName -createGroupInvitation db vr user@User {userId} contact@Contact {contactId, activeConn = Just Connection {customUserProfileId, peerChatVRange}} GroupInvitation {fromMember, invitedMember, connRequest, groupProfile, businessChat} incognitoProfileId = do +createGroupInvitation db vr user@User {userId} contact@Contact {contactId, activeConn = Just Connection {customUserProfileId, peerChatVRange}} GroupInvitation {fromMember, invitedMember, connRequest, groupProfile, business} incognitoProfileId = do liftIO getInvitationGroupId_ >>= \case Nothing -> createGroupInvitation_ Just gId -> do @@ -377,10 +378,10 @@ createGroupInvitation db vr user@User {userId} contact@Contact {contactId, activ [sql| INSERT INTO groups (group_profile_id, local_display_name, inv_queue_info, host_conn_custom_user_profile_id, user_id, enable_ntfs, - created_at, updated_at, chat_ts, user_member_profile_sent_at, business_member_id, business_chat) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?) + created_at, updated_at, chat_ts, user_member_profile_sent_at, business_chat, business_member_id, customer_member_id) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) |] - ((profileId, localDisplayName, connRequest, customUserProfileId, userId, True, currentTs, currentTs, currentTs, currentTs) :. businessChatTuple businessChat) + ((profileId, localDisplayName, connRequest, customUserProfileId, userId, True, currentTs, currentTs, currentTs, currentTs) :. businessChatInfoRow business) insertedRowId db let hostVRange = adjustedMemberVRange vr peerChatVRange GroupMember {groupMemberId} <- createContactMemberInv_ db user groupId Nothing contact fromMember GCHostMember GSMemInvited IBUnknown Nothing currentTs hostVRange @@ -406,10 +407,10 @@ createGroupInvitation db vr user@User {userId} contact@Contact {contactId, activ groupMemberId ) -businessChatTuple :: Maybe BusinessChatInfo -> (Maybe MemberId, Maybe BusinessChatType) -businessChatTuple = \case - Just BusinessChatInfo {memberId, chatType} -> (Just memberId, Just chatType) - Nothing -> (Nothing, Nothing) +businessChatInfoRow :: Maybe BusinessChatInfo -> BusinessChatInfoRow +businessChatInfoRow = \case + Just BusinessChatInfo {chatType, businessId, customerId} -> (Just chatType, Just businessId, Just customerId) + Nothing -> (Nothing, Nothing, Nothing) adjustedMemberVRange :: VersionRangeChat -> VersionRangeChat -> VersionRangeChat adjustedMemberVRange chatVR vr@(VersionRange minV maxV) = @@ -497,7 +498,7 @@ createGroupInvitedViaLink vr user@User {userId, userContactId} Connection {connId, customUserProfileId} - GroupLinkInvitation {fromMember, fromMemberName, invitedMember, groupProfile, businessChat} = do + GroupLinkInvitation {fromMember, fromMemberName, invitedMember, groupProfile, business} = do currentTs <- liftIO getCurrentTime groupId <- insertGroup_ currentTs hostMemberId <- insertHost_ currentTs groupId @@ -521,10 +522,10 @@ createGroupInvitedViaLink [sql| INSERT INTO groups (group_profile_id, local_display_name, host_conn_custom_user_profile_id, user_id, enable_ntfs, - created_at, updated_at, chat_ts, user_member_profile_sent_at, business_member_id, business_chat) - VALUES (?,?,?,?,?,?,?,?,?,?,?) + created_at, updated_at, chat_ts, user_member_profile_sent_at, business_chat, business_member_id, customer_member_id) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?) |] - ((profileId, localDisplayName, customUserProfileId, userId, True, currentTs, currentTs, currentTs, currentTs) :. businessChatTuple businessChat) + ((profileId, localDisplayName, customUserProfileId, userId, True, currentTs, currentTs, currentTs, currentTs) :. businessChatInfoRow business) insertedRowId db insertHost_ currentTs groupId = do let fromMemberProfile = profileFromName fromMemberName @@ -631,7 +632,7 @@ getUserGroupDetails db vr User {userId, userContactId} _contactId_ search_ = SELECT g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, - g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_member_id, g.business_chat, g.ui_themes, g.custom_data, + g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, mu.group_member_id, g.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences FROM groups g @@ -918,9 +919,9 @@ createBusinessRequestGroup UserContactRequest {cReqChatVRange, xContactId, profile = Profile {displayName, fullName, image, contactLink, preferences}} groupPreferences = do currentTs <- liftIO getCurrentTime - (groupId, membership) <- insertGroup_ currentTs + (groupId, membership@GroupMember {memberId = userMemberId}) <- insertGroup_ currentTs (groupMemberId, memberId) <- insertClientMember_ currentTs groupId membership - liftIO $ DB.execute db "UPDATE groups SET business_member_id = ? WHERE group_id = ?" (memberId, groupId) + liftIO $ DB.execute db "UPDATE groups SET business_member_id = ?, customer_member_id = ? WHERE group_id = ?" (userMemberId, memberId, groupId) groupInfo <- getGroupInfo db vr user groupId clientMember <- getGroupMemberById db vr user groupMemberId pure (groupInfo, clientMember) @@ -1370,7 +1371,7 @@ getViaGroupMember db vr User {userId, userContactId} Contact {contactId} = -- GroupInfo g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, - g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_member_id, g.business_chat, g.ui_themes, g.custom_data, + g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, -- GroupInfo {membership} mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, @@ -1456,6 +1457,23 @@ updateGroupProfile db user@User {userId} g@GroupInfo {groupId, localDisplayName, (ldn, currentTs, userId, groupId) safeDeleteLDN db user localDisplayName +updateGroupPreferences :: DB.Connection -> User -> GroupInfo -> GroupPreferences -> IO GroupInfo +updateGroupPreferences db User {userId} g@GroupInfo {groupId, groupProfile = p} ps = do + currentTs <- getCurrentTime + DB.execute + db + [sql| + UPDATE group_profiles + SET preferences = ?, updated_at = ? + WHERE group_profile_id IN ( + SELECT group_profile_id + FROM groups + WHERE user_id = ? AND group_id = ? + ) + |] + (ps, currentTs, userId, groupId) + pure (g :: GroupInfo) {groupProfile = p {groupPreferences = Just ps}} + updateGroupProfileFromMember :: DB.Connection -> User -> GroupInfo -> Profile -> ExceptT StoreError IO GroupInfo updateGroupProfileFromMember db user g@GroupInfo {groupId} Profile {displayName = n, fullName = fn, image = img} = do p <- getGroupProfile -- to avoid any race conditions with UI diff --git a/src/Simplex/Chat/Store/Migrations.hs b/src/Simplex/Chat/Store/Migrations.hs index 6654dec034..65fe8223fe 100644 --- a/src/Simplex/Chat/Store/Migrations.hs +++ b/src/Simplex/Chat/Store/Migrations.hs @@ -118,6 +118,7 @@ import Simplex.Chat.Migrations.M20241023_chat_item_autoincrement_id import Simplex.Chat.Migrations.M20241027_server_operators import Simplex.Chat.Migrations.M20241125_indexes import Simplex.Chat.Migrations.M20241128_business_chats +import Simplex.Chat.Migrations.M20241205_business_chat_members import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..)) schemaMigrations :: [(String, Query, Maybe Query)] @@ -235,7 +236,8 @@ schemaMigrations = ("20241023_chat_item_autoincrement_id", m20241023_chat_item_autoincrement_id, Just down_m20241023_chat_item_autoincrement_id), ("20241027_server_operators", m20241027_server_operators, Just down_m20241027_server_operators), ("20241125_indexes", m20241125_indexes, Just down_m20241125_indexes), - ("20241128_business_chats", m20241128_business_chats, Just down_m20241128_business_chats) + ("20241128_business_chats", m20241128_business_chats, Just down_m20241128_business_chats), + ("20241205_business_chat_members", m20241205_business_chat_members, Just down_m20241205_business_chat_members) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs index b00ba5705f..851078ec1f 100644 --- a/src/Simplex/Chat/Store/Shared.hs +++ b/src/Simplex/Chat/Store/Shared.hs @@ -546,17 +546,19 @@ safeDeleteLDN db User {userId} localDisplayName = do |] (userId, localDisplayName, userId) -type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe Text, Maybe ImageData, Maybe ProfileId, Maybe MsgFilter, Maybe Bool, Bool, Maybe GroupPreferences) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime, Maybe MemberId, Maybe BusinessChatType, Maybe UIThemeEntityOverrides, Maybe CustomData) :. GroupMemberRow +type BusinessChatInfoRow = (Maybe BusinessChatType, Maybe MemberId, Maybe MemberId) + +type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe Text, Maybe ImageData, Maybe ProfileId, Maybe MsgFilter, Maybe Bool, Bool, Maybe GroupPreferences) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime) :. BusinessChatInfoRow :. (Maybe UIThemeEntityOverrides, Maybe CustomData) :. GroupMemberRow type GroupMemberRow = ((Int64, Int64, MemberId, VersionChat, VersionChat, GroupMemberRole, GroupMemberCategory, GroupMemberStatus, Bool, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, ContactName, Maybe ContactId, ProfileId, ProfileId, ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Maybe Preferences)) toGroupInfo :: VersionRangeChat -> Int64 -> GroupInfoRow -> GroupInfo -toGroupInfo vr userContactId ((groupId, localDisplayName, displayName, fullName, description, image, hostConnCustomUserProfileId, enableNtfs_, sendRcpts, favorite, groupPreferences) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt, businessMemberId, businessChatType, uiThemes, customData) :. userMemberRow) = +toGroupInfo vr userContactId ((groupId, localDisplayName, displayName, fullName, description, image, hostConnCustomUserProfileId, enableNtfs_, sendRcpts, favorite, groupPreferences) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt) :. businessRow :. (uiThemes, customData) :. userMemberRow) = let membership = (toGroupMember userContactId userMemberRow) {memberChatVRange = vr} chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts, favorite} fullGroupPreferences = mergeGroupPreferences groupPreferences groupProfile = GroupProfile {displayName, fullName, description, image, groupPreferences} - businessChat = BusinessChatInfo <$> businessMemberId <*> businessChatType + businessChat = toBusinessChatInfo businessRow in GroupInfo {groupId, localDisplayName, groupProfile, businessChat, fullGroupPreferences, membership, hostConnCustomUserProfileId, chatSettings, createdAt, updatedAt, chatTs, userMemberProfileSentAt, uiThemes, customData} toGroupMember :: Int64 -> GroupMemberRow -> GroupMember @@ -569,6 +571,10 @@ toGroupMember userContactId ((groupMemberId, groupId, memberId, minVer, maxVer, memberChatVRange = fromMaybe (versionToRange maxVer) $ safeVersionRange minVer maxVer in GroupMember {..} +toBusinessChatInfo :: BusinessChatInfoRow -> Maybe BusinessChatInfo +toBusinessChatInfo (Just chatType, Just businessId, Just customerId) = Just BusinessChatInfo {chatType, businessId, customerId} +toBusinessChatInfo _ = Nothing + groupInfoQuery :: Query groupInfoQuery = [sql| @@ -576,7 +582,7 @@ groupInfoQuery = -- GroupInfo g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, - g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_member_id, g.business_chat, g.ui_themes, g.custom_data, + g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, -- GroupMember - membership mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index ec8f546d2f..77a02a4bc1 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -617,7 +617,7 @@ data GroupInvitation = GroupInvitation invitedMember :: MemberIdRole, connRequest :: ConnReqInvitation, groupProfile :: GroupProfile, - businessChat :: Maybe BusinessChatInfo, + business :: Maybe BusinessChatInfo, groupLinkId :: Maybe GroupLinkId, groupSize :: Maybe Int } @@ -628,7 +628,7 @@ data GroupLinkInvitation = GroupLinkInvitation fromMemberName :: ContactName, invitedMember :: MemberIdRole, groupProfile :: GroupProfile, - businessChat :: Maybe BusinessChatInfo, + business :: Maybe BusinessChatInfo, groupSize :: Maybe Int } deriving (Eq, Show) @@ -654,8 +654,9 @@ data MemberInfo = MemberInfo deriving (Eq, Show) data BusinessChatInfo = BusinessChatInfo - { memberId :: MemberId, - chatType :: BusinessChatType + { chatType :: BusinessChatType, + businessId :: MemberId, + customerId :: MemberId } deriving (Eq, Show) diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index ebfeecdbca..2bf157419c 100644 --- a/tests/ChatTests/Profiles.hs +++ b/tests/ChatTests/Profiles.hs @@ -734,8 +734,8 @@ testBusinessAddress = testChat3 businessProfile aliceProfile {fullName = "Alice (biz <# "#bob bob_1> hey there") testBusinessUpdateProfiles :: HasCallStack => FilePath -> IO () -testBusinessUpdateProfiles = testChat3 businessProfile aliceProfile bobProfile $ - \biz alice bob -> do +testBusinessUpdateProfiles = testChat4 businessProfile aliceProfile bobProfile cathProfile $ + \biz alice bob cath -> do biz ##> "/ad" cLink <- getContactLink biz True biz ##> "/auto_accept on business text Welcome" @@ -794,9 +794,37 @@ testBusinessUpdateProfiles = testChat3 businessProfile aliceProfile bobProfile $ bob #> "#biz hi there" -- profile update is sent to group with message alice <# "#biz robert> hi there" biz <# "#alisa robert> hi there" + -- add business team member + connectUsers biz cath + biz ##> "/a #alisa cath" + biz <## "invitation to join the group #alisa sent to cath" + cath <## "#alisa: biz invites you to join the group as member" + cath <## "use /j alisa to accept" + cath ##> "/j alisa" + concurrentlyN_ + [ do + cath <## "#alisa: you joined the group" + cath + <### + [ WithTime "#alisa biz> Welcome [>>]", + WithTime "#alisa biz> hi [>>]", + WithTime "#alisa alisa_1> hello [>>]", + WithTime "#alisa alisa_1> hello again [>>]", + WithTime "#alisa robert> hi there [>>]" + ] + cath <## "#alisa: member alisa_1 is connected" + cath <## "#alisa: member robert is connected", + biz <## "#alisa: cath joined the group", + do + alice <## "#biz: biz_1 added cath (Catherine) to the group (connecting...)" + alice <## "#biz: new member cath is connected", + do + bob <## "#biz: biz_1 added cath (Catherine) to the group (connecting...)" + bob <## "#biz: new member cath is connected" + ] -- both customers receive business profile change biz ##> "/p business" - biz <## "user profile is changed to business (your 0 contacts are notified)" + biz <## "user profile is changed to business (your 1 contacts are notified)" biz #> "#alisa hey" concurrentlyN_ [ do @@ -806,7 +834,28 @@ testBusinessUpdateProfiles = testChat3 businessProfile aliceProfile bobProfile $ do bob <## "biz_1 updated group #biz:" bob <## "changed to #business" - bob <# "#business business_1> hey" + bob <# "#business business_1> hey", + do + cath <## "contact biz changed to business" + cath <## "use @business to send messages" + cath <# "#alisa business> hey" + ] + biz ##> "/set voice #alisa on" + biz <## "updated group preferences:" + biz <## "Voice messages: on" + concurrentlyN_ + [ do + alice <## "business_1 updated group #business:" + alice <## "updated group preferences:" + alice <## "Voice messages: on", + do + bob <## "business_1 updated group #business:" + bob <## "updated group preferences:" + bob <## "Voice messages: on", + do + cath <## "business updated group #alisa:" + cath <## "updated group preferences:" + cath <## "Voice messages: on" ] testPlanAddressOkKnown :: HasCallStack => FilePath -> IO () @@ -2512,7 +2561,7 @@ testSetUITheme = a <## "you've shared main profile with this contact" a <## "connection not verified, use /code command to see security code" a <## "quantum resistant end-to-end encryption" - a <## "peer chat protocol version range: (Version 1, Version 10)" + a <## "peer chat protocol version range: (Version 1, Version 11)" groupInfo a = do a <## "group ID: 1" a <## "current members: 1" diff --git a/tests/ProtocolTests.hs b/tests/ProtocolTests.hs index 2eb946d731..523df81ade 100644 --- a/tests/ProtocolTests.hs +++ b/tests/ProtocolTests.hs @@ -133,7 +133,7 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew (MCSimple (extMsgContent (MCText "hello") Nothing))) it "x.msg.new chat message with chat version range" $ - "{\"v\":\"1-10\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" + "{\"v\":\"1-11\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" ##==## ChatMessage supportedChatVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew (MCSimple (extMsgContent (MCText "hello") Nothing))) it "x.msg.new quote" $ "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello to you too\",\"type\":\"text\"},\"quote\":{\"content\":{\"text\":\"hello there!\",\"type\":\"text\"},\"msgRef\":{\"msgId\":\"BQYHCA==\",\"sent\":true,\"sentAt\":\"1970-01-01T00:00:01.000000001Z\"}}}}" @@ -232,10 +232,10 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do ==# XContact testProfile Nothing it "x.grp.inv" $ "{\"v\":\"1\",\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\",\"groupPreferences\":{\"reactions\":{\"enable\":\"on\"},\"voice\":{\"enable\":\"on\"}}},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}}}}" - #==# XGrpInv GroupInvitation {fromMember = MemberIdRole (MemberId "\1\2\3\4") GRAdmin, invitedMember = MemberIdRole (MemberId "\5\6\7\8") GRMember, connRequest = testConnReq, groupProfile = testGroupProfile, businessChat = Nothing, groupLinkId = Nothing, groupSize = Nothing} + #==# XGrpInv GroupInvitation {fromMember = MemberIdRole (MemberId "\1\2\3\4") GRAdmin, invitedMember = MemberIdRole (MemberId "\5\6\7\8") GRMember, connRequest = testConnReq, groupProfile = testGroupProfile, business = Nothing, groupLinkId = Nothing, groupSize = Nothing} it "x.grp.inv with group link id" $ "{\"v\":\"1\",\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\",\"groupPreferences\":{\"reactions\":{\"enable\":\"on\"},\"voice\":{\"enable\":\"on\"}}},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}, \"groupLinkId\":\"AQIDBA==\"}}}" - #==# XGrpInv GroupInvitation {fromMember = MemberIdRole (MemberId "\1\2\3\4") GRAdmin, invitedMember = MemberIdRole (MemberId "\5\6\7\8") GRMember, connRequest = testConnReq, groupProfile = testGroupProfile, businessChat = Nothing, groupLinkId = Just $ GroupLinkId "\1\2\3\4", groupSize = Nothing} + #==# XGrpInv GroupInvitation {fromMember = MemberIdRole (MemberId "\1\2\3\4") GRAdmin, invitedMember = MemberIdRole (MemberId "\5\6\7\8") GRMember, connRequest = testConnReq, groupProfile = testGroupProfile, business = Nothing, groupLinkId = Just $ GroupLinkId "\1\2\3\4", groupSize = Nothing} it "x.grp.acpt without incognito profile" $ "{\"v\":\"1\",\"event\":\"x.grp.acpt\",\"params\":{\"memberId\":\"AQIDBA==\"}}" #==# XGrpAcpt (MemberId "\1\2\3\4") @@ -243,13 +243,13 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do "{\"v\":\"1\",\"event\":\"x.grp.mem.new\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemNew MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile} it "x.grp.mem.new with member chat version range" $ - "{\"v\":\"1\",\"event\":\"x.grp.mem.new\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-10\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" + "{\"v\":\"1\",\"event\":\"x.grp.mem.new\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-11\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemNew MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile} it "x.grp.mem.intro" $ "{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemIntro MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile} Nothing it "x.grp.mem.intro with member chat version range" $ - "{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-10\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" + "{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-11\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemIntro MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile} Nothing it "x.grp.mem.intro with member restrictions" $ "{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberRestrictions\":{\"restriction\":\"blocked\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" @@ -264,7 +264,7 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do "{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"directConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemFwd MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile} IntroInvitation {groupConnReq = testConnReq, directConnReq = Just testConnReq} it "x.grp.mem.fwd with member chat version range and w/t directConnReq" $ - "{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-10\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" + "{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-11\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemFwd MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile} IntroInvitation {groupConnReq = testConnReq, directConnReq = Nothing} it "x.grp.mem.info" $ "{\"v\":\"1\",\"event\":\"x.grp.mem.info\",\"params\":{\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}" From e9bf229a9d374b4be72074a698f2d27b99db11de Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Thu, 5 Dec 2024 18:36:07 +0000 Subject: [PATCH 136/167] core: 6.2.0.6 --- package.yaml | 2 +- simplex-chat.cabal | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.yaml b/package.yaml index 8752a141f7..b9c41ccdc0 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: simplex-chat -version: 6.2.0.5 +version: 6.2.0.6 #synopsis: #description: homepage: https://github.com/simplex-chat/simplex-chat#readme diff --git a/simplex-chat.cabal b/simplex-chat.cabal index e23305faf9..ace5afd851 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplex-chat -version: 6.2.0.5 +version: 6.2.0.6 category: Web, System, Services, Cryptography homepage: https://github.com/simplex-chat/simplex-chat#readme author: simplex.chat From 69e23ad58ffc32509cebaf0749b22ef4486206d1 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Thu, 5 Dec 2024 22:45:19 +0400 Subject: [PATCH 137/167] android, desktop: don't show unwanted notifications (#5328) * android, desktop: don't show unwanted notifications * format * fix * code style --------- Co-authored-by: Avently <7953703+avently@users.noreply.github.com> --- .../chat/simplex/common/model/SimpleXAPI.kt | 16 +++++++++++----- .../chat/simplex/common/platform/NtfManager.kt | 14 +++++++++++--- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index 757d80193c..94ce22d356 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -2436,9 +2436,7 @@ object ChatController { ) { receiveFile(rhId, r.user, file.fileId, auto = true) } - if (cItem.showNotification && (allowedToShowNotification() || chatModel.chatId.value != cInfo.id || chatModel.remoteHostId() != rhId)) { - ntfManager.notifyMessageReceived(r.user, cInfo, cItem) - } + ntfManager.notifyMessageReceived(rhId, r.user, cInfo, cItem) } } is CR.ChatItemsStatusesUpdated -> @@ -2452,7 +2450,7 @@ object ChatController { } } is CR.ChatItemUpdated -> - chatItemSimpleUpdate(rhId, r.user, r.chatItem) + chatItemUpdateNotify(rhId, r.user, r.chatItem) is CR.ChatItemReaction -> { if (active(r.user)) { withChats { @@ -2950,9 +2948,17 @@ object ChatController { } private suspend fun chatItemSimpleUpdate(rh: Long?, user: UserLike, aChatItem: AChatItem) { + if (activeUser(rh, user)) { + val cInfo = aChatItem.chatInfo + val cItem = aChatItem.chatItem + withChats { upsertChatItem(rh, cInfo, cItem) } + } + } + + private suspend fun chatItemUpdateNotify(rh: Long?, user: UserLike, aChatItem: AChatItem) { val cInfo = aChatItem.chatInfo val cItem = aChatItem.chatItem - val notify = { ntfManager.notifyMessageReceived(user, cInfo, cItem) } + val notify = { ntfManager.notifyMessageReceived(rh, user, cInfo, cItem) } if (!activeUser(rh, user)) { notify() } else if (withChats { upsertChatItem(rh, cInfo, cItem) }) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt index 1f1cb45d48..51d26f8ff2 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt @@ -36,9 +36,17 @@ abstract class NtfManager { ) ) - fun notifyMessageReceived(user: UserLike, cInfo: ChatInfo, cItem: ChatItem) { - if (!cInfo.ntfsEnabled) return - displayNotification(user = user, chatId = cInfo.id, displayName = cInfo.displayName, msgText = hideSecrets(cItem)) + fun notifyMessageReceived(rhId: Long?, user: UserLike, cInfo: ChatInfo, cItem: ChatItem) { + if ( + cItem.showNotification && + cInfo.ntfsEnabled && + ( + allowedToShowNotification() || + chatModel.chatId.value != cInfo.id || + chatModel.remoteHostId() != rhId) + ) { + displayNotification(user = user, chatId = cInfo.id, displayName = cInfo.displayName, msgText = hideSecrets(cItem)) + } } fun acceptContactRequestAction(userId: Long?, incognito: Boolean, chatId: ChatId) { From ff504702de112206b9a10a752f079e6218d02c25 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Thu, 5 Dec 2024 21:42:53 +0000 Subject: [PATCH 138/167] ui: translations (#5330) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Translated using Weblate (German) Currently translated at 97.5% (2155 of 2209 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/de/ * Translated using Weblate (French) Currently translated at 93.2% (2060 of 2209 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/fr/ * Translated using Weblate (Italian) Currently translated at 97.5% (2155 of 2209 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/it/ * Translated using Weblate (Spanish) Currently translated at 97.6% (2157 of 2209 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/es/ * Translated using Weblate (Dutch) Currently translated at 97.5% (2154 of 2209 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/nl/ * Translated using Weblate (Portuguese (Brazil)) Currently translated at 92.3% (2041 of 2209 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/pt_BR/ * Translated using Weblate (Arabic) Currently translated at 97.4% (2153 of 2209 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ar/ * Translated using Weblate (Ukrainian) Currently translated at 97.5% (2155 of 2209 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/uk/ * Translated using Weblate (Polish) Currently translated at 93.2% (2059 of 2209 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/pl/ * Translated using Weblate (Russian) Currently translated at 93.2% (2061 of 2210 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ru/ * Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 97.4% (2154 of 2210 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/zh_Hans/ * Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (2210 of 2210 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/zh_Hans/ * Translated using Weblate (Dutch) Currently translated at 100.0% (2210 of 2210 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/nl/ * Translated using Weblate (Dutch) Currently translated at 100.0% (1932 of 1932 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/nl/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2210 of 2210 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (1932 of 1932 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hu/ * Translated using Weblate (Italian) Currently translated at 98.9% (2187 of 2210 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/it/ * Translated using Weblate (Italian) Currently translated at 99.5% (2201 of 2210 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/it/ * Translated using Weblate (Russian) Currently translated at 100.0% (2210 of 2210 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ru/ * Translated using Weblate (Russian) Currently translated at 96.6% (1868 of 1932 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/ru/ * Translated using Weblate (Italian) Currently translated at 100.0% (2210 of 2210 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/it/ * Translated using Weblate (Italian) Currently translated at 100.0% (1932 of 1932 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/it/ * Translated using Weblate (Indonesian) Currently translated at 60.0% (1326 of 2210 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/id/ * Translated using Weblate (Russian) Currently translated at 100.0% (2210 of 2210 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ru/ * Translated using Weblate (Russian) Currently translated at 100.0% (1932 of 1932 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/ru/ * Translated using Weblate (German) Currently translated at 97.5% (2155 of 2209 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/de/ * Translated using Weblate (French) Currently translated at 93.2% (2060 of 2209 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/fr/ * Translated using Weblate (Italian) Currently translated at 97.5% (2155 of 2209 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/it/ * Translated using Weblate (Spanish) Currently translated at 97.6% (2157 of 2209 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/es/ * Translated using Weblate (Dutch) Currently translated at 97.5% (2154 of 2209 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/nl/ * Translated using Weblate (Portuguese (Brazil)) Currently translated at 92.3% (2041 of 2209 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/pt_BR/ * Translated using Weblate (Arabic) Currently translated at 97.4% (2153 of 2209 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ar/ * Translated using Weblate (Ukrainian) Currently translated at 97.5% (2155 of 2209 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/uk/ * Translated using Weblate (Polish) Currently translated at 93.2% (2059 of 2209 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/pl/ * Translated using Weblate (Russian) Currently translated at 93.2% (2061 of 2210 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ru/ * Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 97.4% (2154 of 2210 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/zh_Hans/ * Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (2210 of 2210 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/zh_Hans/ * Translated using Weblate (Dutch) Currently translated at 100.0% (2210 of 2210 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/nl/ * Translated using Weblate (Dutch) Currently translated at 100.0% (1932 of 1932 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/nl/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2210 of 2210 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (1932 of 1932 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hu/ * Translated using Weblate (Italian) Currently translated at 98.9% (2187 of 2210 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/it/ * Translated using Weblate (Italian) Currently translated at 99.5% (2201 of 2210 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/it/ * Translated using Weblate (Russian) Currently translated at 100.0% (2210 of 2210 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ru/ * Translated using Weblate (Russian) Currently translated at 96.6% (1868 of 1932 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/ru/ * Translated using Weblate (Italian) Currently translated at 100.0% (2210 of 2210 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/it/ * Translated using Weblate (Italian) Currently translated at 100.0% (1932 of 1932 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/it/ * Translated using Weblate (Indonesian) Currently translated at 60.0% (1326 of 2210 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/id/ * Translated using Weblate (Russian) Currently translated at 100.0% (2210 of 2210 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ru/ * Translated using Weblate (Russian) Currently translated at 100.0% (1932 of 1932 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/ru/ * process localizations * update translations --------- Co-authored-by: Anonymous Co-authored-by: 大王叫我来巡山 Co-authored-by: M1K4 Co-authored-by: summoner001 Co-authored-by: Random Co-authored-by: Rafi --- .../Onboarding/ChooseServerOperators.swift | 2 +- .../Onboarding/SetNotificationsMode.swift | 3 +- .../NetworkAndServers/OperatorView.swift | 2 +- .../Views/UserSettings/UserProfilesView.swift | 2 +- .../bg.xcloc/Localized Contents/bg.xliff | 28 +- .../cs.xcloc/Localized Contents/cs.xliff | 28 +- .../de.xcloc/Localized Contents/de.xliff | 35 +- .../en.xcloc/Localized Contents/en.xliff | 35 +- .../es.xcloc/Localized Contents/es.xliff | 37 +- .../fi.xcloc/Localized Contents/fi.xliff | 28 +- .../fr.xcloc/Localized Contents/fr.xliff | 28 +- .../hu.xcloc/Localized Contents/hu.xliff | 65 +- .../it.xcloc/Localized Contents/it.xliff | 59 +- .../ja.xcloc/Localized Contents/ja.xliff | 28 +- .../nl.xcloc/Localized Contents/nl.xliff | 103 +-- .../pl.xcloc/Localized Contents/pl.xliff | 28 +- .../ru.xcloc/Localized Contents/ru.xliff | 153 ++++- .../th.xcloc/Localized Contents/th.xliff | 28 +- .../tr.xcloc/Localized Contents/tr.xliff | 28 +- .../uk.xcloc/Localized Contents/uk.xliff | 37 +- .../Localized Contents/zh-Hans.xliff | 28 +- .../SimpleX NSE/ru.lproj/Localizable.strings | 20 +- apps/ios/bg.lproj/Localizable.strings | 42 +- apps/ios/cs.lproj/Localizable.strings | 36 +- apps/ios/de.lproj/Localizable.strings | 51 +- apps/ios/es.lproj/Localizable.strings | 53 +- apps/ios/fi.lproj/Localizable.strings | 36 +- apps/ios/fr.lproj/Localizable.strings | 42 +- apps/ios/hu.lproj/Localizable.strings | 129 +++- apps/ios/it.lproj/Localizable.strings | 123 +++- apps/ios/ja.lproj/Localizable.strings | 36 +- apps/ios/nl.lproj/Localizable.strings | 167 +++-- apps/ios/pl.lproj/Localizable.strings | 42 +- apps/ios/ru.lproj/Localizable.strings | 399 ++++++++++- apps/ios/th.lproj/Localizable.strings | 36 +- apps/ios/tr.lproj/Localizable.strings | 42 +- apps/ios/uk.lproj/Localizable.strings | 50 +- apps/ios/zh-Hans.lproj/Localizable.strings | 42 +- .../commonMain/resources/MR/ar/strings.xml | 3 +- .../commonMain/resources/MR/base/strings.xml | 6 +- .../commonMain/resources/MR/de/strings.xml | 3 +- .../commonMain/resources/MR/es/strings.xml | 3 +- .../commonMain/resources/MR/fr/strings.xml | 3 +- .../commonMain/resources/MR/hu/strings.xml | 95 ++- .../commonMain/resources/MR/in/strings.xml | 53 ++ .../commonMain/resources/MR/it/strings.xml | 78 ++- .../commonMain/resources/MR/nl/strings.xml | 118 ++-- .../commonMain/resources/MR/pl/strings.xml | 3 +- .../resources/MR/pt-rBR/strings.xml | 3 +- .../commonMain/resources/MR/ru/strings.xml | 172 ++++- .../commonMain/resources/MR/uk/strings.xml | 3 +- .../resources/MR/zh-rCN/strings.xml | 621 +++++++++--------- 52 files changed, 2141 insertions(+), 1154 deletions(-) diff --git a/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift b/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift index 14e08ff219..318e0b2f0d 100644 --- a/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift +++ b/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift @@ -323,7 +323,7 @@ struct ChooseServerOperators: View { VStack(alignment: .leading, spacing: 20) { if !operatorsWithConditionsAccepted.isEmpty { Text("Conditions are already accepted for following operator(s): **\(operatorsWithConditionsAccepted.map { $0.legalName_ }.joined(separator: ", "))**.") - Text("Same conditions will apply to operator(s): **\(acceptForOperators.map { $0.legalName_ }.joined(separator: ", "))**.") + Text("The same conditions will apply to operator(s): **\(acceptForOperators.map { $0.legalName_ }.joined(separator: ", "))**.") } else { Text("Conditions will be accepted for operator(s): **\(acceptForOperators.map { $0.legalName_ }.joined(separator: ", "))**.") } diff --git a/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift b/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift index 6164fcae70..642220454c 100644 --- a/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift +++ b/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift @@ -19,7 +19,7 @@ struct SetNotificationsMode: View { GeometryReader { g in ScrollView { VStack(alignment: .center, spacing: 20) { - Text("Push Notifications") + Text("Push notifications") .font(.largeTitle) .bold() .padding(.top, 50) @@ -119,6 +119,7 @@ struct NtfModeSelector: View { Text(ntfModeShortDescription(mode)) .lineLimit(2) .font(.callout) + .fixedSize(horizontal: false, vertical: true) } } .padding(.vertical, 12) diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift index c544d8724c..b1e4d36eda 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift @@ -450,7 +450,7 @@ struct SingleOperatorUsageConditionsView: View { Group { viewHeader() Text("Conditions are already accepted for following operator(s): **\(operatorsWithConditionsAccepted.map { $0.legalName_ }.joined(separator: ", "))**.") - Text("Same conditions will apply to operator **\(userServers[operatorIndex].operator_.legalName_)**.") + Text("The same conditions will apply to operator **\(userServers[operatorIndex].operator_.legalName_)**.") conditionsAppliedToOtherOperatorsText() usageConditionsNavLinkButton() diff --git a/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift b/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift index c3dce183bb..7cd86ef1ef 100644 --- a/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift +++ b/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift @@ -197,7 +197,7 @@ struct UserProfilesView: View { action() } else { authenticate( - reason: NSLocalizedString("Change user profiles", comment: "authentication reason") + reason: NSLocalizedString("Change chat profiles", comment: "authentication reason") ) { laResult in switch laResult { case .success, .unavailable: diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff index 0e8e30d8aa..901bee26dd 100644 --- a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff @@ -1288,6 +1288,10 @@ Промени No comment provided by engineer. + + Change chat profiles + authentication reason + Change database passphrase? Промяна на паролата на базата данни? @@ -1334,10 +1338,6 @@ authentication reason set passcode view - - Change user profiles - authentication reason - Chat No comment provided by engineer. @@ -5344,10 +5344,6 @@ Enable in *Network & servers* settings. Proxy requires password No comment provided by engineer. - - Push Notifications - No comment provided by engineer. - Push notifications Push известия @@ -5752,14 +5748,6 @@ Enable in *Network & servers* settings. По-безопасни групи No comment provided by engineer. - - Same conditions will apply to operator **%@**. - No comment provided by engineer. - - - Same conditions will apply to operator(s): **%@**. - No comment provided by engineer. - Save Запази @@ -6850,6 +6838,14 @@ It can happen because of some bug or when the connection is compromised.Профилът се споделя само с вашите контакти. No comment provided by engineer. + + The same conditions will apply to operator **%@**. + No comment provided by engineer. + + + The same conditions will apply to operator(s): **%@**. + No comment provided by engineer. + The second preset operator in the app! No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff index 10f6355692..fd8958f5f8 100644 --- a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff @@ -1247,6 +1247,10 @@ Změnit No comment provided by engineer. + + Change chat profiles + authentication reason + Change database passphrase? Změnit přístupovou frázi databáze? @@ -1293,10 +1297,6 @@ authentication reason set passcode view - - Change user profiles - authentication reason - Chat No comment provided by engineer. @@ -5166,10 +5166,6 @@ Enable in *Network & servers* settings. Proxy requires password No comment provided by engineer. - - Push Notifications - No comment provided by engineer. - Push notifications Nabízená oznámení @@ -5562,14 +5558,6 @@ Enable in *Network & servers* settings. Safer groups No comment provided by engineer. - - Same conditions will apply to operator **%@**. - No comment provided by engineer. - - - Same conditions will apply to operator(s): **%@**. - No comment provided by engineer. - Save Uložit @@ -6638,6 +6626,14 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován Profil je sdílen pouze s vašimi kontakty. No comment provided by engineer. + + The same conditions will apply to operator **%@**. + No comment provided by engineer. + + + The same conditions will apply to operator(s): **%@**. + No comment provided by engineer. + The second preset operator in the app! No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff index 35e11b4861..77ddbfb69b 100644 --- a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff +++ b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff @@ -1341,6 +1341,11 @@ Ändern No comment provided by engineer. + + Change chat profiles + Chat-Profile wechseln + authentication reason + Change database passphrase? Datenbank-Passwort ändern? @@ -1387,11 +1392,6 @@ authentication reason set passcode view - - Change user profiles - Chat-Profile wechseln - authentication reason - Chat No comment provided by engineer. @@ -5590,11 +5590,6 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Der Proxy benötigt ein Passwort No comment provided by engineer. - - Push Notifications - Push-Benachrichtigungen - No comment provided by engineer. - Push notifications Push-Benachrichtigungen @@ -6021,16 +6016,6 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Sicherere Gruppen No comment provided by engineer. - - Same conditions will apply to operator **%@**. - Dieselben Nutzungsbedingungen gelten auch für den Betreiber **%@**. - No comment provided by engineer. - - - Same conditions will apply to operator(s): **%@**. - Dieselben Nutzungsbedingungen gelten auch für den/die Betreiber: **%@**. - No comment provided by engineer. - Save Speichern @@ -7191,6 +7176,16 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro Das Profil wird nur mit Ihren Kontakten geteilt. No comment provided by engineer. + + The same conditions will apply to operator **%@**. + Dieselben Nutzungsbedingungen gelten auch für den Betreiber **%@**. + No comment provided by engineer. + + + The same conditions will apply to operator(s): **%@**. + Dieselben Nutzungsbedingungen gelten auch für den/die Betreiber: **%@**. + No comment provided by engineer. + The second preset operator in the app! Der zweite voreingestellte Netzwerk-Betreiber in der App! diff --git a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff index 1d7f3f16be..a70a22581b 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff +++ b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff @@ -1346,6 +1346,11 @@ Change No comment provided by engineer. + + Change chat profiles + Change chat profiles + authentication reason + Change database passphrase? Change database passphrase? @@ -1392,11 +1397,6 @@ authentication reason set passcode view - - Change user profiles - Change user profiles - authentication reason - Chat Chat @@ -5611,11 +5611,6 @@ Enable in *Network & servers* settings. Proxy requires password No comment provided by engineer. - - Push Notifications - Push Notifications - No comment provided by engineer. - Push notifications Push notifications @@ -6042,16 +6037,6 @@ Enable in *Network & servers* settings. Safer groups No comment provided by engineer. - - Same conditions will apply to operator **%@**. - Same conditions will apply to operator **%@**. - No comment provided by engineer. - - - Same conditions will apply to operator(s): **%@**. - Same conditions will apply to operator(s): **%@**. - No comment provided by engineer. - Save Save @@ -7213,6 +7198,16 @@ It can happen because of some bug or when the connection is compromised.The profile is only shared with your contacts. No comment provided by engineer. + + The same conditions will apply to operator **%@**. + The same conditions will apply to operator **%@**. + No comment provided by engineer. + + + The same conditions will apply to operator(s): **%@**. + The same conditions will apply to operator(s): **%@**. + No comment provided by engineer. + The second preset operator in the app! The second preset operator in the app! diff --git a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff index 147ad6128f..32af6e9cfd 100644 --- a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff +++ b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff @@ -1341,6 +1341,11 @@ Cambiar No comment provided by engineer. + + Change chat profiles + Cambiar perfil de usuario + authentication reason + Change database passphrase? ¿Cambiar contraseña de la base de datos? @@ -1387,11 +1392,6 @@ authentication reason set passcode view - - Change user profiles - Cambiar perfil de usuario - authentication reason - Chat No comment provided by engineer. @@ -5590,14 +5590,9 @@ Actívalo en ajustes de *Servidores y Redes*. El proxy requiere contraseña No comment provided by engineer. - - Push Notifications - Notificaciones push - No comment provided by engineer. - Push notifications - Notificaciones automáticas + Notificaciones push No comment provided by engineer. @@ -6021,16 +6016,6 @@ Actívalo en ajustes de *Servidores y Redes*. Grupos más seguros No comment provided by engineer. - - Same conditions will apply to operator **%@**. - Las mismas condiciones se aplicarán al operador **%@**. - No comment provided by engineer. - - - Same conditions will apply to operator(s): **%@**. - Las mismas condiciones se aplicarán a el/los operador(es) **%@**. - No comment provided by engineer. - Save Guardar @@ -7191,6 +7176,16 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. El perfil sólo se comparte con tus contactos. No comment provided by engineer. + + The same conditions will apply to operator **%@**. + Las mismas condiciones se aplicarán al operador **%@**. + No comment provided by engineer. + + + The same conditions will apply to operator(s): **%@**. + Las mismas condiciones se aplicarán a el/los operador(es) **%@**. + No comment provided by engineer. + The second preset operator in the app! El segundo operador predefinido! diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff index 39277bbcce..bbd8a338bc 100644 --- a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff @@ -1240,6 +1240,10 @@ Muuta No comment provided by engineer. + + Change chat profiles + authentication reason + Change database passphrase? Muutetaanko tietokannan tunnuslause? @@ -1286,10 +1290,6 @@ authentication reason set passcode view - - Change user profiles - authentication reason - Chat No comment provided by engineer. @@ -5154,10 +5154,6 @@ Enable in *Network & servers* settings. Proxy requires password No comment provided by engineer. - - Push Notifications - No comment provided by engineer. - Push notifications Push-ilmoitukset @@ -5550,14 +5546,6 @@ Enable in *Network & servers* settings. Safer groups No comment provided by engineer. - - Same conditions will apply to operator **%@**. - No comment provided by engineer. - - - Same conditions will apply to operator(s): **%@**. - No comment provided by engineer. - Save Tallenna @@ -6624,6 +6612,14 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.Profiili jaetaan vain kontaktiesi kanssa. No comment provided by engineer. + + The same conditions will apply to operator **%@**. + No comment provided by engineer. + + + The same conditions will apply to operator(s): **%@**. + No comment provided by engineer. + The second preset operator in the app! No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff index c2030ab657..9c44fe91e4 100644 --- a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff @@ -1329,6 +1329,10 @@ Changer No comment provided by engineer. + + Change chat profiles + authentication reason + Change database passphrase? Changer la phrase secrète de la base de données ? @@ -1375,10 +1379,6 @@ authentication reason set passcode view - - Change user profiles - authentication reason - Chat No comment provided by engineer. @@ -5530,10 +5530,6 @@ Activez-le dans les paramètres *Réseau et serveurs*. Le proxy est protégé par un mot de passe No comment provided by engineer. - - Push Notifications - No comment provided by engineer. - Push notifications Notifications push @@ -5958,14 +5954,6 @@ Activez-le dans les paramètres *Réseau et serveurs*. Groupes plus sûrs No comment provided by engineer. - - Same conditions will apply to operator **%@**. - No comment provided by engineer. - - - Same conditions will apply to operator(s): **%@**. - No comment provided by engineer. - Save Enregistrer @@ -7113,6 +7101,14 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. Le profil n'est partagé qu'avec vos contacts. No comment provided by engineer. + + The same conditions will apply to operator **%@**. + No comment provided by engineer. + + + The same conditions will apply to operator(s): **%@**. + No comment provided by engineer. + The second preset operator in the app! No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff index 437d97274d..ffc162633f 100644 --- a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff @@ -637,6 +637,7 @@ Add friends + Barátok hozzáadása No comment provided by engineer. @@ -656,6 +657,7 @@ Add team members + Csapattagok hozzáadása No comment provided by engineer. @@ -670,6 +672,7 @@ Add your team members to the conversations. + Adja hozzá csapattagjait a beszélgetésekhez. No comment provided by engineer. @@ -1209,7 +1212,7 @@ Blur media - Média elhomályosítása + Médiatartalom elhomályosítása No comment provided by engineer. @@ -1244,10 +1247,12 @@ Business address + Üzleti cím No comment provided by engineer. Business chats + Üzleti csevegések No comment provided by engineer. @@ -1341,6 +1346,11 @@ Változtatás No comment provided by engineer. + + Change chat profiles + Felhasználói profilok megváltoztatása + authentication reason + Change database passphrase? Adatbázis-jelmondat megváltoztatása? @@ -1387,21 +1397,19 @@ authentication reason set passcode view - - Change user profiles - Felhasználói profilok megváltoztatása - authentication reason - Chat + Csevegés No comment provided by engineer. Chat already exists + A csevegés már létezik No comment provided by engineer. Chat already exists! + A csevegés már létezik! No comment provided by engineer. @@ -1481,10 +1489,12 @@ Chat will be deleted for all members - this cannot be undone! + A csevegés minden tag számára törlésre kerül - ezt a műveletet nem lehet visszavonni! No comment provided by engineer. Chat will be deleted for you - this cannot be undone! + A csevegés törlésre kerül az Ön számára - ezt a műveletet nem lehet visszavonni! No comment provided by engineer. @@ -2243,6 +2253,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Delete chat + Csevegés törlése No comment provided by engineer. @@ -2257,6 +2268,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Delete chat? + Csevegés törlése? No comment provided by engineer. @@ -2526,6 +2538,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! Direct messages between members are prohibited in this chat. + A tagok közötti közvetlen üzenetek le vannak tiltva ebben a csevegésben. No comment provided by engineer. @@ -4101,6 +4114,7 @@ További fejlesztések hamarosan! Invite to chat + Meghívás a csevegésbe No comment provided by engineer. @@ -4263,10 +4277,12 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! Leave chat + Csevegés elhagyása No comment provided by engineer. Leave chat? + Csevegés elhagyása? No comment provided by engineer. @@ -4401,6 +4417,7 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! Member role will be changed to "%@". All chat members will be notified. + A tag szerepeköre meg fog változni a következőre: "%@". A csevegés tagjai értesítést fognak kapni. No comment provided by engineer. @@ -4415,6 +4432,7 @@ Ez az Ön hivatkozása a(z) %@ nevű csoporthoz! Member will be removed from chat - this cannot be undone! + A tag el lesz távolítva a csevegésből - ezt a műveletet nem lehet visszavonni! No comment provided by engineer. @@ -5032,6 +5050,7 @@ VPN engedélyezése szükséges. Only chat owners can change preferences. + Csak a csevegés tulajdonosai módosíthatják a beállításokat. No comment provided by engineer. @@ -5166,6 +5185,7 @@ VPN engedélyezése szükséges. Or import archive file + Vagy archívumfájl importálása No comment provided by engineer. @@ -5431,6 +5451,7 @@ Hiba: %@ Privacy for your customers. + Az Ön ügyfeleinek adatvédelme. No comment provided by engineer. @@ -5590,11 +5611,6 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. A proxy jelszót igényel No comment provided by engineer. - - Push Notifications - Push értesítések - No comment provided by engineer. - Push notifications Push-értesítések @@ -6021,16 +6037,6 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Biztonságosabb csoportok No comment provided by engineer. - - Same conditions will apply to operator **%@**. - Ugyanezek a feltételek vonatkoznak a következő üzemeltetőre is: **%@**. - No comment provided by engineer. - - - Same conditions will apply to operator(s): **%@**. - Ugyanezek a feltételek lesznek elfogadva a következő üzemeltető(k)re is: **%@**. - No comment provided by engineer. - Save Mentés @@ -6119,7 +6125,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Saved from - Mentve innen: + Elmentve innen: No comment provided by engineer. @@ -7017,6 +7023,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Tap Create SimpleX address in the menu to create it later. + Koppintson a SimpleX-cím létrehozása menüpontra a későbbi létrehozáshoz. No comment provided by engineer. @@ -7191,6 +7198,16 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. A profilja csak az ismerőseivel kerül megosztásra. No comment provided by engineer. + + The same conditions will apply to operator **%@**. + Ugyanezek a feltételek vonatkoznak a következő üzemeltetőre is: **%@**. + No comment provided by engineer. + + + The same conditions will apply to operator(s): **%@**. + Ugyanezek a feltételek lesznek elfogadva a következő üzemeltető(k)re is: **%@**. + No comment provided by engineer. + The second preset operator in the app! A második előre beállított üzemeltető az alkalmazásban! @@ -8057,6 +8074,7 @@ A kapcsolódáshoz kérje meg az ismerősét, hogy hozzon létre egy másik kapc You are already connected with %@. + Ön már kapcsolódva van vele: %@. No comment provided by engineer. @@ -8335,6 +8353,7 @@ Kapcsolatkérés megismétlése? You will stop receiving messages from this chat. Chat history will be preserved. + Ön nem fog több üzenetet kapni ebből a csevegésből, de a csevegés előzményei megmaradnak. No comment provided by engineer. @@ -9211,7 +9230,7 @@ Kapcsolatkérés megismétlése? saved from %@ - mentve innen: %@ + elmentve innen: %@ No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff index f20b0515db..0b578e6a25 100644 --- a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff +++ b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff @@ -637,6 +637,7 @@ Add friends + Aggiungi amici No comment provided by engineer. @@ -656,6 +657,7 @@ Add team members + Aggiungi membri del team No comment provided by engineer. @@ -670,6 +672,7 @@ Add your team members to the conversations. + Aggiungi i membri del tuo team alle conversazioni. No comment provided by engineer. @@ -1244,10 +1247,12 @@ Business address + Indirizzo di lavoro No comment provided by engineer. Business chats + Chat di lavoro No comment provided by engineer. @@ -1341,6 +1346,11 @@ Cambia No comment provided by engineer. + + Change chat profiles + Modifica profili utente + authentication reason + Change database passphrase? Cambiare password del database? @@ -1387,21 +1397,19 @@ authentication reason set passcode view - - Change user profiles - Modifica profili utente - authentication reason - Chat + Chat No comment provided by engineer. Chat already exists + La chat esiste già No comment provided by engineer. Chat already exists! + La chat esiste già! No comment provided by engineer. @@ -1481,10 +1489,12 @@ Chat will be deleted for all members - this cannot be undone! + La chat verrà eliminata per tutti i membri, non è reversibile! No comment provided by engineer. Chat will be deleted for you - this cannot be undone! + La chat verrà eliminata solo per te, non è reversibile! No comment provided by engineer. @@ -2243,6 +2253,7 @@ Questo è il tuo link una tantum! Delete chat + Elimina chat No comment provided by engineer. @@ -2257,6 +2268,7 @@ Questo è il tuo link una tantum! Delete chat? + Eliminare la chat? No comment provided by engineer. @@ -2526,6 +2538,7 @@ Questo è il tuo link una tantum! Direct messages between members are prohibited in this chat. + I messaggi diretti tra i membri sono vietati in questa chat. No comment provided by engineer. @@ -4101,6 +4114,7 @@ Altri miglioramenti sono in arrivo! Invite to chat + Invita in chat No comment provided by engineer. @@ -4263,10 +4277,12 @@ Questo è il tuo link per il gruppo %@! Leave chat + Esci dalla chat No comment provided by engineer. Leave chat? + Uscire dalla chat? No comment provided by engineer. @@ -4401,6 +4417,7 @@ Questo è il tuo link per il gruppo %@! Member role will be changed to "%@". All chat members will be notified. + Il ruolo del membro verrà cambiato in "%@". Verranno notificati tutti i membri della chat. No comment provided by engineer. @@ -4415,6 +4432,7 @@ Questo è il tuo link per il gruppo %@! Member will be removed from chat - this cannot be undone! + Il membro verrà rimosso dalla chat, non è reversibile! No comment provided by engineer. @@ -5032,6 +5050,7 @@ Richiede l'attivazione della VPN. Only chat owners can change preferences. + Solo i proprietari della chat possono modificarne le preferenze. No comment provided by engineer. @@ -5166,6 +5185,7 @@ Richiede l'attivazione della VPN. Or import archive file + O importa file archivio No comment provided by engineer. @@ -5431,6 +5451,7 @@ Errore: %@ Privacy for your customers. + Privacy per i tuoi clienti. No comment provided by engineer. @@ -5590,11 +5611,6 @@ Attivalo nelle impostazioni *Rete e server*. Il proxy richiede una password No comment provided by engineer. - - Push Notifications - Notifiche push - No comment provided by engineer. - Push notifications Notifiche push @@ -6021,16 +6037,6 @@ Attivalo nelle impostazioni *Rete e server*. Gruppi più sicuri No comment provided by engineer. - - Same conditions will apply to operator **%@**. - Le stesse condizioni si applicheranno all'operatore **%@**. - No comment provided by engineer. - - - Same conditions will apply to operator(s): **%@**. - Le stesse condizioni si applicheranno agli operatori **%@**. - No comment provided by engineer. - Save Salva @@ -7017,6 +7023,7 @@ Attivalo nelle impostazioni *Rete e server*. Tap Create SimpleX address in the menu to create it later. + Tocca "Crea indirizzo SimpleX" nel menu per crearlo più tardi. No comment provided by engineer. @@ -7191,6 +7198,16 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa.Il profilo è condiviso solo con i tuoi contatti. No comment provided by engineer. + + The same conditions will apply to operator **%@**. + Le stesse condizioni si applicheranno all'operatore **%@**. + No comment provided by engineer. + + + The same conditions will apply to operator(s): **%@**. + Le stesse condizioni si applicheranno agli operatori **%@**. + No comment provided by engineer. + The second preset operator in the app! Il secondo operatore preimpostato nell'app! @@ -8057,6 +8074,7 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e You are already connected with %@. + Sei già connesso/a con %@. No comment provided by engineer. @@ -8335,6 +8353,7 @@ Ripetere la richiesta di connessione? You will stop receiving messages from this chat. Chat history will be preserved. + Non riceverai più messaggi da questa chat. La cronologia della chat verrà conservata. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff index 288276124c..87058e0bdc 100644 --- a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff +++ b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff @@ -1264,6 +1264,10 @@ 変更 No comment provided by engineer. + + Change chat profiles + authentication reason + Change database passphrase? データベースのパスフレーズを更新しますか? @@ -1310,10 +1314,6 @@ authentication reason set passcode view - - Change user profiles - authentication reason - Chat No comment provided by engineer. @@ -5204,10 +5204,6 @@ Enable in *Network & servers* settings. Proxy requires password No comment provided by engineer. - - Push Notifications - No comment provided by engineer. - Push notifications プッシュ通知 @@ -5599,14 +5595,6 @@ Enable in *Network & servers* settings. Safer groups No comment provided by engineer. - - Same conditions will apply to operator **%@**. - No comment provided by engineer. - - - Same conditions will apply to operator(s): **%@**. - No comment provided by engineer. - Save 保存 @@ -6667,6 +6655,14 @@ It can happen because of some bug or when the connection is compromised.プロフィールは連絡先にしか共有されません。 No comment provided by engineer. + + The same conditions will apply to operator **%@**. + No comment provided by engineer. + + + The same conditions will apply to operator(s): **%@**. + No comment provided by engineer. + The second preset operator in the app! No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff index 6bf4808025..ce3adc3c41 100644 --- a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff @@ -637,6 +637,7 @@ Add friends + Vrienden toevoegen No comment provided by engineer. @@ -656,6 +657,7 @@ Add team members + Teamleden toevoegen No comment provided by engineer. @@ -670,6 +672,7 @@ Add your team members to the conversations. + Voeg uw teamleden toe aan de gesprekken. No comment provided by engineer. @@ -829,7 +832,7 @@ Allow irreversible message deletion only if your contact allows it to you. (24 hours) - Sta het onomkeerbaar verwijderen van berichten alleen toe als uw contact dit toestaat. (24 uur) + Sta het definitief verwijderen van berichten alleen toe als uw contact dit toestaat. (24 uur) No comment provided by engineer. @@ -859,7 +862,7 @@ Allow to irreversibly delete sent messages. (24 hours) - Sta toe om verzonden berichten onomkeerbaar te verwijderen. (24 uur) + Sta toe om verzonden berichten definitief te verwijderen. (24 uur) No comment provided by engineer. @@ -899,7 +902,7 @@ Allow your contacts to irreversibly delete sent messages. (24 hours) - Laat uw contacten verzonden berichten onomkeerbaar verwijderen. (24 uur) + Laat uw contacten verzonden berichten definitief verwijderen. (24 uur) No comment provided by engineer. @@ -1054,7 +1057,7 @@ Audio/video calls are prohibited. - Audio/video gesprekken zijn verboden. + Audio/video gesprekken zijn niet toegestaan. No comment provided by engineer. @@ -1244,10 +1247,12 @@ Business address + Zakelijk adres No comment provided by engineer. Business chats + Zakelijke chats No comment provided by engineer. @@ -1267,7 +1272,7 @@ Calls prohibited! - Bellen verboden! + Bellen niet toegestaan! No comment provided by engineer. @@ -1341,6 +1346,11 @@ Veranderen No comment provided by engineer. + + Change chat profiles + Gebruikersprofielen wijzigen + authentication reason + Change database passphrase? Wachtwoord database wijzigen? @@ -1387,21 +1397,19 @@ authentication reason set passcode view - - Change user profiles - Gebruikersprofielen wijzigen - authentication reason - Chat + Chat No comment provided by engineer. Chat already exists + Chat bestaat al No comment provided by engineer. Chat already exists! + Chat bestaat al! No comment provided by engineer. @@ -1481,10 +1489,12 @@ Chat will be deleted for all members - this cannot be undone! + De chat wordt voor alle leden verwijderd - dit kan niet ongedaan worden gemaakt! No comment provided by engineer. Chat will be deleted for you - this cannot be undone! + De chat wordt voor je verwijderd - dit kan niet ongedaan worden gemaakt! No comment provided by engineer. @@ -2243,6 +2253,7 @@ Dit is uw eigen eenmalige link! Delete chat + Chat verwijderen No comment provided by engineer. @@ -2257,6 +2268,7 @@ Dit is uw eigen eenmalige link! Delete chat? + Chat verwijderen? No comment provided by engineer. @@ -2526,11 +2538,12 @@ Dit is uw eigen eenmalige link! Direct messages between members are prohibited in this chat. + Directe berichten tussen leden zijn in deze chat niet toegestaan. No comment provided by engineer. Direct messages between members are prohibited. - Directe berichten tussen leden zijn verboden in deze groep. + Directe berichten tussen leden zijn niet toegestaan. No comment provided by engineer. @@ -2565,12 +2578,12 @@ Dit is uw eigen eenmalige link! Disappearing messages are prohibited in this chat. - Verdwijnende berichten zijn verboden in dit gesprek. + Verdwijnende berichten zijn niet toegestaan in dit gesprek. No comment provided by engineer. Disappearing messages are prohibited. - Verdwijnende berichten zijn verboden in deze groep. + Verdwijnende berichten zijn niet toegestaan. No comment provided by engineer. @@ -3428,7 +3441,7 @@ Dit is uw eigen eenmalige link! Files and media are prohibited. - Bestanden en media zijn verboden in deze groep. + Bestanden en media zijn niet toegestaan. No comment provided by engineer. @@ -3438,7 +3451,7 @@ Dit is uw eigen eenmalige link! Files and media prohibited! - Bestanden en media verboden! + Bestanden en media niet toegestaan! No comment provided by engineer. @@ -3842,7 +3855,7 @@ Fout: %2$@ If you enter this passcode when opening the app, all app data will be irreversibly removed! - Als u deze toegangscode invoert bij het openen van de app, worden alle app-gegevens onomkeerbaar verwijderd! + Als u deze toegangscode invoert bij het openen van de app, worden alle app-gegevens definitief verwijderd! No comment provided by engineer. @@ -4101,6 +4114,7 @@ Binnenkort meer verbeteringen! Invite to chat + Uitnodigen voor een chat No comment provided by engineer. @@ -4115,12 +4129,12 @@ Binnenkort meer verbeteringen! Irreversible message deletion is prohibited in this chat. - Het onomkeerbaar verwijderen van berichten is verboden in dit gesprek. + Het definitief verwijderen van berichten is niet toegestaan in dit gesprek. No comment provided by engineer. Irreversible message deletion is prohibited. - Het onomkeerbaar verwijderen van berichten is verboden in deze groep. + Het definitief verwijderen van berichten is verbHet definitief verwijderen van berichten is niet toegestaan.. No comment provided by engineer. @@ -4263,10 +4277,12 @@ Dit is jouw link voor groep %@! Leave chat + Chat verlaten No comment provided by engineer. Leave chat? + Chat verlaten? No comment provided by engineer. @@ -4401,6 +4417,7 @@ Dit is jouw link voor groep %@! Member role will be changed to "%@". All chat members will be notified. + De rol van het lid wordt gewijzigd naar "%@". Alle chatleden worden op de hoogte gebracht. No comment provided by engineer. @@ -4415,6 +4432,7 @@ Dit is jouw link voor groep %@! Member will be removed from chat - this cannot be undone! + Lid wordt verwijderd uit de chat - dit kan niet ongedaan worden gemaakt! No comment provided by engineer. @@ -4504,12 +4522,12 @@ Dit is jouw link voor groep %@! Message reactions are prohibited in this chat. - Reacties op berichten zijn verboden in deze chat. + Reacties op berichten zijn niet toegestaan in deze chat. No comment provided by engineer. Message reactions are prohibited. - Reacties op berichten zijn verboden in deze groep. + Reacties op berichten zijn niet toegestaan. No comment provided by engineer. @@ -5032,6 +5050,7 @@ Vereist het inschakelen van VPN. Only chat owners can change preferences. + Alleen chateigenaren kunnen voorkeuren wijzigen. No comment provided by engineer. @@ -5066,7 +5085,7 @@ Vereist het inschakelen van VPN. Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours) - Alleen jij kunt berichten onomkeerbaar verwijderen (je contact kan ze markeren voor verwijdering). (24 uur) + Alleen jij kunt berichten definitief verwijderen (je contact kan ze markeren voor verwijdering). (24 uur) No comment provided by engineer. @@ -5166,6 +5185,7 @@ Vereist het inschakelen van VPN. Or import archive file + Of importeer archiefbestand No comment provided by engineer. @@ -5431,6 +5451,7 @@ Fout: %@ Privacy for your customers. + Privacy voor uw klanten. No comment provided by engineer. @@ -5505,7 +5526,7 @@ Fout: %@ Prohibit irreversible message deletion. - Verbied het onomkeerbaar verwijderen van berichten. + Verbied het definitief verwijderen van berichten. No comment provided by engineer. @@ -5590,11 +5611,6 @@ Schakel dit in in *Netwerk en servers*-instellingen. Proxy vereist wachtwoord No comment provided by engineer. - - Push Notifications - Pushmeldingen - No comment provided by engineer. - Push notifications Push meldingen @@ -6021,16 +6037,6 @@ Schakel dit in in *Netwerk en servers*-instellingen. Veiligere groepen No comment provided by engineer. - - Same conditions will apply to operator **%@**. - Dezelfde voorwaarden gelden voor operator **%@**. - No comment provided by engineer. - - - Same conditions will apply to operator(s): **%@**. - Dezelfde voorwaarden gelden voor operator(s): **%@**. - No comment provided by engineer. - Save Opslaan @@ -6765,7 +6771,7 @@ Schakel dit in in *Netwerk en servers*-instellingen. SimpleX links are prohibited. - SimpleX-links zijn in deze groep verboden. + SimpleX-links zijn niet toegestaan. No comment provided by engineer. @@ -7017,6 +7023,7 @@ Schakel dit in in *Netwerk en servers*-instellingen. Tap Create SimpleX address in the menu to create it later. + Tik op SimpleX-adres maken in het menu om het later te maken. No comment provided by engineer. @@ -7191,6 +7198,16 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast. Het profiel wordt alleen gedeeld met uw contacten. No comment provided by engineer. + + The same conditions will apply to operator **%@**. + Dezelfde voorwaarden gelden voor operator **%@**. + No comment provided by engineer. + + + The same conditions will apply to operator(s): **%@**. + Dezelfde voorwaarden gelden voor operator(s): **%@**. + No comment provided by engineer. + The second preset operator in the app! De tweede vooraf ingestelde operator in de app! @@ -7258,7 +7275,7 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast. This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost. - Deze actie kan niet ongedaan worden gemaakt. Uw profiel, contacten, berichten en bestanden gaan onomkeerbaar verloren. + Deze actie kan niet ongedaan worden gemaakt. Uw profiel, contacten, berichten en bestanden gaan definitief verloren. No comment provided by engineer. @@ -7857,12 +7874,12 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Voice messages are prohibited in this chat. - Spraak berichten zijn verboden in deze chat. + Spraak berichten zijn niet toegestaan in dit gesprek. No comment provided by engineer. Voice messages are prohibited. - Spraak berichten zijn verboden in deze groep. + Spraak berichten zijn niet toegestaan. No comment provided by engineer. @@ -7872,7 +7889,7 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Voice messages prohibited! - Spraak berichten verboden! + Spraak berichten niet toegestaan! No comment provided by engineer. @@ -8057,6 +8074,7 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak You are already connected with %@. + U bent al verbonden met %@. No comment provided by engineer. @@ -8335,6 +8353,7 @@ Verbindingsverzoek herhalen? You will stop receiving messages from this chat. Chat history will be preserved. + U ontvangt geen berichten meer van deze chat. De chatgeschiedenis blijft bewaard. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff index ea40b78582..e99439f3ae 100644 --- a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff @@ -1324,6 +1324,10 @@ Zmień No comment provided by engineer. + + Change chat profiles + authentication reason + Change database passphrase? Zmienić hasło bazy danych? @@ -1370,10 +1374,6 @@ authentication reason set passcode view - - Change user profiles - authentication reason - Chat No comment provided by engineer. @@ -5520,10 +5520,6 @@ Włącz w ustawianiach *Sieć i serwery* . Proxy wymaga hasła No comment provided by engineer. - - Push Notifications - No comment provided by engineer. - Push notifications Powiadomienia push @@ -5948,14 +5944,6 @@ Włącz w ustawianiach *Sieć i serwery* . Bezpieczniejsze grupy No comment provided by engineer. - - Same conditions will apply to operator **%@**. - No comment provided by engineer. - - - Same conditions will apply to operator(s): **%@**. - No comment provided by engineer. - Save Zapisz @@ -7100,6 +7088,14 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom Profil jest udostępniany tylko Twoim kontaktom. No comment provided by engineer. + + The same conditions will apply to operator **%@**. + No comment provided by engineer. + + + The same conditions will apply to operator(s): **%@**. + No comment provided by engineer. + The second preset operator in the app! No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff index 3fd5d194de..85b4aaa4ae 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff @@ -114,10 +114,12 @@ %@ server + %@ сервер No comment provided by engineer. %@ servers + %@ серверы No comment provided by engineer. @@ -382,6 +384,7 @@ **Scan / Paste link**: to connect via a link you received. + **Сканировать / Вставить ссылку**: чтобы соединится через полученную ссылку. No comment provided by engineer. @@ -492,10 +495,12 @@ 1-time link + Одноразовая ссылка No comment provided by engineer. 1-time link can be used *with one contact only* - share in person or via any messenger. + Одноразовая ссылка может быть использована *только с одним контактом* - поделитесь при встрече или через любой мессенджер. No comment provided by engineer. @@ -586,6 +591,7 @@ Accept conditions + Принять условия No comment provided by engineer. @@ -606,6 +612,7 @@ Accepted conditions + Принятые условия No comment provided by engineer. @@ -630,6 +637,7 @@ Add friends + Добавить друзей No comment provided by engineer. @@ -649,6 +657,7 @@ Add team members + Добавить сотрудников No comment provided by engineer. @@ -663,14 +672,17 @@ Add your team members to the conversations. + Добавьте сотрудников в разговор. No comment provided by engineer. Added media & file servers + Дополнительные серверы файлов и медиа No comment provided by engineer. Added message servers + Дополнительные серверы сообщений No comment provided by engineer. @@ -700,10 +712,12 @@ Address or 1-time link? + Адрес или одноразовая ссылка? No comment provided by engineer. Address settings + Настройки адреса No comment provided by engineer. @@ -1233,10 +1247,12 @@ Business address + Бизнес адрес No comment provided by engineer. Business chats + Бизнес разговоры No comment provided by engineer. @@ -1330,6 +1346,11 @@ Поменять No comment provided by engineer. + + Change chat profiles + Поменять профили + authentication reason + Change database passphrase? Поменять пароль базы данных? @@ -1376,20 +1397,19 @@ authentication reason set passcode view - - Change user profiles - authentication reason - Chat + Разговор No comment provided by engineer. Chat already exists + Разговор уже существует No comment provided by engineer. Chat already exists! + Разговор уже существует! No comment provided by engineer. @@ -1469,10 +1489,12 @@ Chat will be deleted for all members - this cannot be undone! + Разговор будет удален для всех участников - это действие нельзя отменить! No comment provided by engineer. Chat will be deleted for you - this cannot be undone! + Разговор будет удален для Вас - это действие нельзя отменить! No comment provided by engineer. @@ -1482,10 +1504,12 @@ Check messages every 20 min. + Проверять сообщения каждые 20 минут. No comment provided by engineer. Check messages when allowed. + Проверять сообщения по возможности. No comment provided by engineer. @@ -1580,38 +1604,47 @@ Conditions accepted on: %@. + Условия приняты: %@. No comment provided by engineer. Conditions are accepted for the operator(s): **%@**. + Условия приняты для оператора(ов): **%@**. No comment provided by engineer. Conditions are already accepted for following operator(s): **%@**. + Условия уже приняты для следующих оператора(ов): **%@**. No comment provided by engineer. Conditions of use + Условия использования No comment provided by engineer. Conditions will be accepted for enabled operators after 30 days. + Условия будут приняты для включенных операторов через 30 дней. No comment provided by engineer. Conditions will be accepted for operator(s): **%@**. + Условия будут приняты для оператора(ов): **%@**. No comment provided by engineer. Conditions will be accepted for the operator(s): **%@**. + Условия будут приняты для оператора(ов): **%@**. No comment provided by engineer. Conditions will be accepted on: %@. + Условия будут приняты: %@. No comment provided by engineer. Conditions will be automatically accepted for enabled operators on: %@. + Условия будут автоматически приняты для включенных операторов: %@. No comment provided by engineer. @@ -1810,6 +1843,7 @@ This is your own one-time link! Connection security + Безопасность соединения No comment provided by engineer. @@ -1929,6 +1963,7 @@ This is your own one-time link! Create 1-time link + Создать одноразовую ссылку No comment provided by engineer. @@ -2018,6 +2053,7 @@ This is your own one-time link! Current conditions text couldn't be loaded, you can review conditions via this link: + Текст условий использования не может быть показан, вы можете посмотреть их через ссылку: No comment provided by engineer. @@ -2217,6 +2253,7 @@ This is your own one-time link! Delete chat + Удалить разговор No comment provided by engineer. @@ -2231,6 +2268,7 @@ This is your own one-time link! Delete chat? + Удалить разговор? No comment provided by engineer. @@ -2395,6 +2433,7 @@ This is your own one-time link! Delivered even when Apple drops them. + Доставляются даже тогда, когда Apple их теряет. No comment provided by engineer. @@ -2499,6 +2538,7 @@ This is your own one-time link! Direct messages between members are prohibited in this chat. + Прямые сообщения между членами запрещены в этом разговоре. No comment provided by engineer. @@ -2684,6 +2724,7 @@ This is your own one-time link! E2E encrypted notifications. + E2E зашифрованные нотификации. No comment provided by engineer. @@ -2708,6 +2749,7 @@ This is your own one-time link! Enable Flux + Включить Flux No comment provided by engineer. @@ -2917,6 +2959,7 @@ This is your own one-time link! Error accepting conditions + Ошибка приема условий alert title @@ -2931,6 +2974,7 @@ This is your own one-time link! Error adding server + Ошибка добавления сервера alert title @@ -3075,6 +3119,7 @@ This is your own one-time link! Error loading servers + Ошибка загрузки серверов alert title @@ -3134,6 +3179,7 @@ This is your own one-time link! Error saving servers + Ошибка сохранения серверов alert title @@ -3208,6 +3254,7 @@ This is your own one-time link! Error updating server + Ошибка сохранения сервера alert title @@ -3257,6 +3304,7 @@ This is your own one-time link! Errors in servers configuration. + Ошибки в настройках серверов. servers error @@ -3463,6 +3511,7 @@ This is your own one-time link! For chat profile %@: + Для профиля чата %@: servers error @@ -3472,14 +3521,17 @@ This is your own one-time link! For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server. + Например, если Ваш контакт получает сообщения через сервер SimpleX Chat, Ваше приложение доставит их через сервер Flux. No comment provided by engineer. For private routing + Для доставки сообщений No comment provided by engineer. For social media + Для социальных сетей No comment provided by engineer. @@ -3758,10 +3810,12 @@ Error: %2$@ How it affects privacy + Как это влияет на конфиденциальность No comment provided by engineer. How it helps privacy + Как это улучшает конфиденциальность No comment provided by engineer. @@ -4059,6 +4113,7 @@ More improvements are coming soon! Invite to chat + Пригласить в разговор No comment provided by engineer. @@ -4221,10 +4276,12 @@ This is your link for group %@! Leave chat + Покинуть разговор No comment provided by engineer. Leave chat? + Покинуть разговор? No comment provided by engineer. @@ -4359,6 +4416,7 @@ This is your link for group %@! Member role will be changed to "%@". All chat members will be notified. + Роль участника будет изменена на "%@". Все участники разговора получат уведомление. No comment provided by engineer. @@ -4373,6 +4431,7 @@ This is your link for group %@! Member will be removed from chat - this cannot be undone! + Член будет удален из разговора - это действие нельзя отменить! No comment provided by engineer. @@ -4637,6 +4696,7 @@ This is your link for group %@! More reliable notifications + Более надежные уведомления No comment provided by engineer. @@ -4676,6 +4736,7 @@ This is your link for group %@! Network decentralization + Децентрализация сети No comment provided by engineer. @@ -4690,6 +4751,7 @@ This is your link for group %@! Network operator + Оператор сети No comment provided by engineer. @@ -4749,6 +4811,7 @@ This is your link for group %@! New events + Новые события notification @@ -4778,6 +4841,7 @@ This is your link for group %@! New server + Новый сервер No comment provided by engineer. @@ -4837,10 +4901,12 @@ This is your link for group %@! No media & file servers. + Нет серверов файлов и медиа. servers error No message servers. + Нет серверов сообщений. servers error @@ -4875,18 +4941,22 @@ This is your link for group %@! No servers for private message routing. + Нет серверов для доставки сообщений. servers error No servers to receive files. + Нет серверов для приема файлов. servers error No servers to receive messages. + Нет серверов для приема сообщений. servers error No servers to send files. + Нет серверов для отправки файлов. servers error @@ -4921,6 +4991,7 @@ This is your link for group %@! Notifications privacy + Конфиденциальность уведомлений No comment provided by engineer. @@ -4978,6 +5049,7 @@ Requires compatible VPN. Only chat owners can change preferences. + Только владельцы разговора могут поменять предпочтения. No comment provided by engineer. @@ -5067,6 +5139,7 @@ Requires compatible VPN. Open changes + Открыть изменения No comment provided by engineer. @@ -5081,6 +5154,7 @@ Requires compatible VPN. Open conditions + Открыть условия No comment provided by engineer. @@ -5100,14 +5174,17 @@ Requires compatible VPN. Operator + Оператор No comment provided by engineer. Operator server + Сервер оператора alert title Or import archive file + Или импортировать файл архива No comment provided by engineer. @@ -5132,6 +5209,7 @@ Requires compatible VPN. Or to share privately + Или поделиться конфиденциально No comment provided by engineer. @@ -5352,6 +5430,7 @@ Error: %@ Preset servers + Серверы по умолчанию No comment provided by engineer. @@ -5371,6 +5450,7 @@ Error: %@ Privacy for your customers. + Конфиденциальность для ваших покупателей. No comment provided by engineer. @@ -5530,10 +5610,6 @@ Enable in *Network & servers* settings. Прокси требует пароль No comment provided by engineer. - - Push Notifications - No comment provided by engineer. - Push notifications Доставка уведомлений @@ -5907,10 +5983,12 @@ Enable in *Network & servers* settings. Review conditions + Посмотреть условия No comment provided by engineer. Review later + Посмотреть позже No comment provided by engineer. @@ -5958,14 +6036,6 @@ Enable in *Network & servers* settings. Более безопасные группы No comment provided by engineer. - - Same conditions will apply to operator **%@**. - No comment provided by engineer. - - - Same conditions will apply to operator(s): **%@**. - No comment provided by engineer. - Save Сохранить @@ -6369,6 +6439,7 @@ Enable in *Network & servers* settings. Server added to operator %@. + Сервер добавлен к оператору %@. alert message @@ -6388,14 +6459,17 @@ Enable in *Network & servers* settings. Server operator changed. + Оператор серверов изменен. alert title Server operators + Операторы серверов No comment provided by engineer. Server protocol changed. + Протокол сервера изменен. alert title @@ -6526,10 +6600,12 @@ Enable in *Network & servers* settings. Share 1-time link with a friend + Поделитесь одноразовой ссылкой с другом No comment provided by engineer. Share SimpleX address on social media. + Поделитесь SimpleX адресом в социальных сетях. No comment provided by engineer. @@ -6539,6 +6615,7 @@ Enable in *Network & servers* settings. Share address publicly + Поделитесь адресом No comment provided by engineer. @@ -6663,10 +6740,12 @@ Enable in *Network & servers* settings. SimpleX address and 1-time links are safe to share via any messenger. + Адрес SimpleX и одноразовые ссылки безопасно отправлять через любой мессенджер. No comment provided by engineer. SimpleX address or 1-time link? + Адрес SimpleX или одноразовая ссылка? No comment provided by engineer. @@ -6762,6 +6841,8 @@ Enable in *Network & servers* settings. Some servers failed the test: %@ + Серверы не прошли тест: +%@ alert message @@ -6941,6 +7022,7 @@ Enable in *Network & servers* settings. Tap Create SimpleX address in the menu to create it later. + Нажмите Создать адрес SimpleX в меню, чтобы создать его позже. No comment provided by engineer. @@ -7032,6 +7114,7 @@ It can happen because of some bug or when the connection is compromised. The app protects your privacy by using different operators in each conversation. + Приложение улучшает конфиденциальность используя разных операторов в каждом разговоре. No comment provided by engineer. @@ -7051,6 +7134,7 @@ It can happen because of some bug or when the connection is compromised. The connection reached the limit of undelivered messages, your contact may be offline. + Соединение достигло предела недоставленных сообщений. Возможно, Ваш контакт не в сети. No comment provided by engineer. @@ -7113,8 +7197,19 @@ It can happen because of some bug or when the connection is compromised.Профиль отправляется только Вашим контактам. No comment provided by engineer. + + The same conditions will apply to operator **%@**. + Те же самые условия будут приняты для оператора **%@**. + No comment provided by engineer. + + + The same conditions will apply to operator(s): **%@**. + Те же самые условия будут приняты для оператора(ов): **%@**. + No comment provided by engineer. + The second preset operator in the app! + Второй оператор серверов в приложении! No comment provided by engineer. @@ -7134,6 +7229,7 @@ It can happen because of some bug or when the connection is compromised. The servers for new files of your current chat profile **%@**. + Серверы для новых файлов Вашего текущего профиля **%@**. No comment provided by engineer. @@ -7153,6 +7249,7 @@ It can happen because of some bug or when the connection is compromised. These conditions will also apply for: **%@**. + Эти условия также будут применены к: **%@**. No comment provided by engineer. @@ -7257,6 +7354,7 @@ It can happen because of some bug or when the connection is compromised. To protect against your link being replaced, you can compare contact security codes. + Чтобы защитить Вашу ссылку от замены, Вы можете сравнить код безопасности. No comment provided by engineer. @@ -7283,6 +7381,7 @@ You will be prompted to complete authentication before this feature is enabled.< To receive + Для получения No comment provided by engineer. @@ -7307,6 +7406,7 @@ You will be prompted to complete authentication before this feature is enabled.< To send + Для оправки No comment provided by engineer. @@ -7316,6 +7416,7 @@ You will be prompted to complete authentication before this feature is enabled.< To use the servers of **%@**, accept conditions of use. + Чтобы использовать серверы оператора **%@**, примите условия использования. No comment provided by engineer. @@ -7410,6 +7511,7 @@ You will be prompted to complete authentication before this feature is enabled.< Undelivered messages + Недоставленные сообщения No comment provided by engineer. @@ -7571,6 +7673,7 @@ To connect, please ask your contact to create another connection link and check Use %@ + Использовать %@ No comment provided by engineer. @@ -7600,10 +7703,12 @@ To connect, please ask your contact to create another connection link and check Use for files + Использовать для файлов No comment provided by engineer. Use for messages + Использовать для сообщений No comment provided by engineer. @@ -7648,6 +7753,7 @@ To connect, please ask your contact to create another connection link and check Use servers + Использовать серверы No comment provided by engineer. @@ -7742,6 +7848,7 @@ To connect, please ask your contact to create another connection link and check View conditions + Посмотреть условия No comment provided by engineer. @@ -7751,6 +7858,7 @@ To connect, please ask your contact to create another connection link and check View updated conditions + Посмотреть измененные условия No comment provided by engineer. @@ -7865,6 +7973,7 @@ To connect, please ask your contact to create another connection link and check When more than one operator is enabled, none of them has metadata to learn who communicates with whom. + Когда больше чем один оператор включен, ни один из них не видит метаданные, чтобы определить, кто соединен с кем. No comment provided by engineer. @@ -7964,6 +8073,7 @@ To connect, please ask your contact to create another connection link and check You are already connected with %@. + Вы уже соединены с %@. No comment provided by engineer. @@ -8030,10 +8140,12 @@ Repeat join request? You can configure operators in Network & servers settings. + Вы можете настроить операторов в настройках Сеть и серверы. No comment provided by engineer. You can configure servers via settings. + Вы можете сконфигурировать серверы через настройки. No comment provided by engineer. @@ -8078,6 +8190,7 @@ Repeat join request? You can set connection name, to remember who the link was shared with. + Вы можете установить имя соединения, чтобы запомнить кому Вы отправили ссылку. No comment provided by engineer. @@ -8239,6 +8352,7 @@ Repeat connection request? You will stop receiving messages from this chat. Chat history will be preserved. + Вы прекратите получать сообщения в этом разговоре. История будет сохранена. No comment provided by engineer. @@ -8383,6 +8497,7 @@ Repeat connection request? Your servers + Ваши серверы No comment provided by engineer. @@ -8807,6 +8922,7 @@ Repeat connection request? for better metadata privacy. + для лучшей конфиденциальности метаданных. No comment provided by engineer. @@ -9438,22 +9554,27 @@ last received msg: %2$@ %d new events + %d новых сообщений notification body From: %@ + От: %@ notification body New events + Новые события notification New messages + Новые сообщения notification New messages in %d chats + Новые сообщения в %d разговоре(ах) notification body diff --git a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff index edf32b06f2..438fae5c47 100644 --- a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff +++ b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff @@ -1232,6 +1232,10 @@ เปลี่ยน No comment provided by engineer. + + Change chat profiles + authentication reason + Change database passphrase? เปลี่ยนรหัสผ่านฐานข้อมูล? @@ -1278,10 +1282,6 @@ authentication reason set passcode view - - Change user profiles - authentication reason - Chat No comment provided by engineer. @@ -5133,10 +5133,6 @@ Enable in *Network & servers* settings. Proxy requires password No comment provided by engineer. - - Push Notifications - No comment provided by engineer. - Push notifications การแจ้งเตือนแบบทันที @@ -5527,14 +5523,6 @@ Enable in *Network & servers* settings. Safer groups No comment provided by engineer. - - Same conditions will apply to operator **%@**. - No comment provided by engineer. - - - Same conditions will apply to operator(s): **%@**. - No comment provided by engineer. - Save บันทึก @@ -6598,6 +6586,14 @@ It can happen because of some bug or when the connection is compromised.โปรไฟล์นี้แชร์กับผู้ติดต่อของคุณเท่านั้น No comment provided by engineer. + + The same conditions will apply to operator **%@**. + No comment provided by engineer. + + + The same conditions will apply to operator(s): **%@**. + No comment provided by engineer. + The second preset operator in the app! No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff index 6d69920454..5919cc4d49 100644 --- a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff @@ -1329,6 +1329,10 @@ Değiştir No comment provided by engineer. + + Change chat profiles + authentication reason + Change database passphrase? Veritabanı parolasını değiştir? @@ -1375,10 +1379,6 @@ authentication reason set passcode view - - Change user profiles - authentication reason - Chat No comment provided by engineer. @@ -5530,10 +5530,6 @@ Enable in *Network & servers* settings. Proxy şifre gerektirir No comment provided by engineer. - - Push Notifications - No comment provided by engineer. - Push notifications Anında bildirimler @@ -5958,14 +5954,6 @@ Enable in *Network & servers* settings. Daha güvenli gruplar No comment provided by engineer. - - Same conditions will apply to operator **%@**. - No comment provided by engineer. - - - Same conditions will apply to operator(s): **%@**. - No comment provided by engineer. - Save Kaydet @@ -7113,6 +7101,14 @@ Bazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir. Profil sadece kişilerinle paylaşılacak. No comment provided by engineer. + + The same conditions will apply to operator **%@**. + No comment provided by engineer. + + + The same conditions will apply to operator(s): **%@**. + No comment provided by engineer. + The second preset operator in the app! No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff index 2f1e5d96a6..72e9896872 100644 --- a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff @@ -1341,6 +1341,11 @@ Зміна No comment provided by engineer. + + Change chat profiles + Зміна профілів користувачів + authentication reason + Change database passphrase? Змінити пароль до бази даних? @@ -1387,11 +1392,6 @@ authentication reason set passcode view - - Change user profiles - Зміна профілів користувачів - authentication reason - Chat No comment provided by engineer. @@ -5590,14 +5590,9 @@ Enable in *Network & servers* settings. Проксі вимагає пароль No comment provided by engineer. - - Push Notifications - Push-сповіщення - No comment provided by engineer. - Push notifications - Push-повідомлення + Push-сповіщення No comment provided by engineer. @@ -6021,16 +6016,6 @@ Enable in *Network & servers* settings. Безпечніші групи No comment provided by engineer. - - Same conditions will apply to operator **%@**. - Такі ж умови діятимуть і для оператора **%@**. - No comment provided by engineer. - - - Same conditions will apply to operator(s): **%@**. - Такі ж умови будуть застосовуватися до оператора(ів): **%@**. - No comment provided by engineer. - Save Зберегти @@ -7191,6 +7176,16 @@ It can happen because of some bug or when the connection is compromised.Профіль доступний лише вашим контактам. No comment provided by engineer. + + The same conditions will apply to operator **%@**. + Такі ж умови діятимуть і для оператора **%@**. + No comment provided by engineer. + + + The same conditions will apply to operator(s): **%@**. + Такі ж умови будуть застосовуватися до оператора(ів): **%@**. + No comment provided by engineer. + The second preset operator in the app! Другий попередньо встановлений оператор у застосунку! diff --git a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff index a113c75603..ede559c968 100644 --- a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff +++ b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff @@ -1316,6 +1316,10 @@ 更改 No comment provided by engineer. + + Change chat profiles + authentication reason + Change database passphrase? 更改数据库密码? @@ -1362,10 +1366,6 @@ authentication reason set passcode view - - Change user profiles - authentication reason - Chat No comment provided by engineer. @@ -5485,10 +5485,6 @@ Enable in *Network & servers* settings. Proxy requires password No comment provided by engineer. - - Push Notifications - No comment provided by engineer. - Push notifications 推送通知 @@ -5911,14 +5907,6 @@ Enable in *Network & servers* settings. 更安全的群组 No comment provided by engineer. - - Same conditions will apply to operator **%@**. - No comment provided by engineer. - - - Same conditions will apply to operator(s): **%@**. - No comment provided by engineer. - Save 保存 @@ -7055,6 +7043,14 @@ It can happen because of some bug or when the connection is compromised.该资料仅与您的联系人共享。 No comment provided by engineer. + + The same conditions will apply to operator **%@**. + No comment provided by engineer. + + + The same conditions will apply to operator(s): **%@**. + No comment provided by engineer. + The second preset operator in the app! No comment provided by engineer. diff --git a/apps/ios/SimpleX NSE/ru.lproj/Localizable.strings b/apps/ios/SimpleX NSE/ru.lproj/Localizable.strings index 5ef592ec70..6ba39ccc63 100644 --- a/apps/ios/SimpleX NSE/ru.lproj/Localizable.strings +++ b/apps/ios/SimpleX NSE/ru.lproj/Localizable.strings @@ -1,7 +1,15 @@ -/* - Localizable.strings - SimpleX +/* notification body */ +"%d new events" = "%d новых сообщений"; + +/* notification body */ +"From: %@" = "От: %@"; + +/* notification */ +"New events" = "Новые события"; + +/* notification */ +"New messages" = "Новые сообщения"; + +/* notification body */ +"New messages in %d chats" = "Новые сообщения в %d разговоре(ах)"; - Created by EP on 30/07/2024. - Copyright © 2024 SimpleX Chat. All rights reserved. -*/ diff --git a/apps/ios/bg.lproj/Localizable.strings b/apps/ios/bg.lproj/Localizable.strings index a63ca87a99..41f6730fdc 100644 --- a/apps/ios/bg.lproj/Localizable.strings +++ b/apps/ios/bg.lproj/Localizable.strings @@ -1905,27 +1905,6 @@ /* No comment provided by engineer. */ "Group links" = "Групови линкове"; -/* No comment provided by engineer. */ -"Members can add message reactions." = "Членовете на групата могат да добавят реакции към съобщенията."; - -/* No comment provided by engineer. */ -"Members can irreversibly delete sent messages. (24 hours)" = "Членовете на групата могат необратимо да изтриват изпратените съобщения. (24 часа)"; - -/* No comment provided by engineer. */ -"Members can send direct messages." = "Членовете на групата могат да изпращат лични съобщения."; - -/* No comment provided by engineer. */ -"Members can send disappearing messages." = "Членовете на групата могат да изпращат изчезващи съобщения."; - -/* No comment provided by engineer. */ -"Members can send files and media." = "Членовете на групата могат да изпращат файлове и медия."; - -/* No comment provided by engineer. */ -"Members can send SimpleX links." = "Членовете на групата могат да изпращат SimpleX линкове."; - -/* No comment provided by engineer. */ -"Members can send voice messages." = "Членовете на групата могат да изпращат гласови съобщения."; - /* notification */ "Group message:" = "Групово съобщение:"; @@ -2376,6 +2355,27 @@ /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "Членът ще бъде премахнат от групата - това не може да бъде отменено!"; +/* No comment provided by engineer. */ +"Members can add message reactions." = "Членовете на групата могат да добавят реакции към съобщенията."; + +/* No comment provided by engineer. */ +"Members can irreversibly delete sent messages. (24 hours)" = "Членовете на групата могат необратимо да изтриват изпратените съобщения. (24 часа)"; + +/* No comment provided by engineer. */ +"Members can send direct messages." = "Членовете на групата могат да изпращат лични съобщения."; + +/* No comment provided by engineer. */ +"Members can send disappearing messages." = "Членовете на групата могат да изпращат изчезващи съобщения."; + +/* No comment provided by engineer. */ +"Members can send files and media." = "Членовете на групата могат да изпращат файлове и медия."; + +/* No comment provided by engineer. */ +"Members can send SimpleX links." = "Членовете на групата могат да изпращат SimpleX линкове."; + +/* No comment provided by engineer. */ +"Members can send voice messages." = "Членовете на групата могат да изпращат гласови съобщения."; + /* item status text */ "Message delivery error" = "Грешка при доставката на съобщението"; diff --git a/apps/ios/cs.lproj/Localizable.strings b/apps/ios/cs.lproj/Localizable.strings index a150c2427f..bbe754aa47 100644 --- a/apps/ios/cs.lproj/Localizable.strings +++ b/apps/ios/cs.lproj/Localizable.strings @@ -1544,24 +1544,6 @@ /* No comment provided by engineer. */ "Group links" = "Odkazy na skupiny"; -/* No comment provided by engineer. */ -"Members can add message reactions." = "Členové skupin mohou přidávat reakce na zprávy."; - -/* No comment provided by engineer. */ -"Members can irreversibly delete sent messages. (24 hours)" = "Členové skupiny mohou nevratně mazat odeslané zprávy. (24 hodin)"; - -/* No comment provided by engineer. */ -"Members can send direct messages." = "Členové skupiny mohou posílat přímé zprávy."; - -/* No comment provided by engineer. */ -"Members can send disappearing messages." = "Členové skupiny mohou posílat mizící zprávy."; - -/* No comment provided by engineer. */ -"Members can send files and media." = "Členové skupiny mohou posílat soubory a média."; - -/* No comment provided by engineer. */ -"Members can send voice messages." = "Členové skupiny mohou posílat hlasové zprávy."; - /* notification */ "Group message:" = "Skupinová zpráva:"; @@ -1934,6 +1916,24 @@ /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "Člen bude odstraněn ze skupiny - toto nelze vzít zpět!"; +/* No comment provided by engineer. */ +"Members can add message reactions." = "Členové skupin mohou přidávat reakce na zprávy."; + +/* No comment provided by engineer. */ +"Members can irreversibly delete sent messages. (24 hours)" = "Členové skupiny mohou nevratně mazat odeslané zprávy. (24 hodin)"; + +/* No comment provided by engineer. */ +"Members can send direct messages." = "Členové skupiny mohou posílat přímé zprávy."; + +/* No comment provided by engineer. */ +"Members can send disappearing messages." = "Členové skupiny mohou posílat mizící zprávy."; + +/* No comment provided by engineer. */ +"Members can send files and media." = "Členové skupiny mohou posílat soubory a média."; + +/* No comment provided by engineer. */ +"Members can send voice messages." = "Členové skupiny mohou posílat hlasové zprávy."; + /* item status text */ "Message delivery error" = "Chyba doručení zprávy"; diff --git a/apps/ios/de.lproj/Localizable.strings b/apps/ios/de.lproj/Localizable.strings index 6231000330..28cc658d30 100644 --- a/apps/ios/de.lproj/Localizable.strings +++ b/apps/ios/de.lproj/Localizable.strings @@ -892,7 +892,7 @@ "Change self-destruct passcode" = "Selbstzerstörungs-Zugangscode ändern"; /* authentication reason */ -"Change user profiles" = "Chat-Profile wechseln"; +"Change chat profiles" = "Chat-Profile wechseln"; /* chat item text */ "changed address for you" = "Wechselte die Empfängeradresse von Ihnen"; @@ -2424,27 +2424,6 @@ /* No comment provided by engineer. */ "Group links" = "Gruppen-Links"; -/* No comment provided by engineer. */ -"Members can add message reactions." = "Gruppenmitglieder können eine Reaktion auf Nachrichten geben."; - -/* No comment provided by engineer. */ -"Members can irreversibly delete sent messages. (24 hours)" = "Gruppenmitglieder können gesendete Nachrichten unwiederbringlich löschen. (24 Stunden)"; - -/* No comment provided by engineer. */ -"Members can send direct messages." = "Gruppenmitglieder können Direktnachrichten versenden."; - -/* No comment provided by engineer. */ -"Members can send disappearing messages." = "Gruppenmitglieder können verschwindende Nachrichten senden."; - -/* No comment provided by engineer. */ -"Members can send files and media." = "Gruppenmitglieder können Dateien und Medien senden."; - -/* No comment provided by engineer. */ -"Members can send SimpleX links." = "Gruppenmitglieder können SimpleX-Links senden."; - -/* No comment provided by engineer. */ -"Members can send voice messages." = "Gruppenmitglieder können Sprachnachrichten versenden."; - /* notification */ "Group message:" = "Grppennachricht:"; @@ -2934,6 +2913,27 @@ /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "Das Mitglied wird aus der Gruppe entfernt. Dies kann nicht rückgängig gemacht werden!"; +/* No comment provided by engineer. */ +"Members can add message reactions." = "Gruppenmitglieder können eine Reaktion auf Nachrichten geben."; + +/* No comment provided by engineer. */ +"Members can irreversibly delete sent messages. (24 hours)" = "Gruppenmitglieder können gesendete Nachrichten unwiederbringlich löschen. (24 Stunden)"; + +/* No comment provided by engineer. */ +"Members can send direct messages." = "Gruppenmitglieder können Direktnachrichten versenden."; + +/* No comment provided by engineer. */ +"Members can send disappearing messages." = "Gruppenmitglieder können verschwindende Nachrichten senden."; + +/* No comment provided by engineer. */ +"Members can send files and media." = "Gruppenmitglieder können Dateien und Medien senden."; + +/* No comment provided by engineer. */ +"Members can send SimpleX links." = "Gruppenmitglieder können SimpleX-Links senden."; + +/* No comment provided by engineer. */ +"Members can send voice messages." = "Gruppenmitglieder können Sprachnachrichten versenden."; + /* No comment provided by engineer. */ "Menus" = "Menüs"; @@ -3671,9 +3671,6 @@ /* No comment provided by engineer. */ "Push notifications" = "Push-Benachrichtigungen"; -/* No comment provided by engineer. */ -"Push Notifications" = "Push-Benachrichtigungen"; - /* No comment provided by engineer. */ "Push server" = "Push-Server"; @@ -3949,10 +3946,10 @@ "Safer groups" = "Sicherere Gruppen"; /* No comment provided by engineer. */ -"Same conditions will apply to operator **%@**." = "Dieselben Nutzungsbedingungen gelten auch für den Betreiber **%@**."; +"The same conditions will apply to operator **%@**." = "Dieselben Nutzungsbedingungen gelten auch für den Betreiber **%@**."; /* No comment provided by engineer. */ -"Same conditions will apply to operator(s): **%@**." = "Dieselben Nutzungsbedingungen gelten auch für den/die Betreiber: **%@**."; +"The same conditions will apply to operator(s): **%@**." = "Dieselben Nutzungsbedingungen gelten auch für den/die Betreiber: **%@**."; /* alert button chat item action */ diff --git a/apps/ios/es.lproj/Localizable.strings b/apps/ios/es.lproj/Localizable.strings index d15e6d75ce..1ccb679069 100644 --- a/apps/ios/es.lproj/Localizable.strings +++ b/apps/ios/es.lproj/Localizable.strings @@ -892,7 +892,7 @@ "Change self-destruct passcode" = "Cambiar código autodestrucción"; /* authentication reason */ -"Change user profiles" = "Cambiar perfil de usuario"; +"Change chat profiles" = "Cambiar perfil de usuario"; /* chat item text */ "changed address for you" = "ha cambiado tu servidor de envío"; @@ -2424,27 +2424,6 @@ /* No comment provided by engineer. */ "Group links" = "Enlaces de grupo"; -/* No comment provided by engineer. */ -"Members can add message reactions." = "Los miembros pueden añadir reacciones a los mensajes."; - -/* No comment provided by engineer. */ -"Members can irreversibly delete sent messages. (24 hours)" = "Los miembros del grupo pueden eliminar mensajes de forma irreversible. (24 horas)"; - -/* No comment provided by engineer. */ -"Members can send direct messages." = "Los miembros del grupo pueden enviar mensajes directos."; - -/* No comment provided by engineer. */ -"Members can send disappearing messages." = "Los miembros del grupo pueden enviar mensajes temporales."; - -/* No comment provided by engineer. */ -"Members can send files and media." = "Los miembros del grupo pueden enviar archivos y multimedia."; - -/* No comment provided by engineer. */ -"Members can send SimpleX links." = "Los miembros del grupo pueden enviar enlaces SimpleX."; - -/* No comment provided by engineer. */ -"Members can send voice messages." = "Los miembros del grupo pueden enviar mensajes de voz."; - /* notification */ "Group message:" = "Mensaje de grupo:"; @@ -2934,6 +2913,27 @@ /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "El miembro será expulsado del grupo. ¡No podrá deshacerse!"; +/* No comment provided by engineer. */ +"Members can add message reactions." = "Los miembros pueden añadir reacciones a los mensajes."; + +/* No comment provided by engineer. */ +"Members can irreversibly delete sent messages. (24 hours)" = "Los miembros del grupo pueden eliminar mensajes de forma irreversible. (24 horas)"; + +/* No comment provided by engineer. */ +"Members can send direct messages." = "Los miembros del grupo pueden enviar mensajes directos."; + +/* No comment provided by engineer. */ +"Members can send disappearing messages." = "Los miembros del grupo pueden enviar mensajes temporales."; + +/* No comment provided by engineer. */ +"Members can send files and media." = "Los miembros del grupo pueden enviar archivos y multimedia."; + +/* No comment provided by engineer. */ +"Members can send SimpleX links." = "Los miembros del grupo pueden enviar enlaces SimpleX."; + +/* No comment provided by engineer. */ +"Members can send voice messages." = "Los miembros del grupo pueden enviar mensajes de voz."; + /* No comment provided by engineer. */ "Menus" = "Menus"; @@ -3669,10 +3669,7 @@ "Proxy requires password" = "El proxy requiere contraseña"; /* No comment provided by engineer. */ -"Push notifications" = "Notificaciones automáticas"; - -/* No comment provided by engineer. */ -"Push Notifications" = "Notificaciones push"; +"Push notifications" = "Notificaciones push"; /* No comment provided by engineer. */ "Push server" = "Servidor push"; @@ -3949,10 +3946,10 @@ "Safer groups" = "Grupos más seguros"; /* No comment provided by engineer. */ -"Same conditions will apply to operator **%@**." = "Las mismas condiciones se aplicarán al operador **%@**."; +"The same conditions will apply to operator **%@**." = "Las mismas condiciones se aplicarán al operador **%@**."; /* No comment provided by engineer. */ -"Same conditions will apply to operator(s): **%@**." = "Las mismas condiciones se aplicarán a el/los operador(es) **%@**."; +"The same conditions will apply to operator(s): **%@**." = "Las mismas condiciones se aplicarán a el/los operador(es) **%@**."; /* alert button chat item action */ diff --git a/apps/ios/fi.lproj/Localizable.strings b/apps/ios/fi.lproj/Localizable.strings index 8946be02b4..486c0e7650 100644 --- a/apps/ios/fi.lproj/Localizable.strings +++ b/apps/ios/fi.lproj/Localizable.strings @@ -1520,24 +1520,6 @@ /* No comment provided by engineer. */ "Group links" = "Ryhmälinkit"; -/* No comment provided by engineer. */ -"Members can add message reactions." = "Ryhmän jäsenet voivat lisätä viestireaktioita."; - -/* No comment provided by engineer. */ -"Members can irreversibly delete sent messages. (24 hours)" = "Ryhmän jäsenet voivat poistaa lähetetyt viestit peruuttamattomasti. (24 tuntia)"; - -/* No comment provided by engineer. */ -"Members can send direct messages." = "Ryhmän jäsenet voivat lähettää suoraviestejä."; - -/* No comment provided by engineer. */ -"Members can send disappearing messages." = "Ryhmän jäsenet voivat lähettää katoavia viestejä."; - -/* No comment provided by engineer. */ -"Members can send files and media." = "Ryhmän jäsenet voivat lähettää tiedostoja ja mediaa."; - -/* No comment provided by engineer. */ -"Members can send voice messages." = "Ryhmän jäsenet voivat lähettää ääniviestejä."; - /* notification */ "Group message:" = "Ryhmäviesti:"; @@ -1910,6 +1892,24 @@ /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "Jäsen poistetaan ryhmästä - tätä ei voi perua!"; +/* No comment provided by engineer. */ +"Members can add message reactions." = "Ryhmän jäsenet voivat lisätä viestireaktioita."; + +/* No comment provided by engineer. */ +"Members can irreversibly delete sent messages. (24 hours)" = "Ryhmän jäsenet voivat poistaa lähetetyt viestit peruuttamattomasti. (24 tuntia)"; + +/* No comment provided by engineer. */ +"Members can send direct messages." = "Ryhmän jäsenet voivat lähettää suoraviestejä."; + +/* No comment provided by engineer. */ +"Members can send disappearing messages." = "Ryhmän jäsenet voivat lähettää katoavia viestejä."; + +/* No comment provided by engineer. */ +"Members can send files and media." = "Ryhmän jäsenet voivat lähettää tiedostoja ja mediaa."; + +/* No comment provided by engineer. */ +"Members can send voice messages." = "Ryhmän jäsenet voivat lähettää ääniviestejä."; + /* item status text */ "Message delivery error" = "Viestin toimitusvirhe"; diff --git a/apps/ios/fr.lproj/Localizable.strings b/apps/ios/fr.lproj/Localizable.strings index f1a8e97758..1a9e289404 100644 --- a/apps/ios/fr.lproj/Localizable.strings +++ b/apps/ios/fr.lproj/Localizable.strings @@ -2301,27 +2301,6 @@ /* No comment provided by engineer. */ "Group links" = "Liens de groupe"; -/* No comment provided by engineer. */ -"Members can add message reactions." = "Les membres du groupe peuvent ajouter des réactions aux messages."; - -/* No comment provided by engineer. */ -"Members can irreversibly delete sent messages. (24 hours)" = "Les membres du groupe peuvent supprimer de manière irréversible les messages envoyés. (24 heures)"; - -/* No comment provided by engineer. */ -"Members can send direct messages." = "Les membres du groupe peuvent envoyer des messages directs."; - -/* No comment provided by engineer. */ -"Members can send disappearing messages." = "Les membres du groupes peuvent envoyer des messages éphémères."; - -/* No comment provided by engineer. */ -"Members can send files and media." = "Les membres du groupe peuvent envoyer des fichiers et des médias."; - -/* No comment provided by engineer. */ -"Members can send SimpleX links." = "Les membres du groupe peuvent envoyer des liens SimpleX."; - -/* No comment provided by engineer. */ -"Members can send voice messages." = "Les membres du groupe peuvent envoyer des messages vocaux."; - /* notification */ "Group message:" = "Message du groupe :"; @@ -2805,6 +2784,27 @@ /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "Ce membre sera retiré du groupe - impossible de revenir en arrière !"; +/* No comment provided by engineer. */ +"Members can add message reactions." = "Les membres du groupe peuvent ajouter des réactions aux messages."; + +/* No comment provided by engineer. */ +"Members can irreversibly delete sent messages. (24 hours)" = "Les membres du groupe peuvent supprimer de manière irréversible les messages envoyés. (24 heures)"; + +/* No comment provided by engineer. */ +"Members can send direct messages." = "Les membres du groupe peuvent envoyer des messages directs."; + +/* No comment provided by engineer. */ +"Members can send disappearing messages." = "Les membres du groupes peuvent envoyer des messages éphémères."; + +/* No comment provided by engineer. */ +"Members can send files and media." = "Les membres du groupe peuvent envoyer des fichiers et des médias."; + +/* No comment provided by engineer. */ +"Members can send SimpleX links." = "Les membres du groupe peuvent envoyer des liens SimpleX."; + +/* No comment provided by engineer. */ +"Members can send voice messages." = "Les membres du groupe peuvent envoyer des messages vocaux."; + /* No comment provided by engineer. */ "Menus" = "Menus"; diff --git a/apps/ios/hu.lproj/Localizable.strings b/apps/ios/hu.lproj/Localizable.strings index c1008ad30b..4893d5a13f 100644 --- a/apps/ios/hu.lproj/Localizable.strings +++ b/apps/ios/hu.lproj/Localizable.strings @@ -388,6 +388,9 @@ /* No comment provided by engineer. */ "Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." = "Cím hozzáadása a profilhoz, hogy az ismerősei megoszthassák másokkal. A profilfrissítés elküldésre kerül az ismerősei számára."; +/* No comment provided by engineer. */ +"Add friends" = "Barátok hozzáadása"; + /* No comment provided by engineer. */ "Add profile" = "Profil hozzáadása"; @@ -397,12 +400,18 @@ /* No comment provided by engineer. */ "Add servers by scanning QR codes." = "Kiszolgáló hozzáadása QR-kód beolvasásával."; +/* No comment provided by engineer. */ +"Add team members" = "Csapattagok hozzáadása"; + /* No comment provided by engineer. */ "Add to another device" = "Hozzáadás egy másik eszközhöz"; /* No comment provided by engineer. */ "Add welcome message" = "Üdvözlőüzenet hozzáadása"; +/* No comment provided by engineer. */ +"Add your team members to the conversations." = "Adja hozzá csapattagjait a beszélgetésekhez."; + /* No comment provided by engineer. */ "Added media & file servers" = "Hozzáadott média- és fájlkiszolgálók"; @@ -770,7 +779,7 @@ "Blur for better privacy." = "Elhomályosítás a jobb adatvédelemért."; /* No comment provided by engineer. */ -"Blur media" = "Média elhomályosítása"; +"Blur media" = "Médiatartalom elhomályosítása"; /* No comment provided by engineer. */ "bold" = "félkövér"; @@ -793,6 +802,12 @@ /* No comment provided by engineer. */ "Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" = "Bolgár, finn, thai és ukrán – köszönet a felhasználóknak és a [Weblate-nek](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"; +/* No comment provided by engineer. */ +"Business address" = "Üzleti cím"; + +/* No comment provided by engineer. */ +"Business chats" = "Üzleti csevegések"; + /* 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)." = "A csevegési profillal (alapértelmezett), vagy a [kapcsolattal] (https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BÉTA)."; @@ -892,7 +907,7 @@ "Change self-destruct passcode" = "Önmegsemmisító jelkód megváltoztatása"; /* authentication reason */ -"Change user profiles" = "Felhasználói profilok megváltoztatása"; +"Change chat profiles" = "Felhasználói profilok megváltoztatása"; /* chat item text */ "changed address for you" = "cím megváltoztatva"; @@ -909,6 +924,15 @@ /* chat item text */ "changing address…" = "cím megváltoztatása…"; +/* No comment provided by engineer. */ +"Chat" = "Csevegés"; + +/* No comment provided by engineer. */ +"Chat already exists" = "A csevegés már létezik"; + +/* No comment provided by engineer. */ +"Chat already exists!" = "A csevegés már létezik!"; + /* No comment provided by engineer. */ "Chat colors" = "Csevegés színei"; @@ -954,6 +978,12 @@ /* No comment provided by engineer. */ "Chat theme" = "Csevegés témája"; +/* No comment provided by engineer. */ +"Chat will be deleted for all members - this cannot be undone!" = "A csevegés minden tag számára törlésre kerül - ezt a műveletet nem lehet visszavonni!"; + +/* No comment provided by engineer. */ +"Chat will be deleted for you - this cannot be undone!" = "A csevegés törlésre kerül az Ön számára - ezt a műveletet nem lehet visszavonni!"; + /* No comment provided by engineer. */ "Chats" = "Csevegések"; @@ -1475,12 +1505,18 @@ /* No comment provided by engineer. */ "Delete and notify contact" = "Törlés, és az ismerős értesítése"; +/* No comment provided by engineer. */ +"Delete chat" = "Csevegés törlése"; + /* No comment provided by engineer. */ "Delete chat profile" = "Csevegési profil törlése"; /* No comment provided by engineer. */ "Delete chat profile?" = "Csevegési profil törlése?"; +/* No comment provided by engineer. */ +"Delete chat?" = "Csevegés törlése?"; + /* No comment provided by engineer. */ "Delete connection" = "Kapcsolat törlése"; @@ -1655,6 +1691,9 @@ /* chat feature */ "Direct messages" = "Közvetlen üzenetek"; +/* No comment provided by engineer. */ +"Direct messages between members are prohibited in this chat." = "A tagok közötti közvetlen üzenetek le vannak tiltva ebben a csevegésben."; + /* No comment provided by engineer. */ "Direct messages between members are prohibited." = "A közvetlen üzenetek küldése a tagok között le van tiltva ebben a csoportban."; @@ -2424,27 +2463,6 @@ /* No comment provided by engineer. */ "Group links" = "Csoporthivatkozások"; -/* No comment provided by engineer. */ -"Members can add message reactions." = "Csoporttagok üzenetreakciókat adhatnak hozzá."; - -/* No comment provided by engineer. */ -"Members can irreversibly delete sent messages. (24 hours)" = "A csoport tagjai véglegesen törölhetik az elküldött üzeneteiket. (24 óra)"; - -/* No comment provided by engineer. */ -"Members can send direct messages." = "A csoport tagjai küldhetnek egymásnak közvetlen üzeneteket."; - -/* No comment provided by engineer. */ -"Members can send disappearing messages." = "A csoport tagjai küldhetnek eltűnő üzeneteket."; - -/* No comment provided by engineer. */ -"Members can send files and media." = "A csoport tagjai küldhetnek fájlokat és médiatartalmakat."; - -/* No comment provided by engineer. */ -"Members can send SimpleX links." = "A csoport tagjai küldhetnek SimpleX-hivatkozásokat."; - -/* No comment provided by engineer. */ -"Members can send voice messages." = "A csoport tagjai küldhetnek hangüzeneteket."; - /* notification */ "Group message:" = "Csoport üzenet:"; @@ -2715,6 +2733,9 @@ /* No comment provided by engineer. */ "Invite members" = "Tagok meghívása"; +/* No comment provided by engineer. */ +"Invite to chat" = "Meghívás a csevegésbe"; + /* No comment provided by engineer. */ "Invite to group" = "Meghívás a csoportba"; @@ -2829,6 +2850,12 @@ /* swipe action */ "Leave" = "Elhagyás"; +/* No comment provided by engineer. */ +"Leave chat" = "Csevegés elhagyása"; + +/* No comment provided by engineer. */ +"Leave chat?" = "Csevegés elhagyása?"; + /* No comment provided by engineer. */ "Leave group" = "Csoport elhagyása"; @@ -2925,15 +2952,42 @@ /* item status text */ "Member inactive" = "Inaktív tag"; +/* No comment provided by engineer. */ +"Member role will be changed to \"%@\". All chat members will be notified." = "A tag szerepeköre meg fog változni a következőre: \"%@\". A csevegés tagjai értesítést fognak kapni."; + /* No comment provided by engineer. */ "Member role will be changed to \"%@\". All group members will be notified." = "A tag szerepköre meg fog változni erre: „%@”. A csoportban az összes tag értesítve lesz."; /* No comment provided by engineer. */ "Member role will be changed to \"%@\". The member will receive a new invitation." = "A tag szerepköre meg fog változni erre: „%@”. A tag új meghívást fog kapni."; +/* No comment provided by engineer. */ +"Member will be removed from chat - this cannot be undone!" = "A tag el lesz távolítva a csevegésből - ezt a műveletet nem lehet visszavonni!"; + /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "A tag eltávolítása a csoportból - ez a művelet nem vonható vissza!"; +/* No comment provided by engineer. */ +"Members can add message reactions." = "Csoporttagok üzenetreakciókat adhatnak hozzá."; + +/* No comment provided by engineer. */ +"Members can irreversibly delete sent messages. (24 hours)" = "A csoport tagjai véglegesen törölhetik az elküldött üzeneteiket. (24 óra)"; + +/* No comment provided by engineer. */ +"Members can send direct messages." = "A csoport tagjai küldhetnek egymásnak közvetlen üzeneteket."; + +/* No comment provided by engineer. */ +"Members can send disappearing messages." = "A csoport tagjai küldhetnek eltűnő üzeneteket."; + +/* No comment provided by engineer. */ +"Members can send files and media." = "A csoport tagjai küldhetnek fájlokat és médiatartalmakat."; + +/* No comment provided by engineer. */ +"Members can send SimpleX links." = "A csoport tagjai küldhetnek SimpleX-hivatkozásokat."; + +/* No comment provided by engineer. */ +"Members can send voice messages." = "A csoport tagjai küldhetnek hangüzeneteket."; + /* No comment provided by engineer. */ "Menus" = "Menük"; @@ -3329,6 +3383,9 @@ /* No comment provided by engineer. */ "Onion hosts will not be used." = "Onion-kiszolgálók nem lesznek használva."; +/* No comment provided by engineer. */ +"Only chat owners can change preferences." = "Csak a csevegés tulajdonosai módosíthatják a beállításokat."; + /* No comment provided by engineer. */ "Only client devices store user profiles, contacts, groups, and messages." = "Csak az eszközök alkalmazásai tárolják a felhasználó-profilokat, névjegyeket, csoportokat és a **2 rétegű végpontok közötti titkosítással** küldött üzeneteket."; @@ -3407,6 +3464,9 @@ /* alert title */ "Operator server" = "Kiszolgáló üzemeltető"; +/* No comment provided by engineer. */ +"Or import archive file" = "Vagy archívumfájl importálása"; + /* No comment provided by engineer. */ "Or paste archive link" = "Vagy az archívum hivatkozásának beillesztése"; @@ -3575,6 +3635,9 @@ /* No comment provided by engineer. */ "Privacy & security" = "Adatvédelem és biztonság"; +/* No comment provided by engineer. */ +"Privacy for your customers." = "Az Ön ügyfeleinek adatvédelme."; + /* No comment provided by engineer. */ "Privacy redefined" = "Adatvédelem újraértelmezve"; @@ -3671,9 +3734,6 @@ /* No comment provided by engineer. */ "Push notifications" = "Push-értesítések"; -/* No comment provided by engineer. */ -"Push Notifications" = "Push értesítések"; - /* No comment provided by engineer. */ "Push server" = "Push-kiszolgáló"; @@ -3949,10 +4009,10 @@ "Safer groups" = "Biztonságosabb csoportok"; /* No comment provided by engineer. */ -"Same conditions will apply to operator **%@**." = "Ugyanezek a feltételek vonatkoznak a következő üzemeltetőre is: **%@**."; +"The same conditions will apply to operator **%@**." = "Ugyanezek a feltételek vonatkoznak a következő üzemeltetőre is: **%@**."; /* No comment provided by engineer. */ -"Same conditions will apply to operator(s): **%@**." = "Ugyanezek a feltételek lesznek elfogadva a következő üzemeltető(k)re is: **%@**."; +"The same conditions will apply to operator(s): **%@**." = "Ugyanezek a feltételek lesznek elfogadva a következő üzemeltető(k)re is: **%@**."; /* alert button chat item action */ @@ -4007,10 +4067,10 @@ "Saved" = "Mentett"; /* No comment provided by engineer. */ -"Saved from" = "Mentve innen:"; +"Saved from" = "Elmentve innen:"; /* No comment provided by engineer. */ -"saved from %@" = "mentve innen: %@"; +"saved from %@" = "elmentve innen: %@"; /* message info title */ "Saved message" = "Mentett üzenet"; @@ -4580,6 +4640,9 @@ /* No comment provided by engineer. */ "Tap button " = "Koppintson a "; +/* No comment provided by engineer. */ +"Tap Create SimpleX address in the menu to create it later." = "Koppintson a SimpleX-cím létrehozása menüpontra a későbbi létrehozáshoz."; + /* No comment provided by engineer. */ "Tap to activate profile." = "A profil aktiválásához koppintson az ikonra."; @@ -5282,6 +5345,9 @@ /* No comment provided by engineer. */ "You are already connected to %@." = "Ön már kapcsolódva van ehhez: %@."; +/* No comment provided by engineer. */ +"You are already connected with %@." = "Ön már kapcsolódva van vele: %@."; + /* No comment provided by engineer. */ "You are already connecting to %@." = "Már folyamatban van a kapcsolódás ehhez: %@."; @@ -5480,6 +5546,9 @@ /* No comment provided by engineer. */ "You will still receive calls and notifications from muted profiles when they are active." = "Továbbra is kap hívásokat és értesítéseket a némított profiloktól, ha azok aktívak."; +/* No comment provided by engineer. */ +"You will stop receiving messages from this chat. Chat history will be preserved." = "Ön nem fog több üzenetet kapni ebből a csevegésből, de a csevegés előzményei megmaradnak."; + /* No comment provided by engineer. */ "You will stop receiving messages from this group. Chat history will be preserved." = "Ettől a csoporttól nem fog értesítéseket kapni. A csevegési előzmények megmaradnak."; diff --git a/apps/ios/it.lproj/Localizable.strings b/apps/ios/it.lproj/Localizable.strings index 42f399b710..2fe1216f35 100644 --- a/apps/ios/it.lproj/Localizable.strings +++ b/apps/ios/it.lproj/Localizable.strings @@ -388,6 +388,9 @@ /* No comment provided by engineer. */ "Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." = "Aggiungi l'indirizzo al tuo profilo, in modo che i tuoi contatti possano condividerlo con altre persone. L'aggiornamento del profilo verrà inviato ai tuoi contatti."; +/* No comment provided by engineer. */ +"Add friends" = "Aggiungi amici"; + /* No comment provided by engineer. */ "Add profile" = "Aggiungi profilo"; @@ -397,12 +400,18 @@ /* No comment provided by engineer. */ "Add servers by scanning QR codes." = "Aggiungi server scansionando codici QR."; +/* No comment provided by engineer. */ +"Add team members" = "Aggiungi membri del team"; + /* No comment provided by engineer. */ "Add to another device" = "Aggiungi ad un altro dispositivo"; /* No comment provided by engineer. */ "Add welcome message" = "Aggiungi messaggio di benvenuto"; +/* No comment provided by engineer. */ +"Add your team members to the conversations." = "Aggiungi i membri del tuo team alle conversazioni."; + /* No comment provided by engineer. */ "Added media & file servers" = "Server di multimediali e file aggiunti"; @@ -793,6 +802,12 @@ /* No comment provided by engineer. */ "Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" = "Bulgaro, finlandese, tailandese e ucraino - grazie agli utenti e a [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"; +/* No comment provided by engineer. */ +"Business address" = "Indirizzo di lavoro"; + +/* No comment provided by engineer. */ +"Business chats" = "Chat di lavoro"; + /* 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)." = "Per profilo di chat (predefinito) o [per connessione](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)."; @@ -892,7 +907,7 @@ "Change self-destruct passcode" = "Cambia codice di autodistruzione"; /* authentication reason */ -"Change user profiles" = "Modifica profili utente"; +"Change chat profiles" = "Modifica profili utente"; /* chat item text */ "changed address for you" = "indirizzo cambiato per te"; @@ -909,6 +924,15 @@ /* chat item text */ "changing address…" = "cambio indirizzo…"; +/* No comment provided by engineer. */ +"Chat" = "Chat"; + +/* No comment provided by engineer. */ +"Chat already exists" = "La chat esiste già"; + +/* No comment provided by engineer. */ +"Chat already exists!" = "La chat esiste già!"; + /* No comment provided by engineer. */ "Chat colors" = "Colori della chat"; @@ -954,6 +978,12 @@ /* No comment provided by engineer. */ "Chat theme" = "Tema della chat"; +/* No comment provided by engineer. */ +"Chat will be deleted for all members - this cannot be undone!" = "La chat verrà eliminata per tutti i membri, non è reversibile!"; + +/* No comment provided by engineer. */ +"Chat will be deleted for you - this cannot be undone!" = "La chat verrà eliminata solo per te, non è reversibile!"; + /* No comment provided by engineer. */ "Chats" = "Chat"; @@ -1475,12 +1505,18 @@ /* No comment provided by engineer. */ "Delete and notify contact" = "Elimina e avvisa il contatto"; +/* No comment provided by engineer. */ +"Delete chat" = "Elimina chat"; + /* No comment provided by engineer. */ "Delete chat profile" = "Elimina il profilo di chat"; /* No comment provided by engineer. */ "Delete chat profile?" = "Eliminare il profilo di chat?"; +/* No comment provided by engineer. */ +"Delete chat?" = "Eliminare la chat?"; + /* No comment provided by engineer. */ "Delete connection" = "Elimina connessione"; @@ -1655,6 +1691,9 @@ /* chat feature */ "Direct messages" = "Messaggi diretti"; +/* No comment provided by engineer. */ +"Direct messages between members are prohibited in this chat." = "I messaggi diretti tra i membri sono vietati in questa chat."; + /* No comment provided by engineer. */ "Direct messages between members are prohibited." = "I messaggi diretti tra i membri sono vietati in questo gruppo."; @@ -2424,27 +2463,6 @@ /* No comment provided by engineer. */ "Group links" = "Link del gruppo"; -/* No comment provided by engineer. */ -"Members can add message reactions." = "I membri del gruppo possono aggiungere reazioni ai messaggi."; - -/* No comment provided by engineer. */ -"Members can irreversibly delete sent messages. (24 hours)" = "I membri del gruppo possono eliminare irreversibilmente i messaggi inviati. (24 ore)"; - -/* No comment provided by engineer. */ -"Members can send direct messages." = "I membri del gruppo possono inviare messaggi diretti."; - -/* No comment provided by engineer. */ -"Members can send disappearing messages." = "I membri del gruppo possono inviare messaggi a tempo."; - -/* No comment provided by engineer. */ -"Members can send files and media." = "I membri del gruppo possono inviare file e contenuti multimediali."; - -/* No comment provided by engineer. */ -"Members can send SimpleX links." = "I membri del gruppo possono inviare link di Simplex."; - -/* No comment provided by engineer. */ -"Members can send voice messages." = "I membri del gruppo possono inviare messaggi vocali."; - /* notification */ "Group message:" = "Messaggio del gruppo:"; @@ -2715,6 +2733,9 @@ /* No comment provided by engineer. */ "Invite members" = "Invita membri"; +/* No comment provided by engineer. */ +"Invite to chat" = "Invita in chat"; + /* No comment provided by engineer. */ "Invite to group" = "Invita al gruppo"; @@ -2829,6 +2850,12 @@ /* swipe action */ "Leave" = "Esci"; +/* No comment provided by engineer. */ +"Leave chat" = "Esci dalla chat"; + +/* No comment provided by engineer. */ +"Leave chat?" = "Uscire dalla chat?"; + /* No comment provided by engineer. */ "Leave group" = "Esci dal gruppo"; @@ -2925,15 +2952,42 @@ /* item status text */ "Member inactive" = "Membro inattivo"; +/* No comment provided by engineer. */ +"Member role will be changed to \"%@\". All chat members will be notified." = "Il ruolo del membro verrà cambiato in \"%@\". Verranno notificati tutti i membri della chat."; + /* No comment provided by engineer. */ "Member role will be changed to \"%@\". All group members will be notified." = "Il ruolo del membro verrà cambiato in \"%@\". Tutti i membri del gruppo verranno avvisati."; /* No comment provided by engineer. */ "Member role will be changed to \"%@\". The member will receive a new invitation." = "Il ruolo del membro verrà cambiato in \"%@\". Il membro riceverà un invito nuovo."; +/* No comment provided by engineer. */ +"Member will be removed from chat - this cannot be undone!" = "Il membro verrà rimosso dalla chat, non è reversibile!"; + /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "Il membro verrà rimosso dal gruppo, non è reversibile!"; +/* No comment provided by engineer. */ +"Members can add message reactions." = "I membri del gruppo possono aggiungere reazioni ai messaggi."; + +/* No comment provided by engineer. */ +"Members can irreversibly delete sent messages. (24 hours)" = "I membri del gruppo possono eliminare irreversibilmente i messaggi inviati. (24 ore)"; + +/* No comment provided by engineer. */ +"Members can send direct messages." = "I membri del gruppo possono inviare messaggi diretti."; + +/* No comment provided by engineer. */ +"Members can send disappearing messages." = "I membri del gruppo possono inviare messaggi a tempo."; + +/* No comment provided by engineer. */ +"Members can send files and media." = "I membri del gruppo possono inviare file e contenuti multimediali."; + +/* No comment provided by engineer. */ +"Members can send SimpleX links." = "I membri del gruppo possono inviare link di Simplex."; + +/* No comment provided by engineer. */ +"Members can send voice messages." = "I membri del gruppo possono inviare messaggi vocali."; + /* No comment provided by engineer. */ "Menus" = "Menu"; @@ -3329,6 +3383,9 @@ /* No comment provided by engineer. */ "Onion hosts will not be used." = "Gli host Onion non verranno usati."; +/* No comment provided by engineer. */ +"Only chat owners can change preferences." = "Solo i proprietari della chat possono modificarne le preferenze."; + /* No comment provided by engineer. */ "Only client devices store user profiles, contacts, groups, and messages." = "Solo i dispositivi client archiviano profili utente, i contatti, i gruppi e i messaggi inviati con la **crittografia end-to-end a 2 livelli**."; @@ -3407,6 +3464,9 @@ /* alert title */ "Operator server" = "Server dell'operatore"; +/* No comment provided by engineer. */ +"Or import archive file" = "O importa file archivio"; + /* No comment provided by engineer. */ "Or paste archive link" = "O incolla il link dell'archivio"; @@ -3575,6 +3635,9 @@ /* No comment provided by engineer. */ "Privacy & security" = "Privacy e sicurezza"; +/* No comment provided by engineer. */ +"Privacy for your customers." = "Privacy per i tuoi clienti."; + /* No comment provided by engineer. */ "Privacy redefined" = "Privacy ridefinita"; @@ -3671,9 +3734,6 @@ /* No comment provided by engineer. */ "Push notifications" = "Notifiche push"; -/* No comment provided by engineer. */ -"Push Notifications" = "Notifiche push"; - /* No comment provided by engineer. */ "Push server" = "Server push"; @@ -3949,10 +4009,10 @@ "Safer groups" = "Gruppi più sicuri"; /* No comment provided by engineer. */ -"Same conditions will apply to operator **%@**." = "Le stesse condizioni si applicheranno all'operatore **%@**."; +"The same conditions will apply to operator **%@**." = "Le stesse condizioni si applicheranno all'operatore **%@**."; /* No comment provided by engineer. */ -"Same conditions will apply to operator(s): **%@**." = "Le stesse condizioni si applicheranno agli operatori **%@**."; +"The same conditions will apply to operator(s): **%@**." = "Le stesse condizioni si applicheranno agli operatori **%@**."; /* alert button chat item action */ @@ -4580,6 +4640,9 @@ /* No comment provided by engineer. */ "Tap button " = "Tocca il pulsante "; +/* No comment provided by engineer. */ +"Tap Create SimpleX address in the menu to create it later." = "Tocca \"Crea indirizzo SimpleX\" nel menu per crearlo più tardi."; + /* No comment provided by engineer. */ "Tap to activate profile." = "Tocca per attivare il profilo."; @@ -5282,6 +5345,9 @@ /* No comment provided by engineer. */ "You are already connected to %@." = "Sei già connesso/a a %@."; +/* No comment provided by engineer. */ +"You are already connected with %@." = "Sei già connesso/a con %@."; + /* No comment provided by engineer. */ "You are already connecting to %@." = "Ti stai già connettendo a %@."; @@ -5480,6 +5546,9 @@ /* No comment provided by engineer. */ "You will still receive calls and notifications from muted profiles when they are active." = "Continuerai a ricevere chiamate e notifiche da profili silenziati quando sono attivi."; +/* No comment provided by engineer. */ +"You will stop receiving messages from this chat. Chat history will be preserved." = "Non riceverai più messaggi da questa chat. La cronologia della chat verrà conservata."; + /* No comment provided by engineer. */ "You will stop receiving messages from this group. Chat history will be preserved." = "Non riceverai più messaggi da questo gruppo. La cronologia della chat verrà conservata."; diff --git a/apps/ios/ja.lproj/Localizable.strings b/apps/ios/ja.lproj/Localizable.strings index 019472b804..06fa3f70b3 100644 --- a/apps/ios/ja.lproj/Localizable.strings +++ b/apps/ios/ja.lproj/Localizable.strings @@ -1661,24 +1661,6 @@ /* No comment provided by engineer. */ "Group links" = "グループのリンク"; -/* No comment provided by engineer. */ -"Members can add message reactions." = "グループメンバーはメッセージへのリアクションを追加できます。"; - -/* No comment provided by engineer. */ -"Members can irreversibly delete sent messages. (24 hours)" = "グループのメンバーがメッセージを完全削除することができます。(24時間)"; - -/* No comment provided by engineer. */ -"Members can send direct messages." = "グループのメンバーがダイレクトメッセージを送信できます。"; - -/* No comment provided by engineer. */ -"Members can send disappearing messages." = "グループのメンバーが消えるメッセージを送信できます。"; - -/* No comment provided by engineer. */ -"Members can send files and media." = "グループメンバーはファイルやメディアを送信できます。"; - -/* No comment provided by engineer. */ -"Members can send voice messages." = "グループのメンバーが音声メッセージを送信できます。"; - /* notification */ "Group message:" = "グループメッセージ:"; @@ -2051,6 +2033,24 @@ /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "メンバーをグループから除名する (※元に戻せません※)!"; +/* No comment provided by engineer. */ +"Members can add message reactions." = "グループメンバーはメッセージへのリアクションを追加できます。"; + +/* No comment provided by engineer. */ +"Members can irreversibly delete sent messages. (24 hours)" = "グループのメンバーがメッセージを完全削除することができます。(24時間)"; + +/* No comment provided by engineer. */ +"Members can send direct messages." = "グループのメンバーがダイレクトメッセージを送信できます。"; + +/* No comment provided by engineer. */ +"Members can send disappearing messages." = "グループのメンバーが消えるメッセージを送信できます。"; + +/* No comment provided by engineer. */ +"Members can send files and media." = "グループメンバーはファイルやメディアを送信できます。"; + +/* No comment provided by engineer. */ +"Members can send voice messages." = "グループのメンバーが音声メッセージを送信できます。"; + /* item status text */ "Message delivery error" = "メッセージ送信エラー"; diff --git a/apps/ios/nl.lproj/Localizable.strings b/apps/ios/nl.lproj/Localizable.strings index 652ccdf63c..edb123334d 100644 --- a/apps/ios/nl.lproj/Localizable.strings +++ b/apps/ios/nl.lproj/Localizable.strings @@ -388,6 +388,9 @@ /* No comment provided by engineer. */ "Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." = "Voeg een adres toe aan uw profiel, zodat uw contacten het met andere mensen kunnen delen. Profiel update wordt naar uw contacten verzonden."; +/* No comment provided by engineer. */ +"Add friends" = "Vrienden toevoegen"; + /* No comment provided by engineer. */ "Add profile" = "Profiel toevoegen"; @@ -397,12 +400,18 @@ /* No comment provided by engineer. */ "Add servers by scanning QR codes." = "Servers toevoegen door QR-codes te scannen."; +/* No comment provided by engineer. */ +"Add team members" = "Teamleden toevoegen"; + /* No comment provided by engineer. */ "Add to another device" = "Toevoegen aan een ander apparaat"; /* No comment provided by engineer. */ "Add welcome message" = "Welkom bericht toevoegen"; +/* No comment provided by engineer. */ +"Add your team members to the conversations." = "Voeg uw teamleden toe aan de gesprekken."; + /* No comment provided by engineer. */ "Added media & file servers" = "Media- en bestandsservers toegevoegd"; @@ -512,7 +521,7 @@ "Allow downgrade" = "Downgraden toestaan"; /* No comment provided by engineer. */ -"Allow irreversible message deletion only if your contact allows it to you. (24 hours)" = "Sta het onomkeerbaar verwijderen van berichten alleen toe als uw contact dit toestaat. (24 uur)"; +"Allow irreversible message deletion only if your contact allows it to you. (24 hours)" = "Sta het definitief verwijderen van berichten alleen toe als uw contact dit toestaat. (24 uur)"; /* No comment provided by engineer. */ "Allow message reactions only if your contact allows them." = "Sta bericht reacties alleen toe als uw contact dit toestaat."; @@ -530,7 +539,7 @@ "Allow sharing" = "Delen toestaan"; /* No comment provided by engineer. */ -"Allow to irreversibly delete sent messages. (24 hours)" = "Sta toe om verzonden berichten onomkeerbaar te verwijderen. (24 uur)"; +"Allow to irreversibly delete sent messages. (24 hours)" = "Sta toe om verzonden berichten definitief te verwijderen. (24 uur)"; /* No comment provided by engineer. */ "Allow to send files and media." = "Sta toe om bestanden en media te verzenden."; @@ -554,7 +563,7 @@ "Allow your contacts to call you." = "Sta toe dat uw contacten u bellen."; /* No comment provided by engineer. */ -"Allow your contacts to irreversibly delete sent messages. (24 hours)" = "Laat uw contacten verzonden berichten onomkeerbaar verwijderen. (24 uur)"; +"Allow your contacts to irreversibly delete sent messages. (24 hours)" = "Laat uw contacten verzonden berichten definitief verwijderen. (24 uur)"; /* No comment provided by engineer. */ "Allow your contacts to send disappearing messages." = "Sta toe dat uw contacten verdwijnende berichten verzenden."; @@ -659,7 +668,7 @@ "Audio/video calls" = "Audio/video oproepen"; /* No comment provided by engineer. */ -"Audio/video calls are prohibited." = "Audio/video gesprekken zijn verboden."; +"Audio/video calls are prohibited." = "Audio/video gesprekken zijn niet toegestaan."; /* PIN entry */ "Authentication cancelled" = "Verificatie geannuleerd"; @@ -793,6 +802,12 @@ /* No comment provided by engineer. */ "Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" = "Bulgaars, Fins, Thais en Oekraïens - dankzij de gebruikers en [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"; +/* No comment provided by engineer. */ +"Business address" = "Zakelijk adres"; + +/* No comment provided by engineer. */ +"Business chats" = "Zakelijke chats"; + /* 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)." = "Via chatprofiel (standaard) of [via verbinding](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)."; @@ -815,7 +830,7 @@ "Calls" = "Oproepen"; /* No comment provided by engineer. */ -"Calls prohibited!" = "Bellen verboden!"; +"Calls prohibited!" = "Bellen niet toegestaan!"; /* No comment provided by engineer. */ "Camera not available" = "Camera niet beschikbaar"; @@ -892,7 +907,7 @@ "Change self-destruct passcode" = "Zelfvernietigings code wijzigen"; /* authentication reason */ -"Change user profiles" = "Gebruikersprofielen wijzigen"; +"Change chat profiles" = "Gebruikersprofielen wijzigen"; /* chat item text */ "changed address for you" = "adres voor u gewijzigd"; @@ -909,6 +924,15 @@ /* chat item text */ "changing address…" = "adres wijzigen…"; +/* No comment provided by engineer. */ +"Chat" = "Chat"; + +/* No comment provided by engineer. */ +"Chat already exists" = "Chat bestaat al"; + +/* No comment provided by engineer. */ +"Chat already exists!" = "Chat bestaat al!"; + /* No comment provided by engineer. */ "Chat colors" = "Chat kleuren"; @@ -954,6 +978,12 @@ /* No comment provided by engineer. */ "Chat theme" = "Chat thema"; +/* No comment provided by engineer. */ +"Chat will be deleted for all members - this cannot be undone!" = "De chat wordt voor alle leden verwijderd - dit kan niet ongedaan worden gemaakt!"; + +/* No comment provided by engineer. */ +"Chat will be deleted for you - this cannot be undone!" = "De chat wordt voor je verwijderd - dit kan niet ongedaan worden gemaakt!"; + /* No comment provided by engineer. */ "Chats" = "Chats"; @@ -1475,12 +1505,18 @@ /* No comment provided by engineer. */ "Delete and notify contact" = "Verwijderen en contact op de hoogte stellen"; +/* No comment provided by engineer. */ +"Delete chat" = "Chat verwijderen"; + /* No comment provided by engineer. */ "Delete chat profile" = "Chatprofiel verwijderen"; /* No comment provided by engineer. */ "Delete chat profile?" = "Chatprofiel verwijderen?"; +/* No comment provided by engineer. */ +"Delete chat?" = "Chat verwijderen?"; + /* No comment provided by engineer. */ "Delete connection" = "Verbinding verwijderen"; @@ -1656,7 +1692,10 @@ "Direct messages" = "Directe berichten"; /* No comment provided by engineer. */ -"Direct messages between members are prohibited." = "Directe berichten tussen leden zijn verboden in deze groep."; +"Direct messages between members are prohibited in this chat." = "Directe berichten tussen leden zijn in deze chat niet toegestaan."; + +/* No comment provided by engineer. */ +"Direct messages between members are prohibited." = "Directe berichten tussen leden zijn niet toegestaan."; /* No comment provided by engineer. */ "Disable (keep overrides)" = "Uitschakelen (overschrijvingen behouden)"; @@ -1680,10 +1719,10 @@ "Disappearing messages" = "Verdwijnende berichten"; /* No comment provided by engineer. */ -"Disappearing messages are prohibited in this chat." = "Verdwijnende berichten zijn verboden in dit gesprek."; +"Disappearing messages are prohibited in this chat." = "Verdwijnende berichten zijn niet toegestaan in dit gesprek."; /* No comment provided by engineer. */ -"Disappearing messages are prohibited." = "Verdwijnende berichten zijn verboden in deze groep."; +"Disappearing messages are prohibited." = "Verdwijnende berichten zijn niet toegestaan."; /* No comment provided by engineer. */ "Disappears at" = "Verdwijnt op"; @@ -2254,13 +2293,13 @@ "Files and media" = "Bestanden en media"; /* No comment provided by engineer. */ -"Files and media are prohibited." = "Bestanden en media zijn verboden in deze groep."; +"Files and media are prohibited." = "Bestanden en media zijn niet toegestaan."; /* No comment provided by engineer. */ "Files and media not allowed" = "Bestanden en media niet toegestaan"; /* No comment provided by engineer. */ -"Files and media prohibited!" = "Bestanden en media verboden!"; +"Files and media prohibited!" = "Bestanden en media niet toegestaan!"; /* No comment provided by engineer. */ "Filter unread and favorite chats." = "Filter ongelezen en favoriete chats."; @@ -2424,27 +2463,6 @@ /* No comment provided by engineer. */ "Group links" = "Groep links"; -/* No comment provided by engineer. */ -"Members can add message reactions." = "Groepsleden kunnen bericht reacties toevoegen."; - -/* No comment provided by engineer. */ -"Members can irreversibly delete sent messages. (24 hours)" = "Groepsleden kunnen verzonden berichten onherroepelijk verwijderen. (24 uur)"; - -/* No comment provided by engineer. */ -"Members can send direct messages." = "Groepsleden kunnen directe berichten sturen."; - -/* No comment provided by engineer. */ -"Members can send disappearing messages." = "Groepsleden kunnen verdwijnende berichten sturen."; - -/* No comment provided by engineer. */ -"Members can send files and media." = "Groepsleden kunnen bestanden en media verzenden."; - -/* No comment provided by engineer. */ -"Members can send SimpleX links." = "Groepsleden kunnen SimpleX-links verzenden."; - -/* No comment provided by engineer. */ -"Members can send voice messages." = "Groepsleden kunnen spraak berichten verzenden."; - /* notification */ "Group message:" = "Groep bericht:"; @@ -2533,7 +2551,7 @@ "If you can't meet in person, show QR code in a video call, or share the link." = "Als je elkaar niet persoonlijk kunt ontmoeten, laat dan de QR-code zien in een videogesprek of deel de link."; /* No comment provided by engineer. */ -"If you enter this passcode when opening the app, all app data will be irreversibly removed!" = "Als u deze toegangscode invoert bij het openen van de app, worden alle app-gegevens onomkeerbaar verwijderd!"; +"If you enter this passcode when opening the app, all app data will be irreversibly removed!" = "Als u deze toegangscode invoert bij het openen van de app, worden alle app-gegevens definitief verwijderd!"; /* No comment provided by engineer. */ "If you enter your self-destruct passcode while opening the app:" = "Als u uw zelfvernietigings wachtwoord invoert tijdens het openen van de app:"; @@ -2715,6 +2733,9 @@ /* No comment provided by engineer. */ "Invite members" = "Nodig leden uit"; +/* No comment provided by engineer. */ +"Invite to chat" = "Uitnodigen voor een chat"; + /* No comment provided by engineer. */ "Invite to group" = "Uitnodigen voor groep"; @@ -2743,10 +2764,10 @@ "Irreversible message deletion" = "Onomkeerbare berichtverwijdering"; /* No comment provided by engineer. */ -"Irreversible message deletion is prohibited in this chat." = "Het onomkeerbaar verwijderen van berichten is verboden in dit gesprek."; +"Irreversible message deletion is prohibited in this chat." = "Het definitief verwijderen van berichten is niet toegestaan in dit gesprek."; /* No comment provided by engineer. */ -"Irreversible message deletion is prohibited." = "Het onomkeerbaar verwijderen van berichten is verboden in deze groep."; +"Irreversible message deletion is prohibited." = "Het definitief verwijderen van berichten is verbHet definitief verwijderen van berichten is niet toegestaan.."; /* No comment provided by engineer. */ "It allows having many anonymous connections without any shared data between them in a single chat profile." = "Het maakt het mogelijk om veel anonieme verbindingen te hebben zonder enige gedeelde gegevens tussen hen in een enkel chatprofiel."; @@ -2829,6 +2850,12 @@ /* swipe action */ "Leave" = "Verlaten"; +/* No comment provided by engineer. */ +"Leave chat" = "Chat verlaten"; + +/* No comment provided by engineer. */ +"Leave chat?" = "Chat verlaten?"; + /* No comment provided by engineer. */ "Leave group" = "Groep verlaten"; @@ -2925,15 +2952,42 @@ /* item status text */ "Member inactive" = "Lid inactief"; +/* No comment provided by engineer. */ +"Member role will be changed to \"%@\". All chat members will be notified." = "De rol van het lid wordt gewijzigd naar \"%@\". Alle chatleden worden op de hoogte gebracht."; + /* No comment provided by engineer. */ "Member role will be changed to \"%@\". All group members will be notified." = "De rol van lid wordt gewijzigd in \"%@\". Alle groepsleden worden op de hoogte gebracht."; /* No comment provided by engineer. */ "Member role will be changed to \"%@\". The member will receive a new invitation." = "De rol van lid wordt gewijzigd in \"%@\". Het lid ontvangt een nieuwe uitnodiging."; +/* No comment provided by engineer. */ +"Member will be removed from chat - this cannot be undone!" = "Lid wordt verwijderd uit de chat - dit kan niet ongedaan worden gemaakt!"; + /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "Lid wordt uit de groep verwijderd, dit kan niet ongedaan worden gemaakt!"; +/* No comment provided by engineer. */ +"Members can add message reactions." = "Groepsleden kunnen bericht reacties toevoegen."; + +/* No comment provided by engineer. */ +"Members can irreversibly delete sent messages. (24 hours)" = "Groepsleden kunnen verzonden berichten onherroepelijk verwijderen. (24 uur)"; + +/* No comment provided by engineer. */ +"Members can send direct messages." = "Groepsleden kunnen directe berichten sturen."; + +/* No comment provided by engineer. */ +"Members can send disappearing messages." = "Groepsleden kunnen verdwijnende berichten sturen."; + +/* No comment provided by engineer. */ +"Members can send files and media." = "Groepsleden kunnen bestanden en media verzenden."; + +/* No comment provided by engineer. */ +"Members can send SimpleX links." = "Groepsleden kunnen SimpleX-links verzenden."; + +/* No comment provided by engineer. */ +"Members can send voice messages." = "Groepsleden kunnen spraak berichten verzenden."; + /* No comment provided by engineer. */ "Menus" = "Menu's"; @@ -2965,10 +3019,10 @@ "Message reactions" = "Reacties op berichten"; /* No comment provided by engineer. */ -"Message reactions are prohibited in this chat." = "Reacties op berichten zijn verboden in deze chat."; +"Message reactions are prohibited in this chat." = "Reacties op berichten zijn niet toegestaan in deze chat."; /* No comment provided by engineer. */ -"Message reactions are prohibited." = "Reacties op berichten zijn verboden in deze groep."; +"Message reactions are prohibited." = "Reacties op berichten zijn niet toegestaan."; /* notification */ "message received" = "bericht ontvangen"; @@ -3329,6 +3383,9 @@ /* No comment provided by engineer. */ "Onion hosts will not be used." = "Onion hosts worden niet gebruikt."; +/* No comment provided by engineer. */ +"Only chat owners can change preferences." = "Alleen chateigenaren kunnen voorkeuren wijzigen."; + /* No comment provided by engineer. */ "Only client devices store user profiles, contacts, groups, and messages." = "Alleen client apparaten slaan gebruikers profielen, contacten, groepen en berichten op die zijn verzonden met **2-laags end-to-end-codering**."; @@ -3348,7 +3405,7 @@ "Only you can add message reactions." = "Alleen jij kunt bericht reacties toevoegen."; /* No comment provided by engineer. */ -"Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)" = "Alleen jij kunt berichten onomkeerbaar verwijderen (je contact kan ze markeren voor verwijdering). (24 uur)"; +"Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)" = "Alleen jij kunt berichten definitief verwijderen (je contact kan ze markeren voor verwijdering). (24 uur)"; /* No comment provided by engineer. */ "Only you can make calls." = "Alleen jij kunt bellen."; @@ -3407,6 +3464,9 @@ /* alert title */ "Operator server" = "Operatorserver"; +/* No comment provided by engineer. */ +"Or import archive file" = "Of importeer archiefbestand"; + /* No comment provided by engineer. */ "Or paste archive link" = "Of plak de archief link"; @@ -3575,6 +3635,9 @@ /* No comment provided by engineer. */ "Privacy & security" = "Privacy en beveiliging"; +/* No comment provided by engineer. */ +"Privacy for your customers." = "Privacy voor uw klanten."; + /* No comment provided by engineer. */ "Privacy redefined" = "Privacy opnieuw gedefinieerd"; @@ -3618,7 +3681,7 @@ "Prohibit audio/video calls." = "Audio/video gesprekken verbieden."; /* No comment provided by engineer. */ -"Prohibit irreversible message deletion." = "Verbied het onomkeerbaar verwijderen van berichten."; +"Prohibit irreversible message deletion." = "Verbied het definitief verwijderen van berichten."; /* No comment provided by engineer. */ "Prohibit message reactions." = "Bericht reacties verbieden."; @@ -3671,9 +3734,6 @@ /* No comment provided by engineer. */ "Push notifications" = "Push meldingen"; -/* No comment provided by engineer. */ -"Push Notifications" = "Pushmeldingen"; - /* No comment provided by engineer. */ "Push server" = "Push server"; @@ -3949,10 +4009,10 @@ "Safer groups" = "Veiligere groepen"; /* No comment provided by engineer. */ -"Same conditions will apply to operator **%@**." = "Dezelfde voorwaarden gelden voor operator **%@**."; +"The same conditions will apply to operator **%@**." = "Dezelfde voorwaarden gelden voor operator **%@**."; /* No comment provided by engineer. */ -"Same conditions will apply to operator(s): **%@**." = "Dezelfde voorwaarden gelden voor operator(s): **%@**."; +"The same conditions will apply to operator(s): **%@**." = "Dezelfde voorwaarden gelden voor operator(s): **%@**."; /* alert button chat item action */ @@ -4416,7 +4476,7 @@ "SimpleX links" = "SimpleX links"; /* No comment provided by engineer. */ -"SimpleX links are prohibited." = "SimpleX-links zijn in deze groep verboden."; +"SimpleX links are prohibited." = "SimpleX-links zijn niet toegestaan."; /* No comment provided by engineer. */ "SimpleX links not allowed" = "SimpleX-links zijn niet toegestaan"; @@ -4580,6 +4640,9 @@ /* No comment provided by engineer. */ "Tap button " = "Tik op de knop "; +/* No comment provided by engineer. */ +"Tap Create SimpleX address in the menu to create it later." = "Tik op SimpleX-adres maken in het menu om het later te maken."; + /* No comment provided by engineer. */ "Tap to activate profile." = "Tik hier om profiel te activeren."; @@ -4734,7 +4797,7 @@ "This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes." = "Deze actie kan niet ongedaan worden gemaakt, de berichten die eerder zijn verzonden en ontvangen dan geselecteerd, worden verwijderd. Het kan enkele minuten duren."; /* No comment provided by engineer. */ -"This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." = "Deze actie kan niet ongedaan worden gemaakt. Uw profiel, contacten, berichten en bestanden gaan onomkeerbaar verloren."; +"This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." = "Deze actie kan niet ongedaan worden gemaakt. Uw profiel, contacten, berichten en bestanden gaan definitief verloren."; /* E2EE info chat item */ "This chat is protected by end-to-end encryption." = "Deze chat is beveiligd met end-to-end codering."; @@ -5145,16 +5208,16 @@ "Voice messages" = "Spraak berichten"; /* No comment provided by engineer. */ -"Voice messages are prohibited in this chat." = "Spraak berichten zijn verboden in deze chat."; +"Voice messages are prohibited in this chat." = "Spraak berichten zijn niet toegestaan in dit gesprek."; /* No comment provided by engineer. */ -"Voice messages are prohibited." = "Spraak berichten zijn verboden in deze groep."; +"Voice messages are prohibited." = "Spraak berichten zijn niet toegestaan."; /* No comment provided by engineer. */ "Voice messages not allowed" = "Spraakberichten niet toegestaan"; /* No comment provided by engineer. */ -"Voice messages prohibited!" = "Spraak berichten verboden!"; +"Voice messages prohibited!" = "Spraak berichten niet toegestaan!"; /* No comment provided by engineer. */ "waiting for answer…" = "wachten op antwoord…"; @@ -5282,6 +5345,9 @@ /* No comment provided by engineer. */ "You are already connected to %@." = "U bent al verbonden met %@."; +/* No comment provided by engineer. */ +"You are already connected with %@." = "U bent al verbonden met %@."; + /* No comment provided by engineer. */ "You are already connecting to %@." = "U maakt al verbinding met %@."; @@ -5480,6 +5546,9 @@ /* No comment provided by engineer. */ "You will still receive calls and notifications from muted profiles when they are active." = "U ontvangt nog steeds oproepen en meldingen van gedempte profielen wanneer deze actief zijn."; +/* No comment provided by engineer. */ +"You will stop receiving messages from this chat. Chat history will be preserved." = "U ontvangt geen berichten meer van deze chat. De chatgeschiedenis blijft bewaard."; + /* No comment provided by engineer. */ "You will stop receiving messages from this group. Chat history will be preserved." = "Je ontvangt geen berichten meer van deze groep. Je gesprek geschiedenis blijft behouden."; diff --git a/apps/ios/pl.lproj/Localizable.strings b/apps/ios/pl.lproj/Localizable.strings index 04279064ee..782e1c18f4 100644 --- a/apps/ios/pl.lproj/Localizable.strings +++ b/apps/ios/pl.lproj/Localizable.strings @@ -2277,27 +2277,6 @@ /* No comment provided by engineer. */ "Group links" = "Linki grupowe"; -/* No comment provided by engineer. */ -"Members can add message reactions." = "Członkowie grupy mogą dodawać reakcje wiadomości."; - -/* No comment provided by engineer. */ -"Members can irreversibly delete sent messages. (24 hours)" = "Członkowie grupy mogą nieodwracalnie usuwać wysłane wiadomości. (24 godziny)"; - -/* No comment provided by engineer. */ -"Members can send direct messages." = "Członkowie grupy mogą wysyłać bezpośrednie wiadomości."; - -/* No comment provided by engineer. */ -"Members can send disappearing messages." = "Członkowie grupy mogą wysyłać znikające wiadomości."; - -/* No comment provided by engineer. */ -"Members can send files and media." = "Członkowie grupy mogą wysyłać pliki i media."; - -/* No comment provided by engineer. */ -"Members can send SimpleX links." = "Członkowie grupy mogą wysyłać linki SimpleX."; - -/* No comment provided by engineer. */ -"Members can send voice messages." = "Członkowie grupy mogą wysyłać wiadomości głosowe."; - /* notification */ "Group message:" = "Wiadomość grupowa:"; @@ -2778,6 +2757,27 @@ /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "Członek zostanie usunięty z grupy - nie można tego cofnąć!"; +/* No comment provided by engineer. */ +"Members can add message reactions." = "Członkowie grupy mogą dodawać reakcje wiadomości."; + +/* No comment provided by engineer. */ +"Members can irreversibly delete sent messages. (24 hours)" = "Członkowie grupy mogą nieodwracalnie usuwać wysłane wiadomości. (24 godziny)"; + +/* No comment provided by engineer. */ +"Members can send direct messages." = "Członkowie grupy mogą wysyłać bezpośrednie wiadomości."; + +/* No comment provided by engineer. */ +"Members can send disappearing messages." = "Członkowie grupy mogą wysyłać znikające wiadomości."; + +/* No comment provided by engineer. */ +"Members can send files and media." = "Członkowie grupy mogą wysyłać pliki i media."; + +/* No comment provided by engineer. */ +"Members can send SimpleX links." = "Członkowie grupy mogą wysyłać linki SimpleX."; + +/* No comment provided by engineer. */ +"Members can send voice messages." = "Członkowie grupy mogą wysyłać wiadomości głosowe."; + /* No comment provided by engineer. */ "Menus" = "Menu"; diff --git a/apps/ios/ru.lproj/Localizable.strings b/apps/ios/ru.lproj/Localizable.strings index bb7ec81ace..c2124b34a4 100644 --- a/apps/ios/ru.lproj/Localizable.strings +++ b/apps/ios/ru.lproj/Localizable.strings @@ -82,6 +82,9 @@ /* No comment provided by engineer. */ "**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from." = "**Рекомендовано**: токен устройства и уведомления отправляются на сервер SimpleX Chat, но сервер не получает сами сообщения, их размер или от кого они."; +/* No comment provided by engineer. */ +"**Scan / Paste link**: to connect via a link you received." = "**Сканировать / Вставить ссылку**: чтобы соединится через полученную ссылку."; + /* No comment provided by engineer. */ "**Warning**: Instant push notifications require passphrase saved in Keychain." = "**Внимание**: для работы мгновенных уведомлений пароль должен быть сохранен в Keychain."; @@ -142,6 +145,12 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ подтверждён"; +/* No comment provided by engineer. */ +"%@ server" = "%@ сервер"; + +/* No comment provided by engineer. */ +"%@ servers" = "%@ серверы"; + /* No comment provided by engineer. */ "%@ uploaded" = "%@ загружено"; @@ -295,6 +304,12 @@ /* time interval */ "1 week" = "1 неделю"; +/* No comment provided by engineer. */ +"1-time link" = "Одноразовая ссылка"; + +/* No comment provided by engineer. */ +"1-time link can be used *with one contact only* - share in person or via any messenger." = "Одноразовая ссылка может быть использована *только с одним контактом* - поделитесь при встрече или через любой мессенджер."; + /* No comment provided by engineer. */ "5 minutes" = "5 минут"; @@ -342,6 +357,9 @@ swipe action */ "Accept" = "Принять"; +/* No comment provided by engineer. */ +"Accept conditions" = "Принять условия"; + /* No comment provided by engineer. */ "Accept connection request?" = "Принять запрос?"; @@ -355,6 +373,9 @@ /* call status */ "accepted call" = "принятый звонок"; +/* No comment provided by engineer. */ +"Accepted conditions" = "Принятые условия"; + /* No comment provided by engineer. */ "Acknowledged" = "Подтверждено"; @@ -367,6 +388,9 @@ /* No comment provided by engineer. */ "Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." = "Добавьте адрес в свой профиль, чтобы Ваши контакты могли поделиться им. Профиль будет отправлен Вашим контактам."; +/* No comment provided by engineer. */ +"Add friends" = "Добавить друзей"; + /* No comment provided by engineer. */ "Add profile" = "Добавить профиль"; @@ -376,12 +400,24 @@ /* No comment provided by engineer. */ "Add servers by scanning QR codes." = "Добавить серверы через QR код."; +/* No comment provided by engineer. */ +"Add team members" = "Добавить сотрудников"; + /* No comment provided by engineer. */ "Add to another device" = "Добавить на другое устройство"; /* No comment provided by engineer. */ "Add welcome message" = "Добавить приветственное сообщение"; +/* No comment provided by engineer. */ +"Add your team members to the conversations." = "Добавьте сотрудников в разговор."; + +/* No comment provided by engineer. */ +"Added media & file servers" = "Дополнительные серверы файлов и медиа"; + +/* No comment provided by engineer. */ +"Added message servers" = "Дополнительные серверы сообщений"; + /* No comment provided by engineer. */ "Additional accent" = "Дополнительный акцент"; @@ -397,6 +433,12 @@ /* No comment provided by engineer. */ "Address change will be aborted. Old receiving address will be used." = "Изменение адреса будет прекращено. Будет использоваться старый адрес."; +/* No comment provided by engineer. */ +"Address or 1-time link?" = "Адрес или одноразовая ссылка?"; + +/* No comment provided by engineer. */ +"Address settings" = "Настройки адреса"; + /* member role */ "admin" = "админ"; @@ -760,6 +802,12 @@ /* No comment provided by engineer. */ "Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" = "Болгарский, финский, тайский и украинский - благодаря пользователям и [Weblate] (https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"; +/* No comment provided by engineer. */ +"Business address" = "Бизнес адрес"; + +/* No comment provided by engineer. */ +"Business chats" = "Бизнес разговоры"; + /* 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)." = "По профилю чата или [по соединению](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (БЕТА)."; @@ -858,6 +906,9 @@ set passcode view */ "Change self-destruct passcode" = "Изменить код самоуничтожения"; +/* authentication reason */ +"Change chat profiles" = "Поменять профили"; + /* chat item text */ "changed address for you" = "поменял(а) адрес для Вас"; @@ -873,6 +924,15 @@ /* chat item text */ "changing address…" = "смена адреса…"; +/* No comment provided by engineer. */ +"Chat" = "Разговор"; + +/* No comment provided by engineer. */ +"Chat already exists" = "Разговор уже существует"; + +/* No comment provided by engineer. */ +"Chat already exists!" = "Разговор уже существует!"; + /* No comment provided by engineer. */ "Chat colors" = "Цвета чата"; @@ -918,9 +978,21 @@ /* No comment provided by engineer. */ "Chat theme" = "Тема чата"; +/* No comment provided by engineer. */ +"Chat will be deleted for all members - this cannot be undone!" = "Разговор будет удален для всех участников - это действие нельзя отменить!"; + +/* No comment provided by engineer. */ +"Chat will be deleted for you - this cannot be undone!" = "Разговор будет удален для Вас - это действие нельзя отменить!"; + /* No comment provided by engineer. */ "Chats" = "Чаты"; +/* No comment provided by engineer. */ +"Check messages every 20 min." = "Проверять сообщения каждые 20 минут."; + +/* No comment provided by engineer. */ +"Check messages when allowed." = "Проверять сообщения по возможности."; + /* alert title */ "Check server address and try again." = "Проверьте адрес сервера и попробуйте снова."; @@ -981,6 +1053,33 @@ /* No comment provided by engineer. */ "Completed" = "Готово"; +/* No comment provided by engineer. */ +"Conditions accepted on: %@." = "Условия приняты: %@."; + +/* No comment provided by engineer. */ +"Conditions are accepted for the operator(s): **%@**." = "Условия приняты для оператора(ов): **%@**."; + +/* No comment provided by engineer. */ +"Conditions are already accepted for following operator(s): **%@**." = "Условия уже приняты для следующих оператора(ов): **%@**."; + +/* No comment provided by engineer. */ +"Conditions of use" = "Условия использования"; + +/* No comment provided by engineer. */ +"Conditions will be accepted for enabled operators after 30 days." = "Условия будут приняты для включенных операторов через 30 дней."; + +/* No comment provided by engineer. */ +"Conditions will be accepted for operator(s): **%@**." = "Условия будут приняты для оператора(ов): **%@**."; + +/* No comment provided by engineer. */ +"Conditions will be accepted for the operator(s): **%@**." = "Условия будут приняты для оператора(ов): **%@**."; + +/* No comment provided by engineer. */ +"Conditions will be accepted on: %@." = "Условия будут приняты: %@."; + +/* No comment provided by engineer. */ +"Conditions will be automatically accepted for enabled operators on: %@." = "Условия будут автоматически приняты для включенных операторов: %@."; + /* No comment provided by engineer. */ "Configure ICE servers" = "Настройка ICE серверов"; @@ -1128,6 +1227,9 @@ /* No comment provided by engineer. */ "Connection request sent!" = "Запрос на соединение отправлен!"; +/* No comment provided by engineer. */ +"Connection security" = "Безопасность соединения"; + /* No comment provided by engineer. */ "Connection terminated" = "Подключение прервано"; @@ -1209,6 +1311,9 @@ /* No comment provided by engineer. */ "Create" = "Создать"; +/* No comment provided by engineer. */ +"Create 1-time link" = "Создать одноразовую ссылку"; + /* No comment provided by engineer. */ "Create a group using a random profile." = "Создайте группу, используя случайный профиль."; @@ -1260,6 +1365,9 @@ /* No comment provided by engineer. */ "creator" = "создатель"; +/* No comment provided by engineer. */ +"Current conditions text couldn't be loaded, you can review conditions via this link:" = "Текст условий использования не может быть показан, вы можете посмотреть их через ссылку:"; + /* No comment provided by engineer. */ "Current Passcode" = "Текущий Код"; @@ -1397,12 +1505,18 @@ /* No comment provided by engineer. */ "Delete and notify contact" = "Удалить и уведомить контакт"; +/* No comment provided by engineer. */ +"Delete chat" = "Удалить разговор"; + /* No comment provided by engineer. */ "Delete chat profile" = "Удалить профиль чата"; /* No comment provided by engineer. */ "Delete chat profile?" = "Удалить профиль?"; +/* No comment provided by engineer. */ +"Delete chat?" = "Удалить разговор?"; + /* No comment provided by engineer. */ "Delete connection" = "Удалить соединение"; @@ -1508,6 +1622,9 @@ /* No comment provided by engineer. */ "Deletion errors" = "Ошибки удаления"; +/* No comment provided by engineer. */ +"Delivered even when Apple drops them." = "Доставляются даже тогда, когда Apple их теряет."; + /* No comment provided by engineer. */ "Delivery" = "Доставка"; @@ -1574,6 +1691,9 @@ /* chat feature */ "Direct messages" = "Прямые сообщения"; +/* No comment provided by engineer. */ +"Direct messages between members are prohibited in this chat." = "Прямые сообщения между членами запрещены в этом разговоре."; + /* No comment provided by engineer. */ "Direct messages between members are prohibited." = "Прямые сообщения между членами группы запрещены."; @@ -1695,6 +1815,9 @@ /* No comment provided by engineer. */ "e2e encrypted" = "e2e зашифровано"; +/* No comment provided by engineer. */ +"E2E encrypted notifications." = "E2E зашифрованные нотификации."; + /* chat item action */ "Edit" = "Редактировать"; @@ -1713,6 +1836,9 @@ /* No comment provided by engineer. */ "Enable camera access" = "Включить доступ к камере"; +/* No comment provided by engineer. */ +"Enable Flux" = "Включить Flux"; + /* No comment provided by engineer. */ "Enable for all" = "Включить для всех"; @@ -1872,12 +1998,18 @@ /* No comment provided by engineer. */ "Error aborting address change" = "Ошибка при прекращении изменения адреса"; +/* alert title */ +"Error accepting conditions" = "Ошибка приема условий"; + /* No comment provided by engineer. */ "Error accepting contact request" = "Ошибка при принятии запроса на соединение"; /* No comment provided by engineer. */ "Error adding member(s)" = "Ошибка при добавлении членов группы"; +/* alert title */ +"Error adding server" = "Ошибка добавления сервера"; + /* No comment provided by engineer. */ "Error changing address" = "Ошибка при изменении адреса"; @@ -1962,6 +2094,9 @@ /* No comment provided by engineer. */ "Error joining group" = "Ошибка при вступлении в группу"; +/* alert title */ +"Error loading servers" = "Ошибка загрузки серверов"; + /* No comment provided by engineer. */ "Error migrating settings" = "Ошибка миграции настроек"; @@ -1995,6 +2130,9 @@ /* No comment provided by engineer. */ "Error saving passphrase to keychain" = "Ошибка сохранения пароля в Keychain"; +/* alert title */ +"Error saving servers" = "Ошибка сохранения серверов"; + /* when migrating */ "Error saving settings" = "Ошибка сохранения настроек"; @@ -2037,6 +2175,9 @@ /* No comment provided by engineer. */ "Error updating message" = "Ошибка при обновлении сообщения"; +/* alert title */ +"Error updating server" = "Ошибка сохранения сервера"; + /* No comment provided by engineer. */ "Error updating settings" = "Ошибка при сохранении настроек сети"; @@ -2064,6 +2205,9 @@ /* No comment provided by engineer. */ "Errors" = "Ошибки"; +/* servers error */ +"Errors in servers configuration." = "Ошибки в настройках серверов."; + /* No comment provided by engineer. */ "Even when disabled in the conversation." = "Даже когда они выключены в разговоре."; @@ -2190,9 +2334,24 @@ /* No comment provided by engineer. */ "Fix not supported by group member" = "Починка не поддерживается членом группы"; +/* No comment provided by engineer. */ +"for better metadata privacy." = "для лучшей конфиденциальности метаданных."; + +/* servers error */ +"For chat profile %@:" = "Для профиля чата %@:"; + /* No comment provided by engineer. */ "For console" = "Для консоли"; +/* No comment provided by engineer. */ +"For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server." = "Например, если Ваш контакт получает сообщения через сервер SimpleX Chat, Ваше приложение доставит их через сервер Flux."; + +/* No comment provided by engineer. */ +"For private routing" = "Для доставки сообщений"; + +/* No comment provided by engineer. */ +"For social media" = "Для социальных сетей"; + /* chat item action */ "Forward" = "Переслать"; @@ -2304,27 +2463,6 @@ /* No comment provided by engineer. */ "Group links" = "Ссылки групп"; -/* No comment provided by engineer. */ -"Members can add message reactions." = "Члены группы могут добавлять реакции на сообщения."; - -/* No comment provided by engineer. */ -"Members can irreversibly delete sent messages. (24 hours)" = "Члены группы могут необратимо удалять отправленные сообщения. (24 часа)"; - -/* No comment provided by engineer. */ -"Members can send direct messages." = "Члены группы могут посылать прямые сообщения."; - -/* No comment provided by engineer. */ -"Members can send disappearing messages." = "Члены группы могут посылать исчезающие сообщения."; - -/* No comment provided by engineer. */ -"Members can send files and media." = "Члены группы могут слать файлы и медиа."; - -/* No comment provided by engineer. */ -"Members can send SimpleX links." = "Члены группы могут отправлять ссылки SimpleX."; - -/* No comment provided by engineer. */ -"Members can send voice messages." = "Члены группы могут отправлять голосовые сообщения."; - /* notification */ "Group message:" = "Групповое сообщение:"; @@ -2385,6 +2523,12 @@ /* time unit */ "hours" = "часов"; +/* No comment provided by engineer. */ +"How it affects privacy" = "Как это влияет на конфиденциальность"; + +/* No comment provided by engineer. */ +"How it helps privacy" = "Как это улучшает конфиденциальность"; + /* No comment provided by engineer. */ "How SimpleX works" = "Как SimpleX работает"; @@ -2589,6 +2733,9 @@ /* No comment provided by engineer. */ "Invite members" = "Пригласить членов группы"; +/* No comment provided by engineer. */ +"Invite to chat" = "Пригласить в разговор"; + /* No comment provided by engineer. */ "Invite to group" = "Пригласить в группу"; @@ -2703,6 +2850,12 @@ /* swipe action */ "Leave" = "Выйти"; +/* No comment provided by engineer. */ +"Leave chat" = "Покинуть разговор"; + +/* No comment provided by engineer. */ +"Leave chat?" = "Покинуть разговор?"; + /* No comment provided by engineer. */ "Leave group" = "Выйти из группы"; @@ -2799,15 +2952,42 @@ /* item status text */ "Member inactive" = "Член неактивен"; +/* No comment provided by engineer. */ +"Member role will be changed to \"%@\". All chat members will be notified." = "Роль участника будет изменена на \"%@\". Все участники разговора получат уведомление."; + /* No comment provided by engineer. */ "Member role will be changed to \"%@\". All group members will be notified." = "Роль члена группы будет изменена на \"%@\". Все члены группы получат сообщение."; /* No comment provided by engineer. */ "Member role will be changed to \"%@\". The member will receive a new invitation." = "Роль члена группы будет изменена на \"%@\". Будет отправлено новое приглашение."; +/* No comment provided by engineer. */ +"Member will be removed from chat - this cannot be undone!" = "Член будет удален из разговора - это действие нельзя отменить!"; + /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "Член группы будет удален - это действие нельзя отменить!"; +/* No comment provided by engineer. */ +"Members can add message reactions." = "Члены группы могут добавлять реакции на сообщения."; + +/* No comment provided by engineer. */ +"Members can irreversibly delete sent messages. (24 hours)" = "Члены группы могут необратимо удалять отправленные сообщения. (24 часа)"; + +/* No comment provided by engineer. */ +"Members can send direct messages." = "Члены группы могут посылать прямые сообщения."; + +/* No comment provided by engineer. */ +"Members can send disappearing messages." = "Члены группы могут посылать исчезающие сообщения."; + +/* No comment provided by engineer. */ +"Members can send files and media." = "Члены группы могут слать файлы и медиа."; + +/* No comment provided by engineer. */ +"Members can send SimpleX links." = "Члены группы могут отправлять ссылки SimpleX."; + +/* No comment provided by engineer. */ +"Members can send voice messages." = "Члены группы могут отправлять голосовые сообщения."; + /* No comment provided by engineer. */ "Menus" = "Меню"; @@ -2961,6 +3141,9 @@ /* No comment provided by engineer. */ "More reliable network connection." = "Более надежное соединение с сетью."; +/* No comment provided by engineer. */ +"More reliable notifications" = "Более надежные уведомления"; + /* item status description */ "Most likely this connection is deleted." = "Скорее всего, соединение удалено."; @@ -2985,12 +3168,18 @@ /* No comment provided by engineer. */ "Network connection" = "Интернет-соединение"; +/* No comment provided by engineer. */ +"Network decentralization" = "Децентрализация сети"; + /* snd error text */ "Network issues - message expired after many attempts to send it." = "Ошибка сети - сообщение не было отправлено после многократных попыток."; /* No comment provided by engineer. */ "Network management" = "Статус сети"; +/* No comment provided by engineer. */ +"Network operator" = "Оператор сети"; + /* No comment provided by engineer. */ "Network settings" = "Настройки сети"; @@ -3018,6 +3207,9 @@ /* No comment provided by engineer. */ "New display name" = "Новое имя"; +/* notification */ +"New events" = "Новые события"; + /* No comment provided by engineer. */ "New in %@" = "Новое в %@"; @@ -3039,6 +3231,9 @@ /* No comment provided by engineer. */ "New passphrase…" = "Новый пароль…"; +/* No comment provided by engineer. */ +"New server" = "Новый сервер"; + /* No comment provided by engineer. */ "New SOCKS credentials will be used every time you start the app." = "Новые учетные данные SOCKS будут использоваться при каждом запуске приложения."; @@ -3084,6 +3279,12 @@ /* No comment provided by engineer. */ "No info, try to reload" = "Нет информации, попробуйте перезагрузить"; +/* servers error */ +"No media & file servers." = "Нет серверов файлов и медиа."; + +/* servers error */ +"No message servers." = "Нет серверов сообщений."; + /* No comment provided by engineer. */ "No network connection" = "Нет интернет-соединения"; @@ -3102,6 +3303,18 @@ /* No comment provided by engineer. */ "No received or sent files" = "Нет полученных или отправленных файлов"; +/* servers error */ +"No servers for private message routing." = "Нет серверов для доставки сообщений."; + +/* servers error */ +"No servers to receive files." = "Нет серверов для приема файлов."; + +/* servers error */ +"No servers to receive messages." = "Нет серверов для приема сообщений."; + +/* servers error */ +"No servers to send files." = "Нет серверов для отправки файлов."; + /* copied message info in history */ "no text" = "нет текста"; @@ -3123,6 +3336,9 @@ /* No comment provided by engineer. */ "Notifications are disabled!" = "Уведомления выключены"; +/* No comment provided by engineer. */ +"Notifications privacy" = "Конфиденциальность уведомлений"; + /* No comment provided by engineer. */ "Now admins can:\n- delete members' messages.\n- disable members (\"observer\" role)" = "Теперь админы могут:\n- удалять сообщения членов.\n- приостанавливать членов (роль \"наблюдатель\")"; @@ -3167,6 +3383,9 @@ /* No comment provided by engineer. */ "Onion hosts will not be used." = "Onion хосты не используются."; +/* No comment provided by engineer. */ +"Only chat owners can change preferences." = "Только владельцы разговора могут поменять предпочтения."; + /* No comment provided by engineer. */ "Only client devices store user profiles, contacts, groups, and messages." = "Только пользовательские устройства хранят контакты, группы и сообщения."; @@ -3215,12 +3434,18 @@ /* No comment provided by engineer. */ "Open" = "Открыть"; +/* No comment provided by engineer. */ +"Open changes" = "Открыть изменения"; + /* No comment provided by engineer. */ "Open chat" = "Открыть чат"; /* authentication reason */ "Open chat console" = "Открыть консоль"; +/* No comment provided by engineer. */ +"Open conditions" = "Открыть условия"; + /* No comment provided by engineer. */ "Open group" = "Открыть группу"; @@ -3233,6 +3458,15 @@ /* No comment provided by engineer. */ "Opening app…" = "Приложение отрывается…"; +/* No comment provided by engineer. */ +"Operator" = "Оператор"; + +/* alert title */ +"Operator server" = "Сервер оператора"; + +/* No comment provided by engineer. */ +"Or import archive file" = "Или импортировать файл архива"; + /* No comment provided by engineer. */ "Or paste archive link" = "Или вставьте ссылку архива"; @@ -3245,6 +3479,9 @@ /* No comment provided by engineer. */ "Or show this code" = "Или покажите этот код"; +/* No comment provided by engineer. */ +"Or to share privately" = "Или поделиться конфиденциально"; + /* No comment provided by engineer. */ "other" = "другое"; @@ -3386,6 +3623,9 @@ /* No comment provided by engineer. */ "Preset server address" = "Адрес сервера по умолчанию"; +/* No comment provided by engineer. */ +"Preset servers" = "Серверы по умолчанию"; + /* No comment provided by engineer. */ "Preview" = "Просмотр"; @@ -3395,6 +3635,9 @@ /* No comment provided by engineer. */ "Privacy & security" = "Конфиденциальность"; +/* No comment provided by engineer. */ +"Privacy for your customers." = "Конфиденциальность для ваших покупателей."; + /* No comment provided by engineer. */ "Privacy redefined" = "Более конфиденциальный"; @@ -3738,6 +3981,12 @@ /* chat item action */ "Reveal" = "Показать"; +/* No comment provided by engineer. */ +"Review conditions" = "Посмотреть условия"; + +/* No comment provided by engineer. */ +"Review later" = "Посмотреть позже"; + /* No comment provided by engineer. */ "Revoke" = "Отозвать"; @@ -3759,6 +4008,12 @@ /* No comment provided by engineer. */ "Safer groups" = "Более безопасные группы"; +/* No comment provided by engineer. */ +"The same conditions will apply to operator **%@**." = "Те же самые условия будут приняты для оператора **%@**."; + +/* No comment provided by engineer. */ +"The same conditions will apply to operator(s): **%@**." = "Те же самые условия будут приняты для оператора(ов): **%@**."; + /* alert button chat item action */ "Save" = "Сохранить"; @@ -4024,6 +4279,9 @@ /* No comment provided by engineer. */ "Server" = "Сервер"; +/* alert message */ +"Server added to operator %@." = "Сервер добавлен к оператору %@."; + /* No comment provided by engineer. */ "Server address" = "Адрес сервера"; @@ -4033,6 +4291,15 @@ /* srv error text. */ "Server address is incompatible with network settings." = "Адрес сервера несовместим с настройками сети."; +/* alert title */ +"Server operator changed." = "Оператор серверов изменен."; + +/* No comment provided by engineer. */ +"Server operators" = "Операторы серверов"; + +/* alert title */ +"Server protocol changed." = "Протокол сервера изменен."; + /* queue info */ "server queue info: %@\n\nlast received msg: %@" = "информация сервера об очереди: %1$@\n\nпоследнее полученное сообщение: %2$@"; @@ -4118,9 +4385,15 @@ /* No comment provided by engineer. */ "Share 1-time link" = "Поделиться одноразовой ссылкой"; +/* No comment provided by engineer. */ +"Share 1-time link with a friend" = "Поделитесь одноразовой ссылкой с другом"; + /* No comment provided by engineer. */ "Share address" = "Поделиться адресом"; +/* No comment provided by engineer. */ +"Share address publicly" = "Поделитесь адресом"; + /* alert title */ "Share address with contacts?" = "Поделиться адресом с контактами?"; @@ -4133,6 +4406,9 @@ /* No comment provided by engineer. */ "Share profile" = "Поделиться профилем"; +/* No comment provided by engineer. */ +"Share SimpleX address on social media." = "Поделитесь SimpleX адресом в социальных сетях."; + /* No comment provided by engineer. */ "Share this 1-time invite link" = "Поделиться одноразовой ссылкой-приглашением"; @@ -4178,6 +4454,12 @@ /* No comment provided by engineer. */ "SimpleX Address" = "Адрес SimpleX"; +/* No comment provided by engineer. */ +"SimpleX address and 1-time links are safe to share via any messenger." = "Адрес SimpleX и одноразовые ссылки безопасно отправлять через любой мессенджер."; + +/* No comment provided by engineer. */ +"SimpleX address or 1-time link?" = "Адрес SimpleX или одноразовая ссылка?"; + /* No comment provided by engineer. */ "SimpleX Chat security was audited by Trail of Bits." = "Безопасность SimpleX Chat была проверена Trail of Bits."; @@ -4253,6 +4535,9 @@ /* No comment provided by engineer. */ "Some non-fatal errors occurred during import:" = "Во время импорта произошли некоторые ошибки:"; +/* alert message */ +"Some servers failed the test:\n%@" = "Серверы не прошли тест:\n%@"; + /* notification title */ "Somebody" = "Контакт"; @@ -4355,6 +4640,9 @@ /* No comment provided by engineer. */ "Tap button " = "Нажмите кнопку "; +/* No comment provided by engineer. */ +"Tap Create SimpleX address in the menu to create it later." = "Нажмите Создать адрес SimpleX в меню, чтобы создать его позже."; + /* No comment provided by engineer. */ "Tap to activate profile." = "Нажмите, чтобы сделать профиль активным."; @@ -4415,6 +4703,9 @@ /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "Приложение может посылать Вам уведомления о сообщениях и запросах на соединение - уведомления можно включить в Настройках."; +/* No comment provided by engineer. */ +"The app protects your privacy by using different operators in each conversation." = "Приложение улучшает конфиденциальность используя разных операторов в каждом разговоре."; + /* No comment provided by engineer. */ "The app will ask to confirm downloads from unknown file servers (except .onion)." = "Приложение будет запрашивать подтверждение загрузки с неизвестных серверов (за исключением .onion адресов)."; @@ -4424,6 +4715,9 @@ /* No comment provided by engineer. */ "The code you scanned is not a SimpleX link QR code." = "Этот QR код не является SimpleX-ccылкой."; +/* No comment provided by engineer. */ +"The connection reached the limit of undelivered messages, your contact may be offline." = "Соединение достигло предела недоставленных сообщений. Возможно, Ваш контакт не в сети."; + /* No comment provided by engineer. */ "The connection you accepted will be cancelled!" = "Подтвержденное соединение будет отменено!"; @@ -4463,6 +4757,9 @@ /* No comment provided by engineer. */ "The profile is only shared with your contacts." = "Профиль отправляется только Вашим контактам."; +/* No comment provided by engineer. */ +"The second preset operator in the app!" = "Второй оператор серверов в приложении!"; + /* No comment provided by engineer. */ "The second tick we missed! ✅" = "Вторая галочка - знать, что доставлено! ✅"; @@ -4472,6 +4769,9 @@ /* No comment provided by engineer. */ "The servers for new connections of your current chat profile **%@**." = "Серверы для новых соединений Вашего текущего профиля чата **%@**."; +/* No comment provided by engineer. */ +"The servers for new files of your current chat profile **%@**." = "Серверы для новых файлов Вашего текущего профиля **%@**."; + /* No comment provided by engineer. */ "The text you pasted is not a SimpleX link." = "Вставленный текст не является SimpleX-ссылкой."; @@ -4481,6 +4781,9 @@ /* No comment provided by engineer. */ "Themes" = "Темы"; +/* No comment provided by engineer. */ +"These conditions will also apply for: **%@**." = "Эти условия также будут применены к: **%@**."; + /* No comment provided by engineer. */ "These settings are for your current profile **%@**." = "Установки для Вашего активного профиля **%@**."; @@ -4544,6 +4847,9 @@ /* No comment provided by engineer. */ "To make a new connection" = "Чтобы соединиться"; +/* No comment provided by engineer. */ +"To protect against your link being replaced, you can compare contact security codes." = "Чтобы защитить Вашу ссылку от замены, Вы можете сравнить код безопасности."; + /* No comment provided by engineer. */ "To protect timezone, image/voice files use UTC." = "Чтобы защитить Ваш часовой пояс, файлы картинок и голосовых сообщений используют UTC."; @@ -4556,6 +4862,9 @@ /* No comment provided by engineer. */ "To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Чтобы защитить Вашу конфиденциальность, SimpleX использует разные идентификаторы для каждого Вашeго контакта."; +/* No comment provided by engineer. */ +"To receive" = "Для получения"; + /* No comment provided by engineer. */ "To record speech please grant permission to use Microphone." = "Для записи речи, пожалуйста, дайте разрешение на использование микрофона."; @@ -4568,9 +4877,15 @@ /* No comment provided by engineer. */ "To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." = "Чтобы показать Ваш скрытый профиль, введите его пароль в поле поиска на странице **Ваши профили чата**."; +/* No comment provided by engineer. */ +"To send" = "Для оправки"; + /* No comment provided by engineer. */ "To support instant push notifications the chat database has to be migrated." = "Для поддержки мгновенный доставки уведомлений данные чата должны быть перемещены."; +/* No comment provided by engineer. */ +"To use the servers of **%@**, accept conditions of use." = "Чтобы использовать серверы оператора **%@**, примите условия использования."; + /* No comment provided by engineer. */ "To verify end-to-end encryption with your contact compare (or scan) the code on your devices." = "Чтобы подтвердить end-to-end шифрование с Вашим контактом сравните (или сканируйте) код безопасности на Ваших устройствах."; @@ -4628,6 +4943,9 @@ /* rcv group event chat item */ "unblocked %@" = "%@ разблокирован"; +/* No comment provided by engineer. */ +"Undelivered messages" = "Недоставленные сообщения"; + /* No comment provided by engineer. */ "Unexpected migration state" = "Неожиданная ошибка при перемещении данных чата"; @@ -4745,12 +5063,21 @@ /* No comment provided by engineer. */ "Use .onion hosts" = "Использовать .onion хосты"; +/* No comment provided by engineer. */ +"Use %@" = "Использовать %@"; + /* No comment provided by engineer. */ "Use chat" = "Использовать чат"; /* No comment provided by engineer. */ "Use current profile" = "Использовать активный профиль"; +/* No comment provided by engineer. */ +"Use for files" = "Использовать для файлов"; + +/* No comment provided by engineer. */ +"Use for messages" = "Использовать для сообщений"; + /* No comment provided by engineer. */ "Use for new connections" = "Использовать для новых соединений"; @@ -4775,6 +5102,9 @@ /* No comment provided by engineer. */ "Use server" = "Использовать сервер"; +/* No comment provided by engineer. */ +"Use servers" = "Использовать серверы"; + /* No comment provided by engineer. */ "Use SimpleX Chat servers?" = "Использовать серверы предосталенные SimpleX Chat?"; @@ -4859,9 +5189,15 @@ /* No comment provided by engineer. */ "Videos and files up to 1gb" = "Видео и файлы до 1гб"; +/* No comment provided by engineer. */ +"View conditions" = "Посмотреть условия"; + /* No comment provided by engineer. */ "View security code" = "Показать код безопасности"; +/* No comment provided by engineer. */ +"View updated conditions" = "Посмотреть измененные условия"; + /* chat feature */ "Visible history" = "Доступ к истории"; @@ -4943,6 +5279,9 @@ /* No comment provided by engineer. */ "when IP hidden" = "когда IP защищен"; +/* No comment provided by engineer. */ +"When more than one operator is enabled, none of them has metadata to learn who communicates with whom." = "Когда больше чем один оператор включен, ни один из них не видит метаданные, чтобы определить, кто соединен с кем."; + /* No comment provided by engineer. */ "When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "Когда Вы соединены с контактом инкогнито, тот же самый инкогнито профиль будет использоваться для групп с этим контактом."; @@ -5006,6 +5345,9 @@ /* No comment provided by engineer. */ "You are already connected to %@." = "Вы уже соединены с контактом %@."; +/* No comment provided by engineer. */ +"You are already connected with %@." = "Вы уже соединены с %@."; + /* No comment provided by engineer. */ "You are already connecting to %@." = "Вы уже соединяетесь с %@."; @@ -5051,6 +5393,12 @@ /* No comment provided by engineer. */ "You can change it in Appearance settings." = "Вы можете изменить это в настройках Интерфейса."; +/* No comment provided by engineer. */ +"You can configure operators in Network & servers settings." = "Вы можете настроить операторов в настройках Сеть и серверы."; + +/* No comment provided by engineer. */ +"You can configure servers via settings." = "Вы можете сконфигурировать серверы через настройки."; + /* No comment provided by engineer. */ "You can create it later" = "Вы можете создать его позже"; @@ -5075,6 +5423,9 @@ /* No comment provided by engineer. */ "You can send messages to %@ from Archived contacts." = "Вы можете отправлять сообщения %@ из Архивированных контактов."; +/* No comment provided by engineer. */ +"You can set connection name, to remember who the link was shared with." = "Вы можете установить имя соединения, чтобы запомнить кому Вы отправили ссылку."; + /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "Вы можете установить просмотр уведомлений на экране блокировки в настройках."; @@ -5195,6 +5546,9 @@ /* No comment provided by engineer. */ "You will still receive calls and notifications from muted profiles when they are active." = "Вы все равно получите звонки и уведомления в профилях без звука, когда они активные."; +/* No comment provided by engineer. */ +"You will stop receiving messages from this chat. Chat history will be preserved." = "Вы прекратите получать сообщения в этом разговоре. История будет сохранена."; + /* No comment provided by engineer. */ "You will stop receiving messages from this group. Chat history will be preserved." = "Вы перестанете получать сообщения от этой группы. История чата будет сохранена."; @@ -5276,6 +5630,9 @@ /* No comment provided by engineer. */ "Your server address" = "Адрес Вашего сервера"; +/* No comment provided by engineer. */ +"Your servers" = "Ваши серверы"; + /* No comment provided by engineer. */ "Your settings" = "Настройки"; diff --git a/apps/ios/th.lproj/Localizable.strings b/apps/ios/th.lproj/Localizable.strings index 962c64b710..b496fe11b4 100644 --- a/apps/ios/th.lproj/Localizable.strings +++ b/apps/ios/th.lproj/Localizable.strings @@ -1469,24 +1469,6 @@ /* No comment provided by engineer. */ "Group links" = "ลิงค์กลุ่ม"; -/* No comment provided by engineer. */ -"Members can add message reactions." = "สมาชิกกลุ่มสามารถเพิ่มการแสดงปฏิกิริยาต่อข้อความได้"; - -/* No comment provided by engineer. */ -"Members can irreversibly delete sent messages. (24 hours)" = "สมาชิกกลุ่มสามารถลบข้อความที่ส่งแล้วอย่างถาวร"; - -/* No comment provided by engineer. */ -"Members can send direct messages." = "สมาชิกกลุ่มสามารถส่งข้อความโดยตรงได้"; - -/* No comment provided by engineer. */ -"Members can send disappearing messages." = "สมาชิกกลุ่มสามารถส่งข้อความที่จะหายไปหลังจากเวลาที่กำหนดหลังการอ่าน (disappearing messages) ได้"; - -/* No comment provided by engineer. */ -"Members can send files and media." = "สมาชิกกลุ่มสามารถส่งไฟล์และสื่อ"; - -/* No comment provided by engineer. */ -"Members can send voice messages." = "สมาชิกกลุ่มสามารถส่งข้อความเสียง"; - /* notification */ "Group message:" = "ข้อความกลุ่ม:"; @@ -1853,6 +1835,24 @@ /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "สมาชิกจะถูกลบออกจากกลุ่ม - ไม่สามารถยกเลิกได้!"; +/* No comment provided by engineer. */ +"Members can add message reactions." = "สมาชิกกลุ่มสามารถเพิ่มการแสดงปฏิกิริยาต่อข้อความได้"; + +/* No comment provided by engineer. */ +"Members can irreversibly delete sent messages. (24 hours)" = "สมาชิกกลุ่มสามารถลบข้อความที่ส่งแล้วอย่างถาวร"; + +/* No comment provided by engineer. */ +"Members can send direct messages." = "สมาชิกกลุ่มสามารถส่งข้อความโดยตรงได้"; + +/* No comment provided by engineer. */ +"Members can send disappearing messages." = "สมาชิกกลุ่มสามารถส่งข้อความที่จะหายไปหลังจากเวลาที่กำหนดหลังการอ่าน (disappearing messages) ได้"; + +/* No comment provided by engineer. */ +"Members can send files and media." = "สมาชิกกลุ่มสามารถส่งไฟล์และสื่อ"; + +/* No comment provided by engineer. */ +"Members can send voice messages." = "สมาชิกกลุ่มสามารถส่งข้อความเสียง"; + /* item status text */ "Message delivery error" = "ข้อผิดพลาดในการส่งข้อความ"; diff --git a/apps/ios/tr.lproj/Localizable.strings b/apps/ios/tr.lproj/Localizable.strings index ec29de0cf3..99668bec79 100644 --- a/apps/ios/tr.lproj/Localizable.strings +++ b/apps/ios/tr.lproj/Localizable.strings @@ -2301,27 +2301,6 @@ /* No comment provided by engineer. */ "Group links" = "Grup bağlantıları"; -/* No comment provided by engineer. */ -"Members can add message reactions." = "Grup üyeleri mesaj tepkileri ekleyebilir."; - -/* No comment provided by engineer. */ -"Members can irreversibly delete sent messages. (24 hours)" = "Grup üyeleri, gönderilen mesajları kalıcı olarak silebilir. (24 saat içinde)"; - -/* No comment provided by engineer. */ -"Members can send direct messages." = "Grup üyeleri doğrudan mesajlar gönderebilir."; - -/* No comment provided by engineer. */ -"Members can send disappearing messages." = "Grup üyeleri kaybolan mesajlar gönderebilir."; - -/* No comment provided by engineer. */ -"Members can send files and media." = "Grup üyeleri dosyalar ve medya gönderebilir."; - -/* No comment provided by engineer. */ -"Members can send SimpleX links." = "Grup üyeleri SimpleX bağlantıları gönderebilir."; - -/* No comment provided by engineer. */ -"Members can send voice messages." = "Grup üyeleri sesli mesajlar gönderebilir."; - /* notification */ "Group message:" = "Grup mesajı:"; @@ -2805,6 +2784,27 @@ /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "Üye gruptan çıkarılacaktır - bu geri alınamaz!"; +/* No comment provided by engineer. */ +"Members can add message reactions." = "Grup üyeleri mesaj tepkileri ekleyebilir."; + +/* No comment provided by engineer. */ +"Members can irreversibly delete sent messages. (24 hours)" = "Grup üyeleri, gönderilen mesajları kalıcı olarak silebilir. (24 saat içinde)"; + +/* No comment provided by engineer. */ +"Members can send direct messages." = "Grup üyeleri doğrudan mesajlar gönderebilir."; + +/* No comment provided by engineer. */ +"Members can send disappearing messages." = "Grup üyeleri kaybolan mesajlar gönderebilir."; + +/* No comment provided by engineer. */ +"Members can send files and media." = "Grup üyeleri dosyalar ve medya gönderebilir."; + +/* No comment provided by engineer. */ +"Members can send SimpleX links." = "Grup üyeleri SimpleX bağlantıları gönderebilir."; + +/* No comment provided by engineer. */ +"Members can send voice messages." = "Grup üyeleri sesli mesajlar gönderebilir."; + /* No comment provided by engineer. */ "Menus" = "Menüler"; diff --git a/apps/ios/uk.lproj/Localizable.strings b/apps/ios/uk.lproj/Localizable.strings index 28cc2839e8..d470a2a1e3 100644 --- a/apps/ios/uk.lproj/Localizable.strings +++ b/apps/ios/uk.lproj/Localizable.strings @@ -892,7 +892,7 @@ "Change self-destruct passcode" = "Змінити пароль самознищення"; /* authentication reason */ -"Change user profiles" = "Зміна профілів користувачів"; +"Change chat profiles" = "Зміна профілів користувачів"; /* chat item text */ "changed address for you" = "змінили для вас адресу"; @@ -2424,27 +2424,6 @@ /* No comment provided by engineer. */ "Group links" = "Групові посилання"; -/* No comment provided by engineer. */ -"Members can add message reactions." = "Учасники групи можуть додавати реакції на повідомлення."; - -/* No comment provided by engineer. */ -"Members can irreversibly delete sent messages. (24 hours)" = "Учасники групи можуть безповоротно видаляти надіслані повідомлення. (24 години)"; - -/* No comment provided by engineer. */ -"Members can send direct messages." = "Учасники групи можуть надсилати прямі повідомлення."; - -/* No comment provided by engineer. */ -"Members can send disappearing messages." = "Учасники групи можуть надсилати зникаючі повідомлення."; - -/* No comment provided by engineer. */ -"Members can send files and media." = "Учасники групи можуть надсилати файли та медіа."; - -/* No comment provided by engineer. */ -"Members can send SimpleX links." = "Учасники групи можуть надсилати посилання SimpleX."; - -/* No comment provided by engineer. */ -"Members can send voice messages." = "Учасники групи можуть надсилати голосові повідомлення."; - /* notification */ "Group message:" = "Групове повідомлення:"; @@ -2934,6 +2913,27 @@ /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "Учасник буде видалений з групи - це неможливо скасувати!"; +/* No comment provided by engineer. */ +"Members can add message reactions." = "Учасники групи можуть додавати реакції на повідомлення."; + +/* No comment provided by engineer. */ +"Members can irreversibly delete sent messages. (24 hours)" = "Учасники групи можуть безповоротно видаляти надіслані повідомлення. (24 години)"; + +/* No comment provided by engineer. */ +"Members can send direct messages." = "Учасники групи можуть надсилати прямі повідомлення."; + +/* No comment provided by engineer. */ +"Members can send disappearing messages." = "Учасники групи можуть надсилати зникаючі повідомлення."; + +/* No comment provided by engineer. */ +"Members can send files and media." = "Учасники групи можуть надсилати файли та медіа."; + +/* No comment provided by engineer. */ +"Members can send SimpleX links." = "Учасники групи можуть надсилати посилання SimpleX."; + +/* No comment provided by engineer. */ +"Members can send voice messages." = "Учасники групи можуть надсилати голосові повідомлення."; + /* No comment provided by engineer. */ "Menus" = "Меню"; @@ -3672,7 +3672,7 @@ "Push notifications" = "Push-повідомлення"; /* No comment provided by engineer. */ -"Push Notifications" = "Push-сповіщення"; +"Push notifications" = "Push-сповіщення"; /* No comment provided by engineer. */ "Push server" = "Push-сервер"; @@ -3949,10 +3949,10 @@ "Safer groups" = "Безпечніші групи"; /* No comment provided by engineer. */ -"Same conditions will apply to operator **%@**." = "Такі ж умови діятимуть і для оператора **%@**."; +"The same conditions will apply to operator **%@**." = "Такі ж умови діятимуть і для оператора **%@**."; /* No comment provided by engineer. */ -"Same conditions will apply to operator(s): **%@**." = "Такі ж умови будуть застосовуватися до оператора(ів): **%@**."; +"The same conditions will apply to operator(s): **%@**." = "Такі ж умови будуть застосовуватися до оператора(ів): **%@**."; /* alert button chat item action */ diff --git a/apps/ios/zh-Hans.lproj/Localizable.strings b/apps/ios/zh-Hans.lproj/Localizable.strings index 5fac1a8577..6a924eea1f 100644 --- a/apps/ios/zh-Hans.lproj/Localizable.strings +++ b/apps/ios/zh-Hans.lproj/Localizable.strings @@ -2214,27 +2214,6 @@ /* No comment provided by engineer. */ "Group links" = "群组链接"; -/* No comment provided by engineer. */ -"Members can add message reactions." = "群组成员可以添加信息回应。"; - -/* No comment provided by engineer. */ -"Members can irreversibly delete sent messages. (24 hours)" = "群组成员可以不可撤回地删除已发送的消息"; - -/* No comment provided by engineer. */ -"Members can send direct messages." = "群组成员可以私信。"; - -/* No comment provided by engineer. */ -"Members can send disappearing messages." = "群组成员可以发送限时消息。"; - -/* No comment provided by engineer. */ -"Members can send files and media." = "群组成员可以发送文件和媒体。"; - -/* No comment provided by engineer. */ -"Members can send SimpleX links." = "群成员可发送 SimpleX 链接。"; - -/* No comment provided by engineer. */ -"Members can send voice messages." = "群组成员可以发送语音消息。"; - /* notification */ "Group message:" = "群组消息:"; @@ -2712,6 +2691,27 @@ /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "成员将被移出群组——此操作无法撤消!"; +/* No comment provided by engineer. */ +"Members can add message reactions." = "群组成员可以添加信息回应。"; + +/* No comment provided by engineer. */ +"Members can irreversibly delete sent messages. (24 hours)" = "群组成员可以不可撤回地删除已发送的消息"; + +/* No comment provided by engineer. */ +"Members can send direct messages." = "群组成员可以私信。"; + +/* No comment provided by engineer. */ +"Members can send disappearing messages." = "群组成员可以发送限时消息。"; + +/* No comment provided by engineer. */ +"Members can send files and media." = "群组成员可以发送文件和媒体。"; + +/* No comment provided by engineer. */ +"Members can send SimpleX links." = "群成员可发送 SimpleX 链接。"; + +/* No comment provided by engineer. */ +"Members can send voice messages." = "群组成员可以发送语音消息。"; + /* No comment provided by engineer. */ "Menus" = "菜单"; diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml index 1171b1d1ae..259411688c 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml @@ -15,8 +15,7 @@ لا يمكن التراجع عن هذا الإجراء - سيتم فقد ملف التعريف وجهات الاتصال والرسائل والملفات الخاصة بك بشكل نهائي. هذه المجموعة لم تعد موجودة. رمز QR هذا ليس رابطًا! - الجيل القادم من -\nالرسائل الخاصة + الجيل القادم من \nالرسائل الخاصة لا يمكن التراجع عن هذا الإجراء - سيتم حذف جميع الملفات والوسائط المستلمة والمرسلة. ستبقى الصور منخفضة الدقة. لا يمكن التراجع عن هذا الإجراء - سيتم حذف الرسائل المرسلة والمستلمة قبل التحديد. قد تأخذ عدة دقائق. ينطبق هذا الإعداد على الرسائل الموجودة في ملف تعريف الدردشة الحالي الخاص بك diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index df0b3d5cd7..d56a7fe87c 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -1745,10 +1745,10 @@ Use %s Current conditions text couldn\'t be loaded, you can review conditions via this link: %s.]]> - %s.]]> - %s.]]> + %s.]]> + %s.]]> %s.]]> - %s.]]> + %s.]]> %s.]]> %s.]]> View conditions diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml index 3c3711be25..e90dd26aff 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml @@ -462,8 +462,7 @@ Verbunden Beendet - Die nächste Generation -\ndes privaten Messagings + Die nächste Generation \ndes privaten Messagings Datenschutz neu definiert Keine Benutzerkennungen. Immun gegen Spam diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml index 98f34e4c81..02f69b0550 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml @@ -737,8 +737,7 @@ ¡La conexión que has aceptado se cancelará! La base de datos no funciona correctamente. Pulsa para conocer más El mensaje será marcado como moderado para todos los miembros. - La nueva generación -\nde mensajería privada + La nueva generación \nde mensajería privada Esta acción es irreversible. Se eliminarán todos los archivos y multimedia recibidos y enviados. Las imágenes de baja resolución permanecerán. Esta acción es irreversible. Los mensajes enviados y recibidos anteriores a la selección serán eliminados. Podría tardar varios minutos. Esta configuración se aplica a los mensajes del perfil actual diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml index 480aa2e10c..5eea31e670 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml @@ -438,8 +438,7 @@ en attente de confirmation… connecté terminé - La nouvelle génération -\nde messagerie privée + La nouvelle génération \nde messagerie privée La vie privée redéfinie Aucun identifiant d\'utilisateur. Protégé du spam diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml index 4f80e33166..34dc1227a7 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml @@ -156,7 +156,7 @@ Az eltűnő üzenetek küldése csak abban az esetben van engedélyezve, ha az ismerőse is engedélyezi. Hang kikapcsolva A közvetlen üzenetek küldése a tagok között engedélyezve van. - Alkalmazás + ALKALMAZÁS Hívás folyamatban Mindkét fél küldhet üzenetreakciókat. Mindkét fél tud hívásokat kezdeményezni. @@ -360,7 +360,7 @@ Letiltás az összes csoport számára Engedélyezés az összes csoport számára engedélyezve az ismerős számára - Az eltűnő üzenetek küldése le van tiltva ebben a csoportban. + Az eltűnő üzenetek küldése le van tiltva. Cím törlése %d hét Számítógép címe @@ -547,15 +547,15 @@ A galériából Engedélyezés (csoport felülírások megtartásával) Hiba az ismerős törlésekor - A csoport tagjai véglegesen törölhetik az elküldött üzeneteiket. (24 óra) + A tagok véglegesen törölhetik az elküldött üzeneteiket. (24 óra) Hiba a szerepkör megváltoztatásakor Javítás - A csoport tagjai küldhetnek eltűnő üzeneteket. + A tagok küldhetnek eltűnő üzeneteket. Kapcsolat javítása Hiba a profil létrehozásakor! Hiba a tag(ok) hozzáadásakor Fájl - A csoport tagjai küldhetnek fájlokat és médiatartalmakat. + A tagok küldhetnek fájlokat és médiatartalmakat. Törlés ennyi idő után Hiba a beállítás megváltoztatásakor Hiba a csoporthivatkozás frissítésekor @@ -565,7 +565,7 @@ Hiba a csevegési adatbázis importálásakor Hiba a kézbesítési jelentések engedélyezésekor! Hiba az XFTP-kiszolgálók mentésekor - A csoport tagjai küldhetnek egymásnak közvetlen üzeneteket. + A tagok küldhetnek egymásnak közvetlen üzeneteket. Hiba a tag eltávolításakor befejeződött A csoport üdvözlőüzenete @@ -589,7 +589,7 @@ Ismerős általi javítás nem támogatott Fájl nem található Kapcsolat bontása - A csoport tagjai üzenetreakciókat adhatnak hozzá. + A tagok reakciókat adhatnak hozzá az üzenetekhez. Adatbázis exportálása Teljes név: Tovább csökkentett akkumulátor-használat @@ -600,7 +600,7 @@ Hiba a csevegési adatbázis törlésekor Teljes hivatkozás Hiba a cím megváltoztatásakor - A csoport tagjai küldhetnek hangüzeneteket. + A tagok küldhetnek hangüzeneteket. Csoportbeállítások Hiba: %s Eltűnő üzenetek @@ -614,7 +614,7 @@ titkosítás-újraegyeztetés szükséges Rejtett csevegési profilok Fájlok és médiatartalmak - A kép mentve a „Galériába” + A kép elmentve a „Galériába” Elrejtés Azonnal A fájlok- és a médiatartalmak küldése le van tiltva! @@ -657,7 +657,7 @@ Az azonnali értesítések le vannak tiltva! Azonnali értesítések! Kép - A fájlok- és a médiatartalmak le vannak tiltva ebben a csoportban. + A fájlok- és a médiatartalmak küldése le van tiltva. Hogyan működik Elrejtés: Hiba az ismerőssel történő kapcsolat létrehozásában @@ -721,11 +721,11 @@ Új számítógép-alkalmazás! Most már az adminisztrátorok is:\n- törölhetik a tagok üzeneteit.\n- letilthatnak tagokat (megfigyelő szerepkör) meghívta őt: %1$s - Az üzenetreakciók küldése le van tiltva ebben a csoportban. + A reakciók küldése az üzenetekre le van tiltva. Nem nincs szöveg TAG - Ez később a beállításokon keresztül módosítható. + Hogyan befolyásolja az akkumulátort Új tag szerepköre Kikapcsolva Érvénytelen hivatkozás! @@ -751,7 +751,7 @@ Moderálás bekapcsolva Japán és portugál kezelőfelület - Az üzenetek végleges törlése le van tiltva ebben a csoportban. + Az üzenetek végleges törlése le van tiltva. %s nevű hordozható eszközzel]]> hónap Üzenetvázlat @@ -793,7 +793,7 @@ Hordozható eszköz társítása Értesítési szolgáltatás Csak a csoporttulajdonosok engedélyezhetik a hangüzenetek küldését. - 2 rétegű végpontok közötti titkosítással küldött üzeneteket.]]> + A felhasználói profilok, névjegyek, csoportok és üzenetek csak az eszközön kerülnek tárolásra a kliensen belül. Érvénytelen átköltöztetési visszaigazolás Csak a csoporttulajdonosok módosíthatják a csoportbeállításokat. Nincsenek előzmények @@ -801,7 +801,7 @@ Megjelölés olvasottként ÉLŐ Megjelölés olvasatlanként - Több + Továbbiak Bejelentkezés hitelesítőadatokkal érvénytelen üzenet formátum Csatlakozás @@ -1163,7 +1163,7 @@ A hangüzenetek küldése le van tiltva. Ismerős nevének beállítása Csak Ön tud eltűnő üzeneteket küldeni. - Média megosztása… + Médiatartalom megosztása… Ön: %1$s Beállítások Színek visszaállítása @@ -1272,7 +1272,7 @@ Figyelmeztetés: néhány adat elveszhet! Koppintson ide az új csevegés indításához Várakozás a számítógépre… - A privát üzenetküldés\nkövetkező generációja + Az üzenetküldés jövője Hálózati beállítások megváltoztatása? Várakozás a hordozható eszköz társítására: Biztonságos kapcsolat hitelesítése @@ -1287,7 +1287,7 @@ Az új üzenetek rendszeresen letöltésre kerülnek az alkalmazás által – naponta néhány százalékot használ az akkumulátorból. Az alkalmazás nem használ push-értesítéseket – az eszközről származó adatok nem kerülnek elküldésre a kiszolgálóknak. Számítógép címének beillesztése kapcsolattartási cím-hivatkozáson keresztül - SimpleX-háttérszolgáltatást használja - az akkumulátornak csak néhány százalékát használja naponta.]]> + a SimpleX a háttérben fut a push értesítések használata helyett.]]> Az ismerősének online kell lennie ahhoz, hogy a kapcsolat létrejöjjön.\nVisszavonhatja ezt az ismerőskérelmet és eltávolíthatja az ismerőst (ezt később ismét megpróbálhatja egy új hivatkozással). A jelszó nem található a Keystore-ban, ezért kézzel szükséges megadni. Ez akkor történhetett meg, ha visszaállította az alkalmazás adatait egy biztonságimentési eszközzel. Ha nem így történt, akkor lépjen kapcsolatba a fejlesztőkkel. Az ismerősei továbbra is kapcsolódva maradnak. @@ -1306,7 +1306,7 @@ A profilja elküldésre kerül az ismerőse számára, akitől ezt a hivatkozást kapta. Az alkalmazás 1 perc után bezárható a háttérben. meghívást kapott a csoportba - engedélyezze a SimpleX háttérben történő futását a következő párbeszédpanelen. Ellenkező esetben az értesítések letiltásra kerülnek.]]> + Engedélyezze a következő párbeszédpanelen az azonnali értesítések fogadásához.]]> A kiszolgálónak engedélyre van szüksége a sorbaállítás létrehozásához, ellenőrizze jelszavát Kapcsolódni fog a csoport összes tagjához. Lehetséges, hogy a kiszolgáló címében szereplő tanúsítvány-ujjlenyomat helytelen @@ -1349,7 +1349,7 @@ Inkognitóprofilt használ ehhez a csoporthoz - fő profilja megosztásának elkerülése érdekében a meghívók küldése le van tiltva Átvitel-izoláció Akkor lesz kapcsolódva, ha a kapcsolatkérése elfogadásra kerül, várjon, vagy ellenőrizze később! - A hangüzenetek küldése le van tiltva ebben a csoportban. + A hangüzenetek küldése le van tiltva. Alkalmazás akkumulátor-használata / Korlátlan módot az alkalmazás beállításaiban.]]> Biztonságos kvantumrezisztens-protokollon keresztül. - 5 perc hosszúságú hangüzenetek.\n- egyéni üzenet-eltűnési időkorlát.\n- előzmények szerkesztése. @@ -1363,7 +1363,7 @@ A profilja az eszközén van tárolva és csak az ismerőseivel kerül megosztásra. A SimpleX-kiszolgálók nem láthatják a profilját. Ön megváltoztatta %s szerepkörét erre: %s Csoportmeghívó elutasítva - Az adatvédelem érdekében (a más csevegési platformokon megszokott felhasználó-azonosítók helyett) a SimpleX csak az üzenetek sorbaállításához használ azonosítókat, az összes ismerőséhez különbözőt. + Az Ön adatainak védelme érdekében a SimpleX külön üzenet-azonosítókat használ minden egyes kapcsolatához. (a megosztáshoz az ismerősével) Csoportmeghívó elküldve Átvitel-izoláció módjának frissítése? @@ -1450,7 +1450,7 @@ Az utolsó üzenet tervezetének megőrzése a mellékletekkel együtt. A mentett WebRTC ICE-kiszolgálók eltávolításra kerülnek. A kézbesítési jelentések engedélyezve vannak %d csoportban - A szerepkör meg fog változni erre: %s. A csoportban az összes tag értesítve lesz. + A szerepkör meg fog változni erre: %s. A csoport tagjai értesítést fognak kapni. Profil és kiszolgálókapcsolatok Egy üzenetküldő- és alkalmazásplatform, amely védi az adatait és biztonságát. A profil aktiválásához koppintson az ikonra. @@ -1685,23 +1685,23 @@ Wi-Fi továbbított A SimpleX-hivatkozások küldése le van tiltva - A csoport tagjai küldhetnek SimpleX-hivatkozásokat. + A tagok küldhetnek SimpleX-hivatkozásokat. tulajdonosok adminisztrátorok összes tag SimpleX-hivatkozások A hangüzenetek küldése le van tiltva - A SimpleX-hivatkozások küldése le van tiltva ebben a csoportban. + A SimpleX-hivatkozások küldése le van tiltva. A SimpleX-hivatkozások küldése le van tiltva A fájlok- és médiatartalmak nincsenek engedélyezve A SimpleX-hivatkozások küldése engedélyezve van. Számukra engedélyezve: mentett - mentve innen: %s + elmentve innen: %s Továbbítva innen: A címzett(ek) nem látja(k), hogy kitől származik ez az üzenet. Mentett - Mentve innen: + Elmentve innen: Letöltés Továbbítás Továbbított @@ -1790,7 +1790,7 @@ További kiemelés 2 Alkalmazás téma Perzsa kezelőfelület - Védje IP-címét az ismerősei által kiválasztott üzenet-közvetítő-kiszolgálókkal szemben.\nEngedélyezze a „Beállítások / Hálózat és kiszolgálók” menüben. + Védje IP-címét az ismerősei által kiválasztott üzenet-közvetítő-kiszolgálókkal szemben.\nEngedélyezze a *Hálózat és kiszolgálók* menüben. Ismeretlen kiszolgálókról származó fájlok megerősítése. Javított üzenetkézbesítés Alkalmazás témájának visszaállítása @@ -1938,7 +1938,7 @@ A(z) %1$s célkiszolgáló verziója nem kompatibilis a(z) %2$s továbbító kiszolgálóval. A(z) %1$s továbbító-kiszolgáló nem tudott csatlakozni a(z) %2$s célkiszolgálóhoz. Próbálja meg később. A(z) %1$s célkiszolgáló címe nem kompatibilis a(z) %2$s továbbító-kiszolgáló beállításaival. - Média elhomályosítása + Médiatartalom elhomályosítása Közepes Kikapcsolva Enyhe @@ -1966,7 +1966,7 @@ Beszélgetés megtartása Biztosan törli az ismerőst? kapcsolódás - Könnyen elérhető eszköztár + Könnyen elérhető alkalmazás-eszköztárak Törlés értesítés nélkül Beállítások keresés @@ -2108,7 +2108,7 @@ Vagy a privát megosztáshoz SimpleX-cím vagy egyszer használható meghívó-hivatkozás? Egyszer használható meghívó-hivatkozás létrehozása - Üzemeltetők kiválasztása + Kiszolgáló-üzemeltetők Hálózati üzemeltetők Amikor egynél több hálózati üzemeltető van engedélyezve, akkor az alkalmazás minden egyes beszélgetéshez a különböző üzemeltetők kiszolgálóit használja. Ha például a SimpleX Chat kiszolgálón keresztül fogadja az üzeneteket, az alkalmazás a Flux egyik kiszolgálóját használja a privát útválasztáshoz. @@ -2170,4 +2170,39 @@ Az Ön jelenlegi csevegőprofiljához tartozó új fájlok kiszolgálói Vagy archívumfájl importálása Távoli hordozható eszközök + Xiaomi eszközök: engedélyezze az automatikus indítást a rendszerbeállításokban, hogy az értesítések működjenek.]]> + A küldéshez másolhatja és csökkentheti az üzenet méretét. + Adja hozzá csapattagjait a beszélgetésekhez. + Üzleti cím + végpontok közötti titkosítással, a közvetlen üzenetek továbbá kvantumrezisztens titkosítással is rendelkeznek.]]> + Hogyan segíti az adatvédelmet + Nincs háttérszolgáltatás + Értesítések és akkumulátor + Az alkalmazás mindig fut a háttérben + Csevegés elhagyása? + Ön nem fog több üzenetet kapni ebből a csevegésből, de a csevegés előzményei megmaradnak. + Csevegés törlése + Meghívás a csevegésbe + Barátok hozzáadása + Csapattagok hozzáadása + A csevegés minden tag számára törlésre kerül - ezt a műveletet nem lehet visszavonni! + A csevegés törlésre kerül az Ön számára - ezt a műveletet nem lehet visszavonni! + Csevegés törlése? + Csevegés elhagyása + Csak a csevegés tulajdonosai módosíthatják a beállításokat. + Könnyen elérhető csevegési eszköztár + A tag el lesz távolítva a csevegésből - ezt a műveletet nem lehet visszavonni! + Csevegés + A szerepkör meg fog változni a következőre: %s. A csevegés tagjai értesítést fognak kapni. + Az Ön csevegési profilja el lesz küldve a csevegésben résztvevő tagok számára + A tagok közötti közvetlen üzenetek le vannak tiltva. + Üzleti csevegések + Az Ön ügyfeleinek adatvédelme. + %1$s.]]> + A csevegés már létezik! + Csökkentse az üzenet méretét, és küldje el újra. + Üzenetek ellenőrzése 10 percenként + Az üzenet túl nagy! + Csökkentse az üzenet méretét vagy távolítsa el a médiát, és küldje el újra. + A tagok közötti közvetlen üzenetek le vannak tiltva ebben a csevegésben. \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml index 1050f1d575..eeedcaa450 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml @@ -1294,4 +1294,57 @@ Hapus anggota? Hapus anggota Pesan tersimpan + Tak dapat memanggil anggota grup + %s.]]> + %s.]]> + Operator + Gunakan server + Gunakan %s + Tak dapat kirim pesan ke anggota grup + Pesan sambutan + Simpan pesan sambutan? + Perbaikan tidak didukung oleh anggota grup + Buat grup rahasia + Negosiasi ulang enkripsi + Masukkan nama grup: + Pratinjau + Kirim pesan untuk aktifkan panggilan. + Kontak dihapus. + Tak dapat memanggil kontak + Perbaiki + Sepenuhnya terdesentralisasi – hanya terlihat oleh anggota. + Perbaiki koneksi + Perbaiki koneksi? + Tinjau ketentuan + Server prasetel + Ketentuan diterima + Ketentuan akan otomatis diterima untuk operator yang diaktifkan pada: %s. + Anda perlu izinkan kontak Anda agar dapat memanggilnya. + Pesan terlalu besar + Masukkan pesan sambutan… + Simpan dan perbarui profil grup + Pesan sambutan terlalu panjang + Perbaikan tidak didukung oleh kontak + Obrolan + Terima kondisi + SERVER + Buat grup + Nama lengkap grup: + Simpan profil grup + Peramban + Server Anda + Gagal simpan profil grup + %s server + Operator jaringan + Ketentuan diterima pada: %s. + Ketentuan akan diterima pada: %s. + Koneksi + Rol + Ganti rol + Profil obrolan Anda akan dikirim ke anggota grup + Profil obrolan Anda akan dikirim ke anggota obrolan + Profil grup disimpan di perangkat anggota, bukan di server. + langsung + Kirim via + Terima via \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml index 2f41824bd1..92c0105d09 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml @@ -211,8 +211,8 @@ A meno che il tuo contatto non abbia eliminato la connessione o che questo link non sia già stato usato, potrebbe essere un errore; per favore segnalalo. \nPer connetterti, chiedi al tuo contatto di creare un altro link di connessione e controlla di avere una connessione di rete stabile. Probabilmente l\'impronta del certificato nell\'indirizzo del server è sbagliata - servizio SimpleX in secondo piano; usa una piccola percentuale di batteria al giorno.]]> - consenti a SimpleX di funzionare in secondo piano nella prossima schermata. Altrimenti le notifiche saranno disattivate.]]> + SimpleX funziona in secondo piano invece di usare le notifiche push.]]> + Consentilo nella prossima schermata per ricevere le notifiche immediatamente.]]> Servizio SimpleX Chat Servizio in secondo piano sempre attivo. Le notifiche verranno mostrate appena i messaggi saranno disponibili. SimpleX Lock @@ -478,7 +478,7 @@ %do %d ora %d ore - I messaggi a tempo sono vietati in questo gruppo. + I messaggi a tempo sono vietati. %dm %d min %d mese @@ -490,10 +490,10 @@ %d settimana %d settimane Link del gruppo - I membri del gruppo possono eliminare irreversibilmente i messaggi inviati. (24 ore) - I membri del gruppo possono inviare messaggi diretti. - I membri del gruppo possono inviare messaggi a tempo. - I membri del gruppo possono inviare messaggi vocali. + I membri possono eliminare irreversibilmente i messaggi inviati. (24 ore) + I membri possono inviare messaggi diretti. + I membri possono inviare messaggi a tempo. + I membri possono inviare messaggi vocali. Confronta i codici di sicurezza con i tuoi contatti. Messaggi a tempo Nascondi la schermata dell\'app nelle app recenti. @@ -648,9 +648,9 @@ Chiamata in arrivo Videochiamata in arrivo Istantaneo - Può essere cambiato in seguito via impostazioni. + Come influisce sulla batteria Crea una connessione privata - crittografia end-to-end a 2 livelli.]]> + Solo i dispositivi client memorizzano i profili utente, i contatti, i gruppi e i messaggi. Chiunque può installare i server. Incolla il link che hai ricevuto Sei tu a decidere chi può connettersi. @@ -660,9 +660,8 @@ repository GitHub.]]> Rifiuta Nessun identificatore utente. - La nuova generazione -\ndi messaggistica privata - Per proteggere la privacy, invece degli ID utente usati da tutte le altre piattaforme, SimpleX dispone di identificatori per le code dei messaggi, separati per ciascuno dei tuoi contatti. + Il futuro dei messaggi + Per proteggere la tua privacy, SimpleX usa ID separati per ciascuno dei tuoi contatti. Usa la chat videochiamata videochiamata (non crittografata e2e) @@ -843,7 +842,7 @@ Le tue preferenze Configurazione del server migliorata Eliminazione irreversibile del messaggio - L\'eliminazione irreversibile dei messaggi è vietata in questo gruppo. + L\'eliminazione irreversibile dei messaggi è vietata. Max 40 secondi, ricevuto istantaneamente. Novità nella %s Solo tu puoi inviare messaggi vocali. @@ -856,7 +855,7 @@ La sicurezza di SimpleX Chat è stata verificata da Trail of Bits. Messaggi vocali I messaggi vocali sono vietati in questa chat. - I messaggi vocali sono vietati in questo gruppo. + I messaggi vocali sono vietati. Novità Con messaggio di benvenuto facoltativo. I tuoi contatti possono consentire l\'eliminazione completa dei messaggi. @@ -1171,7 +1170,7 @@ Cambia codice di autodistruzione Reazioni ai messaggi Le reazioni ai messaggi sono vietate in questa chat. - Le reazioni ai messaggi sono vietate in questo gruppo. + Le reazioni ai messaggi sono vietate. Solo tu puoi aggiungere reazioni ai messaggi. Proibisci le reazioni ai messaggi. Proibisci le reazioni ai messaggi. @@ -1180,7 +1179,7 @@ Solo il tuo contatto può aggiungere reazioni ai messaggi. Consenti ai tuoi contatti di aggiungere reazioni ai messaggi. Consenti reazioni ai messaggi solo se il tuo contatto le consente. - I membri del gruppo possono aggiungere reazioni ai messaggi. + I membri possono aggiungere reazioni ai messaggi. 30 secondi Invia Invia messaggio a tempo @@ -1244,10 +1243,10 @@ Errore nell\'interruzione del cambio di indirizzo Solo i proprietari del gruppo possono attivare file e contenuti multimediali. File e multimediali - I membri del gruppo possono inviare file e contenuti multimediali. + I membri possono inviare file e contenuti multimediali. Proibisci l\'invio di file e contenuti multimediali. Il cambio di indirizzo verrà interrotto. Verrà usato il vecchio indirizzo di ricezione. - File e contenuti multimediali sono vietati in questo gruppo. + File e contenuti multimediali sono vietati. File e contenuti multimediali vietati! Off Nessuna chat filtrata @@ -1723,11 +1722,11 @@ Consenti di inviare link di SimpleX. Vieta l\'invio di link di SimpleX Attivo per - I membri del gruppo possono inviare link di Simplex. + I membri possono inviare link di Simplex. proprietari amministratori tutti i membri - I link di SimpleX sono vietati in questo gruppo. + I link di SimpleX sono vietati. salvato Salvato da Inoltra messaggio… @@ -1999,7 +1998,7 @@ Nessun contatto filtrato Incolla link I tuoi contatti - Barra degli strumenti di chat accessibile + Barre degli strumenti dell\'app accessibili Invita Consentire le chiamate? Chiamate proibite! @@ -2135,7 +2134,7 @@ Operatori di rete Quando più di un operatore di rete è attivato, l\'app userà i server di diversi operatori per ogni conversazione. Puoi configurare gli operatori nelle impostazioni di rete e server. - Scegli gli operatori + Operatori del server Seleziona gli operatori di rete da usare. Continua Aggiorna @@ -2213,4 +2212,39 @@ Condividi indirizzo SimpleX sui social media. O importa file archivio Telefoni remoti + I messaggi diretti tra i membri sono vietati in questa chat. + Dispositivi Xiaomi: attiva l\'avvio automatico nelle impostazioni di sistema per fare funzionare le notifiche.]]> + Aggiungi i membri del tuo team alle conversazioni. + Indirizzo di lavoro + cifrati end-to-end, con sicurezza quantistica nei messaggi diretti.]]> + Controlla i messaggi ogni 10 minuti + Come aiuta la privacy + Invita in chat + Aggiungi membri del team + Chat + I messaggi diretti tra i membri sono vietati. + La chat esiste già! + %1$s.]]> + Uscire dalla chat? + La chat verrà eliminata solo per te, non è reversibile! + Esci dalla chat + Chat di lavoro + La chat verrà eliminata per tutti i membri, non è reversibile! + Aggiungi amici + L\'app funziona sempre in secondo piano + Elimina chat + Eliminare la chat? + Il messaggio è troppo grande! + Riduci la dimensione del messaggio e invialo di nuovo. + Riduci la dimensione del messaggio o rimuovi i media e invialo di nuovo. + Nessun servizio in secondo piano + Il membro verrà rimosso dalla chat, non è reversibile! + Privacy per i tuoi clienti. + Barra degli strumenti di chat accessibile + Solo i proprietari della chat possono modificarne le preferenze. + Notifiche e batteria + Puoi copiare e ridurre la dimensione del messaggio per inviarlo. + Il ruolo verrà cambiato in %s. Verrà notificato a tutti nella chat. + Il tuo profilo di chat verrà inviato ai membri della chat + Non riceverai più messaggi da questa chat. La cronologia della chat verrà conservata. \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml index 26ec40da60..1a5eaa403d 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml @@ -19,7 +19,7 @@ Toegang tot de servers via SOCKS proxy op poort %d\? De proxy moet worden gestart voordat u deze optie inschakelt. Kan geen contacten uitnodigen! Sta het verzenden van directe berichten naar leden toe. - Sta toe om verzonden berichten onomkeerbaar te verwijderen. (24 uur) + Sta toe om verzonden berichten definitief te verwijderen. (24 uur) Sta toe om spraak berichten te verzenden. Chat is actief Wissen @@ -57,7 +57,7 @@ Contact verzoeken automatisch accepteren vetgedrukt Bijvoegen - Sta het onomkeerbaar verwijderen van berichten alleen toe als uw contact dit toestaat. (24 uur) + Sta het definitief verwijderen van berichten alleen toe als uw contact dit toestaat. (24 uur) Sta toe om verdwijnende berichten te verzenden. Sta toe dat uw contacten spraak berichten verzenden. Al uw contacten blijven verbonden. @@ -74,7 +74,7 @@ Alle berichten worden verwijderd, dit kan niet ongedaan worden gemaakt! De berichten worden ALLEEN voor jou verwijderd. Sta verdwijnende berichten alleen toe als uw contact dit toestaat. Sta spraak berichten alleen toe als uw contact ze toestaat. - Laat uw contacten verzonden berichten onomkeerbaar verwijderen. (24 uur) + Laat uw contacten verzonden berichten definitief verwijderen. (24 uur) Sta toe dat uw contacten verdwijnende berichten verzenden. altijd Geluid uit @@ -95,7 +95,7 @@ Batterijoptimalisatie is actief, waardoor achtergrondservice en periodieke verzoeken om nieuwe berichten worden uitgeschakeld. Je kunt ze weer inschakelen via instellingen. Het beste voor de batterij. U ontvangt alleen meldingen wanneer de app wordt uitgevoerd (GEEN achtergrondservice).]]> Het kan worden uitgeschakeld via instellingen, meldingen worden nog steeds weergegeven terwijl de app actief is.]]> - Zowel u als uw contact kunnen verzonden berichten onomkeerbaar verwijderen. (24 uur) + Zowel u als uw contact kunnen verzonden berichten definitief verwijderen. (24 uur) Zowel jij als je contact kunnen verdwijnende berichten sturen. Zowel jij als je contact kunnen spraak berichten verzenden. Let op: u kunt het wachtwoord NIET herstellen of wijzigen als u het kwijt raakt.]]> @@ -254,12 +254,12 @@ Apparaatverificatie is uitgeschakeld. SimpleX Vergrendelen uitschakelen. Vul uw naam in: Apparaatverificatie is niet ingeschakeld. Je kunt SimpleX Vergrendelen inschakelen via Instellingen zodra je apparaatverificatie hebt ingeschakeld. - Directe berichten tussen leden zijn verboden in deze groep. + Directe berichten tussen leden zijn niet toegestaan in deze groep. %d bestand(en) met een totale grootte van %s %d uur Uitzetten Verdwijnende berichten - Verdwijnende berichten zijn verboden in dit gesprek. + Verdwijnende berichten zijn niet toegestaan in dit gesprek. SimpleX Vergrendelen uitschakelen Verdwijnende berichten Verbinding verbreken @@ -272,7 +272,7 @@ %ds Verwijder contact Server verwijderen - Verdwijnende berichten zijn verboden in deze groep. + Verdwijnende berichten zijn niet toegestaan. %d sec %dm %dmth @@ -338,9 +338,9 @@ ingeschakeld ingeschakeld voor contact voor u ingeschakeld - Groepsleden kunnen verzonden berichten onomkeerbaar verwijderen. (24 uur) - Groepsleden kunnen directe berichten sturen - Groepsleden kunnen spraak berichten verzenden. + Leden kunnen verzonden berichten definitief verwijderen. (24 uur) + Leden kunnen directe berichten sturen. + Leden kunnen spraak berichten verzenden. Per chatprofiel (standaard) of per verbinding (BETA). Verschillende namen, avatars en transportisolatie. Franse interface @@ -373,7 +373,7 @@ Video Fout bij opslaan van ICE servers geëindigd - Groepsleden kunnen verdwijnende berichten sturen. + Leden kunnen verdwijnende berichten sturen. %d week %dw %d weken @@ -396,7 +396,7 @@ Afbeelding verzonden Live bericht! Als je een uitnodiging link voor SimpleX Chat hebt ontvangen, kun je deze in je browser openen: - Dit kan later worden gewijzigd via instellingen. + Hoe dit de batterij beïnvloedt Deelnemen aan groep\? Nodig leden uit Geen contacten geselecteerd @@ -439,7 +439,7 @@ Groep verlaten Lokale naam Alleen lokale profielgegevens - Het onomkeerbaar verwijderen van berichten is verboden in deze groep. + Het definitief verwijderen van berichten is niet toegestaan. App scherm verbergen in de recente apps. Incognito modus Berichten @@ -458,7 +458,7 @@ Meer verbeteringen volgen snel! Nodig leden uit Verberg contact en bericht - op de achtergrond uitvoeren. Anders worden de meldingen uitgeschakeld.]]> + Sta dit toe in het volgende dialoogvenster om direct meldingen te ontvangen.]]> Als u ervoor kiest om te weigeren, wordt de afzender NIET op de hoogte gesteld. Onmiddellijk heeft %1$s uitgenodigd @@ -508,7 +508,7 @@ Nee Immuun voor spam Maak een privéverbinding - Het onomkeerbaar verwijderen van berichten is verboden in dit gesprek. + Het definitief verwijderen van berichten is niet toegestaan in dit gesprek. Nieuw in %s Max 40 seconden, direct ontvangen. Verbeterde serverconfiguratie @@ -552,7 +552,7 @@ aan Alleen jij kunt verdwijnende berichten verzenden. Alleen uw contact kan verdwijnende berichten verzenden. - Alleen u kunt berichten onomkeerbaar verwijderen (uw contact kan ze markeren voor verwijdering). (24 uur) + Alleen u kunt berichten definitief verwijderen (uw contact kan ze markeren voor verwijdering). (24 uur) voorgesteld %s: %2s Oud database archief Voer het juiste huidige wachtwoord in. @@ -581,7 +581,7 @@ Alleen uw contact kan berichten onherroepelijk verwijderen (u kunt ze markeren voor verwijdering). (24 uur) Alleen jij kunt spraak berichten verzenden. Alleen uw contact kan spraak berichten verzenden. - Verbied het onomkeerbaar verwijderen van berichten. + Verbied het definitief verwijderen van berichten. voorgesteld %s Sla het wachtwoord veilig op. Als u deze kwijtraakt, heeft u GEEN toegang tot de chats. Bewaar het wachtwoord veilig, u kunt deze NIET wijzigen als u deze kwijtraakt. @@ -590,7 +590,7 @@ Oproep in behandeling Het openen van de link in de browser kan de privacy en beveiliging van de verbinding verminderen. Niet vertrouwde SimpleX links worden rood weergegeven. Werk de app bij en neem contact op met de ontwikkelaars. - 2-laags end-to-end codering.]]> + Alleen clientapparaten slaan gebruikersprofielen, contacten, groepen en berichten op. De afzender heeft mogelijk het verbindingsverzoek verwijderd. Schakel SimpleX Vergrendelen in om uw informatie te beschermen. \nU wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingeschakeld. @@ -611,7 +611,7 @@ Uw profiel, contacten en afgeleverde berichten worden op uw apparaat opgeslagen. beginnen… Video aan - Deze actie kan niet ongedaan worden gemaakt. Uw profiel, contacten, berichten en bestanden gaan onomkeerbaar verloren. + Deze actie kan niet ongedaan worden gemaakt. Uw profiel, contacten, berichten en bestanden gaan definitief verloren. Deze instelling is van toepassing op berichten in uw huidige chatprofiel bijgewerkt groep profiel verwijderd @@ -710,10 +710,9 @@ U kunt markdown gebruiken voor opmaak in berichten: geweigerde oproep geheim - De volgende generatie -\nprivéberichten + De toekomst van berichtenuitwisseling wachten op antwoord… - Om de privacy te beschermen, heeft SimpleX in plaats van gebruikers-ID\'s die door alle andere platforms worden gebruikt, ID\'s voor berichten wachtrijen, afzonderlijk voor elk van uw contacten. + Om uw privacy te beschermen, gebruikt SimpleX voor elk van uw contacten afzonderlijke ID\'s. Gebruik chat Wanneer de app actief is video gesprek (niet e2e versleuteld) @@ -764,7 +763,7 @@ Volledig gedecentraliseerd – alleen zichtbaar voor leden. Timeout van TCP-verbinding Spraak berichten - Spraak berichten zijn verboden in dit gesprek. + Spraak berichten zijn niet toegestaan in dit gesprek. Verbied het verzenden van verdwijnende berichten. Verminderd batterijgebruik Om de tijdzone te beschermen, gebruiken afbeeldings-/spraakbestanden UTC. @@ -804,7 +803,7 @@ Kleuren resetten Systeem ja - gekregen, verboden + gekregen, niet toegestaan Jouw voorkeuren Stel 1 dag in Wat is er nieuw @@ -824,7 +823,7 @@ Spraakbericht… Spraakbericht (%1$s) Contactnaam instellen… - Spraak berichten verboden! + Spraak berichten niet toegestaan! Resetten Verstuur Stuur een live bericht, het wordt bijgewerkt voor de ontvanger(s) terwijl u het typt @@ -886,21 +885,20 @@ Deze string is geen verbinding link! Deze actie kan niet ongedaan worden gemaakt, de berichten die eerder zijn verzonden en ontvangen dan geselecteerd, worden verwijderd. Het kan enkele minuten duren. Het ontvangstadres wordt gewijzigd naar een andere server. Adres wijziging wordt voltooid nadat de afzender online is. - SimpleX achtergrond service - deze gebruikt een paar procent van de batterij per dag.]]> + draait SimpleX op de achtergrond in plaats van pushmeldingen te gebruiken.]]> Jij staat toe Je bent uitgenodigd voor de groep verbinding maken met SimpleX Chat ontwikkelaars om vragen te stellen en updates te ontvangen.]]> Tenzij uw contact de verbinding heeft verwijderd of deze link al is gebruikt, kan het een bug zijn. Meld het alstublieft. \nOm verbinding te maken, vraagt u uw contact om een andere verbinding link te maken en te controleren of u een stabiele netwerkverbinding heeft. SimpleX Chat servers gebruiken\? - Spraak berichten zijn verboden in deze groep. + Spraak berichten zijn niet toegestaan. Welkom %1$s! U kunt de chat starten via app Instellingen / Database of door de app opnieuw op te starten. je hebt het adres gewijzigd voor %s je hebt %1$s verwijderd Je contact heeft een bestand verzonden dat groter is dan de momenteel ondersteunde maximale grootte (%1$s). - Uw huidige chatdatabase wordt VERWIJDERD en VERVANGEN door de geïmporteerde. -\nDeze actie kan niet ongedaan worden gemaakt. Uw profiel, contacten, berichten en bestanden gaan onomkeerbaar verloren. + Uw huidige chatdatabase wordt VERWIJDERD en VERVANGEN door de geïmporteerde. \nDeze actie kan niet ongedaan worden gemaakt. Uw profiel, contacten, berichten en bestanden gaan definitief verloren. Je probeert een contact met wie je een incognito profiel hebt gedeeld uit te nodigen voor de groep waarin je je hoofdprofiel gebruikt Uw profiel wordt op uw apparaat opgeslagen en alleen gedeeld met uw contacten. SimpleX servers kunnen uw profiel niet zien. U moet zich authenticeren wanneer u de app na 30 seconden op de achtergrond start of hervat. @@ -1090,7 +1088,7 @@ " \nBeschikbaar in v5.1" Audio/video gesprekken verbieden. - Audio/video gesprekken zijn verboden. + Audio/video gesprekken zijn niet toegestaan. Snel en niet wachten tot de afzender online is! App toegangscode Stel het in in plaats van systeemverificatie. @@ -1165,19 +1163,19 @@ Zelfvernietigings wachtwoord ingeschakeld! De app-toegangscode wordt vervangen door een zelfvernietigings wachtwoord. Als u uw zelfvernietigings wachtwoord invoert tijdens het openen van de app: - Als u deze toegangscode invoert bij het openen van de app, worden alle app-gegevens onomkeerbaar verwijderd! + Als u deze toegangscode invoert bij het openen van de app, worden alle app-gegevens definitief verwijderd! Toegangscode instellen Bericht reacties verbieden. Alleen jij kunt bericht reacties toevoegen. - Reacties op berichten zijn verboden in deze groep. + Reacties op berichten zijn niet toegestaan. Berichten reacties verbieden. Sta bericht reacties alleen toe als uw contact dit toestaat. Sta uw contactpersonen toe om bericht reacties toe te voegen. Sta bericht reacties toe. - Groepsleden kunnen bericht reacties toevoegen. + Leden kunnen reacties op berichten toevoegen. Zowel u als uw contact kunnen bericht reacties toevoegen. Reacties op berichten - Reacties op berichten zijn verboden in deze chat. + Reacties op berichten zijn niet toegestaan in deze chat. Alleen uw contact kan bericht reacties toevoegen. dagen uren @@ -1241,14 +1239,14 @@ Afbreken Geen gefilterde chats Alleen groep eigenaren kunnen bestanden en media inschakelen. - Bestanden en media zijn verboden in deze groep. + Bestanden en media zijn niet toegestaan. Favoriet - Bestanden en media verboden! + Bestanden en media niet toegestaan! Niet favoriet Bestanden en media Verbied het verzenden van bestanden en media. Sta toe om bestanden en media te verzenden. - Groepsleden kunnen bestanden en media verzenden. + Leden kunnen bestanden en media verzenden. Zoeken Uit Protocol timeout per KB @@ -1722,10 +1720,10 @@ beheerders alle leden eigenaren - SimpleX-links zijn in deze groep verboden. + SimpleX-links zijn niet toegestaan. Ingeschakeld voor Sta het verzenden van SimpleX-links toe. - Groepsleden kunnen SimpleX-links verzenden. + Leden kunnen SimpleX-links verzenden. opgeslagen opgeslagen van %s Doorsturen @@ -1988,7 +1986,7 @@ Oproepen toestaan? bellen Kan geen groepslid bellen - Bellen verboden! + Bellen niet toegestaan! Contact verwijderen bevestigen? Gesprek verwijderd! Verwijderen zonder melding @@ -2009,7 +2007,7 @@ open Plak de link Uitnodiging - Toegankelijke chatwerkbalk + Bereikbare app-toolbars Vraag uw contactpersoon om oproepen in te schakelen. Geen gefilterde contacten Selecteer @@ -2144,7 +2142,7 @@ Of om privé te delen Adres instellingen Eenmalige link maken - Operators kiezen + Serverbeheerders Voor ingeschakelde operators worden de voorwaarden na 30 dagen geaccepteerd. Netwerkbeheerders Later beoordelen @@ -2209,4 +2207,40 @@ Om te verzenden U kunt servers configureren via instellingen. Of importeer archiefbestand + Directe berichten tussen leden zijn in deze chat niet toegestaan. + Externe mobiele telefoons + Xiaomi-apparaten: schakel Automatisch starten in de systeeminstellingen in om meldingen te laten werken.]]> + Bericht is te groot! + Verklein het bericht en verstuur het opnieuw. + U kunt het bericht kopiëren en verkleinen om het te verzenden. + Voeg uw teamleden toe aan de gesprekken. + Zakelijk adres + end-to-end-versleuteld verzonden, met post-kwantumbeveiliging in directe berichten.]]> + App draait altijd op de achtergrond + Controleer berichten elke 10 minuten + Meldingen en batterij + Hoe het de privacy helpt + U ontvangt geen berichten meer van deze chat. De chatgeschiedenis blijft bewaard. + Chat verlaten? + Vrienden toevoegen + Teamleden toevoegen + Uitnodigen voor een chat + De chat wordt voor je verwijderd - dit kan niet ongedaan worden gemaakt! + Chat + Directe berichten tussen leden zijn niet toegestaan. + Chat bestaat al! + Chat verwijderen + Chat verwijderen? + Chat verlaten + Lid wordt verwijderd uit de chat - dit kan niet ongedaan worden gemaakt! + De rol wordt gewijzigd naar %s. Iedereen in de chat wordt op de hoogte gebracht. + Uw chatprofiel wordt naar chatleden verzonden + Privacy voor uw klanten. + %1$s.]]> + De chat wordt voor alle leden verwijderd - dit kan niet ongedaan worden gemaakt! + Bereikbare chat-toolbar + Zakelijke chats + Geen achtergrondservice + Alleen chateigenaren kunnen voorkeuren wijzigen. + Verklein de berichtgrootte of verwijder de media en verzend het bericht opnieuw. \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml index 7b46d10921..56c80f8c89 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml @@ -420,8 +420,7 @@ uruchamianie… strajk Brak identyfikatorów użytkownika. - Następna generacja -\nprywatnych wiadomości + Następna generacja \nprywatnych wiadomości oczekiwanie na odpowiedź… oczekiwanie na potwierdzenie… Możesz używać markdown do formatowania wiadomości: diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/pt-rBR/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/pt-rBR/strings.xml index 034e5d19db..27cc039c34 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/pt-rBR/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/pt-rBR/strings.xml @@ -849,8 +849,7 @@ Para começar um novo bate-papo Ligar Bem-vindo(a)! - A próxima geração -\nde mensageiros privados + A próxima geração \nde mensageiros privados PROXY SOCKS A tentativa de alterar a senha do banco de dados não foi concluída. Pare o bate-papo para exportar, importar ou excluir o banco de dados do chat. Você não poderá receber e enviar mensagens enquanto o chat estiver interrompido. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml index bcd0848e8d..00df4f75cb 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml @@ -88,9 +88,9 @@ Мгновенные уведомления Мгновенные уведомления! Мгновенные уведомления выключены! - фоновый сервис SimpleX, который потребляет несколько процентов батареи в день.]]> + SimpleX выполняется в фоне вместо уведомлений через сервер.]]> Он может быть выключен через Настройки – Вы продолжите получать уведомления о сообщениях пока приложение запущено.]]> - разрешите SimpleX выполняться в фоне в следующем диалоге. Иначе уведомления будут выключены.]]> + Разрешите это в следующем окне чтобы получать нотификации мгновенно.]]> Оптимизация батареи включена, поэтому сервис уведомлений выключен. Вы можете снова включить его через Настройки. Периодические уведомления Периодические уведомления выключены! @@ -463,7 +463,7 @@ соединено завершен - Новое поколение\nприватных сообщений + Будущее коммуникаций Более конфиденциальный Без идентификаторов пользователей. Защищен от спама @@ -475,8 +475,8 @@ Как это работает Как SimpleX работает - Чтобы защитить Вашу конфиденциальность, вместо ID пользователей, которые есть в других платформах, SimpleX использует ID для очередей сообщений, разные для каждого контакта. - с двухуровневым end-to-end шифрованием.]]> + Чтобы защитить Вашу конфиденциальность, SimpleX использует разные ID для всех ваших контактов. + Только пользовательские устройства хранят контакты, группы и сообщения. GitHub репозитория.]]> Использовать чат @@ -878,12 +878,12 @@ Запретить необратимое удаление сообщений. Разрешить отправлять голосовые сообщения. Запретить отправлять голосовые сообщений. - Члены группы могут посылать прямые сообщения. + Члены могут посылать прямые сообщения. Прямые сообщения между членами группы запрещены. - Члены группы могут необратимо удалять отправленные сообщения. (24 часа) - Необратимое удаление сообщений запрещено в этой группе. - Члены группы могут отправлять голосовые сообщения. - Голосовые сообщения запрещены в этой группе. + Члены могут необратимо удалять отправленные сообщения. (24 часа) + Необратимое удаление сообщений запрещено. + Члены могут отправлять голосовые сообщения. + Голосовые сообщения запрещены. Минимальный расход батареи. Вы получите уведомления только когда приложение запущено, без фонового сервиса.]]> Уведомления Когда приложение запущено @@ -891,7 +891,7 @@ Мгновенно Больше расход батареи! Приложение постоянно запущено в фоне - уведомления будут показаны сразу же.]]> Меньше расход батареи. Приложение проверяет сообщения каждые 10 минут. Вы можете пропустить звонки и срочные сообщения.]]> - Можно изменить позже в настройках. + Как это влияет на потребление энергии LIVE Отправить живое сообщение Живое сообщение! @@ -927,7 +927,7 @@ Отправить живое сообщение — оно будет обновляться для получателей по мере того, как Вы его вводите Создать ссылку группы Запретить отправлять исчезающие сообщения. - Исчезающие сообщения запрещены в этой группе. + Исчезающие сообщения запрещены. %dнед %dд %d нед. @@ -940,7 +940,7 @@ Сбросить подтверждение Разрешить исчезающие сообщения, только если Ваш контакт разрешает их Вам. Запретить посылать исчезающие сообщения. - Члены группы могут посылать исчезающие сообщения. + Члены могут посылать исчезающие сообщения. Что нового Новое в %s Аудит безопасности @@ -1193,7 +1193,7 @@ Установить код доступа История Информация - Открыть профили чата + Изменить профили чата Полученное сообщение Отправленное сообщение Исчезающее сообщение @@ -1227,9 +1227,9 @@ Разрешить реакции на сообщения. Разрешить реакции на сообщения, только если ваш контакт разрешает их. Разрешить контактам добавлять реакции на сообщения. - Члены группы могут добавлять реакции на сообщения. + Члены могут добавлять реакции на сообщения. Реакции на сообщения в этом чате запрещены. - Реакции на сообщения запрещены в этой группе. + Реакции на сообщения запрещены. Только Ваш контакт может добавлять реакции на сообщения. Запретить реакции на сообщения. Запретить реакции на сообщения. @@ -1348,8 +1348,8 @@ Нотификации перестанут работать, пока вы не перезапустите приложение Таймаут протокола на KB Разрешить посылать файлы и медиа. - Члены группы могут слать файлы и медиа. - Файлы и медиа запрещены в этой группе. + Члены могут слать файлы и медиа. + Файлы и медиа запрещены. Файлы и медиа запрещены! Только владельцы группы могут разрешить файлы и медиа. Файлы и медиа @@ -1818,7 +1818,7 @@ Ссылки SimpleX Разрешить отправлять ссылки SimpleX. Запретить отправку ссылок SimpleX - Члены группы могут отправлять ссылки SimpleX + Члены могут отправлять ссылки SimpleX админы все члены владельцы @@ -1828,7 +1828,7 @@ Включено для Переслать Переслать и сохранить сообщение - Ссылки SimpleX запрещены в этой группе. + Ссылки SimpleX запрещены. Переслать сообщение… Литовский интерфейс Источник сообщения остаётся конфиденциальным. @@ -2021,7 +2021,7 @@ Слабое Среднее Выключено - Доступная панель чата + Доступная панель приложения Текущий профиль Нет информации, попробуйте перезагрузить Информация о серверах @@ -2199,4 +2199,134 @@ Ваши учетные данные могут быть отправлены в незашифрованном виде. Удалить архив? Загруженный архив базы данных будет навсегда удален с серверов. + Принятые условия + Принять условия + Нет серверов сообщений. + Нет серверов для приема сообщений. + Ошибки в настройках серверов. + Для профиля %s: + Нет серверов файлов и медиа. + Нет серверов для приема файлов. + Нет серверов для отправки файлов. + Недоставленные сообщения + Нажмите Создать адрес SimpleX в меню, чтобы создать его позже. + Адрес или одноразовая ссылка? + Безопасность соединения + Операторы серверов + Ваши серверы + Посмотреть условия + Посмотреть условия + %s.]]> + Условия будут автоматически приняты для включенных операторов: %s + Условия приняты: %s. + Вебсайт + %s.]]> + %s.]]> + %s, примите условия использования.]]> + Для оправки + Дополнительные серверы сообщений + Использовать для файлов + Открыть условия + Ошибка добавления сервера + Сервер оператора + Сервер добавлен к оператору %s. + Тулбары приложения + Прозрачность + Децентрализация сети + Второй оператор серверов в приложении! + Включить Flux + для лучшей конфиденциальности метаданных. + Улучшенная навигация в разговоре + Посмотреть измененные условия + Устройства Xiaomi: пожалуйста, включите опцию Autostart в системных настройках для работы нотификаций.]]> + Нет сообщения + Это сообщение было удалено или еще не получено. + Сообщение слишком большое! + Пожалуйста, уменьшите размер сообщения и отправьте снова. + Пожалуйста, уменьшите размер сообщения или уберите медиа и отправьте снова. + Чтобы отправить сообщение, скопируйте и уменьшите его размер. + Поделитесь одноразовой ссылкой с другом + Поделитесь адресом + Поделитесь SimpleX адресом в социальных сетях. + Адрес SimpleX и одноразовые ссылки безопасно отправлять через любой мессенджер. + Вы можете установить имя соединения, чтобы запомнить кому Вы отправили ссылку. + Новый сервер + Создать одноразовую ссылку + Для социальных сетей + Или поделиться конфиденциально + Адрес SimpleX или одноразовая ссылка? + Настройки адреса + Добавьте сотрудников в разговор. + Бизнес адрес + end-to-end шифрованием, с пост-квантовой безопасностью в прямых разговорах.]]> + Приложение всегда выполняется в фоне + Проверять сообщения каждые 10 минут + Без фонового сервиса + Нотификации и батарейка + Как это улучшает конфиденциальность + Операторы сети + Выберите операторов сети. + Вы можете настроить операторов в настройках Сеть и серверы. + Продолжить + Посмотреть позже + Обновить + Связанные мобильные устройства + Покинуть разговор? + Вы прекратите получать сообщения в этом разговоре. История будет сохранена. + Добавить друзей + Добавить сотрудников + Удалить разговор + Удалить разговор? + Пригласить в разговор + Разговор будет удален для всех участников - это действие нельзя отменить! + Оператор + %s серверы + %s.]]> + Условия будут приняты: %s + Оператор сети + Использовать %s + Использовать серверы + %s.]]> + %s.]]> + Или импортировать файл архива + Доступная панель чата + Разговор будет удален для Вас - это действие нельзя отменить! + Покинуть разговор + Только владельцы разговора могут поменять предпочтения. + Текст условий использования не может быть показан, вы можете посмотреть их через ссылку: + Разговор + Член будет удален из разговора - это действие нельзя отменить! + Серверы по умолчанию + Роль будет изменена на %s. Все участники разговора получат уведомление. + Ваш профиль будет отправлен участникам разговора. + %s.]]> + %s.]]> + Условия использования + Дополнительные серверы файлов и медиа + Ошибка сохранения сервера + Для доставки сообщений + Открыть изменения + Оператор серверов изменен. + Протокол сервера изменен. + Серверы для новых файлов Вашего текущего профиля + Для получения + Использовать для сообщений + Размыть + Прямые сообщения между членами запрещены. + Бизнес разговоры + - Открывает разговор на первом непрочитанном сообщении.\n- Перейти к цитируемому сообщению. + Конфиденциальность для ваших покупателей. + %1$s.]]> + Разговор уже существует! + только с одним контактом - поделитесь при встрече или через любой мессенджер.]]> + Нет серверов для доставки сообщений. + Вы можете сконфигурировать серверы через настройки. + Когда больше чем один оператор сети включен, приложение использует серверы разных операторов в каждом разговоре. + Ошибка сохранения серверов + Условия будут приняты для включенных операторов через 30 дней. + Ошибка приема условий + Соединение достигло предела недоставленных сообщений. Возможно, Ваш контакт не в сети. + Чтобы защитить Вашу ссылку от замены, Вы можете сравнить код безопасности. + Например, если Ваш контакт получает сообщения через сервер SimpleX Chat, Ваше приложение доставит их через сервер Flux. + Прямые сообщения между членами запрещены в этом разговоре. \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml index 73daa373c1..0afb405097 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml @@ -1162,8 +1162,7 @@ кольоровий дзвінок завершено %1$s помилка дзвінка - Наступне покоління -\nприватних повідомлень + Наступне покоління \nприватних повідомлень Кожен може хостити сервери. Інструменти розробника Експериментальні функції diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml index 9df7b74c33..41e9793ba1 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml @@ -3,7 +3,7 @@ a + b 1天 关于 SimpleX - 所有群组成员将保持连接。 + 所有群成员将保持连接。 关于 SimpleX Chat 以上,然后: 接受 @@ -22,7 +22,7 @@ 高级网络设置 接受连接请求? 接受隐身聊天 - 管理员可以创建链接以加入群组。 + 管理员可以创建链接以加入群。 添加预设服务器 通过链接连接 已建立连接 @@ -39,7 +39,7 @@ 消息和文件 添加个人资料 所有聊天记录和消息将被删除——这一行为无法撤销! - 所有聊天记录和消息将被删除——这一行为无法撤销!只有您的消息会被删除。 + 所有聊天记录和消息将被删除——这一行为无法撤销!只有你的消息会被删除。 允许发送语音消息。 允许语音消息? 删除 @@ -52,8 +52,8 @@ 为所有人删除 为我删除 为所有聊天资料删除文件 - 删除群组 - 删除群组? + 删除群 + 删除群? 删除链接 删除链接? 连接 @@ -63,17 +63,17 @@ 连接 通过一次性链接进行连接? 通过联系人地址进行连接? - 加入群组? - 通过群组链接/二维码连接 + 加入群? + 通过群链接/二维码连接 总是通过中继连接 - 允许您的联系人不不可逆地删除已发送消息。(24小时) + 允许你的联系人不不可逆地删除已发送消息。(24小时) 联系人允许 允许语音消息,前提是你的联系人允许这样的消息。 - 您: %1$s - 允许您的联系人发送语音消息。 + 你: %1$s + 允许你的联系人发送语音消息。 始终 始终开启 - 允许您的联系人发送限时消息。 + 允许你的联系人发送限时消息。 应用程序构建:%s 所有联系人会保持连接。 允许 @@ -83,14 +83,14 @@ 删除聊天资料? 删除联系人 删除联系人? - 已删除群组 + 已删除群 删除图片 允许限时消息,前提是你的联系人允许这样的消息。 允许不可逆的消息删除,前提是你的联系人允许这样做。(24小时) 允许不可逆地删除已发送消息。(24小时) 为此删除聊天资料 删除数据库 - 在您重启应用程序或者更换密码后安卓密钥库系统用来安全地保存密码——来确保收到通知。 + 在你重启应用程序或者更换密码后安卓密钥库系统用来安全地保存密码——来确保收到通知。 安卓密钥库系统用来安全地保存密码——来确保通知服务运作。 外观 应用程序版本 @@ -107,23 +107,23 @@ 消息散列值错误 错误消息 ID 语音和视频通话 - 启用电池优化,关闭了后台服务和对新消息的定期请求。您可以在设置里重新启用它们。 + 启用电池优化,关闭了后台服务和对新消息的定期请求。你可以在设置里重新启用它们。 后台服务始终运行——一旦有消息,就会显示通知。 关闭音频 开启音频 已要求接收图片 - ,用于您在应用程序中的每个聊天资料 。]]> - 每个联系人和群成员。\n请注意:如果您有很多连接,您的电池和流量消耗可能会大大增加,并且某些连接可能会失败。]]> + ,用于你在应用程序中的每个聊天资料 。]]> + 每个联系人和群成员。\n请注意:如果你有很多连接,你的电池和流量消耗可能会大大增加,并且某些连接可能会失败。]]> 返回 - 最长续航 。您只会在应用程序运行时收到通知(无后台服务)。]]> - 较长续航 。应用每 10 分钟检查一次消息。您可能会错过来电或者紧急信息。]]> + 最长续航 。你只会在应用程序运行时收到通知(无后台服务)。]]> + 较长续航 。应用每 10 分钟检查一次消息。你可能会错过来电或者紧急信息。]]> 加粗 - 您和您的联系人都可以不可逆地删除已发送的消息。(24小时) - 您和您的联系人都可以发送限时消息。 - 您和您的联系人都可以发送语音消息。 + 你和你的联系人都可以不可逆地删除已发送的消息。(24小时) + 你和你的联系人都可以发送限时消息。 + 你和你的联系人都可以发送语音消息。 可以在设置里禁用它 - 应用程序运行时仍会显示通知。]]> 使用更多电量 !应用始终在后台运行——一即刻显示通知。]]> - 请注意:如果您丢失密码,您将无法恢复或者更改密码。]]> + 请注意:如果你丢失密码,你将无法恢复或者更改密码。]]> 通话已结束! 无法邀请联系人! 无法邀请联系人! @@ -133,7 +133,7 @@ 通话结束 更改数据库密码? 通话错误 - 为您更改地址 + 为你更改地址 通话中 通话进行中 呼叫中…… @@ -147,14 +147,14 @@ 无法接收文件 无法初始化数据库 将 %s 的角色更改为 %s - 将您的角色更改为 %s + 将你的角色更改为 %s 改变角色 - 更改群组角色? + 更改群角色? 取消链接预览 正在为 %s 更改地址…… 更改地址中…… 更改地址中…… - 创建您的资料 + 创建你的资料 聊天数据库已删除 聊天数据库已导入 钥匙串错误 @@ -164,47 +164,47 @@ 聊天运行中 聊天已停止 联系人偏好设置 - 您的偏好设置 - 群组偏好设置 - 只有群主可以改变群组偏好设置。 + 你的偏好设置 + 群偏好设置 + 只有群主可以改变群偏好设置。 保存偏好设置? - 设置群组偏好设置 + 设置群偏好设置 重新定义隐私 改进的隐私和安全 隐身聊天 - 加入群组中 + 加入群中 加入隐身聊天 隐身模式 点击开始一个新聊天 - 您的随机资料 + 你的随机资料 通过联系地址链接隐身 - 通过群组链接隐身 - 您分享了一次性链接隐身聊天 + 通过群链接隐身 + 你分享了一次性链接隐身聊天 点击以加入隐身聊天 - 您的聊天资料将被发送给群组成员 - 您正在尝试邀请与您共享隐身个人资料的联系人加入您使用主要个人资料的群组 - 隐身模式通过为每个联系人使用新的随机配置文件来保护您的隐私。 - 您正在为该群组使用隐身个人资料——为防止共享您的主要个人资料,不允许邀请联系人 + 你的聊天资料将被发送给群成员 + 你正在尝试邀请与你共享隐身个人资料的联系人加入你使用主要个人资料的群 + 隐身模式通过为每个联系人使用新的随机配置文件来保护你的隐私。 + 你正在为该群使用隐身个人资料——为防止共享你的主要个人资料,不允许邀请联系人 通过一次性链接隐身 只有群主可以启用语音信息。 - 您的隐私设置 + 你的隐私设置 隐私和安全 保存服务器 它允许在一个聊天资料中有多个匿名连接,而它们之间没有任何共享数据。 - 当您与某人共享隐身聊天资料时,该资料将用于他们邀请您加入的群组。 + 当你与某人共享隐身聊天资料时,该资料将用于他们邀请你加入的群。 改进的服务器配置 电邮 编辑图片 - 编辑群组资料 + 编辑群资料 加密数据库错误 导出聊天数据库错误 导入聊天数据库错误 - 加入群组错误 + 加入群错误 删除用户资料错误 数据库密码不同于保存在密钥库中的密码。 数据库加密密码将被更新并存储在密钥库中。 数据库将被加密,密码存储在密钥库中。 - 在密匙库中没有找到密码,请手动输入。如果您使用备份工具恢复了应用程序的数据,可能会发生这种情况。如果不是这种情况,请联系开发者。 + 在密匙库中没有找到密码,请手动输入。如果你使用备份工具恢复了应用程序的数据,可能会发生这种情况。如果不是这种情况,请联系开发者。 从密钥库中删除密码? 在密钥库中保存密码 SimpleX Chat 服务 @@ -232,7 +232,7 @@ 关闭按键 配置 ICE 服务器 确认 - 确认您的证书 + 确认你的证书 已连接 已连接 连接 @@ -241,14 +241,14 @@ 连接中 每10分钟检查一次新消息,最长检查1分钟 已连接 - 与您的联系人比较安全码。 + 与你的联系人比较安全码。 语音通话来电 更改设置错误 - 群组邀请不再有效,已被发件人删除。 + 群邀请不再有效,已被发件人删除。 重复的显示名! 创建资料错误! 接受联系人请求错误 - 删除群组错误 + 删除群错误 删除待定的联系人连接错误 接收文件错误 切换资料错误! @@ -259,7 +259,7 @@ 对于每个人 解码错误 图片保存到相册 - 图片将在您的联系人在线时收到,请稍等或稍后查看! + 图片将在你的联系人在线时收到,请稍等或稍后查看! 保存文件错误 文件 未找到文件 @@ -296,17 +296,17 @@ 加密 加密数据库 输入正确密码。 - 不活跃群组 + 不活跃群 创建者 连接中(已接受) 连接中(已宣布) 扩展角色选择 - 群组链接 - 删除群组链接错误 + 群链接 + 删除群链接错误 数据库 ID 删除成员错误 更改角色错误 - 群组 + 限时消息 %d 天 连接中…… @@ -315,7 +315,7 @@ 联系人姓名 连接中(介绍邀请) 连接中…… - 联系人可以将信息标记为删除;您将可以查看这些信息。 + 联系人可以将信息标记为删除;你将可以查看这些信息。 贡献 已检查联系人 联系人已隐藏: @@ -323,12 +323,12 @@ 上下文图标 已复制到剪贴板 连接中…… - 创建群组链接 - 创建私密群组 + 创建群链接 + 创建私密群 创建链接 创建一次性邀请链接 创建队列 - 创建私密群组 + 创建私密群 不同的名字、头像和传输隔离。 法语界面 如何使用它 @@ -352,17 +352,17 @@ 全名: 输入你的名字: 已结束 - 群组已删除 - 将为所有成员删除群组——此操作无法撤消! + 群已删除 + 将为所有成员删除群——此操作无法撤消! 直接 私信 已启用 - 群组成员可以发送语音消息。 - 群组链接 + 成员可以发送语音消息。 + 群链接 启动聊天错误 数据库已加密! 加密数据库? - 数据库使用随机密码进行加密,您可以更改它。 + 数据库使用随机密码进行加密,你可以更改它。 打开聊天需要数据库密码。 停止聊天错误 导入聊天数据库? @@ -371,16 +371,16 @@ 输入密码…… 数据库加密密码将被更新。 数据库错误 - 群组资料已更新 - 群组全名: + 群资料已更新 + 群全名: 深色 已为联系人启用 - 为您启用 - 群组成员可以发送限时消息。 + 为你启用 + 成员可以发送限时消息。 创建个人资料 工作原理 - 未找到群组! - 创建群组链接错误 + 未找到群! + 创建群链接错误 连接中(已介绍) 数据库使用随机密码进行加密。请在导出前更改它。 数据库密码和导出 @@ -397,25 +397,25 @@ 删除联系人错误 更新网络配置错误 删除联系人请求错误 - 保存群组资料错误 + 保存群资料错误 保存 SMP 服务器错误 保存 ICE 服务器错误 发送消息错误 完整链接 - 输入群组名: - 群组邀请已过期 - 将为您删除群组——此操作无法撤消! - 群组资料存储在成员的设备上,而不是服务器上。 - 如果您选择拒绝发件人,将不会收到通知。 - 如何使用您的服务器 - 如果您确认,消息服务器将能够看到您的 IP 地址和您的提供商——以及您正在连接的服务器。 + 输入群名: + 群邀请已过期 + 将为你删除群——此操作无法撤消! + 群资料存储在成员的设备上,而不是服务器上。 + 如果你选择拒绝发件人,将不会收到通知。 + 如何使用你的服务器 + 如果你确认,消息服务器将能够看到你的 IP 地址和你的提供商——以及你正在连接的服务器。 图片 图片已发送 设备验证被禁用。关闭 SimpleX 锁定。 - 没有启用设备验证。一旦启用设备验证,您可以通过设置打开 SimpleX 锁定。 + 没有启用设备验证。一旦启用设备验证,你可以通过设置打开 SimpleX 锁定。 禁用 SimpleX 锁定 目前支持的最大文件尺寸是 %1$s。 - 文件将在您的联系人在线时收到,请稍等或稍后再查看! + 文件将在你的联系人在线时收到,请稍等或稍后再查看! 从图库 照片 视频 @@ -424,9 +424,9 @@ 启用自动删除消息? 用于控制台 此群中禁止成员之间私信。 - 该组禁止限时消息。 - 群组成员可以不可逆地删除已发送的消息。(24小时) - 群组成员可以私信。 + 限时消息被禁止。 + 成员可以不可逆地删除已发送的消息。(24小时) + 成员可以发送私信。 限时消息 在最近的应用程序中隐藏应用程序屏幕。 离开 @@ -434,13 +434,10 @@ 实时消息! 链接预览图片 无效的二维码 - 以后可以通过设置进行更改。 - 它可能在以下情况发生: -\n1. 消息在发送客户端 2 天后或在服务器上 30 天后过期。 -\n2. 消息解密失败,因为您或您的联系人使用了旧的数据库备份。 -\n3.连接被破坏。 - 离开群组? - 通过您的群组链接邀请 + 它如何影响电量 + 它可能在以下情况发生: \n1. 消息在发送客户端 2 天后或在服务器上 30 天后过期。 \n2. 消息解密失败,因为你或你的联系人使用了旧的数据库备份。 \n3.连接被破坏。 + 离开群? + 通过你的群链接邀请 本地名称 无效的消息格式 无效数据 @@ -449,9 +446,9 @@ 无效的连接链接 斜体 已邀请 - 邀请加入群组 + 邀请加入群 加入 - 加入群组? + 加入群? 邀请成员 已离开 浅色 @@ -462,25 +459,24 @@ 无效聊天 无效的服务器地址! 邀请成员 - 离开群组 + 离开群 仅本地配置文件数据 即时通知 即时通知! - 使用您的凭据登录 + 使用你的凭据登录 大文件! 链接无效! 已离开 - 此群组中禁止不可逆消息移除。 + 不可逆消息删除被禁止。 不可逆消息移除 实时消息 消息正文 等待确认中…… 即时 - 只有您的联系人才可以发送限时消息。 + 只有你的联系人才可以发送限时消息。 显示联系人和消息 只显示联系人 - 为保护您的信息,请打开 SimpleX 锁定。 -\n在启用此功能之前,系统将提示您完成身份验证。 + 为保护你的信息,请打开 SimpleX 锁定。 \n在启用此功能之前,系统将提示你完成身份验证。 聊天 分享文件…… 分享媒体…… @@ -488,16 +484,16 @@ 设置联系人姓名…… 已收到回复…… 已受到确认…… - 双层端到端加密 发送的消息。]]> + 仅客户端设备存储用户个人资料、联系人、群和消息。 视频通话(非端到端加密) 定期 私密通知 应用程序运行时 无端到端加密 显示 - 您只能在一台设备上使用最新版本的聊天数据库,否则您可能会停止接收来自某些联系人的消息。 + 你只能在一台设备上使用最新版本的聊天数据库,否则你可能会停止接收来自某些联系人的消息。 新密码…… - 该角色将更改为 %s。群组中每个人都会收到通知。 + 该角色将更改为 %s。群中每个人都会收到通知。 SimpleX 锁定 定期通知 定期启动 @@ -507,8 +503,8 @@ 启动中…… 禁止发送限时消息。 - 只有您可以发送限时消息。 - 只有您的联系人能不可逆地删除消息(您可以将它们标记为删除)。(24小时) + 只有你可以发送限时消息。 + 只有你的联系人能不可逆地删除消息(你可以将它们标记为删除)。(24小时) 禁止发送限时消息。 通知只会在应用程序停止之前发送! @@ -520,18 +516,18 @@ 通知 正在接收消息…… 要接收通知,请输入数据库密码 - SimpleX 后台服务 ——它每天使用百分之几的电池。]]> - 您的设置 - 允许 SimpleX 在后台运行。 否则,通知将被禁用。]]> + SimpleX 在后台运行而不是使用推送通知。]]> + 你的设置 + 允许它 来立即接收通知。]]> 通知预览 需要密码 定期通知被禁用! 在应用程序打开时运行 显示预览 - 该应用程序会定期获取新消息——它每天会消耗百分之几的电量。该应用程序不使用推送通知——您设备中的数据不会发送到服务器。 - 您的联系人可以允许完全删除消息。 + 该应用程序会定期获取新消息——它每天会消耗百分之几的电量。该应用程序不使用推送通知——你设备中的数据不会发送到服务器。 + 你的联系人可以允许完全删除消息。 已发送的消息将在设定的时间后被删除。 - 您的聊天数据库 + 你的聊天数据库 密码错误! 保存 打开 @@ -540,50 +536,48 @@ 打开聊天 更改数据库密码的尝试未完成。 移除 - + 保存 - + 移除 - 您必须在每次应用程序启动时输入密码——它不存储在设备上。 + 你必须在每次应用程序启动时输入密码——它不存储在设备上。 设置密码来导出 请输入正确的当前密码。 更新数据库密码 - 您的聊天数据库未加密——设置密码来保护它。 - 请安全地保存密码,如果您丢失了密码,您将无法访问聊天。 - 请安全地保存密码,如果您丢失了密码,您将无法更改它。 + 你的聊天数据库未加密——设置密码来保护它。 + 请安全地保存密码,如果你丢失了密码,你将无法访问聊天。 + 请安全地保存密码,如果你丢失了密码,你将无法更改它。 数据库密码错误 保存 更新 打开 SimpleX Chat 来接听电话 视频通话 - %1$s 想通过以下方式与您联系 + %1$s 想通过以下账户与你连接 拒接来电 点对点 错误:%s - 扫描视频通话中的二维码,或者您的联系人可以分享邀请链接。]]> - 您的通话 + 扫描视频通话中的二维码,或者你的联系人可以分享邀请链接。]]> + 你的通话 通过中继 未接来电 拒接来电 语音消息 语音消息 SimpleX Chat 通话 - 在视频通话中出示二维码,或分享链接。]]> - 您的聊天资料 + 在视频通话中出示二维码,或分享链接。]]> + 你的聊天资料 未接来电 待定来电 - 除非您的联系人已删除此连接或此链接已被使用,否则它可能是一个错误——请报告。 -\n如果要连接,请让您的联系人创建另一个连接链接,并检查您的网络连接是否稳定。 - 您已经连接到 %1$s。 - 您的聊天资料将被发送 -\n给您的联系人 + 除非你的联系人已删除此连接或此链接已被使用,否则它可能是一个错误——请报告。 \n如果要连接,请让你的联系人创建另一个连接链接,并检查你的网络连接是否稳定。 + 你已经连接到 %1$s。 + 你的聊天资料将被发送 \n给你的联系人 资料和服务器连接 更新网络设置? - 只有您可以不可逆地删除消息(您的联系人可以将它们标记为删除)。(24小时) + 只有你可以不可逆地删除消息(你的联系人可以将它们标记为删除)。(24小时) 重新启动应用程序以创建新的聊天资料。 服务器需要授权才能创建队列,检查密码 测试在步骤 %s 失败。 - 您已经有一个显示名相同的聊天资料。请选择另一个名字。 + 你已经有一个显示名相同的聊天资料。请选择另一个名字。 已发送 静音 资料图片 @@ -591,33 +585,32 @@ 设置 未知错误 未知数据库错误:%s - 已更新的群组资料 + 已更新的群资料 已删除 %1$s - 您删除了 %1$s - 您的个人资料将发送给您收到此链接的联系人。 + 你删除了 %1$s + 你的个人资料将发送给你收到此链接的联系人。 正在尝试连接到用于从该联系人接收消息的服务器(错误:%1$s)。 - 您已连接到用于接收该联系人消息的服务器。 - 您分享了一次性链接 - 很可能此联系人已经删除了与您的联系。 + 你已连接到用于接收该联系人消息的服务器。 + 你分享了一次性链接 + 很可能此联系人已经删除了与你的联系。 资料图片占位符 - 您当前聊天资料的新连接服务器 - 您当前的资料 - 您的资料存储在您的设备上并且仅与您的联系人共享。SimpleX 服务器无法看见您的资料。 - 您的资料、联系人和发送的消息存储在您的设备上。 - 该资料仅与您的联系人共享。 + 你当前聊天资料的新连接服务器 + 你当前的资料 + 你的资料存储在你的设备上并且仅与你的联系人共享。SimpleX 服务器无法看见你的资料。 + 你的资料、联系人和发送的消息存储在你的设备上。 + 该资料仅与你的联系人共享。 开启 - 此操作无法撤消——您的个人资料、联系人、消息和文件将不可逆地丢失。 - 此设置适用于您当前聊天资料中的消息 + 此操作无法撤消——你的个人资料、联系人、消息和文件将不可逆地丢失。 + 此设置适用于你当前聊天资料中的消息 恢复数据库错误 恢复 - 您发送了群组邀请 - 您拒绝了群组邀请 - 您当前的聊天数据库将被删除并替换为导入的数据库。 -\n此操作无法撤消——您的个人资料、联系人、消息和文件将不可逆地丢失。 + 你发送了群邀请 + 你拒绝了群邀请 + 你当前的聊天数据库将被删除并替换为导入的数据库。 \n此操作无法撤消——你的个人资料、联系人、消息和文件将不可逆地丢失。 已邀请 %1$s - 保存群组资料 + 保存群资料 服务器地址中的证书指纹可能不正确 - 请使用 %1$s 检查您的网络连接,然后重试。 + 请使用 %1$s 检查你的网络连接,然后重试。 多个聊天资料 数据库不能正常工作。点击了解更多 消息传递错误 @@ -632,22 +625,22 @@ 标记为已验证 建立私密连接 以 %s 身份加入 - 如果您收到 SimpleX Chat 邀请链接,您可以在浏览器中打开它: + 如果你收到 SimpleX Chat 邀请链接,你可以在浏览器中打开它: 标记为已读 标记为未读 在消息中使用 Markdown 文件:%s - 成员将被移出群组——此操作无法撤消! + 成员将被移出群——此操作无法撤消! 消息草稿 k 标记为已删除 %d 星期 - 您将停止接收来自该群组的消息。聊天记录将被保留。 + 你将停止接收来自该群的消息。聊天记录将被保留。 成员 成员 %d 星期 %d 分钟 - %d 月 + %d 个月 网络和服务器 高级设置 已被管理员移除 @@ -659,8 +652,7 @@ 未选择联系人 一次性邀请链接 关闭 - 连接需要 Onion 主机。 -\n请注意:如果没有 .onion 地址,您将无法连接到服务器。 + 连接需要 Onion 主机。 \n请注意:如果没有 .onion 地址,你将无法连接到服务器。 从不 已提供 %s 已提供 %s:%2s @@ -668,8 +660,8 @@ 一次性邀请链接 好的 没有细节 - (仅由群组成员存储) - 只有您可以发送语音消息。 + (仅由群成员存储) + 只有你可以发送语音消息。 消息将被删除——此操作无法撤消! 一次只能发送10张图片 更多 @@ -693,7 +685,7 @@ 保存并通知联系人 保存并通知联系人 拒绝 - 为了保护隐私,而不是所有其他平台使用的用户 ID,SimpleX 具有消息队列的标识符,每个联系人都是分开的。 + 为了保护隐私,SimpleX 对你的每一个联系人使用不同的 ID。 TCP 连接超时 收到,禁止 设定1天 @@ -706,14 +698,14 @@ PING 次数 禁止发送语音消息。 PING 间隔 - 请检查您使用的链接是否正确,或者让您的联系人给您发送另一个链接。 + 请检查你使用的链接是否正确,或者让你的联系人给你发送另一个链接。 协议超时 拒绝 回复 重置为默认 运行聊天程序 扫码 - 从您联系人的应用程序中扫描安全码。 + 从你联系人的应用程序中扫描安全码。 安全码 秘密 安全评估 @@ -731,7 +723,7 @@ 接收地址将变更到不同的服务器。地址更改将在发件人上线后完成。 此链接不是有效的连接链接! 开始新的聊天 - 要与您的联系人验证端到端加密,请比较(或扫描)您设备上的代码。 + 要与你的联系人验证端到端加密,请比较(或扫描)你设备上的代码。 取消静音 更新传输隔离模式? (从剪贴板扫描或粘贴) @@ -743,31 +735,31 @@ 太多图片! 待办的 更改接收地址? - 请让您的联系人启用发送语音消息。 + 请让你的联系人启用发送语音消息。 录制语音消息 发消息 重置 发送 - 发送实时消息——它会在您键入时为收件人更新 + 发送实时消息——它会在你键入时为收件人更新 开始新聊天 - (与您的联系人分享) + (与你的联系人分享) 通过链接连接 设置联系人姓名 - 您接受的连接将被取消! - 您与之共享此链接的联系人将无法连接! + 你接受的连接将被取消! + 你与之共享此链接的联系人将无法连接! 显示二维码 发送问题和想法 - 保护您的隐私和安全的消息传递和应用程序平台。 + 保护你的隐私和安全的消息传递和应用程序平台。 删去 你决定谁可以连接。 下一代私密通讯软件 - 粘贴您收到的链接 + 粘贴你收到的链接 已跳过消息 支持 SIMPLEX CHAT 发送链接预览 SOCKS 代理 停止聊天程序? - 停止聊天以便导出、导入或删除聊天数据库。在聊天停止期间,您将无法收发消息。 + 停止聊天以便导出、导入或删除聊天数据库。在聊天停止期间,你将无法收发消息。 恢复数据库备份 恢复数据库备份? 删除成员 @@ -792,7 +784,7 @@ 测试服务器 SimpleX 联系地址 SimpleX 一次性邀请 - SimpleX 群组链接 + SimpleX 群链接 SimpleX 链接 发送人可能已删除连接请求。 预设服务器 @@ -803,13 +795,13 @@ 跳过邀请成员 转变 禁止发送语音消息。 - 只有您的联系人可以发送语音消息。 + 只有你的联系人可以发送语音消息。 禁止向成员发送私信。 保护应用程序屏幕 主题 停止聊天以启用数据库操作。 %s 秒 - 该群组已不存在。 + 该群已不存在。 点击加入 停止 重新启动应用程序以使用导入的聊天数据库。 @@ -826,108 +818,107 @@ 停止聊天程序 权限被拒绝! 点击按钮 - 感谢您安装 SimpleX Chat! + 感谢你安装 SimpleX Chat! 相机 预设服务器地址 扫描服务器二维码 服务器测试失败! 一些服务器未通过测试: 在 GitHub 上加星 - 保存并通知群组成员 + 保存并通知群成员 扬声器关闭 扬声器开启 - 已将您移除 + 已将你移除 更新设置会将客户端重新连接到所有服务器。 系统 - 对方会在您键入时看到更新。 + 对方会在你键入时看到更新。 查看安全码 语音消息 (%1$s) 等待图像中 欢迎! 欢迎 %1$s! - 当您的联系人设备在线时,您将可以连接,请稍等或稍后查看! + 当你的联系人设备在线时,你将可以连接,请稍等或稍后查看! 评价此应用程序 使用 SOCKS 代理? %d 个文件,总大小为 %s - 您已加入此群组 - 您被邀请加入群组 + 你已加入此群 + 你被邀请加入群 默认(%s) 此聊天中禁止语音消息。 - 语音信息在该群组中被禁用。 + 语音信息被禁止。 验证安全码 使用 SimpleX Chat 服务器。 通过 %1$s - 邀请至群组 %1$s + 邀请至群 %1$s SimpleX 地址 SimpleX 团队 - %1$s 成员 + %1$s 名成员 - 您将在组主设备上线时连接到该群组,请稍等或稍后再检查! - 当您启动应用或在应用程序驻留后台超过30 秒后,您将需要进行身份验证。 - 连接到 SimpleX Chat 开发者提出任何问题并接收更新 。]]> - 您已接受连接 - 您的 SMP 服务器 - %1$d 已跳过消息 + 你将在组主设备上线时连接到该群,请稍等或稍后再检查! + 当你启动应用或在应用程序驻留后台超过30 秒后,你将需要进行身份验证。 + 连接到 SimpleX Chat 开发者提出任何问题并接收更新 。]]> + 你已接受连接 + 你的 SMP 服务器 + %1$d 条已跳过消息 %ds 更新内容 - 您被邀请加入群组 - 您没有聊天记录 + 你被邀请加入群 + 你没有聊天记录 等待图像中 语音消息 语音消息禁止发送! - 您需要允许您的联系人发送语音消息才能发送它们。 + 你需要允许你的联系人发送语音消息才能发送它们。 扫描二维码 - 您邀请了您的联系人 - 想要与您连接! - 您的联系人需要在线才能完成连接。 -\n您可以取消此连接并删除联系人(稍后尝试使用新链接)。 + 你邀请了一名联系人 + 想要与你连接! + 你的联系人需要在线才能完成连接。 \n你可以取消此连接并删除联系人(稍后尝试使用新链接)。 SimpleX 标志 - 您的 SimpleX 地址 + 你的 SimpleX 地址 为终端安装 SimpleX Chat 使用 SimpleX Chat 服务器? - 我们不会在服务器上存储您的任何联系人或消息(一旦发送)。 + 我们不会在服务器上存储你的任何联系人或消息(一旦发送)。 WebRTC ICE 服务器 - 中继服务器保护您的 IP 地址,但它可以观察通话的持续时间。 - 中继服务器仅在必要时使用。其他人可能会观察到您的IP地址。 - 您的 ICE 服务器 + 中继服务器保护你的 IP 地址,但它可以观察通话的持续时间。 + 中继服务器仅在必要时使用。其他人可能会观察到你的IP地址。 + 你的 ICE 服务器 视频关闭 - 您可以通过应用设置/数据库或重启应用开始聊天。 - 您将 %s 的角色更改为 %s - 您将自己的角色更改为 %s - 您已更改地址 - 您可以共享链接或二维码——任何人都可以加入该群组。如果您稍后将其删除,您不会失去该组的成员。 + 你可以通过应用设置/数据库或重启应用开始聊天。 + 你将 %s 的角色更改为 %s + 你将自己的角色更改为 %s + 你已更改地址 + 你可以共享链接或二维码——任何人都可以加入该群。如果你稍后将其删除,你不会失去该组的成员。 间接(%1$s) - 在移动应用程序中打开按钮。]]> + 在移动应用程序中打开按钮。]]> SimpleX 你将连接到所有群成员。 - 通过群组链接 + 通过群链接 通过一次性链接 通过联系地址链接 通过浏览器 - 您的服务器 + 你的服务器 当可用时 使用 .onion 主机 - 您的 ICE 服务器 + 你的 ICE 服务器 simplexmq: v%s (%2s) - 您的聊天由您掌控! - 您可以使用 markdown 来编排消息格式: + 你的聊天由你掌控! + 你可以使用 markdown 来编排消息格式: %dh %d 天 %dw - 您被邀请加入群组。 加入以与群组成员联系。 - 你加入了这个群组。连接到邀请组成员。 - 您更改了 %s 的地址 - 您已离开 - %d 已选择联系人 - 您允许 + 你被邀请加入群。 加入以与群成员联系。 + 你加入了这个群。连接到邀请组成员。 + 你更改了 %s 的地址 + 你已离开 + 已选择 %d 名联系人 + 你允许 带有可选的欢迎消息。 %dm %dmth 等待文件中 - 您的联系人发送的文件大于当前支持的最大大小 (%1$s). - 当您的连接请求被接受后,您将可以连接,请稍等或稍后检查! + 你的联系人发送的文件大于当前支持的最大大小 (%1$s). + 当你的连接请求被接受后,你将可以连接,请稍等或稍后检查! 使用服务器 - 您的服务器地址 + 你的服务器地址 视频开启 最多 40 秒,立即收到。 验证连接安全 @@ -937,11 +928,11 @@ 该消息将对所有成员标记为已被管理员移除。 删除成员消息? 观察员 - 您是观察者 - 更新群组链接错误 - 您无法发送消息! + 你是观察者 + 更新群链接错误 + 你无法发送消息! 初始角色 - 请联系群组管理员。 + 请联系群管理员。 系统 用于显示的密码 保存个人资料密码 @@ -959,18 +950,18 @@ 现在管理员可以: \n- 删除成员的消息。 \n- 禁用成员(观察员角色) - 使用密码保护您的聊天资料! + 使用密码保护你的聊天资料! 确认密码 更新用户隐私错误 保存用户密码错误 在搜索中输入密码 - 群组欢迎消息 - 群组管理员移除 + 群欢迎消息 + 群管理员移除 隐藏的个人资料密码 隐藏的聊天资料 隐藏个人资料 保存服务器? - 要显示您的隐藏的个人资料,请在您的聊天个人资料页面的搜索字段中输入完整密码。 + 要显示你的隐藏的个人资料,请在你的聊天个人资料页面的搜索字段中输入完整密码。 保存欢迎信息? 点击以激活个人资料。 取消隐藏 @@ -979,8 +970,8 @@ 感谢用户——通过 Weblate 做出贡献! 解除静音 欢迎消息 - 当静音配置文件处于活动状态时,您仍会收到来自静音配置文件的电话和通知。 - 您可以隐藏或静音用户配置文件——长按以显示菜单。 + 当静音配置文件处于活动状态时,你仍会收到来自静音配置文件的电话和通知。 + 你可以隐藏或静音用户配置文件——长按以显示菜单。 欢迎消息 确认数据库升级 实验性 @@ -991,13 +982,13 @@ 数据库版本比应用程序更新,但无法降级迁移:%s 降级并打开聊天 隐藏: - 文件将在您的联系人完成上传后收到。 + 文件将在你的联系人完成上传后收到。 数据库版本不兼容 迁移:%s - 图片将在您的联系人完成上传后收到。 + 图片将在你的联系人完成上传后收到。 显示开发者选项 升级并打开聊天 - 警告:您可能会丢失部分数据! + 警告:你可能会丢失部分数据! 迁移确认无效 显示: 删除个人资料 @@ -1009,15 +1000,15 @@ 过多视频! 视频 等待视频中 - 视频将在您的联系人在线时收到,请稍等或稍后查看! + 视频将在你的联系人在线时收到,请稍等或稍后查看! 等待视频中 视频已发送 要求接收视频 - 视频将在您的联系人完成上传后收到。 + 视频将在你的联系人完成上传后收到。 服务器需要授权来上传,检查密码 上传文件 XFTP 服务器 - 您的 XFTP 服务器 + 你的 XFTP 服务器 Use .onion hosts 设置为否。]]> 使用 SOCKS 代理 端口 @@ -1040,7 +1031,7 @@ 没有应用程序密码 密码输入 请牢记或妥善保管——丢失的密码将无法恢复! - 您可以通过设置开启 SimpleX 锁定。 + 你可以通过设置开启 SimpleX 锁定。 身份验证 身份验证失败 更改密码 @@ -1058,15 +1049,15 @@ 密码已设置! 系统 未启用 SimpleX 锁定! - 您的身份无法验证,请再试一次。 + 你的身份无法验证,请再试一次。 身份验证已取消 当前密码 立即 错误消息散列 错误消息 ID - %1$d 消息解密失败。 - %1$d 已跳过消息。 - 当您或您的连接使用旧数据库备份时,可能会发生这种情况。 + %1$d 条消息解密失败。 + 跳过了 %1$d 条消息。 + 当你或你的连接使用旧数据库备份时,可能会发生这种情况。 解密错误 请向开发者报告。 上一条消息的散列不同。 @@ -1092,30 +1083,30 @@ 最大 1gb 的视频和文件 快速且无需等待发件人在线! 禁止音频/视频通话。 - 您和您的联系人都可以进行呼叫。 - 只有您可以进行呼叫。 - 只有您的联系人可以进行呼叫。 + 你和你的联系人都可以进行呼叫。 + 只有你可以进行呼叫。 + 只有你的联系人可以进行呼叫。 允许联系人呼叫你。 允许通话,前提是你的联系人允许它们。 禁止音频/视频通话。 1分钟 一次性链接 - 您和您的联系人都可以添加消息回应。 + 你和你的联系人都可以添加消息回应。 允许消息回应。 允许消息回应,前提是你的联系人允许它们。 应用程序密码被替换为自毁密码。 更改自毁模式 关于 SimpleX 地址 继续 - 您的所有联系人将保持连接。个人资料更新将发送给您的联系人。 + 你的所有联系人将保持连接。个人资料更新将发送给你的联系人。 自动接受 额外的次要 背景 5分钟 30秒 地址 - 将地址添加到您的个人资料,以便您的联系人可以与其他人共享。个人资料更新将发送给您的联系人。 - 允许您的联系人添加消息回应。 + 将地址添加到你的个人资料,以便你的联系人可以与其他人共享。个人资料更新将发送给你的联系人。 + 允许你的联系人添加消息回应。 额外的强调色 已删除所有应用程序数据。 已创建一个包含所提供名字的空白聊天资料,应用程序照常打开。 @@ -1131,7 +1122,7 @@ 消失于 设置地址错误 自定义主题 - 创建一个地址,让人们与您联系。 + 创建一个地址,让人们与你联系。 创建 SimpleX 地址 输入欢迎消息……(可选) 不创建地址 @@ -1149,9 +1140,9 @@ 已删除于:%s 消失于:%s 禁止消息回应。 - 只有您可以添加消息回应。 - 群组成员可以添加信息回应。 - 该群组禁用了消息回应。 + 只有你可以添加消息回应。 + 成员可以添加信息回应。 + 消息回应被禁止。 自毁密码已更改! 自毁密码已启用! 设置密码 @@ -1159,7 +1150,7 @@ 已发信息 历史记录 发送 - 如果您在打开应用时输入该密码,所有应用程序数据将被不可逆地删除! + 如果你在打开应用时输入该密码,所有应用程序数据将被不可逆地删除! 新的显示名: 已被管理员移除于 已发送于 @@ -1169,18 +1160,18 @@ %s (当前) 发送于 %s 收到的信息 - 只有您的联系人可以添加消息回应。 + 只有你的联系人可以添加消息回应。 打开数据库中…… 更改聊天资料 - 您的联系人可以扫描二维码或使用应用程序中的链接来建立连接。 - 您可以将您的地址作为链接或二维码共享——任何人都可以连接到您。 - 如果您不能亲自见面,可以在视频通话中展示二维码,或分享链接。 + 你的联系人可以扫描二维码或使用应用程序中的链接来建立连接。 + 你可以将你的地址作为链接或二维码共享——任何人都可以连接到你。 + 如果你不能亲自见面,可以在视频通话中展示二维码,或分享链接。 了解更多 - 当人们请求连接时,您可以接受或拒绝它。 - 如果您以后删除您的地址,您不会丢失您的联系人。 + 当人们请求连接时,你可以接受或拒绝它。 + 如果你以后删除你的地址,你不会丢失你的联系人。 用户指南中阅读更多。]]> 界面颜色 - 与您的联系人保持连接。 + 与你的联系人保持连接。 与联系人分享 邀请朋友 保存自动接受设置 @@ -1189,9 +1180,9 @@ 你好! \n用 SimpleX Chat 与我联系:%s 让我们一起在 SimpleX Chat 里聊天 - 您可以以后创建它 + 你可以以后创建它 分享地址 - 您可以与您的联系人分享该地址,让他们与 %s 联系。 + 你可以与你的联系人分享该地址,让他们与 %s 联系。 预览 导入主题 SimpleX @@ -1216,8 +1207,8 @@ 已发信息 消息回应 该聊天禁用了消息回应。 - 如果您在打开应用程序时输入自毁密码: - 个人资料更新将被发送给您的联系人。 + 如果你在打开应用程序时输入自毁密码: + 个人资料更新将被发送给你的联系人。 记录更新于 禁止消息回应。 已收到于 @@ -1232,7 +1223,7 @@ 导入过程中发生了一些非致命错误: 应用程序 重启 - 通知将停止工作直到您重启应用程序 + 通知将停止工作直到你重启应用程序 关闭? 关闭 中止地址更改错误 @@ -1243,8 +1234,8 @@ 允许发送文件和媒体。 文件和媒体 只有组主可以启用文件和媒体。 - 此群组中禁止文件和媒体。 - 群组成员可以发送文件和媒体。 + 文件和媒体被禁止。 + 成员可以发送文件和媒体。 禁止发送文件和媒体。 禁止文件和媒体! 无过滤聊天 @@ -1261,9 +1252,7 @@ 选择一个文件 联系人 协调加密中… - - 更稳定的消息送达. -\n- 更好的群组. -\n- 还有更多! + - 更稳定的消息传送. \n- 更好的群. \n- 还有更多! 一个新的随机个人档案将被分享。 与 %s 协调加密中… 该功能还没支持。请尝试下一个版本。 @@ -1274,10 +1263,10 @@ %s: %s 敬请期待! 数据库将被加密,密码将存储在设置中。 - 您可以稍后在“设置”中启用它 - 对所有群组关闭 + 你可以稍后在“设置”中启用它 + 对所有群关闭 无送货信息 - 您的个人资料 %1$s 将被共享。 + 你的个人资料 %1$s 将被共享。 将为所有联系人启用送达回执功能。 打开应用程序设置 为所有组启用 @@ -1294,13 +1283,12 @@ %s 在 %s 禁用回执? 重新协商加密? - 可以在联系人和群组设置中覆盖它们。 + 可以在联系人和群设置中覆盖它们。 对所有联系人关闭 - 随机密码以明文形式存储在设置中。 -\n您可以稍后更改。 + 随机密码以明文形式存储在设置中。 \n你可以稍后更改。 已禁用 %d 组的送达回执功能 需要为 %s 重新协商加密 - SimpleX 无法在后台运行。只有在应用程序运行时,您才会收到通知。 + SimpleX 无法在后台运行。只有在应用程序运行时,你才会收到通知。 启用(保留覆盖) 即将更新数据库加密密码并将其存储在设置中。 使用当前配置文件 @@ -1312,16 +1300,16 @@ 即使在对话中禁用。 使用随机密码 无后台通话 - 您可以稍后通过应用程序隐私和安全设置启用它们。 + 你可以稍后通过应用程序隐私和安全设置启用它们。 在设置中保存密码 启用 - 该群组成员超过 %1$d ,未发送送达回执。 + 该群成员超过 %1$d ,未发送送达回执。 修复连接? 我们错过的第二个"√"!✅ 设定数据库密码 - 为群组禁用回执吗? + 为群禁用回执吗? %s、%s 和 %s 已连接 - 修复群组成员不支持的问题 + 修复群成员不支持的问题 已为 %d 组启用送达回执功能 重新协商 禁用(保留覆盖) @@ -1336,18 +1324,18 @@ 修复连接 %s 和 %s 已连接 关闭 - 小群组(最多 20 人) + 小群(最多 20 人) 显示最近的消息 将送达回执发送给 启用已读回执时出错! 更改密码或重启应用后,密码将以明文形式保存在设置中。 - 粘贴您收到的链接以与您的联系人联系… + 粘贴你收到的链接以与你的联系人联系… 送达回执 没有选择聊天 可以加密 重新协商加密 禁用(保留组覆盖) - 为群组启用回执吗? + 为群启用回执吗? 修复联系人不支持的问题 对 %s 加密正常 修复还原备份后的加密问题。 @@ -1358,7 +1346,7 @@ 连接请求将发送给该组成员。 密码以明文形式存储在设置中。 同步连接时出错 - 这些设置适用于您当前的配置文件 + 这些设置适用于你当前的配置文件 允许为 %s 重新协商加密 为所有人启用 需要重新协商加密 @@ -1369,14 +1357,12 @@ 全新桌面应用! 6种全新的界面语言 应用程序为新的本地文件(视频除外)加密。 - 发现和加入群组 + 发现和加入群 简化的隐身模式 阿拉伯语、保加利亚语、芬兰语、希伯莱语、泰国语和乌克兰语——得益于用户和Weblate。 在桌面应用里创建新的账号。💻 在连接时切换隐身模式。 - - 连接到目录服务(BETA)! -\n- 发送回执(至多20名成员)。 -\n- 更快,更稳定。 + - 连接到目录服务(BETA)! \n- 发送回执(至多20名成员)。 \n- 更快、更稳定。 打开 创建成员联系人时出错 发送私信来连接 @@ -1394,14 +1380,14 @@ 加入你的群吗? %1$s 群。]]> 这是你自己的一次性链接! - %d 条消息被标记为删除 + %d 条消息被标记为已删除 群已存在! 已经在连接了! 无法解码该视频。请尝试不同视频或联络开发者。 %s 已连接 及其他 %d 个事件 通过链接进行连接吗? - 已经加入了该群组! + 已经加入了该群! %s、 %s 和 %d 名成员 解封成员 连接到你自己? @@ -1480,13 +1466,13 @@ 从已链接移动设备加载文件时请稍候片刻 桌面应用版本 %s 不兼容此应用。 验证连接 - 屏蔽群组成员 - 使用随机身份创建群组 + 屏蔽群成员 + 使用随机身份创建群 连接移动端和桌面端应用程序!🔗 通过安全的、抗量子计算机破解的协议。 隐藏不需要的信息。 - 更佳的群组 - 匿名群组 + 更佳的群 + 匿名群 %s 连接断开]]> 加入速度更快、信息更可靠。 - 可选择通知已删除的联系人。 @@ -1519,10 +1505,10 @@ 不给新成员发送历史消息。 或者显示此码 给新成员发送了最多 100 条历史消息。 - 您扫描的码不是 SimpleX 链接的二维码。 - 您粘贴的文本不是 SimpleX 链接。 + 你扫描的码不是 SimpleX 链接的二维码。 + 你粘贴的文本不是 SimpleX 链接。 启用相机访问 - 您可以在连接详情中再次查看邀请链接。 + 你可以在连接详情中再次查看邀请链接。 保留未使用的邀请吗? 分享此一次性邀请链接 建群: 来建立新群。]]> @@ -1635,7 +1621,7 @@ 应用数据迁移 通过二维码迁移到另一部设备。 画中画通话 - 更安全的群组 + 更安全的群 通话时使用本应用 迁移到此处 或粘贴存档链接 @@ -1720,9 +1706,9 @@ 不允许语音消息 SimpleX 链接 允许发送 SimpleX 链接。 - 群成员可发送 SimpleX 链接。 + 成员可发送 SimpleX 链接。 禁止发送 SimpleX 链接 - 此群禁止 SimpleX 链接。 + SimpleX 链接被禁止。 所有者 启用对象 管理员 @@ -1824,8 +1810,7 @@ 缩放 Webview 初始化失败。更新你的系统到新版本。请联系开发者。 \n错误:%s - 保护您的真实 IP 地址。不让你的联系人选择的消息中继看到它。 -\n在*网络&服务器*设置中开启。 + 保护你的真实 IP 地址。不让你的联系人选择的消息中继看到它。 \n在*网络&服务器*设置中开启。 确认来自未知服务器的文件。 安全地接收文件 改进了消息传递 @@ -1996,7 +1981,7 @@ 你仍可以在聊天列表中查看与 %1$s 的对话。 粘贴链接 联系人 - 单手用户界面 + 单手应用工具栏 正在连接联系人,请等候或稍后检查! 联系人被删除了。 要能够呼叫联系人,你需要先允许联系人进行呼叫。 @@ -2079,8 +2064,7 @@ 分享配置文件 转发消息出错 在你选中消息后这些消息被删除。 - %1$d 个文件错误: -\n%2$s + %1$d 个文件错误:\n%2$s 其他 %1$d 个文件错误。 %1$d 个文件未被下载。 转发 %1$s 条消息? @@ -2134,7 +2118,7 @@ 创建一次性链接 用于社交媒体 或者私下分享 - 选择运营者 + 服务器运营者 网络运营者 30 天后将接受已启用的运营者的条款。 继续 @@ -2211,4 +2195,39 @@ 接受运营者条款的日期:%s 远程移动设备 或者导入压缩文件 + 小米设备:请在系统设置中开启“自动启动”让通知正常工作。]]> + 消息太大! + 你可以复制并减小消息大小来发送它。 + 请减小消息大小或删除媒体并再次发送。 + 将你的团队成员加入对话。 + 企业地址 + 端到端加密,私信具备后量子密码安全性。]]> + 无后台服务 + 每 10 分钟检查消息 + 它如何帮助隐私 + 应用始终在后台运行 + 通知和电量 + 离开聊天? + 你将停止从这个聊天收到消息。聊天历史将被保留。 + 邀请加入聊天 + 将为你删除聊天 - 此操作无法撤销! + 删除聊天 + 删除聊天? + 添加好友 + 添加团队成员 + 将为所有成员删除聊天 - 此操作无法撤销! + 仅聊天所有人可更改首选项。 + 角色将被更改为 %s。聊天中的每个人都会收到通知。 + 成员之间的私信被禁止。 + 此聊天禁止成员之间的私信。 + 企业聊天 + 客户隐私。 + %1$s连接。]]> + 聊天已存在! + 单手聊天工具栏 + 离开聊天 + 你的聊天个人资料将被发送给聊天成员 + 聊天 + 将从聊天中删除成员 - 此操作无法撤销! + 请减小消息尺寸并再次发送。 \ No newline at end of file From 586671c3076cc4c4e39f7deb79dd190d518c4e82 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Thu, 5 Dec 2024 21:46:35 +0000 Subject: [PATCH 139/167] website: translations (#5331) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (258 of 258 strings) Translation: SimpleX Chat/SimpleX Chat website Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/zh_Hans/ * Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (258 of 258 strings) Translation: SimpleX Chat/SimpleX Chat website Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/zh_Hans/ * Translated using Weblate (Italian) Currently translated at 100.0% (258 of 258 strings) Translation: SimpleX Chat/SimpleX Chat website Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/it/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (258 of 258 strings) Translation: SimpleX Chat/SimpleX Chat website Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/hu/ --------- Co-authored-by: 大王叫我来巡山 Co-authored-by: Random Co-authored-by: Ghost of Sparta --- website/langs/hu.json | 3 ++- website/langs/it.json | 3 ++- website/langs/zh_Hans.json | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/website/langs/hu.json b/website/langs/hu.json index b0b76714bc..7702b56f47 100644 --- a/website/langs/hu.json +++ b/website/langs/hu.json @@ -255,5 +255,6 @@ "simplex-chat-via-f-droid": "SimpleX Chat az F-Droidon keresztül", "simplex-chat-repo": "SimpleX Chat tároló", "stable-and-beta-versions-built-by-developers": "A fejlesztők által készített stabil és béta verziók", - "hero-overlay-card-3-p-3": "A Trail of Bits 2024 júliusában felülvizsgálta a SimpleX hálózati protokollok kriptográfiai felépítését. Tudjon meg többet." + "hero-overlay-card-3-p-3": "A Trail of Bits 2024 júliusában felülvizsgálta a SimpleX hálózati protokollok kriptográfiai felépítését. Tudjon meg többet.", + "docs-dropdown-14": "SimpleX üzleti célra" } diff --git a/website/langs/it.json b/website/langs/it.json index bdc4c38d45..502ab6d886 100644 --- a/website/langs/it.json +++ b/website/langs/it.json @@ -255,5 +255,6 @@ "docs-dropdown-10": "Trasparenza", "docs-dropdown-12": "Sicurezza", "docs-dropdown-11": "Domande frequenti", - "hero-overlay-card-3-p-3": "Trail of Bits ha analizzato la progettazione crittografica dei protocolli della rete SimpleX nel luglio 2024. Leggi di più." + "hero-overlay-card-3-p-3": "Trail of Bits ha analizzato la progettazione crittografica dei protocolli della rete SimpleX nel luglio 2024. Leggi di più.", + "docs-dropdown-14": "SimpleX per il lavoro" } diff --git a/website/langs/zh_Hans.json b/website/langs/zh_Hans.json index e482056565..836f7057ee 100644 --- a/website/langs/zh_Hans.json +++ b/website/langs/zh_Hans.json @@ -255,5 +255,6 @@ "docs-dropdown-10": "透明度", "docs-dropdown-11": "常问问题", "docs-dropdown-12": "安全性", - "hero-overlay-card-3-p-3": "Trail of Bits 于 2024 年 7 月审核了 SimpleX 网络协议的加密设计。了解更多信息。" + "hero-overlay-card-3-p-3": "Trail of Bits 于 2024 年 7 月审核了 SimpleX 网络协议的加密设计。了解更多信息。", + "docs-dropdown-14": "企业版 SimpleX" } From bd2ca749872a3d62d396198cae764d0a3c8bcf73 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Thu, 5 Dec 2024 22:22:03 +0000 Subject: [PATCH 140/167] ui: update "server operators privacy" in onboarding --- apps/ios/ru.lproj/Localizable.strings | 2 +- .../common/views/onboarding/ChooseServerOperators.kt | 1 + .../chat/simplex/common/views/onboarding/SimpleXInfo.kt | 2 +- .../common/src/commonMain/resources/MR/base/strings.xml | 5 +++-- .../common/src/commonMain/resources/MR/ru/strings.xml | 7 ++++--- 5 files changed, 10 insertions(+), 7 deletions(-) diff --git a/apps/ios/ru.lproj/Localizable.strings b/apps/ios/ru.lproj/Localizable.strings index c2124b34a4..57c1b91769 100644 --- a/apps/ios/ru.lproj/Localizable.strings +++ b/apps/ios/ru.lproj/Localizable.strings @@ -5397,7 +5397,7 @@ "You can configure operators in Network & servers settings." = "Вы можете настроить операторов в настройках Сеть и серверы."; /* No comment provided by engineer. */ -"You can configure servers via settings." = "Вы можете сконфигурировать серверы через настройки."; +"You can configure servers via settings." = "Вы можете настроить серверы позже."; /* No comment provided by engineer. */ "You can create it later" = "Вы можете создать его позже"; diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt index 782f51c205..132381294f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt @@ -342,6 +342,7 @@ private fun ChooseServerOperatorsInfoView() { ColumnWithScrollBar(Modifier.padding(horizontal = DEFAULT_PADDING)) { AppBarTitle(stringResource(MR.strings.onboarding_network_operators), withPadding = false) ReadableText(stringResource(MR.strings.onboarding_network_operators_app_will_use_different_operators)) + ReadableText(stringResource(MR.strings.onboarding_network_operators_cant_see_who_talks_to_whom)) ReadableText(stringResource(MR.strings.onboarding_network_operators_app_will_use_for_routing)) SectionBottomSpacer() } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt index a77c25dd1d..020d3493b9 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt @@ -181,7 +181,7 @@ fun OnboardingInformationButton( .clip(CircleShape) .clickable { onClick() } ) { - Row(Modifier.padding(8.dp), horizontalArrangement = Arrangement.spacedBy(4.dp) ) { + Row(Modifier.padding(8.dp), horizontalArrangement = Arrangement.spacedBy(4.dp)) { Icon( painterResource(MR.images.ic_info), null, diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index d56a7fe87c..7fc46cf3a7 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -1075,8 +1075,9 @@ Server operators Network operators - When more than one network operator is enabled, the app will use the servers of different operators for each conversation. - For example, if you receive messages via SimpleX Chat server, the app will use one of Flux servers for private routing. + The app protects your privacy by using different operators in each conversation. + When more than one operator is enabled, none of them has metadata to learn who communicates with whom. + For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server. Select network operators to use. How it helps privacy You can configure servers via settings. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml index 00df4f75cb..9e39ad61a4 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml @@ -2264,7 +2264,7 @@ Без фонового сервиса Нотификации и батарейка Как это улучшает конфиденциальность - Операторы сети + Операторы серверов Выберите операторов сети. Вы можете настроить операторов в настройках Сеть и серверы. Продолжить @@ -2320,8 +2320,9 @@ Разговор уже существует! только с одним контактом - поделитесь при встрече или через любой мессенджер.]]> Нет серверов для доставки сообщений. - Вы можете сконфигурировать серверы через настройки. - Когда больше чем один оператор сети включен, приложение использует серверы разных операторов в каждом разговоре. + Вы можете настроить серверы позже. + Приложение улучшает конфиденциальность используя разных операторов в каждом разговоре. + Когда больше чем один оператор включен, ни один из них не видит метаданные, чтобы определить, кто соединен с кем. Ошибка сохранения серверов Условия будут приняты для включенных операторов через 30 дней. Ошибка приема условий From 886dc56de864fdf7bc44ed8b95d2dcd8fcccfdff Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Thu, 5 Dec 2024 23:01:37 +0000 Subject: [PATCH 141/167] ui: update business chat info type --- apps/ios/SimpleXChat/ChatTypes.swift | 3 ++- .../commonMain/kotlin/chat/simplex/common/model/ChatModel.kt | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 5379cce236..a2d44d59d0 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -1962,8 +1962,9 @@ public struct GroupProfile: Codable, NamedChat, Hashable { } public struct BusinessChatInfo: Decodable, Hashable { - public var memberId: String public var chatType: BusinessChatType + public var businessId: String + public var customerId: String } public enum BusinessChatType: String, Codable, Hashable { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index 4d75d37b99..b9e52763a6 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -1546,8 +1546,9 @@ data class GroupProfile ( @Serializable data class BusinessChatInfo ( - val memberId: String, - val chatType: BusinessChatType + val chatType: BusinessChatType, + val businessId: String, + val customerId: String, ) @Serializable From 5ef14ca95e6301d687d3ad7e69777f50720e9237 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Thu, 5 Dec 2024 23:30:05 +0000 Subject: [PATCH 142/167] 6.2-beta.6: ios 253, android 258, desktop 81 --- apps/ios/SimpleX.xcodeproj/project.pbxproj | 36 +++++++++++----------- apps/multiplatform/gradle.properties | 8 ++--- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 690ba2579a..22d5ba971b 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -167,9 +167,9 @@ 648010AB281ADD15009009B9 /* CIFileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648010AA281ADD15009009B9 /* CIFileView.swift */; }; 648679AB2BC96A74006456E7 /* ChatItemForwardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648679AA2BC96A74006456E7 /* ChatItemForwardingView.swift */; }; 649B28DD2CFE07CF00536B68 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 649B28D82CFE07CF00536B68 /* libffi.a */; }; - 649B28DE2CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.5-592uBhlQO6KIf70TJf5KpP-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 649B28D92CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.5-592uBhlQO6KIf70TJf5KpP-ghc9.6.3.a */; }; + 649B28DE2CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.6-5lGV6gtq9gSDlEsE8DHXYo-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 649B28D92CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.6-5lGV6gtq9gSDlEsE8DHXYo-ghc9.6.3.a */; }; 649B28DF2CFE07CF00536B68 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 649B28DA2CFE07CF00536B68 /* libgmpxx.a */; }; - 649B28E02CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.5-592uBhlQO6KIf70TJf5KpP.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 649B28DB2CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.5-592uBhlQO6KIf70TJf5KpP.a */; }; + 649B28E02CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.6-5lGV6gtq9gSDlEsE8DHXYo.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 649B28DB2CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.6-5lGV6gtq9gSDlEsE8DHXYo.a */; }; 649B28E12CFE07CF00536B68 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 649B28DC2CFE07CF00536B68 /* libgmp.a */; }; 649BCDA0280460FD00C3A862 /* ComposeImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649BCD9F280460FD00C3A862 /* ComposeImageView.swift */; }; 649BCDA22805D6EF00C3A862 /* CIImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649BCDA12805D6EF00C3A862 /* CIImageView.swift */; }; @@ -516,9 +516,9 @@ 648679AA2BC96A74006456E7 /* ChatItemForwardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemForwardingView.swift; sourceTree = ""; }; 6493D667280ED77F007A76FB /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; 649B28D82CFE07CF00536B68 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 649B28D92CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.5-592uBhlQO6KIf70TJf5KpP-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.2.0.5-592uBhlQO6KIf70TJf5KpP-ghc9.6.3.a"; sourceTree = ""; }; + 649B28D92CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.6-5lGV6gtq9gSDlEsE8DHXYo-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.2.0.6-5lGV6gtq9gSDlEsE8DHXYo-ghc9.6.3.a"; sourceTree = ""; }; 649B28DA2CFE07CF00536B68 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; - 649B28DB2CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.5-592uBhlQO6KIf70TJf5KpP.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.2.0.5-592uBhlQO6KIf70TJf5KpP.a"; sourceTree = ""; }; + 649B28DB2CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.6-5lGV6gtq9gSDlEsE8DHXYo.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.2.0.6-5lGV6gtq9gSDlEsE8DHXYo.a"; sourceTree = ""; }; 649B28DC2CFE07CF00536B68 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; 649BCD9F280460FD00C3A862 /* ComposeImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeImageView.swift; sourceTree = ""; }; 649BCDA12805D6EF00C3A862 /* CIImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIImageView.swift; sourceTree = ""; }; @@ -671,9 +671,9 @@ 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, 649B28E12CFE07CF00536B68 /* libgmp.a in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, - 649B28E02CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.5-592uBhlQO6KIf70TJf5KpP.a in Frameworks */, + 649B28E02CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.6-5lGV6gtq9gSDlEsE8DHXYo.a in Frameworks */, CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */, - 649B28DE2CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.5-592uBhlQO6KIf70TJf5KpP-ghc9.6.3.a in Frameworks */, + 649B28DE2CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.6-5lGV6gtq9gSDlEsE8DHXYo-ghc9.6.3.a in Frameworks */, 649B28DD2CFE07CF00536B68 /* libffi.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -754,8 +754,8 @@ 649B28D82CFE07CF00536B68 /* libffi.a */, 649B28DC2CFE07CF00536B68 /* libgmp.a */, 649B28DA2CFE07CF00536B68 /* libgmpxx.a */, - 649B28D92CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.5-592uBhlQO6KIf70TJf5KpP-ghc9.6.3.a */, - 649B28DB2CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.5-592uBhlQO6KIf70TJf5KpP.a */, + 649B28D92CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.6-5lGV6gtq9gSDlEsE8DHXYo-ghc9.6.3.a */, + 649B28DB2CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.6-5lGV6gtq9gSDlEsE8DHXYo.a */, ); path = Libraries; sourceTree = ""; @@ -1931,7 +1931,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 251; + CURRENT_PROJECT_VERSION = 253; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -1980,7 +1980,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 251; + CURRENT_PROJECT_VERSION = 253; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -2021,7 +2021,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 251; + CURRENT_PROJECT_VERSION = 253; DEVELOPMENT_TEAM = 5NN7GUYB6T; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; @@ -2041,7 +2041,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 251; + CURRENT_PROJECT_VERSION = 253; DEVELOPMENT_TEAM = 5NN7GUYB6T; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; @@ -2066,7 +2066,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 251; + CURRENT_PROJECT_VERSION = 253; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GCC_OPTIMIZATION_LEVEL = s; @@ -2103,7 +2103,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 251; + CURRENT_PROJECT_VERSION = 253; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_CODE_COVERAGE = NO; @@ -2140,7 +2140,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 251; + CURRENT_PROJECT_VERSION = 253; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -2191,7 +2191,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 251; + CURRENT_PROJECT_VERSION = 253; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -2242,7 +2242,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 251; + CURRENT_PROJECT_VERSION = 253; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -2276,7 +2276,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 251; + CURRENT_PROJECT_VERSION = 253; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; diff --git a/apps/multiplatform/gradle.properties b/apps/multiplatform/gradle.properties index b490c26f87..e620b4992d 100644 --- a/apps/multiplatform/gradle.properties +++ b/apps/multiplatform/gradle.properties @@ -24,11 +24,11 @@ android.nonTransitiveRClass=true kotlin.mpp.androidSourceSetLayoutVersion=2 kotlin.jvm.target=11 -android.version_name=6.2-beta.5 -android.version_code=256 +android.version_name=6.2-beta.6 +android.version_code=258 -desktop.version_name=6.2-beta.5 -desktop.version_code=80 +desktop.version_name=6.2-beta.6 +desktop.version_code=81 kotlin.version=1.9.23 gradle.plugin.version=8.2.0 From 19e2cebd6899bceb47ccf5126967b947ff18a020 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Fri, 6 Dec 2024 01:16:42 +0000 Subject: [PATCH 143/167] android, desktop: remove footer in Run chat section when chat is running --- .../chat/simplex/common/views/database/DatabaseView.kt | 8 +------- .../common/src/commonMain/resources/MR/ar/strings.xml | 1 - .../common/src/commonMain/resources/MR/base/strings.xml | 1 - .../common/src/commonMain/resources/MR/bg/strings.xml | 1 - .../common/src/commonMain/resources/MR/cs/strings.xml | 1 - .../common/src/commonMain/resources/MR/de/strings.xml | 1 - .../common/src/commonMain/resources/MR/es/strings.xml | 1 - .../common/src/commonMain/resources/MR/fa/strings.xml | 1 - .../common/src/commonMain/resources/MR/fi/strings.xml | 1 - .../common/src/commonMain/resources/MR/fr/strings.xml | 1 - .../common/src/commonMain/resources/MR/hu/strings.xml | 1 - .../common/src/commonMain/resources/MR/it/strings.xml | 1 - .../common/src/commonMain/resources/MR/iw/strings.xml | 1 - .../common/src/commonMain/resources/MR/ja/strings.xml | 1 - .../common/src/commonMain/resources/MR/ko/strings.xml | 1 - .../common/src/commonMain/resources/MR/lt/strings.xml | 1 - .../common/src/commonMain/resources/MR/nl/strings.xml | 1 - .../common/src/commonMain/resources/MR/pl/strings.xml | 1 - .../common/src/commonMain/resources/MR/pt-rBR/strings.xml | 1 - .../common/src/commonMain/resources/MR/pt/strings.xml | 1 - .../common/src/commonMain/resources/MR/ru/strings.xml | 1 - .../common/src/commonMain/resources/MR/th/strings.xml | 1 - .../common/src/commonMain/resources/MR/tr/strings.xml | 1 - .../common/src/commonMain/resources/MR/uk/strings.xml | 1 - .../common/src/commonMain/resources/MR/zh-rCN/strings.xml | 1 - .../common/src/commonMain/resources/MR/zh-rTW/strings.xml | 1 - 26 files changed, 1 insertion(+), 32 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt index e1f53760e5..ab908e4c5f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt @@ -192,13 +192,7 @@ fun DatabaseLayout( } RunChatSetting(stopped, toggleEnabled && !progressIndicator, startChat, stopChatAlert) } - SectionTextFooter( - if (stopped) { - stringResource(MR.strings.you_must_use_the_most_recent_version_of_database) - } else { - stringResource(MR.strings.stop_chat_to_enable_database_actions) - } - ) + if (stopped) SectionTextFooter(stringResource(MR.strings.you_must_use_the_most_recent_version_of_database)) SectionDividerSpaced(maxTopPadding = true) } diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml index 259411688c..c67b6258a2 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml @@ -1049,7 +1049,6 @@ إيقاف مشاركة العنوان؟ إيقاف المشاركة أوقف الدردشة لتصدير أو استيراد أو حذف قاعدة بيانات الدردشة. لن تتمكّن من استلام الرسائل وإرسالها أثناء إيقاف الدردشة. - أوقف الدردشة لتمكين إجراءات قاعدة البيانات. %s ثانية/ثواني يبدأ… تم تشغيل القفل SimpleX diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 7fc46cf3a7..43ffded767 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -1306,7 +1306,6 @@ Chat database deleted Restart the app to create a new chat profile. 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. - Stop chat to enable database actions. Files & media Delete files for all chat profiles Delete all files diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/bg/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/bg/strings.xml index 22e93b041a..c51b33e456 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/bg/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/bg/strings.xml @@ -1133,7 +1133,6 @@ Запази настройките\? Високоговорителят е включен Високоговорителят е изключен - Спрете чата, за да активирате действията с базата данни. Роля Запази Нулирай цветовете diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml index a6ea5b1208..c80d24f0bd 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml @@ -280,7 +280,6 @@ Tuto akci nelze vzít zpět! Váš profil, kontakty, zprávy a soubory budou nenávratně ztraceny. Restartujte aplikaci a vytvořte nový chat profil. Nejnovější verzi databáze chatu musíte používat POUZE v jednom zařízení, jinak se může stát, že přestanete přijímat zprávy od některých kontaktů. - Zastavte chat a povolte akce s databází. Soubory a média Smazat soubory a média\? Odstranit zprávy diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml index e90dd26aff..5ab956ae85 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml @@ -597,7 +597,6 @@ Chat-Datenbank gelöscht Starten Sie die App neu, um ein neues Chat-Profil zu erstellen. Sie dürfen die neueste Version Ihrer Chat-Datenbank NUR auf einem Gerät verwenden, andernfalls erhalten Sie möglicherweise keine Nachrichten mehr von einigen Ihrer Kontakte. - Chat beenden, um Datenbankaktionen zu erlauben. Dateien und Medien löschen? Diese Aktion kann nicht rückgängig gemacht werden! Es werden alle empfangenen und gesendeten Dateien und Medien gelöscht. Bilder mit niedriger Auflösung bleiben erhalten. Keine empfangenen oder gesendeten Dateien diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml index 02f69b0550..0e53f59c06 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml @@ -733,7 +733,6 @@ Compartir enlace de un uso ¿Actualizar el modo de aislamiento de transporte\? Altavoz activado - Para habilitar las acciones sobre la base de datos, debes parar SimpleX ¡La conexión que has aceptado se cancelará! La base de datos no funciona correctamente. Pulsa para conocer más El mensaje será marcado como moderado para todos los miembros. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml index 7e59b69082..866506460c 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml @@ -942,7 +942,6 @@ این عمل قابل برگشت نیست - تمام پرونده‌ها و رسانه دریافتی حذف خواهند شد. عکس‌های با کیفیت پایین باقی خواهند ماند. شما باید از تازه‌ترین نسخه پایگاه داده گپ خود روی فقط یک دستگاه استفاده کنید، در غیر این صورت ممکن است از بعضی از مخاطب‌ها ‌دیگر پیامی دریافت نکنید. پیام‌ها - به منظور فعال‌سازی اقدامات پایگاه داده، گپ را متوقف کنید. این عمل قابل برگشت نیست - پیام‌های ارسالی و دریافتی قدیمی‌تر از زمان انتخابی حذف خواهند شد. این کار ممکن است چندین دقیقه زمان ببرد. خطا در تغییر تنظیمات ذخیره عبارت عبور در تنظیمات diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/fi/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/fi/strings.xml index 62a02986b2..26847aeaf5 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/fi/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/fi/strings.xml @@ -666,7 +666,6 @@ Itsetuhoutuva pääsykoodi Käynnistä sovellus uudelleen käyttääksesi tuotua keskustelutietokantaa. Vanha tietokanta-arkisto - Pysäytä keskustelu, jotta tietokantatoiminnot voidaan ottaa käyttöön. Luo uusi keskusteluprofiili käynnistämällä sovellus uudelleen. Viestit ei koskaan diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml index 5eea31e670..62891fd8b8 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml @@ -617,7 +617,6 @@ Erreur lors de l\'importation de la base de données du chat Base de données du chat importée Supprimer le profil du chat \? - Arrêter le chat pour agir sur la base de données. Supprimer les fichiers et médias \? Cette action ne peut être annulée - tous les fichiers et médias reçus et envoyés seront supprimés. Les photos à faible résolution seront conservées. Aucun fichier reçu ou envoyé diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml index 34dc1227a7..83f408054f 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml @@ -922,7 +922,6 @@ Csak az ismerőse tud hívást indítani. TÉMÁK Túl sok videó! - Csevegési szolgáltatás megállítása az adatbázis műveletek elvégzéséhez. Üdvözöljük! Önmegsemmisítési jelkód (beolvasás, vagy beillesztés a vágólapról) diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml index 92c0105d09..c342319dbb 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml @@ -874,7 +874,6 @@ Rimuovere la password dal Keystore\? Salva la password nel Keystore %s secondo/i - Ferma la chat per attivare le azioni del database. Questa azione non può essere annullata: tutti i file e i media ricevuti e inviati verranno eliminati. Rimarranno le immagini a bassa risoluzione. Questa azione non può essere annullata: i messaggi inviati e ricevuti prima di quanto selezionato verranno eliminati. Potrebbe richiedere diversi minuti. Aggiorna diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/iw/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/iw/strings.xml index d9ddd08a57..2bb007b6e8 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/iw/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/iw/strings.xml @@ -981,7 +981,6 @@ לעצור צ׳אט\? עיצרו את הצ׳אט כדי לייצא, לייבא או למחוק את מסד הנתונים. לא תוכלו לקבל ולשלוח הודעות בזמן שהצ׳אט מופסק. עצור - עיצרו את הצ׳אט כדי לאפשר פעולות מסד נתונים. דלג על הזמנת חברים שתף כתובת SimpleX diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml index c6775f6639..ff6b4e456c 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml @@ -662,7 +662,6 @@ スピーカーオフ あなたのチャットデータベース 停止 - データベース操作をするにはチャットを停止する必要があります。 SimpleX連絡先アドレス SimpleX使い捨て招待リンク 連絡先アドレスリンク経由 diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ko/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ko/strings.xml index c03b4e648b..bf07c10a6f 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ko/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ko/strings.xml @@ -851,7 +851,6 @@ 제출하기 암호를 모르면 채팅에 액세스할 수 없으니 암호를 안전하게 보관해 주세요. 채팅 기능을 중지할까요\? - 데이터베이스 작업을 할 수 있도록 채팅 기능을 중지하기 수신 주소 바꾸기 복호화 오류 패스코드 확인 diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/lt/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/lt/strings.xml index 5db1442fc6..fbad2dc4e9 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/lt/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/lt/strings.xml @@ -1219,7 +1219,6 @@ Nustatyti duomenų slaptafrazę Nustatyti slaptafrazę Rodyti paskutines žinutes - Sustabdykite pokalbius, kad įgalinti duomenų bazės veiksmus. PALAIKYKITE SIMPLEX CHAT Jų galima nepaisyti kontaktų ir grupių nustatymuose. Šis veiksmas negali būti atšauktas - žinutės išsiųstos ir gautos anksčiau nei pasirinkta bus ištrintos. Tai gali užtrukti kelias minutes. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml index 1a5eaa403d..f00182b469 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml @@ -740,7 +740,6 @@ Start de app opnieuw om een nieuw chatprofiel aan te maken. U mag ALLEEN de meest recente versie van uw chat-database op één apparaat gebruiken, anders ontvangt u mogelijk geen berichten meer van sommige contacten. Start de app opnieuw om de geïmporteerde chat database te gebruiken. - Stop de chat om database acties mogelijk te maken. Deze actie kan niet ongedaan worden gemaakt, alle ontvangen en verzonden bestanden en media worden verwijderd. Foto\'s met een lage resolutie blijven behouden. Wachtwoord verwijderen uit Keychain\? Verwijderen diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml index 56c80f8c89..a748bf2741 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml @@ -562,7 +562,6 @@ Uruchom ponownie aplikację, aby utworzyć nowy profil czatu. Zapisz hasło w Keystore %s sekund(y) - Zatrzymaj czat, aby umożliwić działania na bazie danych. Tego działania nie można cofnąć - wiadomości wysłane i odebrane wcześniej niż wybrane zostaną usunięte. Może to potrwać kilka minut. Tego działania nie można cofnąć - Twój profil, kontakty, wiadomości i pliki zostaną nieodwracalnie utracone. To ustawienie dotyczy wiadomości Twojego bieżącego profilu czatu diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/pt-rBR/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/pt-rBR/strings.xml index 27cc039c34..3b139013fc 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/pt-rBR/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/pt-rBR/strings.xml @@ -853,7 +853,6 @@ PROXY SOCKS A tentativa de alterar a senha do banco de dados não foi concluída. Pare o bate-papo para exportar, importar ou excluir o banco de dados do chat. Você não poderá receber e enviar mensagens enquanto o chat estiver interrompido. - Pare o bate-papo para ativar ações no banco de dados. %s segundo(s) Erro de banco de dados desconhecido: %s Erro desconhecido diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/pt/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/pt/strings.xml index 5cdc67e9e5..c9db7de2e6 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/pt/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/pt/strings.xml @@ -760,7 +760,6 @@ %s, %s e %d membros Iniciar nova conversa Sistema - Parar conversa para habilitar ações do banco de dados Toque para participar %s, %s e %s conectado Tempo esgotado da conexão TCP diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml index 9e39ad61a4..584377bc99 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml @@ -600,7 +600,6 @@ Данные чата удалены Перезапустите приложение, чтобы создать новый профиль. Используйте самую последнюю версию архива чата и ТОЛЬКО на одном устройстве, иначе Вы можете перестать получать сообщения от некоторых контактов. - Остановите чат, чтобы разблокировать операции с архивом чата. Удалить файлы во всех профилях чата Удалить все файлы Удалить файлы и медиа? diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/th/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/th/strings.xml index a0027df1d8..ebf57836b5 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/th/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/th/strings.xml @@ -937,7 +937,6 @@ ติดดาวบน GitHub เปลี่ยน กำลังเริ่มต้น… - หยุดการแชทเพื่อเปิดใช้งานการดำเนินการกับฐานข้อมูล หยุดแชทเพื่อส่งออก นำเข้า หรือลบฐานข้อมูลแชท คุณจะไม่สามารถรับและส่งข้อความได้ในขณะที่การแชทหยุดลง รองรับบลูทูธและการปรับปรุงอื่นๆ หยุดแชร์ที่อยู่ไหม\? diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml index 67b6226b0e..503d82158f 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml @@ -1034,7 +1034,6 @@ Dosyayı durdur Hata ProfilProfil oluştur - Veri tabanı eylemlerini etkinleştirmek için sohbeti durdur. Dosya göndermeyi durdur? Sohbeti durdur Mevcut profili kullan diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml index 0afb405097..548e29f836 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml @@ -899,7 +899,6 @@ Увімкнути блокування Пароль не змінено! Змінити режим блокування - Зупиніть чат, щоб увімкнути дії з базою даних. Перезапустіть додаток, щоб створити новий профіль чату. Видалити файли для всіх профілів чату Видалити файли та медіа? diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml index 41e9793ba1..ca42ccc902 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml @@ -799,7 +799,6 @@ 禁止向成员发送私信。 保护应用程序屏幕 主题 - 停止聊天以启用数据库操作。 %s 秒 该群已不存在。 点击加入 diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml index 11a086f795..fd58811439 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml @@ -119,7 +119,6 @@ 聊天室已停止運作 停止 已刪除數據庫的對話內容 - 停止聊天室以啟用數據庫功能。 修改數據庫密碼? 確定要退出群組? 退出 From 9b82cc33030469012a12d48d266f17e43421978f Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Fri, 6 Dec 2024 10:18:48 +0000 Subject: [PATCH 144/167] core: fix feature items when updating preferences in business chats --- src/Simplex/Chat/Store/Groups.hs | 2 +- tests/ChatTests/Profiles.hs | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index 49158a60c9..36ce7f3575 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -1472,7 +1472,7 @@ updateGroupPreferences db User {userId} g@GroupInfo {groupId, groupProfile = p} ) |] (ps, currentTs, userId, groupId) - pure (g :: GroupInfo) {groupProfile = p {groupPreferences = Just ps}} + pure (g :: GroupInfo) {groupProfile = p {groupPreferences = Just ps}, fullGroupPreferences = mergeGroupPreferences $ Just ps} updateGroupProfileFromMember :: DB.Connection -> User -> GroupInfo -> Profile -> ExceptT StoreError IO GroupInfo updateGroupProfileFromMember db user g@GroupInfo {groupId} Profile {displayName = n, fullName = fn, image = img} = do diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index 2bf157419c..71edbb93b0 100644 --- a/tests/ChatTests/Profiles.hs +++ b/tests/ChatTests/Profiles.hs @@ -857,6 +857,10 @@ testBusinessUpdateProfiles = testChat4 businessProfile aliceProfile bobProfile c cath <## "updated group preferences:" cath <## "Voice messages: on" ] + biz #$> ("/_get chat #1 count=1", chat, [(1, "Voice messages: on")]) + alice #$> ("/_get chat #1 count=1", chat, [(0, "Voice messages: on")]) + bob #$> ("/_get chat #1 count=1", chat, [(0, "Voice messages: on")]) + cath #$> ("/_get chat #1 count=1", chat, [(0, "Voice messages: on")]) testPlanAddressOkKnown :: HasCallStack => FilePath -> IO () testPlanAddressOkKnown = From 924273191ec1dd34d13e94f43ce11872bfc06671 Mon Sep 17 00:00:00 2001 From: Diogo Date: Fri, 6 Dec 2024 10:21:58 +0000 Subject: [PATCH 145/167] ios: ask for confirmation of save on group preferences sheet dismiss (#5327) * ios: ask for confirmation of save on group preferences sheet dismiss * fix exit without saving temporary state and also apply fix on dismiss during group creation --- apps/ios/Shared/Views/Chat/ChatView.swift | 3 +- .../Chat/Group/AddGroupMembersView.swift | 5 +- .../Views/Chat/Group/GroupChatInfoView.swift | 26 ++++++++-- .../Chat/Group/GroupPreferencesView.swift | 47 +++++++++++-------- .../Shared/Views/NewChat/AddGroupView.swift | 1 + 5 files changed, 54 insertions(+), 28 deletions(-) diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index cfbbfe6080..f2f25ff272 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -246,7 +246,8 @@ struct ChatView: View { chat.created = Date.now } ), - onSearch: { focusSearch() } + onSearch: { focusSearch() }, + preferences: groupInfo.fullGroupPreferences ) } } else if case .local = cInfo { diff --git a/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift b/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift index 925f4120bc..0d03b21ca0 100644 --- a/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift +++ b/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift @@ -15,7 +15,7 @@ struct AddGroupMembersView: View { var groupInfo: GroupInfo var body: some View { - AddGroupMembersViewCommon(chat: chat, groupInfo: groupInfo, addedMembersCb: { _ in dismiss() }) + AddGroupMembersViewCommon(chat: chat, groupInfo: groupInfo, preferences: groupInfo.fullGroupPreferences, addedMembersCb: { _ in dismiss() }) } } @@ -24,6 +24,7 @@ struct AddGroupMembersViewCommon: View { @EnvironmentObject var theme: AppTheme var chat: Chat @State var groupInfo: GroupInfo + @State var preferences: FullGroupPreferences var creatingGroup: Bool = false var showFooterCounter: Bool = true var addedMembersCb: ((Set) -> Void) @@ -78,7 +79,7 @@ struct AddGroupMembersViewCommon: View { let count = selectedContacts.count Section { if creatingGroup { - groupPreferencesButton($groupInfo, true) + groupPreferencesButton($groupInfo, $preferences, true) } rolePicker() inviteMembersButton() diff --git a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift index 27aa0edb5b..640b8c6b1d 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift @@ -19,6 +19,7 @@ struct GroupChatInfoView: View { @Binding var groupInfo: GroupInfo var onSearch: () -> Void @State private var alert: GroupChatInfoViewAlert? = nil + @State var preferences: FullGroupPreferences @State private var groupLink: String? @State private var groupLinkMemberRole: GroupMemberRole = .member @State private var groupLinkNavLinkActive: Bool = false @@ -87,7 +88,7 @@ struct GroupChatInfoView: View { if groupInfo.groupProfile.description != nil || (groupInfo.isOwner && groupInfo.businessChat == nil) { addOrEditWelcomeMessage() } - groupPreferencesButton($groupInfo) + groupPreferencesButton($groupInfo, $preferences) if members.filter({ $0.wrapped.memberCurrent }).count <= SMALL_GROUPS_RCPS_MEM_LIMIT { sendReceiptsOption() } else { @@ -654,18 +655,32 @@ func deleteGroupAlertMessage(_ groupInfo: GroupInfo) -> Text { ) } -func groupPreferencesButton(_ groupInfo: Binding, _ creatingGroup: Bool = false) -> some View { +func groupPreferencesButton(_ groupInfo: Binding, _ preferences: Binding, _ creatingGroup: Bool = false) -> some View { let label: LocalizedStringKey = groupInfo.wrappedValue.businessChat == nil ? "Group preferences" : "Chat preferences" return NavigationLink { GroupPreferencesView( groupInfo: groupInfo, - preferences: groupInfo.wrappedValue.fullGroupPreferences, - currentPreferences: groupInfo.wrappedValue.fullGroupPreferences, + preferences: preferences, + currentPreferences: groupInfo.fullGroupPreferences, creatingGroup: creatingGroup ) .navigationBarTitle(label) .modifier(ThemedBackground(grouped: true)) .navigationBarTitleDisplayMode(.large) + .onDisappear { + let saveText = NSLocalizedString(creatingGroup ? "Save" : "Save and notify group members", comment: "alert button") + + if groupInfo.fullGroupPreferences.wrappedValue != preferences.wrappedValue { + showAlert( + title: NSLocalizedString("Save preferences?", comment: "alert title"), + buttonTitle: saveText, + buttonAction: { + savePreferences(groupInfo: groupInfo, preferences: preferences, currentPreferences: groupInfo.fullGroupPreferences) + }, + cancelButton: true + ) + } + } } label: { if creatingGroup { Text("Set group preferences") @@ -694,7 +709,8 @@ struct GroupChatInfoView_Previews: PreviewProvider { GroupChatInfoView( chat: Chat(chatInfo: ChatInfo.sampleData.group, chatItems: []), groupInfo: Binding.constant(GroupInfo.sampleData), - onSearch: {} + onSearch: {}, + preferences: GroupInfo.sampleData.fullGroupPreferences ) } } diff --git a/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift b/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift index bbbbe4d4c3..b27ff37d95 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift @@ -20,8 +20,8 @@ struct GroupPreferencesView: View { @EnvironmentObject var chatModel: ChatModel @EnvironmentObject var theme: AppTheme @Binding var groupInfo: GroupInfo - @State var preferences: FullGroupPreferences - @State var currentPreferences: FullGroupPreferences + @Binding var preferences: FullGroupPreferences + @Binding var currentPreferences: FullGroupPreferences let creatingGroup: Bool @State private var showSaveDialogue = false @@ -41,7 +41,7 @@ struct GroupPreferencesView: View { if groupInfo.isOwner { Section { Button("Reset") { preferences = currentPreferences } - Button(saveText) { savePreferences() } + Button(saveText) { savePreferences(groupInfo: $groupInfo, preferences: $preferences, currentPreferences: $currentPreferences) } } .disabled(currentPreferences == preferences) } @@ -65,10 +65,13 @@ struct GroupPreferencesView: View { }) .confirmationDialog("Save preferences?", isPresented: $showSaveDialogue) { Button(saveText) { - savePreferences() + savePreferences(groupInfo: $groupInfo, preferences: $preferences, currentPreferences: $currentPreferences) + dismiss() + } + Button("Exit without saving") { + preferences = currentPreferences dismiss() } - Button("Exit without saving") { dismiss() } } } @@ -132,21 +135,25 @@ struct GroupPreferencesView: View { } } } +} - private func savePreferences() { - Task { - do { - var gp = groupInfo.groupProfile - gp.groupPreferences = toGroupPreferences(preferences) - let gInfo = try await apiUpdateGroup(groupInfo.groupId, gp) - await MainActor.run { - groupInfo = gInfo - chatModel.updateGroup(gInfo) - currentPreferences = preferences - } - } catch { - logger.error("GroupPreferencesView apiUpdateGroup error: \(responseError(error))") +func savePreferences( + groupInfo: Binding, + preferences: Binding, + currentPreferences: Binding +) { + Task { + do { + var gp = groupInfo.groupProfile.wrappedValue + gp.groupPreferences = toGroupPreferences(preferences.wrappedValue) + let gInfo = try await apiUpdateGroup(groupInfo.groupId.wrappedValue, gp) + await MainActor.run { + groupInfo.wrappedValue = gInfo + ChatModel.shared.updateGroup(gInfo) + currentPreferences.wrappedValue = preferences.wrappedValue } + } catch { + logger.error("GroupPreferencesView apiUpdateGroup error: \(responseError(error))") } } } @@ -155,8 +162,8 @@ struct GroupPreferencesView_Previews: PreviewProvider { static var previews: some View { GroupPreferencesView( groupInfo: Binding.constant(GroupInfo.sampleData), - preferences: FullGroupPreferences.sampleData, - currentPreferences: FullGroupPreferences.sampleData, + preferences: Binding.constant(FullGroupPreferences.sampleData), + currentPreferences: Binding.constant(FullGroupPreferences.sampleData), creatingGroup: false ) } diff --git a/apps/ios/Shared/Views/NewChat/AddGroupView.swift b/apps/ios/Shared/Views/NewChat/AddGroupView.swift index 0c7f6136ff..5207c3a472 100644 --- a/apps/ios/Shared/Views/NewChat/AddGroupView.swift +++ b/apps/ios/Shared/Views/NewChat/AddGroupView.swift @@ -32,6 +32,7 @@ struct AddGroupView: View { AddGroupMembersViewCommon( chat: chat, groupInfo: groupInfo, + preferences: groupInfo.fullGroupPreferences, creatingGroup: true, showFooterCounter: false ) { _ in From 2e431c5afa7f62972daf6740cc6c7edb5cc36936 Mon Sep 17 00:00:00 2001 From: Diogo Date: Fri, 6 Dec 2024 11:10:52 +0000 Subject: [PATCH 146/167] ios: fix some real time updates in group members (#5332) * ios: fix some real time updates in group members * use chat instead of binding for group info updates --- apps/ios/Shared/Views/Chat/ChatView.swift | 9 ++++++++- apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift | 2 +- .../Shared/Views/Chat/Group/GroupMemberInfoView.swift | 7 +++++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index f2f25ff272..b436147a8e 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -133,7 +133,12 @@ struct ChatView: View { .appSheet(item: $selectedMember) { member in Group { if case let .group(groupInfo) = chat.chatInfo { - GroupMemberInfoView(groupInfo: groupInfo, groupMember: member, navigation: true) + GroupMemberInfoView( + groupInfo: groupInfo, + chat: chat, + groupMember: member, + navigation: true + ) } } } @@ -1123,6 +1128,7 @@ struct ChatView: View { } else { let mem = GMember.init(member) m.groupMembers.append(mem) + m.groupMembersIndexes[member.groupMemberId] = m.groupMembers.count - 1 selectedMember = mem } } @@ -1878,6 +1884,7 @@ struct ReactionContextMenu: View { } else { let member = GMember.init(mem) m.groupMembers.append(member) + m.groupMembersIndexes[member.groupMemberId] = m.groupMembers.count - 1 selectedMember = member } } label: { diff --git a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift index 640b8c6b1d..7dc57f9642 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift @@ -440,7 +440,7 @@ struct GroupChatInfoView: View { } private func memberInfoView(_ groupMember: GMember) -> some View { - GroupMemberInfoView(groupInfo: groupInfo, groupMember: groupMember) + GroupMemberInfoView(groupInfo: groupInfo, chat: chat, groupMember: groupMember) .navigationBarHidden(false) } diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift index 90d6829d93..b73c5e10f5 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift @@ -14,6 +14,7 @@ struct GroupMemberInfoView: View { @EnvironmentObject var theme: AppTheme @Environment(\.dismiss) var dismiss: DismissAction @State var groupInfo: GroupInfo + @ObservedObject var chat: Chat @ObservedObject var groupMember: GMember var navigation: Bool = false @State private var connectionStats: ConnectionStats? = nil @@ -261,6 +262,11 @@ struct GroupMemberInfoView: View { ProgressView().scaleEffect(2) } } + .onChange(of: chat.chatInfo) { c in + if case let .group(gI) = chat.chatInfo { + groupInfo = gI + } + } .modifier(ThemedBackground(grouped: true)) } @@ -758,6 +764,7 @@ struct GroupMemberInfoView_Previews: PreviewProvider { static var previews: some View { GroupMemberInfoView( groupInfo: GroupInfo.sampleData, + chat: Chat.sampleData, groupMember: GMember.sampleData ) } From 945c5015d8b6cd0d0ac660c0c6ab3e5ecf3b91ff Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Fri, 6 Dec 2024 15:35:26 +0400 Subject: [PATCH 147/167] ui: improve pending connection texts (#5333) * ui: improve contact request text * android * ternary * shorter * kotlin * change --------- Co-authored-by: Evgeny Poberezkin --- apps/ios/SimpleXChat/ChatTypes.swift | 6 ++++-- .../kotlin/chat/simplex/common/model/ChatModel.kt | 5 +++-- .../common/src/commonMain/resources/MR/base/strings.xml | 2 ++ 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index a2d44d59d0..da1ce24b73 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -1782,9 +1782,11 @@ public struct PendingContactConnection: Decodable, NamedChat, Hashable { public var displayName: String { get { if let initiated = pccConnStatus.initiated { - return initiated && !viaContactUri + return viaContactUri + ? NSLocalizedString("requested to connect", comment: "chat list item title") + : initiated ? NSLocalizedString("invited to connect", comment: "chat list item title") - : NSLocalizedString("connecting…", comment: "chat list item title") + : NSLocalizedString("accepted invitation", comment: "chat list item title") } else { // this should not be in the list return NSLocalizedString("connection established", comment: "chat list item title (it should not be shown") diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index b9e52763a6..d407174e52 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -1895,8 +1895,9 @@ class PendingContactConnection( generalGetString(MR.strings.display_name_connection_established) } else { generalGetString( - if (initiated && !viaContactUri) MR.strings.display_name_invited_to_connect - else MR.strings.display_name_connecting + if (viaContactUri) MR.strings.display_name_requested_to_connect + else if (initiated) MR.strings.display_name_invited_to_connect + else MR.strings.display_name_accepted_invitation ) } } diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 43ffded767..c412ec42ee 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -71,6 +71,8 @@ connection %1$d connection established invited to connect + requested to connect + accepted invitation connecting… you shared one-time link you shared one-time link incognito From f4089880353b1de274d846d2f7b8dee37dcff960 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Fri, 6 Dec 2024 17:05:39 +0400 Subject: [PATCH 148/167] ios: fix oneHandUI setting becoming enabled on import (#5335) --- apps/ios/SimpleXChat/APITypes.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 2bd76dea63..a9cf2ee599 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -2725,7 +2725,7 @@ public struct AppSettings: Codable, Equatable { uiDarkColorScheme: DefaultTheme.SIMPLEX.themeName, uiCurrentThemeIds: nil as [String: String]?, uiThemes: nil as [ThemeOverrides]?, - oneHandUI: false, + oneHandUI: true, chatBottomBar: true ) } From 1408d75eb333c06e3ab49758cb1a1ae56aa63f3c Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Fri, 6 Dec 2024 17:21:55 +0400 Subject: [PATCH 149/167] ios: use async getServerOperators api (#5334) --- apps/ios/Shared/Model/SimpleXAPI.swift | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index e29748f3af..51be3191ec 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -519,7 +519,14 @@ func testProtoServer(server: String) async throws -> Result<(), ProtocolTestFail throw r } -func getServerOperators() throws -> ServerOperatorConditions { +func getServerOperators() async throws -> ServerOperatorConditions { + let r = await chatSendCmd(.apiGetServerOperators) + if case let .serverOperatorConditions(conditions) = r { return conditions } + logger.error("getServerOperators error: \(String(describing: r))") + throw r +} + +func getServerOperatorsSync() throws -> ServerOperatorConditions { let r = chatSendCmdSync(.apiGetServerOperators) if case let .serverOperatorConditions(conditions) = r { return conditions } logger.error("getServerOperators error: \(String(describing: r))") @@ -1599,7 +1606,7 @@ func initializeChat(start: Bool, confirmStart: Bool = false, dbKey: String? = ni try apiSetEncryptLocalFiles(privacyEncryptLocalFilesGroupDefault.get()) m.chatInitialized = true m.currentUser = try apiGetActiveUser() - m.conditions = try getServerOperators() + m.conditions = try getServerOperatorsSync() if shouldImportAppSettingsDefault.get() { do { let appSettings = try apiGetAppSettings(settings: AppSettings.current.prepareForExport()) From ae8ad5c639a9d4a8af925e78d336fa7e33598195 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Fri, 6 Dec 2024 17:49:57 +0400 Subject: [PATCH 150/167] ios: operators info on onboarding (#5336) --- .../Onboarding/ChooseServerOperators.swift | 56 ++++++++++++++----- 1 file changed, 42 insertions(+), 14 deletions(-) diff --git a/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift b/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift index 318e0b2f0d..cc47374257 100644 --- a/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift +++ b/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift @@ -409,26 +409,54 @@ struct ChooseServerOperators: View { let operatorsPostLink = URL(string: "https://simplex.chat/blog/20241125-servers-operated-by-flux-true-privacy-and-decentralization-for-all-users.html")! struct ChooseServerOperatorsInfoView: View { + @Environment(\.colorScheme) var colorScheme: ColorScheme + @EnvironmentObject var theme: AppTheme + var body: some View { - VStack(alignment: .leading) { - Text("Server operators") - .font(.largeTitle) - .bold() - .padding(.vertical) - ScrollView { + NavigationView { + List { VStack(alignment: .leading) { - Group { - Text("The app protects your privacy by using different operators in each conversation.") - Text("When more than one operator is enabled, none of them has metadata to learn who communicates with whom.") - Text("For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server.") + Text("The app protects your privacy by using different operators in each conversation.") + .padding(.bottom) + Text("When more than one operator is enabled, none of them has metadata to learn who communicates with whom.") + .padding(.bottom) + Text("For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server.") + } + .listRowBackground(Color.clear) + .listRowSeparator(.hidden) + .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) + .padding(.top) + + Section { + ForEach(ChatModel.shared.conditions.serverOperators) { op in + operatorInfoNavLinkView(op) } - .padding(.bottom) + } header: { + Text("About operators") + .foregroundColor(theme.colors.secondary) } } + .navigationTitle("Server operators") + .navigationBarTitleDisplayMode(.large) + .modifier(ThemedBackground(grouped: true)) + } + } + + private func operatorInfoNavLinkView(_ op: ServerOperator) -> some View { + NavigationLink() { + OperatorInfoView(serverOperator: op) + .navigationBarTitle("Network operator") + .modifier(ThemedBackground(grouped: true)) + .navigationBarTitleDisplayMode(.large) + } label: { + HStack { + Image(op.logo(colorScheme)) + .resizable() + .scaledToFit() + .frame(width: 24, height: 24) + Text(op.tradeName) + } } - .padding() - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) - .modifier(ThemedBackground()) } } From 7d43a43e826158c9040cd11ee593a13a2b112aaa Mon Sep 17 00:00:00 2001 From: Diogo Date: Fri, 6 Dec 2024 14:44:56 +0000 Subject: [PATCH 151/167] ios: ask for confirmation of save on contact preferences sheet dismiss (#5337) --- apps/ios/Shared/Views/Chat/ChatInfoView.swift | 36 +++++++++++++++++-- apps/ios/Shared/Views/Chat/ChatView.swift | 2 ++ .../Views/Chat/ContactPreferencesView.swift | 32 ++++++----------- 3 files changed, 46 insertions(+), 24 deletions(-) diff --git a/apps/ios/Shared/Views/Chat/ChatInfoView.swift b/apps/ios/Shared/Views/Chat/ChatInfoView.swift index c829e1a2b9..ea9daa74bc 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoView.swift @@ -96,6 +96,8 @@ struct ChatInfoView: View { @ObservedObject var chat: Chat @State var contact: Contact @State var localAlias: String + @State var featuresAllowed: ContactFeaturesAllowed + @State var currentFeaturesAllowed: ContactFeaturesAllowed var onSearch: () -> Void @State private var connectionStats: ConnectionStats? = nil @State private var customUserProfile: Profile? = nil @@ -327,6 +329,16 @@ struct ChatInfoView: View { $0.content } } + .onDisappear { + if currentFeaturesAllowed != featuresAllowed { + showAlert( + title: NSLocalizedString("Save preferences?", comment: "alert title"), + buttonTitle: NSLocalizedString("Save and notify contact", comment: "alert button"), + buttonAction: { savePreferences() }, + cancelButton: true + ) + } + } } private func contactInfoHeader() -> some View { @@ -447,8 +459,9 @@ struct ChatInfoView: View { NavigationLink { ContactPreferencesView( contact: $contact, - featuresAllowed: contactUserPrefsToFeaturesAllowed(contact.mergedPreferences), - currentFeaturesAllowed: contactUserPrefsToFeaturesAllowed(contact.mergedPreferences) + featuresAllowed: $featuresAllowed, + currentFeaturesAllowed: $currentFeaturesAllowed, + savePreferences: savePreferences ) .navigationBarTitle("Contact preferences") .modifier(ThemedBackground(grouped: true)) @@ -617,6 +630,23 @@ struct ChatInfoView: View { } } } + + private func savePreferences() { + Task { + do { + let prefs = contactFeaturesAllowedToPrefs(featuresAllowed) + if let toContact = try await apiSetContactPrefs(contactId: contact.contactId, preferences: prefs) { + await MainActor.run { + contact = toContact + chatModel.updateContact(toContact) + currentFeaturesAllowed = featuresAllowed + } + } + } catch { + logger.error("ContactPreferencesView apiSetContactPrefs error: \(responseError(error))") + } + } + } } struct AudioCallButton: View { @@ -1173,6 +1203,8 @@ struct ChatInfoView_Previews: PreviewProvider { chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []), contact: Contact.sampleData, localAlias: "", + featuresAllowed: contactUserPrefsToFeaturesAllowed(Contact.sampleData.mergedPreferences), + currentFeaturesAllowed: contactUserPrefsToFeaturesAllowed(Contact.sampleData.mergedPreferences), onSearch: {} ) } diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index b436147a8e..df5fde9e7a 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -231,6 +231,8 @@ struct ChatView: View { chat: chat, contact: contact, localAlias: chat.chatInfo.localAlias, + featuresAllowed: contactUserPrefsToFeaturesAllowed(contact.mergedPreferences), + currentFeaturesAllowed: contactUserPrefsToFeaturesAllowed(contact.mergedPreferences), onSearch: { focusSearch() } ) } diff --git a/apps/ios/Shared/Views/Chat/ContactPreferencesView.swift b/apps/ios/Shared/Views/Chat/ContactPreferencesView.swift index b3fab958bc..e4489e46ee 100644 --- a/apps/ios/Shared/Views/Chat/ContactPreferencesView.swift +++ b/apps/ios/Shared/Views/Chat/ContactPreferencesView.swift @@ -14,9 +14,10 @@ struct ContactPreferencesView: View { @EnvironmentObject var chatModel: ChatModel @EnvironmentObject var theme: AppTheme @Binding var contact: Contact - @State var featuresAllowed: ContactFeaturesAllowed - @State var currentFeaturesAllowed: ContactFeaturesAllowed + @Binding var featuresAllowed: ContactFeaturesAllowed + @Binding var currentFeaturesAllowed: ContactFeaturesAllowed @State private var showSaveDialogue = false + let savePreferences: () -> Void var body: some View { let user: User = chatModel.currentUser! @@ -48,7 +49,10 @@ struct ContactPreferencesView: View { savePreferences() dismiss() } - Button("Exit without saving") { dismiss() } + Button("Exit without saving") { + featuresAllowed = currentFeaturesAllowed + dismiss() + } } } @@ -118,31 +122,15 @@ struct ContactPreferencesView: View { private func featureFooter(_ feature: ChatFeature, _ enabled: FeatureEnabled) -> some View { Text(feature.enabledDescription(enabled)) } - - private func savePreferences() { - Task { - do { - let prefs = contactFeaturesAllowedToPrefs(featuresAllowed) - if let toContact = try await apiSetContactPrefs(contactId: contact.contactId, preferences: prefs) { - await MainActor.run { - contact = toContact - chatModel.updateContact(toContact) - currentFeaturesAllowed = featuresAllowed - } - } - } catch { - logger.error("ContactPreferencesView apiSetContactPrefs error: \(responseError(error))") - } - } - } } struct ContactPreferencesView_Previews: PreviewProvider { static var previews: some View { ContactPreferencesView( contact: Binding.constant(Contact.sampleData), - featuresAllowed: ContactFeaturesAllowed.sampleData, - currentFeaturesAllowed: ContactFeaturesAllowed.sampleData + featuresAllowed: Binding.constant(ContactFeaturesAllowed.sampleData), + currentFeaturesAllowed: Binding.constant(ContactFeaturesAllowed.sampleData), + savePreferences: {} ) } } From df1a471c563a7ffa556a12bc1e4f95915e2038e4 Mon Sep 17 00:00:00 2001 From: Diogo Date: Fri, 6 Dec 2024 15:55:15 +0000 Subject: [PATCH 152/167] ios: remove all unsafe warnings in group preferences save (#5340) --- apps/ios/Shared/Views/Chat/ChatView.swift | 3 +- .../Chat/Group/AddGroupMembersView.swift | 10 +- .../Views/Chat/Group/GroupChatInfoView.swift | 93 ++++++++++++------- .../Chat/Group/GroupPreferencesView.swift | 33 ++----- .../Shared/Views/NewChat/AddGroupView.swift | 1 - 5 files changed, 76 insertions(+), 64 deletions(-) diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index df5fde9e7a..0c5a458930 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -253,8 +253,7 @@ struct ChatView: View { chat.created = Date.now } ), - onSearch: { focusSearch() }, - preferences: groupInfo.fullGroupPreferences + onSearch: { focusSearch() } ) } } else if case .local = cInfo { diff --git a/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift b/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift index 0d03b21ca0..bdef8d0a62 100644 --- a/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift +++ b/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift @@ -15,7 +15,7 @@ struct AddGroupMembersView: View { var groupInfo: GroupInfo var body: some View { - AddGroupMembersViewCommon(chat: chat, groupInfo: groupInfo, preferences: groupInfo.fullGroupPreferences, addedMembersCb: { _ in dismiss() }) + AddGroupMembersViewCommon(chat: chat, groupInfo: groupInfo, addedMembersCb: { _ in dismiss() }) } } @@ -24,7 +24,6 @@ struct AddGroupMembersViewCommon: View { @EnvironmentObject var theme: AppTheme var chat: Chat @State var groupInfo: GroupInfo - @State var preferences: FullGroupPreferences var creatingGroup: Bool = false var showFooterCounter: Bool = true var addedMembersCb: ((Set) -> Void) @@ -79,7 +78,12 @@ struct AddGroupMembersViewCommon: View { let count = selectedContacts.count Section { if creatingGroup { - groupPreferencesButton($groupInfo, $preferences, true) + GroupPreferencesButton( + groupInfo: $groupInfo, + preferences: groupInfo.fullGroupPreferences, + currentPreferences: groupInfo.fullGroupPreferences, + creatingGroup: true + ) } rolePicker() inviteMembersButton() diff --git a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift index 7dc57f9642..c4df91bb8b 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift @@ -19,7 +19,6 @@ struct GroupChatInfoView: View { @Binding var groupInfo: GroupInfo var onSearch: () -> Void @State private var alert: GroupChatInfoViewAlert? = nil - @State var preferences: FullGroupPreferences @State private var groupLink: String? @State private var groupLinkMemberRole: GroupMemberRole = .member @State private var groupLinkNavLinkActive: Bool = false @@ -88,7 +87,7 @@ struct GroupChatInfoView: View { if groupInfo.groupProfile.description != nil || (groupInfo.isOwner && groupInfo.businessChat == nil) { addOrEditWelcomeMessage() } - groupPreferencesButton($groupInfo, $preferences) + GroupPreferencesButton(groupInfo: $groupInfo, preferences: groupInfo.fullGroupPreferences, currentPreferences: groupInfo.fullGroupPreferences) if members.filter({ $0.wrapped.memberCurrent }).count <= SMALL_GROUPS_RCPS_MEM_LIMIT { sendReceiptsOption() } else { @@ -655,41 +654,72 @@ func deleteGroupAlertMessage(_ groupInfo: GroupInfo) -> Text { ) } -func groupPreferencesButton(_ groupInfo: Binding, _ preferences: Binding, _ creatingGroup: Bool = false) -> some View { - let label: LocalizedStringKey = groupInfo.wrappedValue.businessChat == nil ? "Group preferences" : "Chat preferences" - return NavigationLink { - GroupPreferencesView( - groupInfo: groupInfo, - preferences: preferences, - currentPreferences: groupInfo.fullGroupPreferences, - creatingGroup: creatingGroup - ) - .navigationBarTitle(label) - .modifier(ThemedBackground(grouped: true)) - .navigationBarTitleDisplayMode(.large) - .onDisappear { - let saveText = NSLocalizedString(creatingGroup ? "Save" : "Save and notify group members", comment: "alert button") - - if groupInfo.fullGroupPreferences.wrappedValue != preferences.wrappedValue { - showAlert( - title: NSLocalizedString("Save preferences?", comment: "alert title"), - buttonTitle: saveText, - buttonAction: { - savePreferences(groupInfo: groupInfo, preferences: preferences, currentPreferences: groupInfo.fullGroupPreferences) - }, - cancelButton: true +struct GroupPreferencesButton: View { + @Binding var groupInfo: GroupInfo + @State var preferences: FullGroupPreferences + @State var currentPreferences: FullGroupPreferences + var creatingGroup: Bool = false + + private var label: LocalizedStringKey { + groupInfo.businessChat == nil ? "Group preferences" : "Chat preferences" + } + + var body: some View { + NavigationLink { + GroupPreferencesView( + groupInfo: $groupInfo, + preferences: $preferences, + currentPreferences: currentPreferences, + creatingGroup: creatingGroup, + savePreferences: savePreferences + ) + .navigationBarTitle(label) + .modifier(ThemedBackground(grouped: true)) + .navigationBarTitleDisplayMode(.large) + .onDisappear { + let saveText = NSLocalizedString( + creatingGroup ? "Save" : "Save and notify group members", + comment: "alert button" ) + + if groupInfo.fullGroupPreferences != preferences { + showAlert( + title: NSLocalizedString("Save preferences?", comment: "alert title"), + buttonTitle: saveText, + buttonAction: { savePreferences() }, + cancelButton: true + ) + } + } + } label: { + if creatingGroup { + Text("Set group preferences") + } else { + Label(label, systemImage: "switch.2") } } - } label: { - if creatingGroup { - Text("Set group preferences") - } else { - Label(label, systemImage: "switch.2") + } + + private func savePreferences() { + Task { + do { + var gp = groupInfo.groupProfile + gp.groupPreferences = toGroupPreferences(preferences) + let gInfo = try await apiUpdateGroup(groupInfo.groupId, gp) + await MainActor.run { + groupInfo = gInfo + ChatModel.shared.updateGroup(gInfo) + currentPreferences = preferences + } + } catch { + logger.error("GroupPreferencesView apiUpdateGroup error: \(responseError(error))") + } } } + } + func cantInviteIncognitoAlert() -> Alert { Alert( title: Text("Can't invite contacts!"), @@ -709,8 +739,7 @@ struct GroupChatInfoView_Previews: PreviewProvider { GroupChatInfoView( chat: Chat(chatInfo: ChatInfo.sampleData.group, chatItems: []), groupInfo: Binding.constant(GroupInfo.sampleData), - onSearch: {}, - preferences: GroupInfo.sampleData.fullGroupPreferences + onSearch: {} ) } } diff --git a/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift b/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift index b27ff37d95..9ef53258aa 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift @@ -21,8 +21,9 @@ struct GroupPreferencesView: View { @EnvironmentObject var theme: AppTheme @Binding var groupInfo: GroupInfo @Binding var preferences: FullGroupPreferences - @Binding var currentPreferences: FullGroupPreferences + var currentPreferences: FullGroupPreferences let creatingGroup: Bool + let savePreferences: () -> Void @State private var showSaveDialogue = false var body: some View { @@ -41,7 +42,7 @@ struct GroupPreferencesView: View { if groupInfo.isOwner { Section { Button("Reset") { preferences = currentPreferences } - Button(saveText) { savePreferences(groupInfo: $groupInfo, preferences: $preferences, currentPreferences: $currentPreferences) } + Button(saveText) { savePreferences() } } .disabled(currentPreferences == preferences) } @@ -65,7 +66,7 @@ struct GroupPreferencesView: View { }) .confirmationDialog("Save preferences?", isPresented: $showSaveDialogue) { Button(saveText) { - savePreferences(groupInfo: $groupInfo, preferences: $preferences, currentPreferences: $currentPreferences) + savePreferences() dismiss() } Button("Exit without saving") { @@ -137,34 +138,14 @@ struct GroupPreferencesView: View { } } -func savePreferences( - groupInfo: Binding, - preferences: Binding, - currentPreferences: Binding -) { - Task { - do { - var gp = groupInfo.groupProfile.wrappedValue - gp.groupPreferences = toGroupPreferences(preferences.wrappedValue) - let gInfo = try await apiUpdateGroup(groupInfo.groupId.wrappedValue, gp) - await MainActor.run { - groupInfo.wrappedValue = gInfo - ChatModel.shared.updateGroup(gInfo) - currentPreferences.wrappedValue = preferences.wrappedValue - } - } catch { - logger.error("GroupPreferencesView apiUpdateGroup error: \(responseError(error))") - } - } -} - struct GroupPreferencesView_Previews: PreviewProvider { static var previews: some View { GroupPreferencesView( groupInfo: Binding.constant(GroupInfo.sampleData), preferences: Binding.constant(FullGroupPreferences.sampleData), - currentPreferences: Binding.constant(FullGroupPreferences.sampleData), - creatingGroup: false + currentPreferences: FullGroupPreferences.sampleData, + creatingGroup: false, + savePreferences: {} ) } } diff --git a/apps/ios/Shared/Views/NewChat/AddGroupView.swift b/apps/ios/Shared/Views/NewChat/AddGroupView.swift index 5207c3a472..0c7f6136ff 100644 --- a/apps/ios/Shared/Views/NewChat/AddGroupView.swift +++ b/apps/ios/Shared/Views/NewChat/AddGroupView.swift @@ -32,7 +32,6 @@ struct AddGroupView: View { AddGroupMembersViewCommon( chat: chat, groupInfo: groupInfo, - preferences: groupInfo.fullGroupPreferences, creatingGroup: true, showFooterCounter: false ) { _ in From 362581432cb9b7f236d9807b3efa1637a6ff0e4e Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Fri, 6 Dec 2024 20:01:55 +0400 Subject: [PATCH 153/167] ios: export localizations, add translations (#5339) * ios: export localizations, add translations * import --- .../bg.xcloc/Localized Contents/bg.xliff | 16 ++++++++-- .../cs.xcloc/Localized Contents/cs.xliff | 16 ++++++++-- .../de.xcloc/Localized Contents/de.xliff | 16 ++++++++-- .../en.xcloc/Localized Contents/en.xliff | 19 ++++++++++-- .../es.xcloc/Localized Contents/es.xliff | 16 ++++++++-- .../fi.xcloc/Localized Contents/fi.xliff | 16 ++++++++-- .../fr.xcloc/Localized Contents/fr.xliff | 16 ++++++++-- .../hu.xcloc/Localized Contents/hu.xliff | 16 ++++++++-- .../it.xcloc/Localized Contents/it.xliff | 16 ++++++++-- .../ja.xcloc/Localized Contents/ja.xliff | 16 ++++++++-- .../nl.xcloc/Localized Contents/nl.xliff | 16 ++++++++-- .../pl.xcloc/Localized Contents/pl.xliff | 16 ++++++++-- .../ru.xcloc/Localized Contents/ru.xliff | 21 +++++++++++-- .../th.xcloc/Localized Contents/th.xliff | 16 ++++++++-- .../tr.xcloc/Localized Contents/tr.xliff | 16 ++++++++-- .../uk.xcloc/Localized Contents/uk.xliff | 16 ++++++++-- .../Localized Contents/zh-Hans.xliff | 16 ++++++++-- apps/ios/bg.lproj/Localizable.strings | 4 +-- apps/ios/cs.lproj/Localizable.strings | 4 +-- apps/ios/de.lproj/Localizable.strings | 22 ++++++------- apps/ios/es.lproj/Localizable.strings | 22 ++++++------- apps/ios/fi.lproj/Localizable.strings | 4 +-- apps/ios/fr.lproj/Localizable.strings | 4 +-- apps/ios/hu.lproj/Localizable.strings | 22 ++++++------- apps/ios/it.lproj/Localizable.strings | 22 ++++++------- apps/ios/ja.lproj/Localizable.strings | 4 +-- apps/ios/nl.lproj/Localizable.strings | 22 ++++++------- apps/ios/pl.lproj/Localizable.strings | 4 +-- apps/ios/ru.lproj/Localizable.strings | 31 ++++++++++++------- apps/ios/th.lproj/Localizable.strings | 4 +-- apps/ios/tr.lproj/Localizable.strings | 4 +-- apps/ios/uk.lproj/Localizable.strings | 25 +++++++-------- apps/ios/zh-Hans.lproj/Localizable.strings | 4 +-- 33 files changed, 349 insertions(+), 133 deletions(-) diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff index 901bee26dd..4af0007eea 100644 --- a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff @@ -566,6 +566,10 @@ За SimpleX Chat No comment provided by engineer. + + About operators + No comment provided by engineer. + Accent No comment provided by engineer. @@ -5796,7 +5800,7 @@ Enable in *Network & servers* settings. Save preferences? Запази настройките? - No comment provided by engineer. + alert title Save profile password @@ -8118,6 +8122,10 @@ Repeat connection request? обаждането прието call status + + accepted invitation + chat list item title + admin админ @@ -8304,7 +8312,7 @@ Repeat connection request? connecting… свързване… - chat list item title + No comment provided by engineer. connection established @@ -8788,6 +8796,10 @@ Repeat connection request? ви острани rcv group event chat item + + requested to connect + chat list item title + saved запазено diff --git a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff index fd8958f5f8..7d92f62f12 100644 --- a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff @@ -548,6 +548,10 @@ O SimpleX chat No comment provided by engineer. + + About operators + No comment provided by engineer. + Accent No comment provided by engineer. @@ -5606,7 +5610,7 @@ Enable in *Network & servers* settings. Save preferences? Uložit předvolby? - No comment provided by engineer. + alert title Save profile password @@ -7849,6 +7853,10 @@ Repeat connection request? přijatý hovor call status + + accepted invitation + chat list item title + admin správce @@ -8028,7 +8036,7 @@ Repeat connection request? connecting… připojení… - chat list item title + No comment provided by engineer. connection established @@ -8503,6 +8511,10 @@ Repeat connection request? odstranil vás rcv group event chat item + + requested to connect + chat list item title + saved No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff index 77ddbfb69b..516baf49b7 100644 --- a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff +++ b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff @@ -577,6 +577,10 @@ Über SimpleX Chat No comment provided by engineer. + + About operators + No comment provided by engineer. + Accent Akzent @@ -6065,7 +6069,7 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Save preferences? Präferenzen speichern? - No comment provided by engineer. + alert title Save profile password @@ -8517,6 +8521,10 @@ Verbindungsanfrage wiederholen? Anruf angenommen call status + + accepted invitation + chat list item title + admin Admin @@ -8705,7 +8713,7 @@ Verbindungsanfrage wiederholen? connecting… Verbinde… - chat list item title + No comment provided by engineer. connection established @@ -9199,6 +9207,10 @@ Verbindungsanfrage wiederholen? hat Sie aus der Gruppe entfernt rcv group event chat item + + requested to connect + chat list item title + saved abgespeichert diff --git a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff index a70a22581b..699091e2d8 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff +++ b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff @@ -577,6 +577,11 @@ About SimpleX Chat No comment provided by engineer. + + About operators + About operators + No comment provided by engineer. + Accent Accent @@ -6086,7 +6091,7 @@ Enable in *Network & servers* settings. Save preferences? Save preferences? - No comment provided by engineer. + alert title Save profile password @@ -8541,6 +8546,11 @@ Repeat connection request? accepted call call status + + accepted invitation + accepted invitation + chat list item title + admin admin @@ -8729,7 +8739,7 @@ Repeat connection request? connecting… connecting… - chat list item title + No comment provided by engineer. connection established @@ -9223,6 +9233,11 @@ Repeat connection request? removed you rcv group event chat item + + requested to connect + requested to connect + chat list item title + saved saved diff --git a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff index 32af6e9cfd..ddb5d6b1a1 100644 --- a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff +++ b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff @@ -577,6 +577,10 @@ Sobre SimpleX Chat No comment provided by engineer. + + About operators + No comment provided by engineer. + Accent Color @@ -6065,7 +6069,7 @@ Actívalo en ajustes de *Servidores y Redes*. Save preferences? ¿Guardar preferencias? - No comment provided by engineer. + alert title Save profile password @@ -8517,6 +8521,10 @@ Repeat connection request? llamada aceptada call status + + accepted invitation + chat list item title + admin administrador @@ -8705,7 +8713,7 @@ Repeat connection request? connecting… conectando… - chat list item title + No comment provided by engineer. connection established @@ -9199,6 +9207,10 @@ Repeat connection request? te ha expulsado rcv group event chat item + + requested to connect + chat list item title + saved guardado diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff index bbd8a338bc..12c8d0e3ca 100644 --- a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff @@ -543,6 +543,10 @@ Tietoja SimpleX Chatistä No comment provided by engineer. + + About operators + No comment provided by engineer. + Accent No comment provided by engineer. @@ -5594,7 +5598,7 @@ Enable in *Network & servers* settings. Save preferences? Tallenna asetukset? - No comment provided by engineer. + alert title Save profile password @@ -7834,6 +7838,10 @@ Repeat connection request? hyväksytty puhelu call status + + accepted invitation + chat list item title + admin ylläpitäjä @@ -8012,7 +8020,7 @@ Repeat connection request? connecting… yhdistää… - chat list item title + No comment provided by engineer. connection established @@ -8488,6 +8496,10 @@ Repeat connection request? poisti sinut rcv group event chat item + + requested to connect + chat list item title + saved No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff index 9c44fe91e4..e82f01e33b 100644 --- a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff @@ -572,6 +572,10 @@ À propos de SimpleX Chat No comment provided by engineer. + + About operators + No comment provided by engineer. + Accent Principale @@ -6003,7 +6007,7 @@ Activez-le dans les paramètres *Réseau et serveurs*. Save preferences? Enregistrer les préférences ? - No comment provided by engineer. + alert title Save profile password @@ -8421,6 +8425,10 @@ Répéter la demande de connexion ? appel accepté call status + + accepted invitation + chat list item title + admin admin @@ -8609,7 +8617,7 @@ Répéter la demande de connexion ? connecting… connexion… - chat list item title + No comment provided by engineer. connection established @@ -9102,6 +9110,10 @@ Répéter la demande de connexion ? vous a retiré rcv group event chat item + + requested to connect + chat list item title + saved enregistré diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff index ffc162633f..5f05efaa4d 100644 --- a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff @@ -577,6 +577,10 @@ A SimpleX Chatről No comment provided by engineer. + + About operators + No comment provided by engineer. + Accent Kiemelés @@ -6086,7 +6090,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Save preferences? Beállítások mentése? - No comment provided by engineer. + alert title Save profile password @@ -8541,6 +8545,10 @@ Kapcsolatkérés megismétlése? elfogadott hívás call status + + accepted invitation + chat list item title + admin adminisztrátor @@ -8729,7 +8737,7 @@ Kapcsolatkérés megismétlése? connecting… kapcsolódás… - chat list item title + No comment provided by engineer. connection established @@ -9223,6 +9231,10 @@ Kapcsolatkérés megismétlése? eltávolította Önt rcv group event chat item + + requested to connect + chat list item title + saved mentett diff --git a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff index 0b578e6a25..ebfba7e415 100644 --- a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff +++ b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff @@ -577,6 +577,10 @@ Riguardo SimpleX Chat No comment provided by engineer. + + About operators + No comment provided by engineer. + Accent Principale @@ -6086,7 +6090,7 @@ Attivalo nelle impostazioni *Rete e server*. Save preferences? Salvare le preferenze? - No comment provided by engineer. + alert title Save profile password @@ -8541,6 +8545,10 @@ Ripetere la richiesta di connessione? chiamata accettata call status + + accepted invitation + chat list item title + admin amministratore @@ -8729,7 +8737,7 @@ Ripetere la richiesta di connessione? connecting… in connessione… - chat list item title + No comment provided by engineer. connection established @@ -9223,6 +9231,10 @@ Ripetere la richiesta di connessione? ti ha rimosso/a rcv group event chat item + + requested to connect + chat list item title + saved salvato diff --git a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff index 87058e0bdc..ba35db0c03 100644 --- a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff +++ b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff @@ -560,6 +560,10 @@ SimpleX Chat について No comment provided by engineer. + + About operators + No comment provided by engineer. + Accent No comment provided by engineer. @@ -5643,7 +5647,7 @@ Enable in *Network & servers* settings. Save preferences? この設定でよろしいですか? - No comment provided by engineer. + alert title Save profile password @@ -7876,6 +7880,10 @@ Repeat connection request? 受けた通話 call status + + accepted invitation + chat list item title + admin 管理者 @@ -8054,7 +8062,7 @@ Repeat connection request? connecting… 接続待ち… - chat list item title + No comment provided by engineer. connection established @@ -8530,6 +8538,10 @@ Repeat connection request? あなたを除名しました rcv group event chat item + + requested to connect + chat list item title + saved No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff index ce3adc3c41..5631d9bd7d 100644 --- a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff @@ -577,6 +577,10 @@ Over SimpleX Chat No comment provided by engineer. + + About operators + No comment provided by engineer. + Accent Accent @@ -6086,7 +6090,7 @@ Schakel dit in in *Netwerk en servers*-instellingen. Save preferences? Voorkeuren opslaan? - No comment provided by engineer. + alert title Save profile password @@ -8541,6 +8545,10 @@ Verbindingsverzoek herhalen? geaccepteerde oproep call status + + accepted invitation + chat list item title + admin Beheerder @@ -8729,7 +8737,7 @@ Verbindingsverzoek herhalen? connecting… Verbinden… - chat list item title + No comment provided by engineer. connection established @@ -9223,6 +9231,10 @@ Verbindingsverzoek herhalen? heeft je verwijderd rcv group event chat item + + requested to connect + chat list item title + saved opgeslagen diff --git a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff index e99439f3ae..dbc95b6527 100644 --- a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff @@ -572,6 +572,10 @@ O SimpleX Chat No comment provided by engineer. + + About operators + No comment provided by engineer. + Accent Akcent @@ -5993,7 +5997,7 @@ Włącz w ustawianiach *Sieć i serwery* . Save preferences? Zapisać preferencje? - No comment provided by engineer. + alert title Save profile password @@ -8408,6 +8412,10 @@ Powtórzyć prośbę połączenia? zaakceptowane połączenie call status + + accepted invitation + chat list item title + admin administrator @@ -8596,7 +8604,7 @@ Powtórzyć prośbę połączenia? connecting… łączenie… - chat list item title + No comment provided by engineer. connection established @@ -9089,6 +9097,10 @@ Powtórzyć prośbę połączenia? usunął cię rcv group event chat item + + requested to connect + chat list item title + saved zapisane diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff index 85b4aaa4ae..95bd74e484 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff @@ -577,6 +577,11 @@ Информация о SimpleX Chat No comment provided by engineer. + + About operators + Об операторах + No comment provided by engineer. + Accent Акцент @@ -6085,7 +6090,7 @@ Enable in *Network & servers* settings. Save preferences? Сохранить предпочтения? - No comment provided by engineer. + alert title Save profile password @@ -8145,7 +8150,7 @@ Repeat join request? You can configure servers via settings. - Вы можете сконфигурировать серверы через настройки. + Вы можете настроить серверы позже. No comment provided by engineer. @@ -8540,6 +8545,11 @@ Repeat connection request? принятый звонок call status + + accepted invitation + принятое приглашение + chat list item title + admin админ @@ -8728,7 +8738,7 @@ Repeat connection request? connecting… соединяется… - chat list item title + No comment provided by engineer. connection established @@ -9222,6 +9232,11 @@ Repeat connection request? удалил(а) Вас из группы rcv group event chat item + + requested to connect + запрошено соединение + chat list item title + saved сохранено diff --git a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff index 438fae5c47..14827be1b5 100644 --- a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff +++ b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff @@ -536,6 +536,10 @@ เกี่ยวกับ SimpleX Chat No comment provided by engineer. + + About operators + No comment provided by engineer. + Accent No comment provided by engineer. @@ -5571,7 +5575,7 @@ Enable in *Network & servers* settings. Save preferences? บันทึกการตั้งค่า? - No comment provided by engineer. + alert title Save profile password @@ -7802,6 +7806,10 @@ Repeat connection request? รับสายแล้ว call status + + accepted invitation + chat list item title + admin ผู้ดูแลระบบ @@ -7980,7 +7988,7 @@ Repeat connection request? connecting… กำลังเชื่อมต่อ… - chat list item title + No comment provided by engineer. connection established @@ -8454,6 +8462,10 @@ Repeat connection request? ลบคุณออกแล้ว rcv group event chat item + + requested to connect + chat list item title + saved No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff index 5919cc4d49..d12ef93f69 100644 --- a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff @@ -572,6 +572,10 @@ SimpleX Chat hakkında No comment provided by engineer. + + About operators + No comment provided by engineer. + Accent Ana renk @@ -6003,7 +6007,7 @@ Enable in *Network & servers* settings. Save preferences? Tercihler kaydedilsin mi? - No comment provided by engineer. + alert title Save profile password @@ -8421,6 +8425,10 @@ Bağlantı isteği tekrarlansın mı? kabul edilen arama call status + + accepted invitation + chat list item title + admin yönetici @@ -8609,7 +8617,7 @@ Bağlantı isteği tekrarlansın mı? connecting… bağlanılıyor… - chat list item title + No comment provided by engineer. connection established @@ -9102,6 +9110,10 @@ Bağlantı isteği tekrarlansın mı? sen kaldırıldın rcv group event chat item + + requested to connect + chat list item title + saved kaydedildi diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff index 72e9896872..e228fd01e6 100644 --- a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff @@ -577,6 +577,10 @@ Про чат SimpleX No comment provided by engineer. + + About operators + No comment provided by engineer. + Accent Акцент @@ -6065,7 +6069,7 @@ Enable in *Network & servers* settings. Save preferences? Зберегти настройки? - No comment provided by engineer. + alert title Save profile password @@ -8517,6 +8521,10 @@ Repeat connection request? прийнято виклик call status + + accepted invitation + chat list item title + admin адмін @@ -8705,7 +8713,7 @@ Repeat connection request? connecting… з'єднання… - chat list item title + No comment provided by engineer. connection established @@ -9199,6 +9207,10 @@ Repeat connection request? прибрали вас rcv group event chat item + + requested to connect + chat list item title + saved збережено diff --git a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff index ede559c968..0b1b568385 100644 --- a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff +++ b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff @@ -566,6 +566,10 @@ 关于SimpleX Chat No comment provided by engineer. + + About operators + No comment provided by engineer. + Accent 强调 @@ -5956,7 +5960,7 @@ Enable in *Network & servers* settings. Save preferences? 保存偏好设置? - No comment provided by engineer. + alert title Save profile password @@ -8354,6 +8358,10 @@ Repeat connection request? 已接受通话 call status + + accepted invitation + chat list item title + admin 管理员 @@ -8542,7 +8550,7 @@ Repeat connection request? connecting… 连接中…… - chat list item title + No comment provided by engineer. connection established @@ -9035,6 +9043,10 @@ Repeat connection request? 已将您移除 rcv group event chat item + + requested to connect + chat list item title + saved 已保存 diff --git a/apps/ios/bg.lproj/Localizable.strings b/apps/ios/bg.lproj/Localizable.strings index 41f6730fdc..91606d8569 100644 --- a/apps/ios/bg.lproj/Localizable.strings +++ b/apps/ios/bg.lproj/Localizable.strings @@ -918,7 +918,7 @@ /* No comment provided by engineer. */ "Connecting to desktop" = "Свързване с настолно устройство"; -/* chat list item title */ +/* No comment provided by engineer. */ "connecting…" = "свързване…"; /* No comment provided by engineer. */ @@ -3157,7 +3157,7 @@ /* No comment provided by engineer. */ "Save passphrase in Keychain" = "Запази паролата в Keychain"; -/* No comment provided by engineer. */ +/* alert title */ "Save preferences?" = "Запази настройките?"; /* No comment provided by engineer. */ diff --git a/apps/ios/cs.lproj/Localizable.strings b/apps/ios/cs.lproj/Localizable.strings index bbe754aa47..96b149a8d5 100644 --- a/apps/ios/cs.lproj/Localizable.strings +++ b/apps/ios/cs.lproj/Localizable.strings @@ -729,7 +729,7 @@ /* No comment provided by engineer. */ "Connecting server… (error: %@)" = "Připojování k serveru... (chyba: %@)"; -/* chat list item title */ +/* No comment provided by engineer. */ "connecting…" = "připojení…"; /* No comment provided by engineer. */ @@ -2556,7 +2556,7 @@ /* No comment provided by engineer. */ "Save passphrase in Keychain" = "Uložit přístupovou frázi do Klíčenky"; -/* No comment provided by engineer. */ +/* alert title */ "Save preferences?" = "Uložit předvolby?"; /* No comment provided by engineer. */ diff --git a/apps/ios/de.lproj/Localizable.strings b/apps/ios/de.lproj/Localizable.strings index 28cc658d30..25f9cf32c1 100644 --- a/apps/ios/de.lproj/Localizable.strings +++ b/apps/ios/de.lproj/Localizable.strings @@ -863,6 +863,9 @@ /* No comment provided by engineer. */ "Change" = "Ändern"; +/* authentication reason */ +"Change chat profiles" = "Chat-Profile wechseln"; + /* No comment provided by engineer. */ "Change database passphrase?" = "Datenbank-Passwort ändern?"; @@ -891,9 +894,6 @@ set passcode view */ "Change self-destruct passcode" = "Selbstzerstörungs-Zugangscode ändern"; -/* authentication reason */ -"Change chat profiles" = "Chat-Profile wechseln"; - /* chat item text */ "changed address for you" = "Wechselte die Empfängeradresse von Ihnen"; @@ -1173,7 +1173,7 @@ /* No comment provided by engineer. */ "Connecting to desktop" = "Mit dem Desktop verbinden"; -/* chat list item title */ +/* No comment provided by engineer. */ "connecting…" = "Verbinde…"; /* No comment provided by engineer. */ @@ -3945,12 +3945,6 @@ /* No comment provided by engineer. */ "Safer groups" = "Sicherere Gruppen"; -/* No comment provided by engineer. */ -"The same conditions will apply to operator **%@**." = "Dieselben Nutzungsbedingungen gelten auch für den Betreiber **%@**."; - -/* No comment provided by engineer. */ -"The same conditions will apply to operator(s): **%@**." = "Dieselben Nutzungsbedingungen gelten auch für den/die Betreiber: **%@**."; - /* alert button chat item action */ "Save" = "Speichern"; @@ -3979,7 +3973,7 @@ /* No comment provided by engineer. */ "Save passphrase in Keychain" = "Passwort im Schlüsselbund speichern"; -/* No comment provided by engineer. */ +/* alert title */ "Save preferences?" = "Präferenzen speichern?"; /* No comment provided by engineer. */ @@ -4691,6 +4685,12 @@ /* No comment provided by engineer. */ "The profile is only shared with your contacts." = "Das Profil wird nur mit Ihren Kontakten geteilt."; +/* No comment provided by engineer. */ +"The same conditions will apply to operator **%@**." = "Dieselben Nutzungsbedingungen gelten auch für den Betreiber **%@**."; + +/* No comment provided by engineer. */ +"The same conditions will apply to operator(s): **%@**." = "Dieselben Nutzungsbedingungen gelten auch für den/die Betreiber: **%@**."; + /* No comment provided by engineer. */ "The second preset operator in the app!" = "Der zweite voreingestellte Netzwerk-Betreiber in der App!"; diff --git a/apps/ios/es.lproj/Localizable.strings b/apps/ios/es.lproj/Localizable.strings index 1ccb679069..a1665cd716 100644 --- a/apps/ios/es.lproj/Localizable.strings +++ b/apps/ios/es.lproj/Localizable.strings @@ -863,6 +863,9 @@ /* No comment provided by engineer. */ "Change" = "Cambiar"; +/* authentication reason */ +"Change chat profiles" = "Cambiar perfil de usuario"; + /* No comment provided by engineer. */ "Change database passphrase?" = "¿Cambiar contraseña de la base de datos?"; @@ -891,9 +894,6 @@ set passcode view */ "Change self-destruct passcode" = "Cambiar código autodestrucción"; -/* authentication reason */ -"Change chat profiles" = "Cambiar perfil de usuario"; - /* chat item text */ "changed address for you" = "ha cambiado tu servidor de envío"; @@ -1173,7 +1173,7 @@ /* No comment provided by engineer. */ "Connecting to desktop" = "Conectando con ordenador"; -/* chat list item title */ +/* No comment provided by engineer. */ "connecting…" = "conectando…"; /* No comment provided by engineer. */ @@ -3945,12 +3945,6 @@ /* No comment provided by engineer. */ "Safer groups" = "Grupos más seguros"; -/* No comment provided by engineer. */ -"The same conditions will apply to operator **%@**." = "Las mismas condiciones se aplicarán al operador **%@**."; - -/* No comment provided by engineer. */ -"The same conditions will apply to operator(s): **%@**." = "Las mismas condiciones se aplicarán a el/los operador(es) **%@**."; - /* alert button chat item action */ "Save" = "Guardar"; @@ -3979,7 +3973,7 @@ /* No comment provided by engineer. */ "Save passphrase in Keychain" = "Guardar la contraseña en Keychain"; -/* No comment provided by engineer. */ +/* alert title */ "Save preferences?" = "¿Guardar preferencias?"; /* No comment provided by engineer. */ @@ -4691,6 +4685,12 @@ /* No comment provided by engineer. */ "The profile is only shared with your contacts." = "El perfil sólo se comparte con tus contactos."; +/* No comment provided by engineer. */ +"The same conditions will apply to operator **%@**." = "Las mismas condiciones se aplicarán al operador **%@**."; + +/* No comment provided by engineer. */ +"The same conditions will apply to operator(s): **%@**." = "Las mismas condiciones se aplicarán a el/los operador(es) **%@**."; + /* No comment provided by engineer. */ "The second preset operator in the app!" = "El segundo operador predefinido!"; diff --git a/apps/ios/fi.lproj/Localizable.strings b/apps/ios/fi.lproj/Localizable.strings index 486c0e7650..e4b56e76a4 100644 --- a/apps/ios/fi.lproj/Localizable.strings +++ b/apps/ios/fi.lproj/Localizable.strings @@ -711,7 +711,7 @@ /* No comment provided by engineer. */ "Connecting server… (error: %@)" = "Yhteyden muodostaminen palvelimeen... (virhe: %@)"; -/* chat list item title */ +/* No comment provided by engineer. */ "connecting…" = "yhdistää…"; /* No comment provided by engineer. */ @@ -2526,7 +2526,7 @@ /* No comment provided by engineer. */ "Save passphrase in Keychain" = "Tallenna tunnuslause Avainnippuun"; -/* No comment provided by engineer. */ +/* alert title */ "Save preferences?" = "Tallenna asetukset?"; /* No comment provided by engineer. */ diff --git a/apps/ios/fr.lproj/Localizable.strings b/apps/ios/fr.lproj/Localizable.strings index 1a9e289404..e50d2c0967 100644 --- a/apps/ios/fr.lproj/Localizable.strings +++ b/apps/ios/fr.lproj/Localizable.strings @@ -1101,7 +1101,7 @@ /* No comment provided by engineer. */ "Connecting to desktop" = "Connexion au bureau"; -/* chat list item title */ +/* No comment provided by engineer. */ "connecting…" = "connexion…"; /* No comment provided by engineer. */ @@ -3784,7 +3784,7 @@ /* No comment provided by engineer. */ "Save passphrase in Keychain" = "Enregistrer la phrase secrète dans la Keychain"; -/* No comment provided by engineer. */ +/* alert title */ "Save preferences?" = "Enregistrer les préférences ?"; /* No comment provided by engineer. */ diff --git a/apps/ios/hu.lproj/Localizable.strings b/apps/ios/hu.lproj/Localizable.strings index 4893d5a13f..8c0da0ed57 100644 --- a/apps/ios/hu.lproj/Localizable.strings +++ b/apps/ios/hu.lproj/Localizable.strings @@ -878,6 +878,9 @@ /* No comment provided by engineer. */ "Change" = "Változtatás"; +/* authentication reason */ +"Change chat profiles" = "Felhasználói profilok megváltoztatása"; + /* No comment provided by engineer. */ "Change database passphrase?" = "Adatbázis-jelmondat megváltoztatása?"; @@ -906,9 +909,6 @@ set passcode view */ "Change self-destruct passcode" = "Önmegsemmisító jelkód megváltoztatása"; -/* authentication reason */ -"Change chat profiles" = "Felhasználói profilok megváltoztatása"; - /* chat item text */ "changed address for you" = "cím megváltoztatva"; @@ -1203,7 +1203,7 @@ /* No comment provided by engineer. */ "Connecting to desktop" = "Kapcsolódás a számítógéphez"; -/* chat list item title */ +/* No comment provided by engineer. */ "connecting…" = "kapcsolódás…"; /* No comment provided by engineer. */ @@ -4008,12 +4008,6 @@ /* No comment provided by engineer. */ "Safer groups" = "Biztonságosabb csoportok"; -/* No comment provided by engineer. */ -"The same conditions will apply to operator **%@**." = "Ugyanezek a feltételek vonatkoznak a következő üzemeltetőre is: **%@**."; - -/* No comment provided by engineer. */ -"The same conditions will apply to operator(s): **%@**." = "Ugyanezek a feltételek lesznek elfogadva a következő üzemeltető(k)re is: **%@**."; - /* alert button chat item action */ "Save" = "Mentés"; @@ -4042,7 +4036,7 @@ /* No comment provided by engineer. */ "Save passphrase in Keychain" = "Jelmondat mentése a kulcstartóba"; -/* No comment provided by engineer. */ +/* alert title */ "Save preferences?" = "Beállítások mentése?"; /* No comment provided by engineer. */ @@ -4757,6 +4751,12 @@ /* No comment provided by engineer. */ "The profile is only shared with your contacts." = "A profilja csak az ismerőseivel kerül megosztásra."; +/* No comment provided by engineer. */ +"The same conditions will apply to operator **%@**." = "Ugyanezek a feltételek vonatkoznak a következő üzemeltetőre is: **%@**."; + +/* No comment provided by engineer. */ +"The same conditions will apply to operator(s): **%@**." = "Ugyanezek a feltételek lesznek elfogadva a következő üzemeltető(k)re is: **%@**."; + /* No comment provided by engineer. */ "The second preset operator in the app!" = "A második előre beállított üzemeltető az alkalmazásban!"; diff --git a/apps/ios/it.lproj/Localizable.strings b/apps/ios/it.lproj/Localizable.strings index 2fe1216f35..1242b488ac 100644 --- a/apps/ios/it.lproj/Localizable.strings +++ b/apps/ios/it.lproj/Localizable.strings @@ -878,6 +878,9 @@ /* No comment provided by engineer. */ "Change" = "Cambia"; +/* authentication reason */ +"Change chat profiles" = "Modifica profili utente"; + /* No comment provided by engineer. */ "Change database passphrase?" = "Cambiare password del database?"; @@ -906,9 +909,6 @@ set passcode view */ "Change self-destruct passcode" = "Cambia codice di autodistruzione"; -/* authentication reason */ -"Change chat profiles" = "Modifica profili utente"; - /* chat item text */ "changed address for you" = "indirizzo cambiato per te"; @@ -1203,7 +1203,7 @@ /* No comment provided by engineer. */ "Connecting to desktop" = "Connessione al desktop"; -/* chat list item title */ +/* No comment provided by engineer. */ "connecting…" = "in connessione…"; /* No comment provided by engineer. */ @@ -4008,12 +4008,6 @@ /* No comment provided by engineer. */ "Safer groups" = "Gruppi più sicuri"; -/* No comment provided by engineer. */ -"The same conditions will apply to operator **%@**." = "Le stesse condizioni si applicheranno all'operatore **%@**."; - -/* No comment provided by engineer. */ -"The same conditions will apply to operator(s): **%@**." = "Le stesse condizioni si applicheranno agli operatori **%@**."; - /* alert button chat item action */ "Save" = "Salva"; @@ -4042,7 +4036,7 @@ /* No comment provided by engineer. */ "Save passphrase in Keychain" = "Salva password nel portachiavi"; -/* No comment provided by engineer. */ +/* alert title */ "Save preferences?" = "Salvare le preferenze?"; /* No comment provided by engineer. */ @@ -4757,6 +4751,12 @@ /* No comment provided by engineer. */ "The profile is only shared with your contacts." = "Il profilo è condiviso solo con i tuoi contatti."; +/* No comment provided by engineer. */ +"The same conditions will apply to operator **%@**." = "Le stesse condizioni si applicheranno all'operatore **%@**."; + +/* No comment provided by engineer. */ +"The same conditions will apply to operator(s): **%@**." = "Le stesse condizioni si applicheranno agli operatori **%@**."; + /* No comment provided by engineer. */ "The second preset operator in the app!" = "Il secondo operatore preimpostato nell'app!"; diff --git a/apps/ios/ja.lproj/Localizable.strings b/apps/ios/ja.lproj/Localizable.strings index 06fa3f70b3..3aa64f9b55 100644 --- a/apps/ios/ja.lproj/Localizable.strings +++ b/apps/ios/ja.lproj/Localizable.strings @@ -828,7 +828,7 @@ /* No comment provided by engineer. */ "Connecting to desktop" = "デスクトップに接続中"; -/* chat list item title */ +/* No comment provided by engineer. */ "connecting…" = "接続待ち…"; /* No comment provided by engineer. */ @@ -2673,7 +2673,7 @@ /* No comment provided by engineer. */ "Save passphrase in Keychain" = "パスフレーズをキーチェーンに保存"; -/* No comment provided by engineer. */ +/* alert title */ "Save preferences?" = "この設定でよろしいですか?"; /* No comment provided by engineer. */ diff --git a/apps/ios/nl.lproj/Localizable.strings b/apps/ios/nl.lproj/Localizable.strings index edb123334d..4ead9a726d 100644 --- a/apps/ios/nl.lproj/Localizable.strings +++ b/apps/ios/nl.lproj/Localizable.strings @@ -878,6 +878,9 @@ /* No comment provided by engineer. */ "Change" = "Veranderen"; +/* authentication reason */ +"Change chat profiles" = "Gebruikersprofielen wijzigen"; + /* No comment provided by engineer. */ "Change database passphrase?" = "Wachtwoord database wijzigen?"; @@ -906,9 +909,6 @@ set passcode view */ "Change self-destruct passcode" = "Zelfvernietigings code wijzigen"; -/* authentication reason */ -"Change chat profiles" = "Gebruikersprofielen wijzigen"; - /* chat item text */ "changed address for you" = "adres voor u gewijzigd"; @@ -1203,7 +1203,7 @@ /* No comment provided by engineer. */ "Connecting to desktop" = "Verbinding maken met desktop"; -/* chat list item title */ +/* No comment provided by engineer. */ "connecting…" = "Verbinden…"; /* No comment provided by engineer. */ @@ -4008,12 +4008,6 @@ /* No comment provided by engineer. */ "Safer groups" = "Veiligere groepen"; -/* No comment provided by engineer. */ -"The same conditions will apply to operator **%@**." = "Dezelfde voorwaarden gelden voor operator **%@**."; - -/* No comment provided by engineer. */ -"The same conditions will apply to operator(s): **%@**." = "Dezelfde voorwaarden gelden voor operator(s): **%@**."; - /* alert button chat item action */ "Save" = "Opslaan"; @@ -4042,7 +4036,7 @@ /* No comment provided by engineer. */ "Save passphrase in Keychain" = "Sla het wachtwoord op in de Keychain"; -/* No comment provided by engineer. */ +/* alert title */ "Save preferences?" = "Voorkeuren opslaan?"; /* No comment provided by engineer. */ @@ -4757,6 +4751,12 @@ /* No comment provided by engineer. */ "The profile is only shared with your contacts." = "Het profiel wordt alleen gedeeld met uw contacten."; +/* No comment provided by engineer. */ +"The same conditions will apply to operator **%@**." = "Dezelfde voorwaarden gelden voor operator **%@**."; + +/* No comment provided by engineer. */ +"The same conditions will apply to operator(s): **%@**." = "Dezelfde voorwaarden gelden voor operator(s): **%@**."; + /* No comment provided by engineer. */ "The second preset operator in the app!" = "De tweede vooraf ingestelde operator in de app!"; diff --git a/apps/ios/pl.lproj/Localizable.strings b/apps/ios/pl.lproj/Localizable.strings index 782e1c18f4..e0bcedc965 100644 --- a/apps/ios/pl.lproj/Localizable.strings +++ b/apps/ios/pl.lproj/Localizable.strings @@ -1086,7 +1086,7 @@ /* No comment provided by engineer. */ "Connecting to desktop" = "Łączenie z komputerem"; -/* chat list item title */ +/* No comment provided by engineer. */ "connecting…" = "łączenie…"; /* No comment provided by engineer. */ @@ -3757,7 +3757,7 @@ /* No comment provided by engineer. */ "Save passphrase in Keychain" = "Zapisz hasło w pęku kluczy"; -/* No comment provided by engineer. */ +/* alert title */ "Save preferences?" = "Zapisać preferencje?"; /* No comment provided by engineer. */ diff --git a/apps/ios/ru.lproj/Localizable.strings b/apps/ios/ru.lproj/Localizable.strings index 57c1b91769..09ee9a2e5a 100644 --- a/apps/ios/ru.lproj/Localizable.strings +++ b/apps/ios/ru.lproj/Localizable.strings @@ -343,6 +343,9 @@ /* No comment provided by engineer. */ "Abort changing address?" = "Прекратить изменение адреса?"; +/* No comment provided by engineer. */ +"About operators" = "Об операторах"; + /* No comment provided by engineer. */ "About SimpleX Chat" = "Информация о SimpleX Chat"; @@ -376,6 +379,9 @@ /* No comment provided by engineer. */ "Accepted conditions" = "Принятые условия"; +/* chat list item title */ +"accepted invitation" = "принятое приглашение"; + /* No comment provided by engineer. */ "Acknowledged" = "Подтверждено"; @@ -878,6 +884,9 @@ /* No comment provided by engineer. */ "Change" = "Поменять"; +/* authentication reason */ +"Change chat profiles" = "Поменять профили"; + /* No comment provided by engineer. */ "Change database passphrase?" = "Поменять пароль базы данных?"; @@ -906,9 +915,6 @@ set passcode view */ "Change self-destruct passcode" = "Изменить код самоуничтожения"; -/* authentication reason */ -"Change chat profiles" = "Поменять профили"; - /* chat item text */ "changed address for you" = "поменял(а) адрес для Вас"; @@ -1203,7 +1209,7 @@ /* No comment provided by engineer. */ "Connecting to desktop" = "Подключение к компьютеру"; -/* chat list item title */ +/* No comment provided by engineer. */ "connecting…" = "соединяется…"; /* No comment provided by engineer. */ @@ -3930,6 +3936,9 @@ /* chat item action */ "Reply" = "Ответить"; +/* chat list item title */ +"requested to connect" = "запрошено соединение"; + /* No comment provided by engineer. */ "Required" = "Обязательно"; @@ -4008,12 +4017,6 @@ /* No comment provided by engineer. */ "Safer groups" = "Более безопасные группы"; -/* No comment provided by engineer. */ -"The same conditions will apply to operator **%@**." = "Те же самые условия будут приняты для оператора **%@**."; - -/* No comment provided by engineer. */ -"The same conditions will apply to operator(s): **%@**." = "Те же самые условия будут приняты для оператора(ов): **%@**."; - /* alert button chat item action */ "Save" = "Сохранить"; @@ -4042,7 +4045,7 @@ /* No comment provided by engineer. */ "Save passphrase in Keychain" = "Сохранить пароль в Keychain"; -/* No comment provided by engineer. */ +/* alert title */ "Save preferences?" = "Сохранить предпочтения?"; /* No comment provided by engineer. */ @@ -4757,6 +4760,12 @@ /* No comment provided by engineer. */ "The profile is only shared with your contacts." = "Профиль отправляется только Вашим контактам."; +/* No comment provided by engineer. */ +"The same conditions will apply to operator **%@**." = "Те же самые условия будут приняты для оператора **%@**."; + +/* No comment provided by engineer. */ +"The same conditions will apply to operator(s): **%@**." = "Те же самые условия будут приняты для оператора(ов): **%@**."; + /* No comment provided by engineer. */ "The second preset operator in the app!" = "Второй оператор серверов в приложении!"; diff --git a/apps/ios/th.lproj/Localizable.strings b/apps/ios/th.lproj/Localizable.strings index b496fe11b4..3fee154931 100644 --- a/apps/ios/th.lproj/Localizable.strings +++ b/apps/ios/th.lproj/Localizable.strings @@ -681,7 +681,7 @@ /* No comment provided by engineer. */ "Connecting server… (error: %@)" = "กำลังเชื่อมต่อกับเซิร์ฟเวอร์... (ข้อผิดพลาด: %@)"; -/* chat list item title */ +/* No comment provided by engineer. */ "connecting…" = "กำลังเชื่อมต่อ…"; /* No comment provided by engineer. */ @@ -2457,7 +2457,7 @@ /* No comment provided by engineer. */ "Save passphrase in Keychain" = "บันทึกข้อความรหัสผ่านใน Keychain"; -/* No comment provided by engineer. */ +/* alert title */ "Save preferences?" = "บันทึกการตั้งค่า?"; /* No comment provided by engineer. */ diff --git a/apps/ios/tr.lproj/Localizable.strings b/apps/ios/tr.lproj/Localizable.strings index 99668bec79..a78faed4cd 100644 --- a/apps/ios/tr.lproj/Localizable.strings +++ b/apps/ios/tr.lproj/Localizable.strings @@ -1101,7 +1101,7 @@ /* No comment provided by engineer. */ "Connecting to desktop" = "Bilgisayara bağlanıyor"; -/* chat list item title */ +/* No comment provided by engineer. */ "connecting…" = "bağlanılıyor…"; /* No comment provided by engineer. */ @@ -3784,7 +3784,7 @@ /* No comment provided by engineer. */ "Save passphrase in Keychain" = "Parolayı Anahtar Zincirinde kaydet"; -/* No comment provided by engineer. */ +/* alert title */ "Save preferences?" = "Tercihler kaydedilsin mi?"; /* No comment provided by engineer. */ diff --git a/apps/ios/uk.lproj/Localizable.strings b/apps/ios/uk.lproj/Localizable.strings index d470a2a1e3..ce607753fe 100644 --- a/apps/ios/uk.lproj/Localizable.strings +++ b/apps/ios/uk.lproj/Localizable.strings @@ -863,6 +863,9 @@ /* No comment provided by engineer. */ "Change" = "Зміна"; +/* authentication reason */ +"Change chat profiles" = "Зміна профілів користувачів"; + /* No comment provided by engineer. */ "Change database passphrase?" = "Змінити пароль до бази даних?"; @@ -891,9 +894,6 @@ set passcode view */ "Change self-destruct passcode" = "Змінити пароль самознищення"; -/* authentication reason */ -"Change chat profiles" = "Зміна профілів користувачів"; - /* chat item text */ "changed address for you" = "змінили для вас адресу"; @@ -1173,7 +1173,7 @@ /* No comment provided by engineer. */ "Connecting to desktop" = "Підключення до ПК"; -/* chat list item title */ +/* No comment provided by engineer. */ "connecting…" = "з'єднання…"; /* No comment provided by engineer. */ @@ -3668,9 +3668,6 @@ /* No comment provided by engineer. */ "Proxy requires password" = "Проксі вимагає пароль"; -/* No comment provided by engineer. */ -"Push notifications" = "Push-повідомлення"; - /* No comment provided by engineer. */ "Push notifications" = "Push-сповіщення"; @@ -3948,12 +3945,6 @@ /* No comment provided by engineer. */ "Safer groups" = "Безпечніші групи"; -/* No comment provided by engineer. */ -"The same conditions will apply to operator **%@**." = "Такі ж умови діятимуть і для оператора **%@**."; - -/* No comment provided by engineer. */ -"The same conditions will apply to operator(s): **%@**." = "Такі ж умови будуть застосовуватися до оператора(ів): **%@**."; - /* alert button chat item action */ "Save" = "Зберегти"; @@ -3982,7 +3973,7 @@ /* No comment provided by engineer. */ "Save passphrase in Keychain" = "Збережіть парольну фразу в Keychain"; -/* No comment provided by engineer. */ +/* alert title */ "Save preferences?" = "Зберегти настройки?"; /* No comment provided by engineer. */ @@ -4694,6 +4685,12 @@ /* No comment provided by engineer. */ "The profile is only shared with your contacts." = "Профіль доступний лише вашим контактам."; +/* No comment provided by engineer. */ +"The same conditions will apply to operator **%@**." = "Такі ж умови діятимуть і для оператора **%@**."; + +/* No comment provided by engineer. */ +"The same conditions will apply to operator(s): **%@**." = "Такі ж умови будуть застосовуватися до оператора(ів): **%@**."; + /* No comment provided by engineer. */ "The second preset operator in the app!" = "Другий попередньо встановлений оператор у застосунку!"; diff --git a/apps/ios/zh-Hans.lproj/Localizable.strings b/apps/ios/zh-Hans.lproj/Localizable.strings index 6a924eea1f..627bfd0c30 100644 --- a/apps/ios/zh-Hans.lproj/Localizable.strings +++ b/apps/ios/zh-Hans.lproj/Localizable.strings @@ -1059,7 +1059,7 @@ /* No comment provided by engineer. */ "Connecting to desktop" = "正连接到桌面"; -/* chat list item title */ +/* No comment provided by engineer. */ "connecting…" = "连接中……"; /* No comment provided by engineer. */ @@ -3655,7 +3655,7 @@ /* No comment provided by engineer. */ "Save passphrase in Keychain" = "在钥匙串中保存密码"; -/* No comment provided by engineer. */ +/* alert title */ "Save preferences?" = "保存偏好设置?"; /* No comment provided by engineer. */ From e0c2272fcb3099d3e9ac87eb02108ec7d57dc5e0 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Fri, 6 Dec 2024 21:35:10 +0400 Subject: [PATCH 154/167] android, desktop: operators info on onboarding (#5341) --- .../views/onboarding/ChooseServerOperators.kt | 50 ++++++++++++++++--- .../networkAndServers/OperatorView.kt | 2 +- .../commonMain/resources/MR/base/strings.xml | 1 + 3 files changed, 46 insertions(+), 7 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt index 132381294f..e706a0d8e9 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt @@ -1,7 +1,11 @@ package chat.simplex.common.views.onboarding import SectionBottomSpacer +import SectionDividerSpaced +import SectionItemView import SectionTextFooter +import SectionView +import TextIconSpaced import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.* @@ -12,8 +16,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import chat.simplex.common.model.* import chat.simplex.common.model.ChatController.appPrefs -import chat.simplex.common.model.ServerOperator import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* @@ -339,11 +343,45 @@ private fun enabledOperators(operators: List, selectedOperatorId @Composable private fun ChooseServerOperatorsInfoView() { - ColumnWithScrollBar(Modifier.padding(horizontal = DEFAULT_PADDING)) { - AppBarTitle(stringResource(MR.strings.onboarding_network_operators), withPadding = false) - ReadableText(stringResource(MR.strings.onboarding_network_operators_app_will_use_different_operators)) - ReadableText(stringResource(MR.strings.onboarding_network_operators_cant_see_who_talks_to_whom)) - ReadableText(stringResource(MR.strings.onboarding_network_operators_app_will_use_for_routing)) + ColumnWithScrollBar { + AppBarTitle(stringResource(MR.strings.onboarding_network_operators)) + + Column( + Modifier.padding(horizontal = DEFAULT_PADDING) + ) { + ReadableText(stringResource(MR.strings.onboarding_network_operators_app_will_use_different_operators)) + ReadableText(stringResource(MR.strings.onboarding_network_operators_cant_see_who_talks_to_whom)) + ReadableText(stringResource(MR.strings.onboarding_network_operators_app_will_use_for_routing)) + } + + SectionDividerSpaced() + + SectionView(title = stringResource(MR.strings.onboarding_network_about_operators).uppercase()) { + chatModel.conditions.value.serverOperators.forEach { op -> + ServerOperatorRow(op) + } + } SectionBottomSpacer() } } + +@Composable() +private fun ServerOperatorRow( + operator: ServerOperator +) { + SectionItemView( + { + ModalManager.start.showModalCloseable { close -> + OperatorInfoView(operator) + } + } + ) { + Image( + painterResource(operator.logo), + operator.tradeName, + modifier = Modifier.size(24.dp) + ) + TextIconSpaced() + Text(operator.tradeName) + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt index 3836d5b6df..28ca88584d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt @@ -410,7 +410,7 @@ fun OperatorViewLayout( } @Composable -private fun OperatorInfoView(serverOperator: ServerOperator) { +fun OperatorInfoView(serverOperator: ServerOperator) { ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.operator_info_title)) diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index c412ec42ee..29df338079 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -1080,6 +1080,7 @@ The app protects your privacy by using different operators in each conversation. When more than one operator is enabled, none of them has metadata to learn who communicates with whom. For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server. + About operators Select network operators to use. How it helps privacy You can configure servers via settings. From 615c4839122177cd08b854716c0c784ea8eb62ff Mon Sep 17 00:00:00 2001 From: Diogo Date: Sat, 7 Dec 2024 14:20:01 +0000 Subject: [PATCH 155/167] android: onboarding small design adjustments (#5346) * android: onboarding small design adjustments * bigger --------- Co-authored-by: Evgeny Poberezkin --- .../kotlin/chat/simplex/common/ui/theme/Theme.kt | 1 + .../kotlin/chat/simplex/common/views/WelcomeView.kt | 4 ++-- .../views/onboarding/ChooseServerOperators.kt | 13 +++++++------ .../common/views/onboarding/SetNotificationsMode.kt | 6 +++--- .../views/onboarding/SetupDatabasePassphrase.kt | 2 +- .../simplex/common/views/onboarding/SimpleXInfo.kt | 11 ++++++----- 6 files changed, 20 insertions(+), 17 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt index 80542ced02..01e19ea478 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt @@ -609,6 +609,7 @@ fun themedBackgroundBrush(): Brush = Brush.linearGradient( ) val DEFAULT_PADDING = 20.dp +val DEFAULT_ONBOARDING_HORIZONTAL_PADDING = 25.dp val DEFAULT_SPACE_AFTER_ICON = 4.dp val DEFAULT_PADDING_HALF = DEFAULT_PADDING / 2 val DEFAULT_BOTTOM_PADDING = 48.dp diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt index 024929030e..8317c6cf6c 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt @@ -117,7 +117,7 @@ fun CreateFirstProfile(chatModel: ChatModel, close: () -> Unit) { ColumnWithScrollBar { val displayName = rememberSaveable { mutableStateOf("") } val focusRequester = remember { FocusRequester() } - Column(if (appPlatform.isAndroid) Modifier.fillMaxSize().padding(start = DEFAULT_PADDING * 2, end = DEFAULT_PADDING * 2, bottom = DEFAULT_PADDING) else Modifier.widthIn(max = 600.dp).fillMaxHeight().padding(horizontal = DEFAULT_PADDING).align(Alignment.CenterHorizontally)) { + Column(if (appPlatform.isAndroid) Modifier.fillMaxSize().padding(start = DEFAULT_ONBOARDING_HORIZONTAL_PADDING * 2, end = DEFAULT_ONBOARDING_HORIZONTAL_PADDING * 2, bottom = DEFAULT_PADDING) else Modifier.widthIn(max = 600.dp).fillMaxHeight().padding(horizontal = DEFAULT_PADDING).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) { Box(Modifier.align(Alignment.CenterHorizontally)) { AppBarTitle(stringResource(MR.strings.create_your_profile), bottomPadding = DEFAULT_PADDING, withPadding = false) } @@ -130,7 +130,7 @@ fun CreateFirstProfile(chatModel: ChatModel, close: () -> Unit) { 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), + if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_ONBOARDING_HORIZONTAL_PADDING).fillMaxWidth() else Modifier.widthIn(min = 300.dp), labelId = MR.strings.create_profile_button, onboarding = null, enabled = canCreateProfile(displayName.value), diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt index e706a0d8e9..dde1fb68ce 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt @@ -61,7 +61,8 @@ fun ModalData.ChooseServerOperators( Column(( if (appPlatform.isDesktop) Modifier.width(600.dp).align(Alignment.CenterHorizontally) else Modifier) .fillMaxWidth() - .padding(horizontal = DEFAULT_PADDING) + .padding(horizontal = DEFAULT_ONBOARDING_HORIZONTAL_PADDING), + horizontalAlignment = Alignment.CenterHorizontally ) { serverOperators.value.forEachIndexed { index, srvOperator -> OperatorCheckView(srvOperator, selectedOperatorIds) @@ -173,7 +174,7 @@ private fun ReviewConditionsButton( modalManager: ModalManager ) { OnboardingActionButton( - modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_PADDING * 2).fillMaxWidth() else Modifier.widthIn(min = 300.dp), + modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_ONBOARDING_HORIZONTAL_PADDING).fillMaxWidth() else Modifier.widthIn(min = 300.dp), labelId = MR.strings.operator_review_conditions, onboarding = null, enabled = enabled, @@ -188,7 +189,7 @@ private fun ReviewConditionsButton( @Composable private fun SetOperatorsButton(enabled: Boolean, onboarding: Boolean, serverOperators: State>, selectedOperatorIds: State>, close: () -> Unit) { OnboardingActionButton( - modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_PADDING * 2).fillMaxWidth() else Modifier.widthIn(min = 300.dp), + modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_ONBOARDING_HORIZONTAL_PADDING).fillMaxWidth() else Modifier.widthIn(min = 300.dp), labelId = MR.strings.onboarding_network_operators_update, onboarding = null, enabled = enabled, @@ -210,7 +211,7 @@ private fun SetOperatorsButton(enabled: Boolean, onboarding: Boolean, serverOper @Composable private fun ContinueButton(enabled: Boolean, onboarding: Boolean, close: () -> Unit) { OnboardingActionButton( - modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_PADDING * 2).fillMaxWidth() else Modifier.widthIn(min = 300.dp), + modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_ONBOARDING_HORIZONTAL_PADDING).fillMaxWidth() else Modifier.widthIn(min = 300.dp), labelId = MR.strings.onboarding_network_operators_continue, onboarding = null, enabled = enabled, @@ -238,7 +239,7 @@ private fun ReviewConditionsView( // remembering both since we don't want to reload the view after the user accepts conditions val operatorsWithConditionsAccepted = remember { chatModel.conditions.value.serverOperators.filter { it.conditionsAcceptance.conditionsAccepted } } val acceptForOperators = remember { selectedOperators.value.filter { !it.conditionsAcceptance.conditionsAccepted } } - ColumnWithScrollBar(modifier = Modifier.fillMaxSize().padding(horizontal = DEFAULT_PADDING)) { + ColumnWithScrollBar(modifier = Modifier.fillMaxSize().padding(horizontal = if (onboarding) DEFAULT_ONBOARDING_HORIZONTAL_PADDING else DEFAULT_PADDING)) { AppBarTitle(stringResource(MR.strings.operator_conditions_of_use), withPadding = false, enableAlphaChanges = false, bottomPadding = DEFAULT_PADDING) if (operatorsWithConditionsAccepted.isNotEmpty()) { ReadableText(MR.strings.operator_conditions_accepted_for_some, args = operatorsWithConditionsAccepted.joinToString(", ") { it.legalName_ }) @@ -271,7 +272,7 @@ private fun AcceptConditionsButton( } } OnboardingActionButton( - modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_PADDING * 2).fillMaxWidth() else Modifier, + modifier = if (appPlatform.isAndroid) Modifier.fillMaxWidth() else Modifier, labelId = MR.strings.accept_conditions, onboarding = null, onclick = { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetNotificationsMode.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetNotificationsMode.kt index 9e6287771f..84f473067f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetNotificationsMode.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetNotificationsMode.kt @@ -35,14 +35,14 @@ fun SetNotificationsMode(m: ChatModel) { AppBarTitle(stringResource(MR.strings.onboarding_notifications_mode_title), bottomPadding = DEFAULT_PADDING) } val currentMode = rememberSaveable { mutableStateOf(NotificationsMode.default) } - Column(Modifier.padding(horizontal = DEFAULT_PADDING).fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { + Column(Modifier.padding(horizontal = DEFAULT_ONBOARDING_HORIZONTAL_PADDING).fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { OnboardingInformationButton( stringResource(MR.strings.onboarding_notifications_mode_subtitle), onClick = { ModalManager.fullscreen.showModalCloseable { NotificationBatteryUsageInfo() } } ) } Spacer(Modifier.weight(1f)) - Column(Modifier.padding(horizontal = DEFAULT_PADDING)) { + Column(Modifier.padding(horizontal = DEFAULT_ONBOARDING_HORIZONTAL_PADDING)) { SelectableCard(currentMode, NotificationsMode.SERVICE, stringResource(MR.strings.onboarding_notifications_mode_service), annotatedStringResource(MR.strings.onboarding_notifications_mode_service_desc_short)) { currentMode.value = NotificationsMode.SERVICE } @@ -56,7 +56,7 @@ fun SetNotificationsMode(m: ChatModel) { Spacer(Modifier.weight(1f)) Column(Modifier.widthIn(max = if (appPlatform.isAndroid) 450.dp else 1000.dp).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) { OnboardingActionButton( - modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_PADDING * 2).fillMaxWidth() else Modifier, + modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_ONBOARDING_HORIZONTAL_PADDING).fillMaxWidth() else Modifier, labelId = MR.strings.use_chat, onboarding = OnboardingStage.OnboardingComplete, onclick = { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetupDatabasePassphrase.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetupDatabasePassphrase.kt index e7db51c768..c6eceb0ce2 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetupDatabasePassphrase.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetupDatabasePassphrase.kt @@ -190,7 +190,7 @@ private fun SetupDatabasePassphraseLayout( @Composable private fun SetPassphraseButton(disabled: Boolean, onClick: () -> Unit) { OnboardingActionButton( - if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_PADDING * 2).fillMaxWidth() else Modifier.widthIn(min = 300.dp), + if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_PADDING).fillMaxWidth() else Modifier.widthIn(min = 300.dp), labelId = MR.strings.set_database_passphrase, onboarding = null, onclick = onClick, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt index 020d3493b9..85ef1b513a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt @@ -5,6 +5,7 @@ import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -52,7 +53,7 @@ fun SimpleXInfoLayout( user: User?, onboardingStage: SharedPreference? ) { - ColumnWithScrollBar(Modifier.padding(horizontal = DEFAULT_PADDING), horizontalAlignment = Alignment.CenterHorizontally) { + ColumnWithScrollBar(Modifier.padding(horizontal = DEFAULT_ONBOARDING_HORIZONTAL_PADDING), horizontalAlignment = Alignment.CenterHorizontally) { Box(Modifier.widthIn(max = if (appPlatform.isAndroid) 250.dp else 500.dp).padding(top = DEFAULT_PADDING + 8.dp), contentAlignment = Alignment.Center) { SimpleXLogo() } @@ -73,7 +74,7 @@ fun SimpleXInfoLayout( Column(Modifier.fillMaxHeight().weight(1f)) { } if (onboardingStage != null) { - Column(Modifier.padding(horizontal = DEFAULT_PADDING).widthIn(max = if (appPlatform.isAndroid) 450.dp else 1000.dp).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally,) { + Column(Modifier.widthIn(max = if (appPlatform.isAndroid) 450.dp else 1000.dp).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally,) { OnboardingActionButton(user, onboardingStage) TextButtonBelowOnboardingButton(stringResource(MR.strings.migrate_from_another_device)) { chatModel.migrationState.value = MigrationToState.PasteOrScanLink @@ -139,7 +140,7 @@ fun OnboardingActionButton( shape = CircleShape, enabled = enabled, // elevation = ButtonDefaults.elevation(defaultElevation = 0.dp, focusedElevation = 0.dp, pressedElevation = 0.dp, hoveredElevation = 0.dp), - contentPadding = PaddingValues(horizontal = if (icon == null) DEFAULT_PADDING * 2 else DEFAULT_PADDING * 1.5f, vertical = DEFAULT_PADDING), + contentPadding = PaddingValues(horizontal = if (icon == null) DEFAULT_PADDING * 2 else DEFAULT_PADDING * 1.5f, vertical = 17.dp), colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary, disabledBackgroundColor = MaterialTheme.colors.secondary) ) { if (icon != null) { @@ -153,8 +154,8 @@ fun OnboardingActionButton( fun TextButtonBelowOnboardingButton(text: String, onClick: (() -> Unit)?) { val state = getKeyboardState() val enabled = onClick != null - val topPadding by animateDpAsState(if (appPlatform.isAndroid && state.value == KeyboardState.Opened) 0.dp else DEFAULT_PADDING_HALF) - val bottomPadding by animateDpAsState(if (appPlatform.isAndroid && state.value == KeyboardState.Opened) 0.dp else DEFAULT_PADDING_HALF) + val topPadding by animateDpAsState(if (appPlatform.isAndroid && state.value == KeyboardState.Opened) 0.dp else 7.5.dp) + val bottomPadding by animateDpAsState(if (appPlatform.isAndroid && state.value == KeyboardState.Opened) 0.dp else 7.5.dp) if ((appPlatform.isAndroid && state.value == KeyboardState.Closed) || topPadding > 0.dp) { TextButton({ onClick?.invoke() }, Modifier.padding(top = topPadding, bottom = bottomPadding).clip(CircleShape), enabled = enabled) { Text( From 83f0bd9fd33c982a8e8b5e8ea87abdacb352891d Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Sat, 7 Dec 2024 21:24:14 +0700 Subject: [PATCH 156/167] android, desktop: onboarding button multiline layout (#5348) Co-authored-by: Evgeny Poberezkin --- .../common/views/onboarding/SimpleXInfo.kt | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt index 85ef1b513a..e5d00fddd1 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt @@ -14,6 +14,8 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.layout +import androidx.compose.ui.text.TextLayoutResult import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.font.FontWeight @@ -28,6 +30,8 @@ import chat.simplex.common.views.migration.MigrateToDeviceView import chat.simplex.common.views.migration.MigrationToState import chat.simplex.res.MR import dev.icerock.moko.resources.StringResource +import kotlin.math.ceil +import kotlin.math.floor @Composable fun SimpleXInfo(chatModel: ChatModel, onboarding: Boolean = true) { @@ -188,7 +192,35 @@ fun OnboardingInformationButton( null, tint = MaterialTheme.colors.primary ) - Text(text, style = MaterialTheme.typography.button, color = MaterialTheme.colors.primary) + // https://issuetracker.google.com/issues/206039942#comment32 + var textLayoutResult: TextLayoutResult? by remember { mutableStateOf(null) } + Text( + text, + Modifier + .layout { measurable, constraints -> + val placeable = measurable.measure(constraints) + val newTextLayoutResult = textLayoutResult + + if (newTextLayoutResult == null || newTextLayoutResult.lineCount == 0) { + // Default behavior if there is no text or the text layout is not measured yet + layout(placeable.width, placeable.height) { + placeable.placeRelative(0, 0) + } + } else { + val minX = (0 until newTextLayoutResult.lineCount).minOf(newTextLayoutResult::getLineLeft) + val maxX = (0 until newTextLayoutResult.lineCount).maxOf(newTextLayoutResult::getLineRight) + + layout(ceil(maxX - minX).toInt(), placeable.height) { + placeable.place(-floor(minX).toInt(), 0) + } + } + }, + onTextLayout = { + textLayoutResult = it + }, + style = MaterialTheme.typography.button, + color = MaterialTheme.colors.primary + ) } } } From cbb3da8f835246f0bad43f5176aa74659b2ba6b7 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sat, 7 Dec 2024 14:40:35 +0000 Subject: [PATCH 157/167] core: 6.2.0.7 (simplexmq: 6.2.0.7) --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- src/Simplex/Chat/Remote.hs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cabal.project b/cabal.project index 3212768506..ae24afd374 100644 --- a/cabal.project +++ b/cabal.project @@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 9893935e7c3cf8d102c85730a4e48d32f05c2ec7 + tag: 79e9447b73cc315ce35042b0a5f210c07ea39b07 source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 098851e9ff..d0411c584d 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."9893935e7c3cf8d102c85730a4e48d32f05c2ec7" = "1bpgsdnmk8fml6ad9bjbvyichvd0kq0nqj562xyy5y1npymaxpyn"; + "https://github.com/simplex-chat/simplexmq.git"."79e9447b73cc315ce35042b0a5f210c07ea39b07" = "16z7z5a3f7gw0h188manykp008d1bqpydlrj7h497mgyjmp4cy9m"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; diff --git a/src/Simplex/Chat/Remote.hs b/src/Simplex/Chat/Remote.hs index ba713420fc..cfc4fe2fa0 100644 --- a/src/Simplex/Chat/Remote.hs +++ b/src/Simplex/Chat/Remote.hs @@ -73,11 +73,11 @@ import UnliftIO.Directory (copyFile, createDirectoryIfMissing, doesDirectoryExis -- when acting as host minRemoteCtrlVersion :: AppVersion -minRemoteCtrlVersion = AppVersion [6, 2, 0, 4] +minRemoteCtrlVersion = AppVersion [6, 2, 0, 7] -- when acting as controller minRemoteHostVersion :: AppVersion -minRemoteHostVersion = AppVersion [6, 2, 0, 4] +minRemoteHostVersion = AppVersion [6, 2, 0, 7] currentAppVersion :: AppVersion currentAppVersion = AppVersion SC.version From fe0d811bf7e579b96089ead4ee0d7b42c81ad10c Mon Sep 17 00:00:00 2001 From: Evgeny Date: Sat, 7 Dec 2024 14:41:54 +0000 Subject: [PATCH 158/167] ui: operator information (#5343) * ios: operator information * android, desktop: operator information * move texts, simplify navigation --- .../Onboarding/ChooseServerOperators.swift | 58 +++++------- .../Views/Onboarding/CreateProfile.swift | 1 - .../Onboarding/SetNotificationsMode.swift | 2 +- .../NetworkAndServers/OperatorView.swift | 94 ++++++++----------- .../bg.xcloc/Localized Contents/bg.xliff | 10 +- .../cs.xcloc/Localized Contents/cs.xliff | 10 +- .../de.xcloc/Localized Contents/de.xliff | 11 ++- .../en.xcloc/Localized Contents/en.xliff | 13 ++- .../es.xcloc/Localized Contents/es.xliff | 11 ++- .../fi.xcloc/Localized Contents/fi.xliff | 10 +- .../fr.xcloc/Localized Contents/fr.xliff | 10 +- .../hu.xcloc/Localized Contents/hu.xliff | 10 +- .../it.xcloc/Localized Contents/it.xliff | 10 +- .../ja.xcloc/Localized Contents/ja.xliff | 10 +- .../nl.xcloc/Localized Contents/nl.xliff | 11 ++- .../pl.xcloc/Localized Contents/pl.xliff | 10 +- .../ru.xcloc/Localized Contents/ru.xliff | 11 ++- .../th.xcloc/Localized Contents/th.xliff | 10 +- .../tr.xcloc/Localized Contents/tr.xliff | 10 +- .../uk.xcloc/Localized Contents/uk.xliff | 10 +- .../Localized Contents/zh-Hans.xliff | 10 +- apps/ios/SimpleXChat/APITypes.swift | 61 +++--------- apps/ios/bg.lproj/Localizable.strings | 2 +- apps/ios/cs.lproj/Localizable.strings | 2 +- apps/ios/de.lproj/Localizable.strings | 7 +- apps/ios/es.lproj/Localizable.strings | 7 +- apps/ios/fi.lproj/Localizable.strings | 2 +- apps/ios/fr.lproj/Localizable.strings | 2 +- apps/ios/hu.lproj/Localizable.strings | 4 +- apps/ios/it.lproj/Localizable.strings | 4 +- apps/ios/ja.lproj/Localizable.strings | 2 +- apps/ios/nl.lproj/Localizable.strings | 7 +- apps/ios/pl.lproj/Localizable.strings | 2 +- apps/ios/ru.lproj/Localizable.strings | 7 +- apps/ios/th.lproj/Localizable.strings | 2 +- apps/ios/tr.lproj/Localizable.strings | 2 +- apps/ios/uk.lproj/Localizable.strings | 4 +- apps/ios/zh-Hans.lproj/Localizable.strings | 2 +- .../chat/simplex/common/model/SimpleXAPI.kt | 53 ++--------- .../views/onboarding/ChooseServerOperators.kt | 3 + .../networkAndServers/OperatorView.kt | 18 ++-- .../commonMain/resources/MR/base/strings.xml | 1 + .../commonMain/resources/MR/ru/strings.xml | 1 + 43 files changed, 254 insertions(+), 273 deletions(-) diff --git a/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift b/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift index cc47374257..1a0a736acd 100644 --- a/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift +++ b/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift @@ -62,7 +62,6 @@ struct ChooseServerOperators: View { var onboarding: Bool @State private var serverOperators: [ServerOperator] = [] @State private var selectedOperatorIds = Set() - @State private var reviewConditionsNavLinkActive = false @State private var sheetItem: ChooseServerOperatorsSheet? = nil @State private var notificationsModeNavLinkActive = false @State private var justOpened = true @@ -79,7 +78,7 @@ struct ChooseServerOperators: View { .frame(maxWidth: .infinity, alignment: .center) if onboarding { - title.padding(.top, 50) + title.padding(.top, 25) } else { title } @@ -92,11 +91,14 @@ struct ChooseServerOperators: View { ForEach(serverOperators) { srvOperator in operatorCheckView(srvOperator) } - Text("You can configure servers via settings.") - .font(.footnote) - .multilineTextAlignment(.center) - .frame(maxWidth: .infinity, alignment: .center) - .padding(.horizontal, 32) + VStack { + Text("SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app.").padding(.bottom, 8) + Text("You can configure servers via settings.") + } + .font(.footnote) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity, alignment: .center) + .padding(.horizontal, 16) Spacer() @@ -166,8 +168,9 @@ struct ChooseServerOperators: View { .modifier(ThemedBackground(grouped: true)) } } + .frame(maxHeight: .infinity, alignment: .top) } - .frame(maxHeight: .infinity) + .frame(maxHeight: .infinity, alignment: .top) .padding(onboarding ? 25 : 16) } @@ -214,23 +217,15 @@ struct ChooseServerOperators: View { } private func reviewConditionsButton() -> some View { - ZStack { - Button { - reviewConditionsNavLinkActive = true - } label: { - Text("Review conditions") - } - .buttonStyle(OnboardingButtonStyle(isDisabled: selectedOperatorIds.isEmpty)) - .disabled(selectedOperatorIds.isEmpty) - - NavigationLink(isActive: $reviewConditionsNavLinkActive) { - reviewConditionsDestinationView() - } label: { - EmptyView() - } - .frame(width: 1, height: 1) - .hidden() + NavigationLink("Review conditions") { + reviewConditionsView() + .navigationTitle("Conditions of use") + .navigationBarTitleDisplayMode(.large) + .toolbar { ToolbarItem(placement: .navigationBarTrailing, content: conditionsLinkButton) } + .modifier(ThemedBackground(grouped: true)) } + .buttonStyle(OnboardingButtonStyle(isDisabled: selectedOperatorIds.isEmpty)) + .disabled(selectedOperatorIds.isEmpty) } private func setOperatorsButton() -> some View { @@ -309,20 +304,12 @@ struct ChooseServerOperators: View { .modifier(ThemedBackground()) } - private func reviewConditionsDestinationView() -> some View { - reviewConditionsView() - .navigationTitle("Conditions of use") - .navigationBarTitleDisplayMode(.large) - .toolbar { ToolbarItem(placement: .navigationBarTrailing, content: conditionsLinkButton) } - .modifier(ThemedBackground(grouped: true)) - } - @ViewBuilder private func reviewConditionsView() -> some View { let operatorsWithConditionsAccepted = ChatModel.shared.conditions.serverOperators.filter { $0.conditionsAcceptance.conditionsAccepted } let acceptForOperators = selectedOperators.filter { !$0.conditionsAcceptance.conditionsAccepted } VStack(alignment: .leading, spacing: 20) { if !operatorsWithConditionsAccepted.isEmpty { - Text("Conditions are already accepted for following operator(s): **\(operatorsWithConditionsAccepted.map { $0.legalName_ }.joined(separator: ", "))**.") + Text("Conditions are already accepted for these operator(s): **\(operatorsWithConditionsAccepted.map { $0.legalName_ }.joined(separator: ", "))**.") Text("The same conditions will apply to operator(s): **\(acceptForOperators.map { $0.legalName_ }.joined(separator: ", "))**.") } else { Text("Conditions will be accepted for operator(s): **\(acceptForOperators.map { $0.legalName_ }.joined(separator: ", "))**.") @@ -415,13 +402,12 @@ struct ChooseServerOperatorsInfoView: View { var body: some View { NavigationView { List { - VStack(alignment: .leading) { + VStack(alignment: .leading, spacing: 12) { Text("The app protects your privacy by using different operators in each conversation.") - .padding(.bottom) Text("When more than one operator is enabled, none of them has metadata to learn who communicates with whom.") - .padding(.bottom) Text("For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server.") } + .fixedSize(horizontal: false, vertical: true) .listRowBackground(Color.clear) .listRowSeparator(.hidden) .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) diff --git a/apps/ios/Shared/Views/Onboarding/CreateProfile.swift b/apps/ios/Shared/Views/Onboarding/CreateProfile.swift index 7665e57cc1..14ad9dfb08 100644 --- a/apps/ios/Shared/Views/Onboarding/CreateProfile.swift +++ b/apps/ios/Shared/Views/Onboarding/CreateProfile.swift @@ -136,7 +136,6 @@ struct CreateFirstProfile: View { .multilineTextAlignment(.center) } .frame(maxWidth: .infinity) // Ensures it takes up the full width - .padding(.top, 25) .padding(.horizontal, 10) HStack { diff --git a/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift b/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift index 642220454c..97e1f49382 100644 --- a/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift +++ b/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift @@ -22,7 +22,7 @@ struct SetNotificationsMode: View { Text("Push notifications") .font(.largeTitle) .bold() - .padding(.top, 50) + .padding(.top, 25) infoText() diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift index b1e4d36eda..cea9dd0635 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift @@ -331,9 +331,12 @@ struct OperatorInfoView: View { Text(d) } } + Link(serverOperator.info.website.absoluteString, destination: serverOperator.info.website) } - Section { - Link("\(serverOperator.info.website)", destination: URL(string: serverOperator.info.website)!) + if let selfhost = serverOperator.info.selfhost { + Section { + Link(selfhost.text, destination: selfhost.link) + } } } } @@ -421,7 +424,6 @@ struct SingleOperatorUsageConditionsView: View { @Binding var userServers: [UserOperatorServers] @Binding var serverErrors: [UserServersError] var operatorIndex: Int - @State private var usageConditionsNavLinkActive: Bool = false var body: some View { viewBody() @@ -433,52 +435,45 @@ struct SingleOperatorUsageConditionsView: View { // In current UI implementation this branch doesn't get shown - as conditions can't be opened from inside operator once accepted VStack(alignment: .leading, spacing: 20) { - Group { - viewHeader() - ConditionsTextView() - .padding(.bottom) - .padding(.bottom) - } - .padding(.horizontal) + viewHeader() + ConditionsTextView() } + .padding(.bottom) + .padding(.bottom) + .padding(.horizontal) .frame(maxHeight: .infinity) } else if !operatorsWithConditionsAccepted.isEmpty { NavigationView { VStack(alignment: .leading, spacing: 20) { - Group { - viewHeader() - Text("Conditions are already accepted for following operator(s): **\(operatorsWithConditionsAccepted.map { $0.legalName_ }.joined(separator: ", "))**.") - Text("The same conditions will apply to operator **\(userServers[operatorIndex].operator_.legalName_)**.") - conditionsAppliedToOtherOperatorsText() - usageConditionsNavLinkButton() + viewHeader() + Text("Conditions are already accepted for these operator(s): **\(operatorsWithConditionsAccepted.map { $0.legalName_ }.joined(separator: ", "))**.") + Text("The same conditions will apply to operator **\(userServers[operatorIndex].operator_.legalName_)**.") + conditionsAppliedToOtherOperatorsText() + Spacer() - Spacer() - - acceptConditionsButton() - .padding(.bottom) - .padding(.bottom) - } - .padding(.horizontal) + acceptConditionsButton() + usageConditionsNavLinkButton() } + .padding(.bottom) + .padding(.bottom) + .padding(.horizontal) .frame(maxHeight: .infinity) } } else { VStack(alignment: .leading, spacing: 20) { - Group { - viewHeader() - Text("To use the servers of **\(userServers[operatorIndex].operator_.legalName_)**, accept conditions of use.") - conditionsAppliedToOtherOperatorsText() - ConditionsTextView() - acceptConditionsButton() - .padding(.bottom) - .padding(.bottom) - } - .padding(.horizontal) + viewHeader() + Text("To use the servers of **\(userServers[operatorIndex].operator_.legalName_)**, accept conditions of use.") + conditionsAppliedToOtherOperatorsText() + ConditionsTextView() + acceptConditionsButton() + .padding(.bottom) + .padding(.bottom) } + .padding(.horizontal) .frame(maxHeight: .infinity) } @@ -545,31 +540,16 @@ struct SingleOperatorUsageConditionsView: View { } private func usageConditionsNavLinkButton() -> some View { - ZStack { - Button { - usageConditionsNavLinkActive = true - } label: { - Text("View conditions") - } - - NavigationLink(isActive: $usageConditionsNavLinkActive) { - usageConditionsDestinationView() - } label: { - EmptyView() - } - .frame(width: 1, height: 1) - .hidden() + NavigationLink("View conditions") { + ConditionsTextView() + .padding() + .navigationTitle("Conditions of use") + .navigationBarTitleDisplayMode(.large) + .toolbar { ToolbarItem(placement: .navigationBarTrailing, content: conditionsLinkButton) } + .modifier(ThemedBackground(grouped: true)) } - } - - private func usageConditionsDestinationView() -> some View { - ConditionsTextView() - .padding() - .padding(.bottom) - .navigationTitle("Conditions of use") - .navigationBarTitleDisplayMode(.large) - .toolbar { ToolbarItem(placement: .navigationBarTrailing, content: conditionsLinkButton) } - .modifier(ThemedBackground(grouped: true)) + .font(.callout) + .frame(maxWidth: .infinity, alignment: .center) } } diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff index 4af0007eea..9260ac41c0 100644 --- a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff @@ -1537,8 +1537,8 @@ Conditions are accepted for the operator(s): **%@**. No comment provided by engineer. - - Conditions are already accepted for following operator(s): **%@**. + + Conditions are already accepted for these operator(s): **%@**. No comment provided by engineer. @@ -5766,7 +5766,7 @@ Enable in *Network & servers* settings. Save and notify contact Запази и уведоми контакта - No comment provided by engineer. + alert button Save and notify group members @@ -6380,6 +6380,10 @@ Enable in *Network & servers* settings. SimpleX Адрес No comment provided by engineer. + + SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app. + No comment provided by engineer. + SimpleX Chat security was audited by Trail of Bits. Сигурността на SimpleX Chat беше одитирана от Trail of Bits. diff --git a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff index 7d92f62f12..d921471f7f 100644 --- a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff @@ -1492,8 +1492,8 @@ Conditions are accepted for the operator(s): **%@**. No comment provided by engineer. - - Conditions are already accepted for following operator(s): **%@**. + + Conditions are already accepted for these operator(s): **%@**. No comment provided by engineer. @@ -5576,7 +5576,7 @@ Enable in *Network & servers* settings. Save and notify contact Uložit a upozornit kontakt - No comment provided by engineer. + alert button Save and notify group members @@ -6178,6 +6178,10 @@ Enable in *Network & servers* settings. SimpleX Adresa No comment provided by engineer. + + SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app. + No comment provided by engineer. + SimpleX Chat security was audited by Trail of Bits. Zabezpečení SimpleX chatu bylo auditováno společností Trail of Bits. diff --git a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff index 516baf49b7..053a1faf73 100644 --- a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff +++ b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff @@ -1606,8 +1606,8 @@ Die Nutzungsbedingungen der/des Betreiber(s) werden akzeptiert: **%@**. No comment provided by engineer. - - Conditions are already accepted for following operator(s): **%@**. + + Conditions are already accepted for these operator(s): **%@**. Die Nutzungsbedingungen der/des folgenden Betreiber(s) wurden schon akzeptiert: **%@**. No comment provided by engineer. @@ -6034,7 +6034,7 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Save and notify contact Speichern und Kontakt benachrichtigen - No comment provided by engineer. + alert button Save and notify group members @@ -6692,6 +6692,11 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. SimpleX-Adresse No comment provided by engineer. + + SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app. + SimpleX-Chat und Flux haben vereinbart, die von Flux betriebenen Server in die App aufzunehmen. + No comment provided by engineer. + SimpleX Chat security was audited by Trail of Bits. Die Sicherheit von SimpleX Chat wurde von Trail of Bits überprüft. diff --git a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff index 699091e2d8..004d7f0d31 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff +++ b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff @@ -1617,9 +1617,9 @@ Conditions are accepted for the operator(s): **%@**. No comment provided by engineer. - - Conditions are already accepted for following operator(s): **%@**. - Conditions are already accepted for following operator(s): **%@**. + + Conditions are already accepted for these operator(s): **%@**. + Conditions are already accepted for these operator(s): **%@**. No comment provided by engineer. @@ -6056,7 +6056,7 @@ Enable in *Network & servers* settings. Save and notify contact Save and notify contact - No comment provided by engineer. + alert button Save and notify group members @@ -6714,6 +6714,11 @@ Enable in *Network & servers* settings. SimpleX Address No comment provided by engineer. + + SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app. + SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app. + No comment provided by engineer. + SimpleX Chat security was audited by Trail of Bits. SimpleX Chat security was audited by Trail of Bits. diff --git a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff index ddb5d6b1a1..ea966ea63b 100644 --- a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff +++ b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff @@ -1606,8 +1606,8 @@ Las condiciones se han aceptado para el(los) operador(s): **%@**. No comment provided by engineer. - - Conditions are already accepted for following operator(s): **%@**. + + Conditions are already accepted for these operator(s): **%@**. Las condiciones ya se han aceptado para el/los siguiente(s) operador(s): **%@**. No comment provided by engineer. @@ -6034,7 +6034,7 @@ Actívalo en ajustes de *Servidores y Redes*. Save and notify contact Guardar y notificar contacto - No comment provided by engineer. + alert button Save and notify group members @@ -6692,6 +6692,11 @@ Actívalo en ajustes de *Servidores y Redes*. Dirección SimpleX No comment provided by engineer. + + SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app. + Simplex Chat y Flux han acordado incluir servidores operados por Flux en la aplicación + No comment provided by engineer. + SimpleX Chat security was audited by Trail of Bits. La seguridad de SimpleX Chat ha sido auditada por Trail of Bits. diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff index 12c8d0e3ca..2f67ee9d7d 100644 --- a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff @@ -1485,8 +1485,8 @@ Conditions are accepted for the operator(s): **%@**. No comment provided by engineer. - - Conditions are already accepted for following operator(s): **%@**. + + Conditions are already accepted for these operator(s): **%@**. No comment provided by engineer. @@ -5564,7 +5564,7 @@ Enable in *Network & servers* settings. Save and notify contact Tallenna ja ilmoita kontaktille - No comment provided by engineer. + alert button Save and notify group members @@ -6165,6 +6165,10 @@ Enable in *Network & servers* settings. SimpleX-osoite No comment provided by engineer. + + SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app. + No comment provided by engineer. + SimpleX Chat security was audited by Trail of Bits. Trail of Bits on tarkastanut SimpleX Chatin tietoturvan. diff --git a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff index e82f01e33b..74002293d7 100644 --- a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff @@ -1589,8 +1589,8 @@ Conditions are accepted for the operator(s): **%@**. No comment provided by engineer. - - Conditions are already accepted for following operator(s): **%@**. + + Conditions are already accepted for these operator(s): **%@**. No comment provided by engineer. @@ -5972,7 +5972,7 @@ Activez-le dans les paramètres *Réseau et serveurs*. Save and notify contact Enregistrer et en informer le contact - No comment provided by engineer. + alert button Save and notify group members @@ -6623,6 +6623,10 @@ Activez-le dans les paramètres *Réseau et serveurs*. Adresse SimpleX No comment provided by engineer. + + SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app. + No comment provided by engineer. + SimpleX Chat security was audited by Trail of Bits. La sécurité de SimpleX Chat a été auditée par Trail of Bits. diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff index 5f05efaa4d..598bee5485 100644 --- a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff @@ -1616,8 +1616,8 @@ A következő üzemeltető(k) számára elfogadott feltételek: **%@**. No comment provided by engineer. - - Conditions are already accepted for following operator(s): **%@**. + + Conditions are already accepted for these operator(s): **%@**. A feltételek már el lettek fogadva a következő üzemeltető(k) számára: **%@**. No comment provided by engineer. @@ -6055,7 +6055,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. Save and notify contact Mentés és az ismerős értesítése - No comment provided by engineer. + alert button Save and notify group members @@ -6713,6 +6713,10 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. SimpleX-cím No comment provided by engineer. + + SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app. + No comment provided by engineer. + SimpleX Chat security was audited by Trail of Bits. A SimpleX Chat biztonsága a Trail of Bits által lett auditálva. diff --git a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff index ebfba7e415..67633b7ae8 100644 --- a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff +++ b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff @@ -1616,8 +1616,8 @@ Le condizioni sono state accettate per gli operatori: **%@**. No comment provided by engineer. - - Conditions are already accepted for following operator(s): **%@**. + + Conditions are already accepted for these operator(s): **%@**. Le condizioni sono già state accettate per i seguenti operatori: **%@**. No comment provided by engineer. @@ -6055,7 +6055,7 @@ Attivalo nelle impostazioni *Rete e server*. Save and notify contact Salva e avvisa il contatto - No comment provided by engineer. + alert button Save and notify group members @@ -6713,6 +6713,10 @@ Attivalo nelle impostazioni *Rete e server*. Indirizzo SimpleX No comment provided by engineer. + + SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app. + No comment provided by engineer. + SimpleX Chat security was audited by Trail of Bits. La sicurezza di SimpleX Chat è stata verificata da Trail of Bits. diff --git a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff index ba35db0c03..43e6f24cf7 100644 --- a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff +++ b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff @@ -1515,8 +1515,8 @@ Conditions are accepted for the operator(s): **%@**. No comment provided by engineer. - - Conditions are already accepted for following operator(s): **%@**. + + Conditions are already accepted for these operator(s): **%@**. No comment provided by engineer. @@ -5613,7 +5613,7 @@ Enable in *Network & servers* settings. Save and notify contact 保存して、連絡先にに知らせる - No comment provided by engineer. + alert button Save and notify group members @@ -6207,6 +6207,10 @@ Enable in *Network & servers* settings. SimpleXアドレス No comment provided by engineer. + + SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app. + No comment provided by engineer. + SimpleX Chat security was audited by Trail of Bits. SimpleX Chat のセキュリティは Trail of Bits によって監査されました。 diff --git a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff index 5631d9bd7d..c30370fc5a 100644 --- a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff @@ -1616,8 +1616,8 @@ Voorwaarden worden geaccepteerd voor de operator(s): **%@**. No comment provided by engineer. - - Conditions are already accepted for following operator(s): **%@**. + + Conditions are already accepted for these operator(s): **%@**. Voorwaarden zijn reeds geaccepteerd voor de volgende operator(s): **%@**. No comment provided by engineer. @@ -6055,7 +6055,7 @@ Schakel dit in in *Netwerk en servers*-instellingen. Save and notify contact Opslaan en Contact melden - No comment provided by engineer. + alert button Save and notify group members @@ -6713,6 +6713,11 @@ Schakel dit in in *Netwerk en servers*-instellingen. SimpleX adres No comment provided by engineer. + + SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app. + Simplex-chat en flux hebben een overeenkomst gemaakt om door flux geëxploiteerde servers in de app op te nemen. + No comment provided by engineer. + SimpleX Chat security was audited by Trail of Bits. De beveiliging van SimpleX Chat is gecontroleerd door Trail of Bits. diff --git a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff index dbc95b6527..e7c9863152 100644 --- a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff @@ -1584,8 +1584,8 @@ Conditions are accepted for the operator(s): **%@**. No comment provided by engineer. - - Conditions are already accepted for following operator(s): **%@**. + + Conditions are already accepted for these operator(s): **%@**. No comment provided by engineer. @@ -5962,7 +5962,7 @@ Włącz w ustawianiach *Sieć i serwery* . Save and notify contact Zapisz i powiadom kontakt - No comment provided by engineer. + alert button Save and notify group members @@ -6613,6 +6613,10 @@ Włącz w ustawianiach *Sieć i serwery* . Adres SimpleX No comment provided by engineer. + + SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app. + No comment provided by engineer. + SimpleX Chat security was audited by Trail of Bits. Bezpieczeństwo SimpleX Chat zostało zaudytowane przez Trail of Bits. diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff index 95bd74e484..943ea67ef4 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff @@ -1617,8 +1617,8 @@ Условия приняты для оператора(ов): **%@**. No comment provided by engineer. - - Conditions are already accepted for following operator(s): **%@**. + + Conditions are already accepted for these operator(s): **%@**. Условия уже приняты для следующих оператора(ов): **%@**. No comment provided by engineer. @@ -6055,7 +6055,7 @@ Enable in *Network & servers* settings. Save and notify contact Сохранить и уведомить контакт - No comment provided by engineer. + alert button Save and notify group members @@ -6713,6 +6713,11 @@ Enable in *Network & servers* settings. Адрес SimpleX No comment provided by engineer. + + SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app. + SimpleX Chat и Flux заключили соглашение добавить серверы под управлением Flux в приложение. + No comment provided by engineer. + SimpleX Chat security was audited by Trail of Bits. Безопасность SimpleX Chat была проверена Trail of Bits. diff --git a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff index 14827be1b5..177f426c1a 100644 --- a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff +++ b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff @@ -1477,8 +1477,8 @@ Conditions are accepted for the operator(s): **%@**. No comment provided by engineer. - - Conditions are already accepted for following operator(s): **%@**. + + Conditions are already accepted for these operator(s): **%@**. No comment provided by engineer. @@ -5541,7 +5541,7 @@ Enable in *Network & servers* settings. Save and notify contact บันทึกและแจ้งผู้ติดต่อ - No comment provided by engineer. + alert button Save and notify group members @@ -6139,6 +6139,10 @@ Enable in *Network & servers* settings. ที่อยู่ SimpleX No comment provided by engineer. + + SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app. + No comment provided by engineer. + SimpleX Chat security was audited by Trail of Bits. ความปลอดภัยของ SimpleX Chat ได้รับการตรวจสอบโดย Trail of Bits diff --git a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff index d12ef93f69..d88adc3235 100644 --- a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff @@ -1589,8 +1589,8 @@ Conditions are accepted for the operator(s): **%@**. No comment provided by engineer. - - Conditions are already accepted for following operator(s): **%@**. + + Conditions are already accepted for these operator(s): **%@**. No comment provided by engineer. @@ -5972,7 +5972,7 @@ Enable in *Network & servers* settings. Save and notify contact Kaydet ve kişilere bildir - No comment provided by engineer. + alert button Save and notify group members @@ -6623,6 +6623,10 @@ Enable in *Network & servers* settings. SimpleX Adresi No comment provided by engineer. + + SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app. + No comment provided by engineer. + SimpleX Chat security was audited by Trail of Bits. SimpleX Chat güvenliği Trails of Bits tarafından denetlenmiştir. diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff index e228fd01e6..d68b5abbe1 100644 --- a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff @@ -1606,8 +1606,8 @@ Для оператора(ів) приймаються умови: **%@**. No comment provided by engineer. - - Conditions are already accepted for following operator(s): **%@**. + + Conditions are already accepted for these operator(s): **%@**. Умови вже прийняті для наступних операторів: **%@**. No comment provided by engineer. @@ -6034,7 +6034,7 @@ Enable in *Network & servers* settings. Save and notify contact Зберегти та повідомити контакт - No comment provided by engineer. + alert button Save and notify group members @@ -6692,6 +6692,10 @@ Enable in *Network & servers* settings. Адреса SimpleX No comment provided by engineer. + + SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app. + No comment provided by engineer. + SimpleX Chat security was audited by Trail of Bits. Безпека SimpleX Chat була перевірена компанією Trail of Bits. diff --git a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff index 0b1b568385..99d4a5077f 100644 --- a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff +++ b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff @@ -1575,8 +1575,8 @@ Conditions are accepted for the operator(s): **%@**. No comment provided by engineer. - - Conditions are already accepted for following operator(s): **%@**. + + Conditions are already accepted for these operator(s): **%@**. No comment provided by engineer. @@ -5925,7 +5925,7 @@ Enable in *Network & servers* settings. Save and notify contact 保存并通知联系人 - No comment provided by engineer. + alert button Save and notify group members @@ -6570,6 +6570,10 @@ Enable in *Network & servers* settings. SimpleX 地址 No comment provided by engineer. + + SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app. + No comment provided by engineer. + SimpleX Chat security was audited by Trail of Bits. SimpleX Chat 的安全性 由 Trail of Bits 审核。 diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index a9cf2ee599..884993f542 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -1212,13 +1212,12 @@ public enum ServerProtocol: String, Decodable { public enum OperatorTag: String, Codable { case simplex = "simplex" case flux = "flux" - case xyz = "xyz" - case demo = "demo" } -public struct ServerOperatorInfo: Decodable { +public struct ServerOperatorInfo { public var description: [String] - public var website: String + public var website: URL + public var selfhost: (text: String, link: URL)? = nil public var logo: String public var largeLogo: String public var logoDarkMode: String @@ -1228,10 +1227,10 @@ public struct ServerOperatorInfo: Decodable { public let operatorsInfo: Dictionary = [ .simplex: ServerOperatorInfo( description: [ - "SimpleX Chat is the first communication network that has no user profile IDs of any kind, not even random numbers or keys that identify the users.", + "SimpleX Chat is the first communication network that has no user profile IDs of any kind, not even random numbers or identity keys.", "SimpleX Chat Ltd develops the communication software for SimpleX network." ], - website: "https://simplex.chat", + website: URL(string: "https://simplex.chat")!, logo: "decentralized", largeLogo: "logo", logoDarkMode: "decentralized-light", @@ -1239,31 +1238,17 @@ public let operatorsInfo: Dictionary = [ ), .flux: ServerOperatorInfo( description: [ - "Flux is the largest decentralized cloud infrastructure, leveraging a global network of user-operated computational nodes.", - "Flux offers a powerful, scalable, and affordable platform designed to support individuals, businesses, and cutting-edge technologies like AI. With high uptime and worldwide distribution, Flux ensures reliable, accessible cloud computing for all." + "Flux is the largest decentralized cloud, based on a global network of user-operated nodes.", + "Flux offers a powerful, scalable, and affordable cutting edge technology platform for all.", + "Flux operates servers in SimpleX network to improve its privacy and decentralization." ], - website: "https://runonflux.com", + website: URL(string: "https://runonflux.com")!, + selfhost: (text: "Self-host SimpleX servers on Flux", link: URL(string: "https://home.runonflux.io/apps/marketplace?q=simplex")!), logo: "flux_logo_symbol", largeLogo: "flux_logo", logoDarkMode: "flux_logo_symbol", largeLogoDarkMode: "flux_logo-light" ), - .xyz: ServerOperatorInfo( - description: ["XYZ servers"], - website: "XYZ website", - logo: "shield", - largeLogo: "logo", - logoDarkMode: "shield", - largeLogoDarkMode: "logo-light" - ), - .demo: ServerOperatorInfo( - description: ["Demo operator"], - website: "Demo website", - logo: "decentralized", - largeLogo: "logo", - logoDarkMode: "decentralized-light", - largeLogoDarkMode: "logo-light" - ) ] public struct UsageConditions: Decodable { @@ -1358,7 +1343,7 @@ public struct ServerOperator: Identifiable, Equatable, Codable { public static let dummyOperatorInfo = ServerOperatorInfo( description: ["Default"], - website: "Default", + website: URL(string: "https://simplex.chat")!, logo: "decentralized", largeLogo: "logo", logoDarkMode: "decentralized-light", @@ -1384,30 +1369,6 @@ public struct ServerOperator: Identifiable, Equatable, Codable { smpRoles: ServerRoles(storage: true, proxy: true), xftpRoles: ServerRoles(storage: true, proxy: true) ) - - public static var sampleData2 = ServerOperator( - operatorId: 2, - operatorTag: .xyz, - tradeName: "XYZ", - legalName: nil, - serverDomains: ["xyz.com"], - conditionsAcceptance: .required(deadline: nil), - enabled: false, - smpRoles: ServerRoles(storage: false, proxy: true), - xftpRoles: ServerRoles(storage: false, proxy: true) - ) - - public static var sampleData3 = ServerOperator( - operatorId: 3, - operatorTag: .demo, - tradeName: "Demo", - legalName: nil, - serverDomains: ["demo.com"], - conditionsAcceptance: .required(deadline: nil), - enabled: false, - smpRoles: ServerRoles(storage: true, proxy: false), - xftpRoles: ServerRoles(storage: true, proxy: false) - ) } public struct ServerRoles: Equatable, Codable { diff --git a/apps/ios/bg.lproj/Localizable.strings b/apps/ios/bg.lproj/Localizable.strings index 91606d8569..f2059d5627 100644 --- a/apps/ios/bg.lproj/Localizable.strings +++ b/apps/ios/bg.lproj/Localizable.strings @@ -3139,7 +3139,7 @@ /* alert button */ "Save (and notify contacts)" = "Запази (и уведоми контактите)"; -/* No comment provided by engineer. */ +/* alert button */ "Save and notify contact" = "Запази и уведоми контакта"; /* No comment provided by engineer. */ diff --git a/apps/ios/cs.lproj/Localizable.strings b/apps/ios/cs.lproj/Localizable.strings index 96b149a8d5..837e76ebbf 100644 --- a/apps/ios/cs.lproj/Localizable.strings +++ b/apps/ios/cs.lproj/Localizable.strings @@ -2538,7 +2538,7 @@ /* alert button */ "Save (and notify contacts)" = "Uložit (a informovat kontakty)"; -/* No comment provided by engineer. */ +/* alert button */ "Save and notify contact" = "Uložit a upozornit kontakt"; /* No comment provided by engineer. */ diff --git a/apps/ios/de.lproj/Localizable.strings b/apps/ios/de.lproj/Localizable.strings index 25f9cf32c1..a510b30477 100644 --- a/apps/ios/de.lproj/Localizable.strings +++ b/apps/ios/de.lproj/Localizable.strings @@ -1030,7 +1030,7 @@ "Conditions are accepted for the operator(s): **%@**." = "Die Nutzungsbedingungen der/des Betreiber(s) werden akzeptiert: **%@**."; /* No comment provided by engineer. */ -"Conditions are already accepted for following operator(s): **%@**." = "Die Nutzungsbedingungen der/des folgenden Betreiber(s) wurden schon akzeptiert: **%@**."; +"Conditions are already accepted for these operator(s): **%@**." = "Die Nutzungsbedingungen der/des folgenden Betreiber(s) wurden schon akzeptiert: **%@**."; /* No comment provided by engineer. */ "Conditions of use" = "Nutzungsbedingungen"; @@ -3952,7 +3952,7 @@ /* alert button */ "Save (and notify contacts)" = "Speichern (und Kontakte benachrichtigen)"; -/* No comment provided by engineer. */ +/* alert button */ "Save and notify contact" = "Speichern und Kontakt benachrichtigen"; /* No comment provided by engineer. */ @@ -4391,6 +4391,9 @@ /* No comment provided by engineer. */ "SimpleX address or 1-time link?" = "SimpleX-Adresse oder Einmal-Link?"; +/* No comment provided by engineer. */ +"SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app." = "SimpleX-Chat und Flux haben vereinbart, die von Flux betriebenen Server in die App aufzunehmen."; + /* No comment provided by engineer. */ "SimpleX Chat security was audited by Trail of Bits." = "Die Sicherheit von SimpleX Chat wurde von Trail of Bits überprüft."; diff --git a/apps/ios/es.lproj/Localizable.strings b/apps/ios/es.lproj/Localizable.strings index a1665cd716..9c0b815ad4 100644 --- a/apps/ios/es.lproj/Localizable.strings +++ b/apps/ios/es.lproj/Localizable.strings @@ -1030,7 +1030,7 @@ "Conditions are accepted for the operator(s): **%@**." = "Las condiciones se han aceptado para el(los) operador(s): **%@**."; /* No comment provided by engineer. */ -"Conditions are already accepted for following operator(s): **%@**." = "Las condiciones ya se han aceptado para el/los siguiente(s) operador(s): **%@**."; +"Conditions are already accepted for these operator(s): **%@**." = "Las condiciones ya se han aceptado para el/los siguiente(s) operador(s): **%@**."; /* No comment provided by engineer. */ "Conditions of use" = "Condiciones de uso"; @@ -3952,7 +3952,7 @@ /* alert button */ "Save (and notify contacts)" = "Guardar (y notificar contactos)"; -/* No comment provided by engineer. */ +/* alert button */ "Save and notify contact" = "Guardar y notificar contacto"; /* No comment provided by engineer. */ @@ -4391,6 +4391,9 @@ /* No comment provided by engineer. */ "SimpleX address or 1-time link?" = "Dirección SimpleX o enlace de un uso?"; +/* No comment provided by engineer. */ +"SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app." = "Simplex Chat y Flux han acordado incluir servidores operados por Flux en la aplicación"; + /* No comment provided by engineer. */ "SimpleX Chat security was audited by Trail of Bits." = "La seguridad de SimpleX Chat ha sido auditada por Trail of Bits."; diff --git a/apps/ios/fi.lproj/Localizable.strings b/apps/ios/fi.lproj/Localizable.strings index e4b56e76a4..f0987f3e1b 100644 --- a/apps/ios/fi.lproj/Localizable.strings +++ b/apps/ios/fi.lproj/Localizable.strings @@ -2508,7 +2508,7 @@ /* alert button */ "Save (and notify contacts)" = "Tallenna (ja ilmoita kontakteille)"; -/* No comment provided by engineer. */ +/* alert button */ "Save and notify contact" = "Tallenna ja ilmoita kontaktille"; /* No comment provided by engineer. */ diff --git a/apps/ios/fr.lproj/Localizable.strings b/apps/ios/fr.lproj/Localizable.strings index e50d2c0967..2de5997f07 100644 --- a/apps/ios/fr.lproj/Localizable.strings +++ b/apps/ios/fr.lproj/Localizable.strings @@ -3763,7 +3763,7 @@ /* alert button */ "Save (and notify contacts)" = "Enregistrer (et en informer les contacts)"; -/* No comment provided by engineer. */ +/* alert button */ "Save and notify contact" = "Enregistrer et en informer le contact"; /* No comment provided by engineer. */ diff --git a/apps/ios/hu.lproj/Localizable.strings b/apps/ios/hu.lproj/Localizable.strings index 8c0da0ed57..58d28cd8ed 100644 --- a/apps/ios/hu.lproj/Localizable.strings +++ b/apps/ios/hu.lproj/Localizable.strings @@ -1060,7 +1060,7 @@ "Conditions are accepted for the operator(s): **%@**." = "A következő üzemeltető(k) számára elfogadott feltételek: **%@**."; /* No comment provided by engineer. */ -"Conditions are already accepted for following operator(s): **%@**." = "A feltételek már el lettek fogadva a következő üzemeltető(k) számára: **%@**."; +"Conditions are already accepted for these operator(s): **%@**." = "A feltételek már el lettek fogadva a következő üzemeltető(k) számára: **%@**."; /* No comment provided by engineer. */ "Conditions of use" = "Használati feltételek"; @@ -4015,7 +4015,7 @@ /* alert button */ "Save (and notify contacts)" = "Mentés és az ismerősök értesítése"; -/* No comment provided by engineer. */ +/* alert button */ "Save and notify contact" = "Mentés és az ismerős értesítése"; /* No comment provided by engineer. */ diff --git a/apps/ios/it.lproj/Localizable.strings b/apps/ios/it.lproj/Localizable.strings index 1242b488ac..25a672da26 100644 --- a/apps/ios/it.lproj/Localizable.strings +++ b/apps/ios/it.lproj/Localizable.strings @@ -1060,7 +1060,7 @@ "Conditions are accepted for the operator(s): **%@**." = "Le condizioni sono state accettate per gli operatori: **%@**."; /* No comment provided by engineer. */ -"Conditions are already accepted for following operator(s): **%@**." = "Le condizioni sono già state accettate per i seguenti operatori: **%@**."; +"Conditions are already accepted for these operator(s): **%@**." = "Le condizioni sono già state accettate per i seguenti operatori: **%@**."; /* No comment provided by engineer. */ "Conditions of use" = "Condizioni d'uso"; @@ -4015,7 +4015,7 @@ /* alert button */ "Save (and notify contacts)" = "Salva (e avvisa i contatti)"; -/* No comment provided by engineer. */ +/* alert button */ "Save and notify contact" = "Salva e avvisa il contatto"; /* No comment provided by engineer. */ diff --git a/apps/ios/ja.lproj/Localizable.strings b/apps/ios/ja.lproj/Localizable.strings index 3aa64f9b55..da0ba42a86 100644 --- a/apps/ios/ja.lproj/Localizable.strings +++ b/apps/ios/ja.lproj/Localizable.strings @@ -2655,7 +2655,7 @@ /* alert button */ "Save (and notify contacts)" = "保存(連絡先に通知)"; -/* No comment provided by engineer. */ +/* alert button */ "Save and notify contact" = "保存して、連絡先にに知らせる"; /* No comment provided by engineer. */ diff --git a/apps/ios/nl.lproj/Localizable.strings b/apps/ios/nl.lproj/Localizable.strings index 4ead9a726d..ba28bd1f59 100644 --- a/apps/ios/nl.lproj/Localizable.strings +++ b/apps/ios/nl.lproj/Localizable.strings @@ -1060,7 +1060,7 @@ "Conditions are accepted for the operator(s): **%@**." = "Voorwaarden worden geaccepteerd voor de operator(s): **%@**."; /* No comment provided by engineer. */ -"Conditions are already accepted for following operator(s): **%@**." = "Voorwaarden zijn reeds geaccepteerd voor de volgende operator(s): **%@**."; +"Conditions are already accepted for these operator(s): **%@**." = "Voorwaarden zijn reeds geaccepteerd voor de volgende operator(s): **%@**."; /* No comment provided by engineer. */ "Conditions of use" = "Gebruiksvoorwaarden"; @@ -4015,7 +4015,7 @@ /* alert button */ "Save (and notify contacts)" = "Bewaar (en informeer contacten)"; -/* No comment provided by engineer. */ +/* alert button */ "Save and notify contact" = "Opslaan en Contact melden"; /* No comment provided by engineer. */ @@ -4454,6 +4454,9 @@ /* No comment provided by engineer. */ "SimpleX address or 1-time link?" = "SimpleX adres of eenmalige link?"; +/* No comment provided by engineer. */ +"SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app." = "Simplex-chat en flux hebben een overeenkomst gemaakt om door flux geëxploiteerde servers in de app op te nemen."; + /* No comment provided by engineer. */ "SimpleX Chat security was audited by Trail of Bits." = "De beveiliging van SimpleX Chat is gecontroleerd door Trail of Bits."; diff --git a/apps/ios/pl.lproj/Localizable.strings b/apps/ios/pl.lproj/Localizable.strings index e0bcedc965..e48e9f2ed8 100644 --- a/apps/ios/pl.lproj/Localizable.strings +++ b/apps/ios/pl.lproj/Localizable.strings @@ -3736,7 +3736,7 @@ /* alert button */ "Save (and notify contacts)" = "Zapisz (i powiadom kontakty)"; -/* No comment provided by engineer. */ +/* alert button */ "Save and notify contact" = "Zapisz i powiadom kontakt"; /* No comment provided by engineer. */ diff --git a/apps/ios/ru.lproj/Localizable.strings b/apps/ios/ru.lproj/Localizable.strings index 09ee9a2e5a..09c95d4203 100644 --- a/apps/ios/ru.lproj/Localizable.strings +++ b/apps/ios/ru.lproj/Localizable.strings @@ -1066,7 +1066,7 @@ "Conditions are accepted for the operator(s): **%@**." = "Условия приняты для оператора(ов): **%@**."; /* No comment provided by engineer. */ -"Conditions are already accepted for following operator(s): **%@**." = "Условия уже приняты для следующих оператора(ов): **%@**."; +"Conditions are already accepted for these operator(s): **%@**." = "Условия уже приняты для следующих оператора(ов): **%@**."; /* No comment provided by engineer. */ "Conditions of use" = "Условия использования"; @@ -4024,7 +4024,7 @@ /* alert button */ "Save (and notify contacts)" = "Сохранить (и уведомить контакты)"; -/* No comment provided by engineer. */ +/* alert button */ "Save and notify contact" = "Сохранить и уведомить контакт"; /* No comment provided by engineer. */ @@ -4463,6 +4463,9 @@ /* No comment provided by engineer. */ "SimpleX address or 1-time link?" = "Адрес SimpleX или одноразовая ссылка?"; +/* No comment provided by engineer. */ +"SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app." = "SimpleX Chat и Flux заключили соглашение добавить серверы под управлением Flux в приложение."; + /* No comment provided by engineer. */ "SimpleX Chat security was audited by Trail of Bits." = "Безопасность SimpleX Chat была проверена Trail of Bits."; diff --git a/apps/ios/th.lproj/Localizable.strings b/apps/ios/th.lproj/Localizable.strings index 3fee154931..4fdc49139a 100644 --- a/apps/ios/th.lproj/Localizable.strings +++ b/apps/ios/th.lproj/Localizable.strings @@ -2439,7 +2439,7 @@ /* alert button */ "Save (and notify contacts)" = "บันทึก (และแจ้งผู้ติดต่อ)"; -/* No comment provided by engineer. */ +/* alert button */ "Save and notify contact" = "บันทึกและแจ้งผู้ติดต่อ"; /* No comment provided by engineer. */ diff --git a/apps/ios/tr.lproj/Localizable.strings b/apps/ios/tr.lproj/Localizable.strings index a78faed4cd..3670e57955 100644 --- a/apps/ios/tr.lproj/Localizable.strings +++ b/apps/ios/tr.lproj/Localizable.strings @@ -3763,7 +3763,7 @@ /* alert button */ "Save (and notify contacts)" = "Kaydet (ve kişilere bildir)"; -/* No comment provided by engineer. */ +/* alert button */ "Save and notify contact" = "Kaydet ve kişilere bildir"; /* No comment provided by engineer. */ diff --git a/apps/ios/uk.lproj/Localizable.strings b/apps/ios/uk.lproj/Localizable.strings index ce607753fe..4e2b1680fd 100644 --- a/apps/ios/uk.lproj/Localizable.strings +++ b/apps/ios/uk.lproj/Localizable.strings @@ -1030,7 +1030,7 @@ "Conditions are accepted for the operator(s): **%@**." = "Для оператора(ів) приймаються умови: **%@**."; /* No comment provided by engineer. */ -"Conditions are already accepted for following operator(s): **%@**." = "Умови вже прийняті для наступних операторів: **%@**."; +"Conditions are already accepted for these operator(s): **%@**." = "Умови вже прийняті для наступних операторів: **%@**."; /* No comment provided by engineer. */ "Conditions of use" = "Умови використання"; @@ -3952,7 +3952,7 @@ /* alert button */ "Save (and notify contacts)" = "Зберегти (і повідомити контактам)"; -/* No comment provided by engineer. */ +/* alert button */ "Save and notify contact" = "Зберегти та повідомити контакт"; /* No comment provided by engineer. */ diff --git a/apps/ios/zh-Hans.lproj/Localizable.strings b/apps/ios/zh-Hans.lproj/Localizable.strings index 627bfd0c30..c40833b67b 100644 --- a/apps/ios/zh-Hans.lproj/Localizable.strings +++ b/apps/ios/zh-Hans.lproj/Localizable.strings @@ -3634,7 +3634,7 @@ /* alert button */ "Save (and notify contacts)" = "保存(并通知联系人)"; -/* No comment provided by engineer. */ +/* alert button */ "Save and notify contact" = "保存并通知联系人"; /* No comment provided by engineer. */ diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index 94ce22d356..6d13ff191f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -3669,14 +3669,13 @@ enum class ServerProtocol { @Serializable enum class OperatorTag { @SerialName("simplex") SimpleX, - @SerialName("flux") Flux, - @SerialName("xyz") XYZ, - @SerialName("demo") Demo + @SerialName("flux") Flux } data class ServerOperatorInfo( val description: List, val website: String, + val selfhost: Pair? = null, val logo: ImageResource, val largeLogo: ImageResource, val logoDarkMode: ImageResource, @@ -3696,31 +3695,17 @@ val operatorsInfo: Map = mapOf( ), OperatorTag.Flux to ServerOperatorInfo( description = listOf( - "Flux is the largest decentralized cloud infrastructure, leveraging a global network of user-operated computational nodes.", - "Flux offers a powerful, scalable, and affordable platform designed to support individuals, businesses, and cutting-edge technologies like AI. With high uptime and worldwide distribution, Flux ensures reliable, accessible cloud computing for all." + "Flux is the largest decentralized cloud, based on a global network of user-operated nodes.", + "Flux offers a powerful, scalable, and affordable cutting edge technology platform for all.", + "Flux operates servers in SimpleX network to improve its privacy and decentralization." ), website = "https://runonflux.com", + selfhost = "Self-host SimpleX servers on Flux" to "https://home.runonflux.io/apps/marketplace?q=simplex", logo = MR.images.flux_logo_symbol, largeLogo = MR.images.flux_logo, logoDarkMode = MR.images.flux_logo_symbol, largeLogoDarkMode = MR.images.flux_logo_light ), - OperatorTag.XYZ to ServerOperatorInfo( - description = listOf("XYZ servers"), - website = "XYZ website", - logo = MR.images.shield, - largeLogo = MR.images.logo, - logoDarkMode = MR.images.shield, - largeLogoDarkMode = MR.images.logo_light - ), - OperatorTag.Demo to ServerOperatorInfo( - description = listOf("Demo operator"), - website = "Demo website", - logo = MR.images.decentralized, - largeLogo = MR.images.logo, - logoDarkMode = MR.images.decentralized_light, - largeLogoDarkMode = MR.images.logo_light - ) ) @Serializable @@ -3800,7 +3785,7 @@ data class ServerOperator( companion object { val dummyOperatorInfo = ServerOperatorInfo( description = listOf("Default"), - website = "Default", + website = "https://simplex.chat", logo = MR.images.decentralized, largeLogo = MR.images.logo, logoDarkMode = MR.images.decentralized_light, @@ -3818,30 +3803,6 @@ data class ServerOperator( smpRoles = ServerRoles(storage = true, proxy = true), xftpRoles = ServerRoles(storage = true, proxy = true) ) - - val sampleData2 = ServerOperator( - operatorId = 2, - operatorTag = OperatorTag.XYZ, - tradeName = "XYZ", - legalName = null, - serverDomains = listOf("xyz.com"), - conditionsAcceptance = ConditionsAcceptance.Required(deadline = null), - enabled = false, - smpRoles = ServerRoles(storage = false, proxy = true), - xftpRoles = ServerRoles(storage = false, proxy = true) - ) - - val sampleData3 = ServerOperator( - operatorId = 3, - operatorTag = OperatorTag.Demo, - tradeName = "Demo", - legalName = null, - serverDomains = listOf("demo.com"), - conditionsAcceptance = ConditionsAcceptance.Required(deadline = null), - enabled = false, - smpRoles = ServerRoles(storage = true, proxy = false), - xftpRoles = ServerRoles(storage = true, proxy = false) - ) } val id: Long diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt index dde1fb68ce..dcb7d7e133 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt @@ -14,8 +14,10 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import chat.simplex.common.model.* import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.platform.* @@ -72,6 +74,7 @@ fun ModalData.ChooseServerOperators( } Spacer(Modifier.height(DEFAULT_PADDING_HALF)) + SectionTextFooter(annotatedStringResource(MR.strings.onboarding_network_operators_simplex_flux_agreement), textAlign = TextAlign.Center) SectionTextFooter(annotatedStringResource(MR.strings.onboarding_network_operators_configure_via_settings), textAlign = TextAlign.Center) } Spacer(Modifier.weight(1f)) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt index 28ca88584d..dcb1bc9de1 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt @@ -427,23 +427,27 @@ fun OperatorInfoView(serverOperator: ServerOperator) { SectionDividerSpaced(maxBottomPadding = false) + val uriHandler = LocalUriHandler.current SectionView { SectionItemView { Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { serverOperator.info.description.forEach { d -> Text(d) } + val website = serverOperator.info.website + Text(website, color = MaterialTheme.colors.primary, modifier = Modifier.clickable { uriHandler.openUriCatching(website) }) } } } - SectionDividerSpaced() - - SectionView(generalGetString(MR.strings.operator_website).uppercase()) { - SectionItemView { - val website = serverOperator.info.website - val uriHandler = LocalUriHandler.current - Text(website, color = MaterialTheme.colors.primary, modifier = Modifier.clickable { uriHandler.openUriCatching(website) }) + val selfhost = serverOperator.info.selfhost + if (selfhost != null) { + SectionDividerSpaced(maxBottomPadding = false) + SectionView { + SectionItemView { + val (text, link) = selfhost + Text(text, color = MaterialTheme.colors.primary, modifier = Modifier.clickable { uriHandler.openUriCatching(link) }) + } } } } diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 29df338079..82bf5bc8dc 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -1077,6 +1077,7 @@ Server operators Network operators + SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app. The app protects your privacy by using different operators in each conversation. When more than one operator is enabled, none of them has metadata to learn who communicates with whom. For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml index 584377bc99..a0c5ccf01b 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml @@ -2320,6 +2320,7 @@ только с одним контактом - поделитесь при встрече или через любой мессенджер.]]> Нет серверов для доставки сообщений. Вы можете настроить серверы позже. + SimpleX Chat и Flux заключили соглашение добавить серверы под управлением Flux в приложение. Приложение улучшает конфиденциальность используя разных операторов в каждом разговоре. Когда больше чем один оператор включен, ни один из них не видит метаданные, чтобы определить, кто соединен с кем. Ошибка сохранения серверов From ea4927c9b012744b38f8352d8dc1825d75c02ab4 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sat, 7 Dec 2024 16:01:57 +0000 Subject: [PATCH 159/167] core: 6.2.0.7 updated version --- package.yaml | 2 +- simplex-chat.cabal | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.yaml b/package.yaml index b9c41ccdc0..b476741597 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: simplex-chat -version: 6.2.0.6 +version: 6.2.0.7 #synopsis: #description: homepage: https://github.com/simplex-chat/simplex-chat#readme diff --git a/simplex-chat.cabal b/simplex-chat.cabal index ace5afd851..29e748c4e8 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplex-chat -version: 6.2.0.6 +version: 6.2.0.7 category: Web, System, Services, Cryptography homepage: https://github.com/simplex-chat/simplex-chat#readme author: simplex.chat From 93319d947ddb0f8b27e063097bae7dff3c447a4b Mon Sep 17 00:00:00 2001 From: Evgeny Date: Sat, 7 Dec 2024 16:11:30 +0000 Subject: [PATCH 160/167] website: translations (#5350) * Translated using Weblate (Arabic) Currently translated at 100.0% (258 of 258 strings) Translation: SimpleX Chat/SimpleX Chat website Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/ar/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (258 of 258 strings) Translation: SimpleX Chat/SimpleX Chat website Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/uk/ * Translated using Weblate (Arabic) Currently translated at 100.0% (258 of 258 strings) Translation: SimpleX Chat/SimpleX Chat website Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/ar/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (258 of 258 strings) Translation: SimpleX Chat/SimpleX Chat website Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/uk/ * Translated using Weblate (German) Currently translated at 100.0% (258 of 258 strings) Translation: SimpleX Chat/SimpleX Chat website Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/de/ --------- Co-authored-by: jonnysemon Co-authored-by: Bezruchenko Simon Co-authored-by: mlanp --- website/langs/ar.json | 3 ++- website/langs/de.json | 3 ++- website/langs/uk.json | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/website/langs/ar.json b/website/langs/ar.json index 9274e71850..f257c1c747 100644 --- a/website/langs/ar.json +++ b/website/langs/ar.json @@ -255,5 +255,6 @@ "docs-dropdown-10": "الشفافية", "docs-dropdown-11": "الأسئلة الأكثر شيوعًا", "docs-dropdown-12": "الأمان", - "hero-overlay-card-3-p-3": "قامت Trail of Bits بمراجعة التصميم التعموي لبروتوكولات شبكة SimpleX في يوليو 2024. اقرأ المزيد." + "hero-overlay-card-3-p-3": "قامت Trail of Bits بمراجعة التصميم التعموي لبروتوكولات شبكة SimpleX في يوليو 2024. اقرأ المزيد.", + "docs-dropdown-14": "SimpleX للأعمال التجارية" } diff --git a/website/langs/de.json b/website/langs/de.json index c5d1fceefa..3b1e9d34e8 100644 --- a/website/langs/de.json +++ b/website/langs/de.json @@ -255,5 +255,6 @@ "docs-dropdown-10": "Transparent", "docs-dropdown-11": "FAQ", "docs-dropdown-12": "Sicherheit", - "hero-overlay-card-3-p-3": "Trail of Bits hat das kryptografische Design des Netzwerk-Protokolls von SimpleX im Juli 2024 überprüft. Hier finden Sie weitere Informationen dazu." + "hero-overlay-card-3-p-3": "Trail of Bits hat das kryptografische Design des Netzwerk-Protokolls von SimpleX im Juli 2024 überprüft. Hier finden Sie weitere Informationen dazu.", + "docs-dropdown-14": "SimpleX für geschäftliche Anwendungen" } diff --git a/website/langs/uk.json b/website/langs/uk.json index d055aa68a2..794c65c956 100644 --- a/website/langs/uk.json +++ b/website/langs/uk.json @@ -255,5 +255,6 @@ "docs-dropdown-11": "ПОШИРЕНІ ЗАПИТАННЯ", "docs-dropdown-10": "Прозорість", "docs-dropdown-12": "Безпека", - "hero-overlay-card-3-p-3": "Trail of Bits переглянув криптографічний дизайн мережевих протоколів SimpleX в липні 2024 року. Детальніше." + "hero-overlay-card-3-p-3": "Trail of Bits переглянув криптографічний дизайн мережевих протоколів SimpleX в липні 2024 року. Детальніше.", + "docs-dropdown-14": "SimpleX для бізнесу" } From 7d6c7c58d7d9c1174f22318ba330684ee2bda512 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Sat, 7 Dec 2024 16:52:34 +0000 Subject: [PATCH 161/167] ui: translations (#5338) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (2211 of 2211 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/zh_Hans/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2210 of 2210 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2210 of 2210 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Russian) Currently translated at 99.8% (2206 of 2210 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ru/ * Translated using Weblate (Russian) Currently translated at 100.0% (1931 of 1931 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/ru/ * Translated using Weblate (Italian) Currently translated at 100.0% (2210 of 2210 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/it/ * Translated using Weblate (Italian) Currently translated at 100.0% (1931 of 1931 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/it/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2210 of 2210 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Arabic) Currently translated at 97.4% (2153 of 2210 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ar/ * Translated using Weblate (Ukrainian) Currently translated at 99.8% (2206 of 2210 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/uk/ * Translated using Weblate (Arabic) Currently translated at 98.3% (2174 of 2210 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ar/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (2210 of 2210 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/uk/ * Translated using Weblate (Italian) Currently translated at 100.0% (2212 of 2212 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/it/ * Translated using Weblate (Italian) Currently translated at 100.0% (1931 of 1931 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/it/ * Translated using Weblate (Arabic) Currently translated at 100.0% (2212 of 2212 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ar/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (2212 of 2212 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/uk/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2212 of 2212 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (1931 of 1931 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hu/ * Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (2211 of 2211 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/zh_Hans/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2210 of 2210 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2210 of 2210 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Russian) Currently translated at 99.8% (2206 of 2210 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ru/ * Translated using Weblate (Russian) Currently translated at 100.0% (1931 of 1931 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/ru/ * Translated using Weblate (Italian) Currently translated at 100.0% (2210 of 2210 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/it/ * Translated using Weblate (Italian) Currently translated at 100.0% (1931 of 1931 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/it/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2210 of 2210 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Arabic) Currently translated at 97.4% (2153 of 2210 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ar/ * Translated using Weblate (Ukrainian) Currently translated at 99.8% (2206 of 2210 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/uk/ * Translated using Weblate (Arabic) Currently translated at 98.3% (2174 of 2210 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ar/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (2210 of 2210 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/uk/ * Translated using Weblate (Italian) Currently translated at 100.0% (2212 of 2212 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/it/ * Translated using Weblate (Italian) Currently translated at 100.0% (1931 of 1931 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/it/ * Translated using Weblate (Arabic) Currently translated at 100.0% (2212 of 2212 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ar/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (2212 of 2212 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/uk/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2212 of 2212 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (1931 of 1931 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hu/ * Translated using Weblate (Arabic) Currently translated at 100.0% (2212 of 2212 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ar/ * Translated using Weblate (Dutch) Currently translated at 99.8% (2208 of 2212 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/nl/ * Translated using Weblate (Arabic) Currently translated at 100.0% (2212 of 2212 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ar/ * Translated using Weblate (Dutch) Currently translated at 100.0% (2212 of 2212 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/nl/ * Translated using Weblate (Arabic) Currently translated at 100.0% (2212 of 2212 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ar/ * Translated using Weblate (Arabic) Currently translated at 100.0% (2212 of 2212 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ar/ * Translated using Weblate (German) Currently translated at 97.5% (2158 of 2212 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/de/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2212 of 2212 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (1934 of 1934 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hu/ * Translated using Weblate (German) Currently translated at 98.7% (2185 of 2213 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/de/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (2213 of 2213 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/uk/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2213 of 2213 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (1934 of 1934 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hu/ * Translated using Weblate (German) Currently translated at 100.0% (2213 of 2213 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/de/ * Translated using Weblate (German) Currently translated at 100.0% (1934 of 1934 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/de/ * Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (2213 of 2213 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/zh_Hans/ * Translated using Weblate (Spanish) Currently translated at 100.0% (2213 of 2213 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/es/ * Translated using Weblate (Spanish) Currently translated at 100.0% (1934 of 1934 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/es/ * Translated using Weblate (Dutch) Currently translated at 100.0% (2213 of 2213 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/nl/ * Translated using Weblate (Dutch) Currently translated at 100.0% (1934 of 1934 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/nl/ * Translated using Weblate (Arabic) Currently translated at 100.0% (2213 of 2213 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ar/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2213 of 2213 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (1934 of 1934 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2214 of 2214 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (1935 of 1935 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hu/ * process localizations --------- Co-authored-by: 大王叫我来巡山 Co-authored-by: summoner001 Co-authored-by: Ghost of Sparta Co-authored-by: J R Co-authored-by: Random Co-authored-by: jonnysemon Co-authored-by: Bezruchenko Simon Co-authored-by: M1K4 Co-authored-by: mlanp Co-authored-by: No name --- .../de.xcloc/Localized Contents/de.xliff | 31 ++- .../es.xcloc/Localized Contents/es.xliff | 49 +++- .../hu.xcloc/Localized Contents/hu.xliff | 18 +- .../it.xcloc/Localized Contents/it.xliff | 18 +- .../nl.xcloc/Localized Contents/nl.xliff | 3 + .../ru.xcloc/Localized Contents/ru.xliff | 6 +- apps/ios/de.lproj/Localizable.strings | 85 ++++++- apps/ios/es.lproj/Localizable.strings | 103 +++++++- apps/ios/hu.lproj/Localizable.strings | 26 +- apps/ios/it.lproj/Localizable.strings | 18 +- apps/ios/nl.lproj/Localizable.strings | 9 + apps/ios/ru.lproj/Localizable.strings | 6 +- .../commonMain/resources/MR/ar/strings.xml | 235 ++++++++++-------- .../commonMain/resources/MR/de/strings.xml | 120 +++++---- .../commonMain/resources/MR/es/strings.xml | 121 ++++++--- .../commonMain/resources/MR/hu/strings.xml | 23 +- .../commonMain/resources/MR/it/strings.xml | 32 +-- .../commonMain/resources/MR/nl/strings.xml | 8 +- .../commonMain/resources/MR/ru/strings.xml | 7 +- .../commonMain/resources/MR/uk/strings.xml | 101 +++++--- .../resources/MR/zh-rCN/strings.xml | 8 +- 21 files changed, 714 insertions(+), 313 deletions(-) diff --git a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff index 053a1faf73..1fc614becf 100644 --- a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff +++ b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff @@ -579,6 +579,7 @@ About operators + Über Betreiber No comment provided by engineer. @@ -641,6 +642,7 @@ Add friends + Freunde aufnehmen No comment provided by engineer. @@ -660,6 +662,7 @@ Add team members + Team-Mitglieder aufnehmen No comment provided by engineer. @@ -674,6 +677,7 @@ Add your team members to the conversations. + Nehmen Sie Team-Mitglieder in Ihre Unterhaltungen auf. No comment provided by engineer. @@ -1248,10 +1252,12 @@ Business address + Geschäftliche Adresse No comment provided by engineer. Business chats + Geschäftliche Chats No comment provided by engineer. @@ -1398,14 +1404,17 @@ Chat + Chat No comment provided by engineer. Chat already exists + Chat besteht bereits No comment provided by engineer. Chat already exists! + Chat besteht bereits! No comment provided by engineer. @@ -1485,10 +1494,12 @@ Chat will be deleted for all members - this cannot be undone! + Der Chat wird für alle Mitglieder gelöscht. Dies kann nicht rückgängig gemacht werden! No comment provided by engineer. Chat will be deleted for you - this cannot be undone! + Der Chat wird für Sie gelöscht. Dies kann nicht rückgängig gemacht werden! No comment provided by engineer. @@ -2247,6 +2258,7 @@ Das ist Ihr eigener Einmal-Link! Delete chat + Chat löschen No comment provided by engineer. @@ -2261,6 +2273,7 @@ Das ist Ihr eigener Einmal-Link! Delete chat? + Chat löschen? No comment provided by engineer. @@ -2530,6 +2543,7 @@ Das ist Ihr eigener Einmal-Link! Direct messages between members are prohibited in this chat. + In diesem Chat sind Direktnachrichten zwischen Mitgliedern nicht erlaubt. No comment provided by engineer. @@ -4105,6 +4119,7 @@ Weitere Verbesserungen sind bald verfügbar! Invite to chat + Zum Chat einladen No comment provided by engineer. @@ -4267,10 +4282,12 @@ Das ist Ihr Link für die Gruppe %@! Leave chat + Chat verlassen No comment provided by engineer. Leave chat? + Chat verlassen? No comment provided by engineer. @@ -4405,6 +4422,7 @@ Das ist Ihr Link für die Gruppe %@! Member role will be changed to "%@". All chat members will be notified. + Die Rolle des Mitglieds wird auf "%@" geändert. Alle Chat-Mitglieder werden darüber informiert. No comment provided by engineer. @@ -4419,6 +4437,7 @@ Das ist Ihr Link für die Gruppe %@! Member will be removed from chat - this cannot be undone! + Das Mitglied wird aus dem Chat entfernt. Dies kann nicht rückgängig gemacht werden! No comment provided by engineer. @@ -5036,6 +5055,7 @@ Dies erfordert die Aktivierung eines VPNs. Only chat owners can change preferences. + Nur Chat-Eigentümer können die Präferenzen ändern. No comment provided by engineer. @@ -5170,6 +5190,7 @@ Dies erfordert die Aktivierung eines VPNs. Or import archive file + Oder importieren Sie eine Archiv-Datei No comment provided by engineer. @@ -5435,6 +5456,7 @@ Fehler: %@ Privacy for your customers. + Schutz der Privatsphäre Ihrer Kunden. No comment provided by engineer. @@ -7011,6 +7033,7 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Tap Create SimpleX address in the menu to create it later. + Tippen Sie im Menü auf SimpleX-Adresse erstellen, um sie später zu erstellen. No comment provided by engineer. @@ -7212,12 +7235,12 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro The servers for new connections of your current chat profile **%@**. - Mögliche Server für neue Verbindungen von Ihrem aktuellen Chat-Profil **%@**. + Nachrichten-Server für neue Verbindungen über Ihr aktuelles Chat-Profil **%@**. No comment provided by engineer. The servers for new files of your current chat profile **%@**. - Die Server Deines aktuellen Chat-Profils für neue Dateien **%@**. + Medien- und Datei-Server für neue Daten über Ihr aktuelles Chat-Profil **%@**. No comment provided by engineer. @@ -8061,6 +8084,7 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s You are already connected with %@. + Sie sind bereits mit %@ verbunden. No comment provided by engineer. @@ -8339,6 +8363,7 @@ Verbindungsanfrage wiederholen? You will stop receiving messages from this chat. Chat history will be preserved. + Sie werden von diesem Chat keine Nachrichten mehr erhalten. Der Nachrichtenverlauf wird beibehalten. No comment provided by engineer. @@ -8528,6 +8553,7 @@ Verbindungsanfrage wiederholen? accepted invitation + Einladung akzeptiert chat list item title @@ -9214,6 +9240,7 @@ Verbindungsanfrage wiederholen? requested to connect + Zur Verbindung aufgefordert chat list item title diff --git a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff index ea966ea63b..a96aebebae 100644 --- a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff +++ b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff @@ -579,6 +579,7 @@ About operators + Acerca de los operadores No comment provided by engineer. @@ -641,6 +642,7 @@ Add friends + Añadir amigos No comment provided by engineer. @@ -660,6 +662,7 @@ Add team members + Añadir miembros del equipo No comment provided by engineer. @@ -674,6 +677,7 @@ Add your team members to the conversations. + Añade a los miembros de tu equipo a las conversaciones. No comment provided by engineer. @@ -1248,10 +1252,12 @@ Business address + Dirección empresarial No comment provided by engineer. Business chats + Chats empresariales No comment provided by engineer. @@ -1398,14 +1404,17 @@ Chat + Chat No comment provided by engineer. Chat already exists + El chat ya existe No comment provided by engineer. Chat already exists! + ¡El chat ya existe! No comment provided by engineer. @@ -1485,10 +1494,12 @@ Chat will be deleted for all members - this cannot be undone! + El chat será eliminado para todos los miembros. ¡No podrá deshacerse! No comment provided by engineer. Chat will be deleted for you - this cannot be undone! + El chat será eliminado para tí. ¡No podrá deshacerse! No comment provided by engineer. @@ -2247,6 +2258,7 @@ This is your own one-time link! Delete chat + Eliminar chat No comment provided by engineer. @@ -2261,6 +2273,7 @@ This is your own one-time link! Delete chat? + ¿Eliminar chat? No comment provided by engineer. @@ -2295,7 +2308,7 @@ This is your own one-time link! Delete files and media? - Eliminar archivos y multimedia? + ¿Eliminar archivos y multimedia? No comment provided by engineer. @@ -2530,6 +2543,7 @@ This is your own one-time link! Direct messages between members are prohibited in this chat. + Mensajes directos no permitidos entre miembros de este chat. No comment provided by engineer. @@ -2740,7 +2754,7 @@ This is your own one-time link! Enable Flux - Habilitar Flux + Habilita Flux No comment provided by engineer. @@ -4105,6 +4119,7 @@ More improvements are coming soon! Invite to chat + Invitar al chat No comment provided by engineer. @@ -4267,10 +4282,12 @@ This is your link for group %@! Leave chat + Salir del chat No comment provided by engineer. Leave chat? + ¿Salir del chat? No comment provided by engineer. @@ -4405,6 +4422,7 @@ This is your link for group %@! Member role will be changed to "%@". All chat members will be notified. + El rol del miembro cambiará a "%@" y todos serán notificados. No comment provided by engineer. @@ -4419,6 +4437,7 @@ This is your link for group %@! Member will be removed from chat - this cannot be undone! + El miembro será eliminado del chat. ¡No podrá deshacerse! No comment provided by engineer. @@ -5036,6 +5055,7 @@ Requiere activación de la VPN. Only chat owners can change preferences. + Sólo los propietarios del chat pueden cambiar las preferencias. No comment provided by engineer. @@ -5170,6 +5190,7 @@ Requiere activación de la VPN. Or import archive file + O importa desde un archivo No comment provided by engineer. @@ -5410,7 +5431,7 @@ Error: %@ Preset server address - Dirección del servidor predefinida + Dirección predefinida del servidor No comment provided by engineer. @@ -5435,6 +5456,7 @@ Error: %@ Privacy for your customers. + Privacidad para tus clientes. No comment provided by engineer. @@ -5631,7 +5653,7 @@ Actívalo en ajustes de *Servidores y Redes*. Read more - Conoce más + Saber más No comment provided by engineer. @@ -6589,7 +6611,7 @@ Actívalo en ajustes de *Servidores y Redes*. Share SimpleX address on social media. - Compartir dirección SimpleX en redes sociales. + Comparte tu dirección SimpleX en redes sociales. No comment provided by engineer. @@ -6729,12 +6751,12 @@ Actívalo en ajustes de *Servidores y Redes*. SimpleX address and 1-time links are safe to share via any messenger. - Compartir enlaces de un uso y direcciones SimpleX es seguro a través de cualquier medio. + Compartir los enlaces de un uso y las direcciones SimpleX es seguro a través de cualquier medio. No comment provided by engineer. SimpleX address or 1-time link? - Dirección SimpleX o enlace de un uso? + ¿Dirección SimpleX o enlace de un uso? No comment provided by engineer. @@ -7011,6 +7033,7 @@ Actívalo en ajustes de *Servidores y Redes*. Tap Create SimpleX address in the menu to create it later. + Pulsa Crear dirección SimpleX en el menú para crearla más tarde. No comment provided by engineer. @@ -7197,7 +7220,7 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. The second preset operator in the app! - El segundo operador predefinido! + ¡Segundo operador predefinido! No comment provided by engineer. @@ -7212,7 +7235,7 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. The servers for new connections of your current chat profile **%@**. - Lista de servidores para las conexiones nuevas de tu perfil actual **%@**. + Lista de servidores para las conexiones nuevas del perfil **%@**. No comment provided by engineer. @@ -8056,11 +8079,12 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión You are already connected to %@. - Ya estás conectado a %@. + Ya estás conectado con %@. No comment provided by engineer. You are already connected with %@. + Ya estás conectado con %@. No comment provided by engineer. @@ -8339,6 +8363,7 @@ Repeat connection request? You will stop receiving messages from this chat. Chat history will be preserved. + Dejarás de recibir mensajes de este chat. El historial del chat se conserva. No comment provided by engineer. @@ -8528,6 +8553,7 @@ Repeat connection request? accepted invitation + invitación aceptada chat list item title @@ -8912,7 +8938,7 @@ Repeat connection request? for better metadata privacy. - para mayor privacidad de los metadatos. + para mejorar la privacidad de los metadatos. No comment provided by engineer. @@ -9214,6 +9240,7 @@ Repeat connection request? requested to connect + solicitado para conectar chat list item title diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff index 598bee5485..c682a02d8c 100644 --- a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff @@ -574,11 +574,12 @@ About SimpleX Chat - A SimpleX Chatről + SimpleX Chat névjegye No comment provided by engineer. About operators + Az üzemeltetőkről No comment provided by engineer. @@ -1352,7 +1353,7 @@ Change chat profiles - Felhasználói profilok megváltoztatása + Csevegési profilok megváltoztatása authentication reason @@ -3525,7 +3526,7 @@ Ez az Ön egyszer használható meghívó-hivatkozása! For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server. - Ha például az ismerőse a SimpleX Chat kiszolgálón keresztül fogadja az üzeneteket, az Ön alkalmazása a Flux egyik kiszolgálóját használja a kézbesítéshez. + Például, ha az Ön ismerőse egy SimpleX Chat-kiszolgálón keresztül fogadja az üzeneteket, az Ön alkalmazása egy Flux-kiszolgálón keresztül fogja azokat kézbesíteni. No comment provided by engineer. @@ -5570,7 +5571,7 @@ Hiba: %@ Protect IP address - IP-cím védelem + IP-cím védelme No comment provided by engineer. @@ -6715,6 +6716,7 @@ Engedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app. + A SimpleX Chat és a Flux megállapodást kötött arról, hogy a Flux által üzemeltetett kiszolgálókat beépítik az alkalmazásba. No comment provided by engineer. @@ -7208,7 +7210,7 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. The same conditions will apply to operator **%@**. - Ugyanezek a feltételek vonatkoznak a következő üzemeltetőre is: **%@**. + Ugyanezek a feltételek lesznek elfogadva a következő üzemeltetőre is: **%@**. No comment provided by engineer. @@ -8154,7 +8156,7 @@ Csatlakozáskérés megismétlése? You can configure servers via settings. - A kiszolgálókat a beállításokon keresztül konfigurálhatja. + A kiszolgálókat a „Hálózat és kiszolgálók” menüben konfigurálhatja. No comment provided by engineer. @@ -8204,7 +8206,7 @@ Csatlakozáskérés megismétlése? You can set lock screen notification preview via settings. - A beállításokon keresztül beállíthatja a lezárási képernyő értesítési előnézetét. + A lezárási képernyő értesítési előnézetét az „Értesítések” menüben állíthatja be. No comment provided by engineer. @@ -8551,6 +8553,7 @@ Kapcsolatkérés megismétlése? accepted invitation + elfogadott meghívó chat list item title @@ -9237,6 +9240,7 @@ Kapcsolatkérés megismétlése? requested to connect + kérelmezve a kapcsolódáshoz chat list item title diff --git a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff index 67633b7ae8..a3aa28580c 100644 --- a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff +++ b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff @@ -1091,12 +1091,12 @@ Auto-accept contact requests - Auto-accetta richieste di contatto + Auto-accetta le richieste di contatto No comment provided by engineer. Auto-accept images - Auto-accetta immagini + Auto-accetta le immagini No comment provided by engineer. @@ -1216,7 +1216,7 @@ Blur media - Sfocatura file multimediali + Sfocatura dei file multimediali No comment provided by engineer. @@ -4501,7 +4501,7 @@ Questo è il tuo link per il gruppo %@! Message draft - Bozza dei messaggi + Bozza del messaggio No comment provided by engineer. @@ -5988,12 +5988,12 @@ Attivalo nelle impostazioni *Rete e server*. Review conditions - Esamina le condizioni + Leggi le condizioni No comment provided by engineer. Review later - Esamina più tardi + Leggi più tardi No comment provided by engineer. @@ -6289,7 +6289,7 @@ Attivalo nelle impostazioni *Rete e server*. Send link previews - Invia anteprime dei link + Invia le anteprime dei link No comment provided by engineer. @@ -6610,7 +6610,7 @@ Attivalo nelle impostazioni *Rete e server*. Share SimpleX address on social media. - Condividi indirizzo SimpleX sui social media. + Condividi l'indirizzo SimpleX sui social media. No comment provided by engineer. @@ -7031,7 +7031,7 @@ Attivalo nelle impostazioni *Rete e server*. Tap Create SimpleX address in the menu to create it later. - Tocca "Crea indirizzo SimpleX" nel menu per crearlo più tardi. + Tocca Crea indirizzo SimpleX nel menu per crearlo più tardi. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff index c30370fc5a..73a9d05b73 100644 --- a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff @@ -579,6 +579,7 @@ About operators + Over operatoren No comment provided by engineer. @@ -8552,6 +8553,7 @@ Verbindingsverzoek herhalen? accepted invitation + geaccepteerde uitnodiging chat list item title @@ -9238,6 +9240,7 @@ Verbindingsverzoek herhalen? requested to connect + gevraagd om verbinding te maken chat list item title diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff index 943ea67ef4..814b878a03 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff @@ -384,7 +384,7 @@ **Scan / Paste link**: to connect via a link you received. - **Сканировать / Вставить ссылку**: чтобы соединится через полученную ссылку. + **Сканировать / Вставить ссылку**: чтобы соединиться через полученную ссылку. No comment provided by engineer. @@ -5582,7 +5582,7 @@ Error: %@ Protect your IP address from the messaging relays chosen by your contacts. Enable in *Network & servers* settings. Защитите ваш IP адрес от серверов сообщений, выбранных Вашими контактами. -Включите в настройках *Сеть и серверы*. +Включите в настройках *Сети и серверов*. No comment provided by engineer. @@ -8150,7 +8150,7 @@ Repeat join request? You can configure operators in Network & servers settings. - Вы можете настроить операторов в настройках Сеть и серверы. + Вы можете настроить операторов в настройках Сети и серверов. No comment provided by engineer. diff --git a/apps/ios/de.lproj/Localizable.strings b/apps/ios/de.lproj/Localizable.strings index a510b30477..7d69adc1d5 100644 --- a/apps/ios/de.lproj/Localizable.strings +++ b/apps/ios/de.lproj/Localizable.strings @@ -343,6 +343,9 @@ /* No comment provided by engineer. */ "Abort changing address?" = "Wechsel der Empfängeradresse beenden?"; +/* No comment provided by engineer. */ +"About operators" = "Über Betreiber"; + /* No comment provided by engineer. */ "About SimpleX Chat" = "Über SimpleX Chat"; @@ -376,6 +379,9 @@ /* No comment provided by engineer. */ "Accepted conditions" = "Akzeptierte Nutzungsbedingungen"; +/* chat list item title */ +"accepted invitation" = "Einladung akzeptiert"; + /* No comment provided by engineer. */ "Acknowledged" = "Bestätigt"; @@ -388,6 +394,9 @@ /* No comment provided by engineer. */ "Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." = "Fügen Sie die Adresse Ihrem Profil hinzu, damit Ihre Kontakte sie mit anderen Personen teilen können. Es wird eine Profilaktualisierung an Ihre Kontakte gesendet."; +/* No comment provided by engineer. */ +"Add friends" = "Freunde aufnehmen"; + /* No comment provided by engineer. */ "Add profile" = "Profil hinzufügen"; @@ -397,12 +406,18 @@ /* No comment provided by engineer. */ "Add servers by scanning QR codes." = "Fügen Sie Server durch Scannen der QR Codes hinzu."; +/* No comment provided by engineer. */ +"Add team members" = "Team-Mitglieder aufnehmen"; + /* No comment provided by engineer. */ "Add to another device" = "Einem anderen Gerät hinzufügen"; /* No comment provided by engineer. */ "Add welcome message" = "Begrüßungsmeldung hinzufügen"; +/* No comment provided by engineer. */ +"Add your team members to the conversations." = "Nehmen Sie Team-Mitglieder in Ihre Unterhaltungen auf."; + /* No comment provided by engineer. */ "Added media & file servers" = "Medien- und Dateiserver hinzugefügt"; @@ -793,6 +808,12 @@ /* No comment provided by engineer. */ "Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" = "Bulgarisch, Finnisch, Thailändisch und Ukrainisch - Dank der Nutzer und [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"; +/* No comment provided by engineer. */ +"Business address" = "Geschäftliche Adresse"; + +/* No comment provided by engineer. */ +"Business chats" = "Geschäftliche Chats"; + /* 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)." = "Per Chat-Profil (Voreinstellung) oder [per Verbindung](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)."; @@ -909,6 +930,15 @@ /* chat item text */ "changing address…" = "Wechsel der Empfängeradresse wurde gestartet…"; +/* No comment provided by engineer. */ +"Chat" = "Chat"; + +/* No comment provided by engineer. */ +"Chat already exists" = "Chat besteht bereits"; + +/* No comment provided by engineer. */ +"Chat already exists!" = "Chat besteht bereits!"; + /* No comment provided by engineer. */ "Chat colors" = "Chat-Farben"; @@ -954,6 +984,12 @@ /* No comment provided by engineer. */ "Chat theme" = "Chat-Design"; +/* No comment provided by engineer. */ +"Chat will be deleted for all members - this cannot be undone!" = "Der Chat wird für alle Mitglieder gelöscht. Dies kann nicht rückgängig gemacht werden!"; + +/* No comment provided by engineer. */ +"Chat will be deleted for you - this cannot be undone!" = "Der Chat wird für Sie gelöscht. Dies kann nicht rückgängig gemacht werden!"; + /* No comment provided by engineer. */ "Chats" = "Chats"; @@ -1475,12 +1511,18 @@ /* No comment provided by engineer. */ "Delete and notify contact" = "Kontakt löschen und benachrichtigen"; +/* No comment provided by engineer. */ +"Delete chat" = "Chat löschen"; + /* No comment provided by engineer. */ "Delete chat profile" = "Chat-Profil löschen"; /* No comment provided by engineer. */ "Delete chat profile?" = "Chat-Profil löschen?"; +/* No comment provided by engineer. */ +"Delete chat?" = "Chat löschen?"; + /* No comment provided by engineer. */ "Delete connection" = "Verbindung löschen"; @@ -1655,6 +1697,9 @@ /* chat feature */ "Direct messages" = "Direkte Nachrichten"; +/* No comment provided by engineer. */ +"Direct messages between members are prohibited in this chat." = "In diesem Chat sind Direktnachrichten zwischen Mitgliedern nicht erlaubt."; + /* No comment provided by engineer. */ "Direct messages between members are prohibited." = "In dieser Gruppe sind Direktnachrichten zwischen Mitgliedern nicht erlaubt."; @@ -2694,6 +2739,9 @@ /* No comment provided by engineer. */ "Invite members" = "Mitglieder einladen"; +/* No comment provided by engineer. */ +"Invite to chat" = "Zum Chat einladen"; + /* No comment provided by engineer. */ "Invite to group" = "In Gruppe einladen"; @@ -2808,6 +2856,12 @@ /* swipe action */ "Leave" = "Verlassen"; +/* No comment provided by engineer. */ +"Leave chat" = "Chat verlassen"; + +/* No comment provided by engineer. */ +"Leave chat?" = "Chat verlassen?"; + /* No comment provided by engineer. */ "Leave group" = "Gruppe verlassen"; @@ -2904,12 +2958,18 @@ /* item status text */ "Member inactive" = "Mitglied inaktiv"; +/* No comment provided by engineer. */ +"Member role will be changed to \"%@\". All chat members will be notified." = "Die Rolle des Mitglieds wird auf \"%@\" geändert. Alle Chat-Mitglieder werden darüber informiert."; + /* No comment provided by engineer. */ "Member role will be changed to \"%@\". All group members will be notified." = "Die Mitgliederrolle wird auf \"%@\" geändert. Alle Mitglieder der Gruppe werden benachrichtigt."; /* No comment provided by engineer. */ "Member role will be changed to \"%@\". The member will receive a new invitation." = "Die Mitgliederrolle wird auf \"%@\" geändert. Das Mitglied wird eine neue Einladung erhalten."; +/* No comment provided by engineer. */ +"Member will be removed from chat - this cannot be undone!" = "Das Mitglied wird aus dem Chat entfernt. Dies kann nicht rückgängig gemacht werden!"; + /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "Das Mitglied wird aus der Gruppe entfernt. Dies kann nicht rückgängig gemacht werden!"; @@ -3329,6 +3389,9 @@ /* No comment provided by engineer. */ "Onion hosts will not be used." = "Onion-Hosts werden nicht verwendet."; +/* No comment provided by engineer. */ +"Only chat owners can change preferences." = "Nur Chat-Eigentümer können die Präferenzen ändern."; + /* No comment provided by engineer. */ "Only client devices store user profiles, contacts, groups, and messages." = "Nur die Endgeräte speichern die Benutzerprofile, Kontakte, Gruppen und Nachrichten, welche über eine **2-Schichten Ende-zu-Ende-Verschlüsselung** gesendet werden."; @@ -3407,6 +3470,9 @@ /* alert title */ "Operator server" = "Betreiber-Server"; +/* No comment provided by engineer. */ +"Or import archive file" = "Oder importieren Sie eine Archiv-Datei"; + /* No comment provided by engineer. */ "Or paste archive link" = "Oder fügen Sie den Archiv-Link ein"; @@ -3575,6 +3641,9 @@ /* No comment provided by engineer. */ "Privacy & security" = "Datenschutz & Sicherheit"; +/* No comment provided by engineer. */ +"Privacy for your customers." = "Schutz der Privatsphäre Ihrer Kunden."; + /* No comment provided by engineer. */ "Privacy redefined" = "Datenschutz neu definiert"; @@ -3867,6 +3936,9 @@ /* chat item action */ "Reply" = "Antwort"; +/* chat list item title */ +"requested to connect" = "Zur Verbindung aufgefordert"; + /* No comment provided by engineer. */ "Required" = "Erforderlich"; @@ -4574,6 +4646,9 @@ /* No comment provided by engineer. */ "Tap button " = "Schaltfläche antippen "; +/* No comment provided by engineer. */ +"Tap Create SimpleX address in the menu to create it later." = "Tippen Sie im Menü auf SimpleX-Adresse erstellen, um sie später zu erstellen."; + /* No comment provided by engineer. */ "Tap to activate profile." = "Zum Aktivieren des Profils tippen."; @@ -4704,10 +4779,10 @@ "The sender will NOT be notified" = "Der Absender wird NICHT benachrichtigt"; /* No comment provided by engineer. */ -"The servers for new connections of your current chat profile **%@**." = "Mögliche Server für neue Verbindungen von Ihrem aktuellen Chat-Profil **%@**."; +"The servers for new connections of your current chat profile **%@**." = "Nachrichten-Server für neue Verbindungen über Ihr aktuelles Chat-Profil **%@**."; /* No comment provided by engineer. */ -"The servers for new files of your current chat profile **%@**." = "Die Server Deines aktuellen Chat-Profils für neue Dateien **%@**."; +"The servers for new files of your current chat profile **%@**." = "Medien- und Datei-Server für neue Daten über Ihr aktuelles Chat-Profil **%@**."; /* No comment provided by engineer. */ "The text you pasted is not a SimpleX link." = "Der von Ihnen eingefügte Text ist kein SimpleX-Link."; @@ -5282,6 +5357,9 @@ /* No comment provided by engineer. */ "You are already connected to %@." = "Sie sind bereits mit %@ verbunden."; +/* No comment provided by engineer. */ +"You are already connected with %@." = "Sie sind bereits mit %@ verbunden."; + /* No comment provided by engineer. */ "You are already connecting to %@." = "Sie sind bereits mit %@ verbunden."; @@ -5480,6 +5558,9 @@ /* No comment provided by engineer. */ "You will still receive calls and notifications from muted profiles when they are active." = "Sie können Anrufe und Benachrichtigungen auch von stummgeschalteten Profilen empfangen, solange diese aktiv sind."; +/* No comment provided by engineer. */ +"You will stop receiving messages from this chat. Chat history will be preserved." = "Sie werden von diesem Chat keine Nachrichten mehr erhalten. Der Nachrichtenverlauf wird beibehalten."; + /* No comment provided by engineer. */ "You will stop receiving messages from this group. Chat history will be preserved." = "Sie werden von dieser Gruppe keine Nachrichten mehr erhalten. Der Nachrichtenverlauf wird beibehalten."; diff --git a/apps/ios/es.lproj/Localizable.strings b/apps/ios/es.lproj/Localizable.strings index 9c0b815ad4..ce36dec953 100644 --- a/apps/ios/es.lproj/Localizable.strings +++ b/apps/ios/es.lproj/Localizable.strings @@ -343,6 +343,9 @@ /* No comment provided by engineer. */ "Abort changing address?" = "¿Cancelar el cambio de servidor?"; +/* No comment provided by engineer. */ +"About operators" = "Acerca de los operadores"; + /* No comment provided by engineer. */ "About SimpleX Chat" = "Sobre SimpleX Chat"; @@ -376,6 +379,9 @@ /* No comment provided by engineer. */ "Accepted conditions" = "Condiciones aceptadas"; +/* chat list item title */ +"accepted invitation" = "invitación aceptada"; + /* No comment provided by engineer. */ "Acknowledged" = "Confirmaciones"; @@ -388,6 +394,9 @@ /* No comment provided by engineer. */ "Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." = "Añade la dirección a tu perfil para que tus contactos puedan compartirla con otros. La actualización del perfil se enviará a tus contactos."; +/* No comment provided by engineer. */ +"Add friends" = "Añadir amigos"; + /* No comment provided by engineer. */ "Add profile" = "Añadir perfil"; @@ -397,12 +406,18 @@ /* No comment provided by engineer. */ "Add servers by scanning QR codes." = "Añadir servidores mediante el escaneo de códigos QR."; +/* No comment provided by engineer. */ +"Add team members" = "Añadir miembros del equipo"; + /* No comment provided by engineer. */ "Add to another device" = "Añadir a otro dispositivo"; /* No comment provided by engineer. */ "Add welcome message" = "Añadir mensaje de bienvenida"; +/* No comment provided by engineer. */ +"Add your team members to the conversations." = "Añade a los miembros de tu equipo a las conversaciones."; + /* No comment provided by engineer. */ "Added media & file servers" = "Servidores de archivos y multimedia añadidos"; @@ -793,6 +808,12 @@ /* No comment provided by engineer. */ "Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" = "Búlgaro, Finlandés, Tailandés y Ucraniano - gracias a los usuarios y [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"; +/* No comment provided by engineer. */ +"Business address" = "Dirección empresarial"; + +/* No comment provided by engineer. */ +"Business chats" = "Chats empresariales"; + /* 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)." = "Mediante perfil (predeterminado) o [por conexión](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)."; @@ -909,6 +930,15 @@ /* chat item text */ "changing address…" = "cambiando de servidor…"; +/* No comment provided by engineer. */ +"Chat" = "Chat"; + +/* No comment provided by engineer. */ +"Chat already exists" = "El chat ya existe"; + +/* No comment provided by engineer. */ +"Chat already exists!" = "¡El chat ya existe!"; + /* No comment provided by engineer. */ "Chat colors" = "Colores del chat"; @@ -954,6 +984,12 @@ /* No comment provided by engineer. */ "Chat theme" = "Tema de chat"; +/* No comment provided by engineer. */ +"Chat will be deleted for all members - this cannot be undone!" = "El chat será eliminado para todos los miembros. ¡No podrá deshacerse!"; + +/* No comment provided by engineer. */ +"Chat will be deleted for you - this cannot be undone!" = "El chat será eliminado para tí. ¡No podrá deshacerse!"; + /* No comment provided by engineer. */ "Chats" = "Chats"; @@ -1475,12 +1511,18 @@ /* No comment provided by engineer. */ "Delete and notify contact" = "Eliminar y notificar contacto"; +/* No comment provided by engineer. */ +"Delete chat" = "Eliminar chat"; + /* No comment provided by engineer. */ "Delete chat profile" = "Eliminar perfil"; /* No comment provided by engineer. */ "Delete chat profile?" = "¿Eliminar perfil?"; +/* No comment provided by engineer. */ +"Delete chat?" = "¿Eliminar chat?"; + /* No comment provided by engineer. */ "Delete connection" = "Eliminar conexión"; @@ -1500,7 +1542,7 @@ "Delete file" = "Eliminar archivo"; /* No comment provided by engineer. */ -"Delete files and media?" = "Eliminar archivos y multimedia?"; +"Delete files and media?" = "¿Eliminar archivos y multimedia?"; /* No comment provided by engineer. */ "Delete files for all chat profiles" = "Eliminar archivos de todos los perfiles"; @@ -1655,6 +1697,9 @@ /* chat feature */ "Direct messages" = "Mensajes directos"; +/* No comment provided by engineer. */ +"Direct messages between members are prohibited in this chat." = "Mensajes directos no permitidos entre miembros de este chat."; + /* No comment provided by engineer. */ "Direct messages between members are prohibited." = "Los mensajes directos entre miembros del grupo no están permitidos."; @@ -1798,7 +1843,7 @@ "Enable camera access" = "Permitir acceso a la cámara"; /* No comment provided by engineer. */ -"Enable Flux" = "Habilitar Flux"; +"Enable Flux" = "Habilita Flux"; /* No comment provided by engineer. */ "Enable for all" = "Activar para todos"; @@ -2296,7 +2341,7 @@ "Fix not supported by group member" = "Corrección no compatible con miembro del grupo"; /* No comment provided by engineer. */ -"for better metadata privacy." = "para mayor privacidad de los metadatos."; +"for better metadata privacy." = "para mejorar la privacidad de los metadatos."; /* servers error */ "For chat profile %@:" = "Para el perfil de chat %@:"; @@ -2694,6 +2739,9 @@ /* No comment provided by engineer. */ "Invite members" = "Invitar miembros"; +/* No comment provided by engineer. */ +"Invite to chat" = "Invitar al chat"; + /* No comment provided by engineer. */ "Invite to group" = "Invitar al grupo"; @@ -2808,6 +2856,12 @@ /* swipe action */ "Leave" = "Salir"; +/* No comment provided by engineer. */ +"Leave chat" = "Salir del chat"; + +/* No comment provided by engineer. */ +"Leave chat?" = "¿Salir del chat?"; + /* No comment provided by engineer. */ "Leave group" = "Salir del grupo"; @@ -2904,12 +2958,18 @@ /* item status text */ "Member inactive" = "Miembro inactivo"; +/* No comment provided by engineer. */ +"Member role will be changed to \"%@\". All chat members will be notified." = "El rol del miembro cambiará a \"%@\" y todos serán notificados."; + /* No comment provided by engineer. */ "Member role will be changed to \"%@\". All group members will be notified." = "El rol del miembro cambiará a \"%@\" y se notificará al grupo."; /* No comment provided by engineer. */ "Member role will be changed to \"%@\". The member will receive a new invitation." = "El rol del miembro cambiará a \"%@\" y recibirá una invitación nueva."; +/* No comment provided by engineer. */ +"Member will be removed from chat - this cannot be undone!" = "El miembro será eliminado del chat. ¡No podrá deshacerse!"; + /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "El miembro será expulsado del grupo. ¡No podrá deshacerse!"; @@ -3329,6 +3389,9 @@ /* No comment provided by engineer. */ "Onion hosts will not be used." = "No se usarán hosts .onion."; +/* No comment provided by engineer. */ +"Only chat owners can change preferences." = "Sólo los propietarios del chat pueden cambiar las preferencias."; + /* No comment provided by engineer. */ "Only client devices store user profiles, contacts, groups, and messages." = "Sólo los dispositivos cliente almacenan perfiles de usuario, contactos, grupos y mensajes enviados con **cifrado de extremo a extremo de 2 capas**."; @@ -3407,6 +3470,9 @@ /* alert title */ "Operator server" = "Servidor del operador"; +/* No comment provided by engineer. */ +"Or import archive file" = "O importa desde un archivo"; + /* No comment provided by engineer. */ "Or paste archive link" = "O pegar enlace del archivo"; @@ -3561,7 +3627,7 @@ "Preserve the last message draft, with attachments." = "Conserva el último borrador del mensaje con los datos adjuntos."; /* No comment provided by engineer. */ -"Preset server address" = "Dirección del servidor predefinida"; +"Preset server address" = "Dirección predefinida del servidor"; /* No comment provided by engineer. */ "Preset servers" = "Servidores predefinidos"; @@ -3575,6 +3641,9 @@ /* No comment provided by engineer. */ "Privacy & security" = "Seguridad y Privacidad"; +/* No comment provided by engineer. */ +"Privacy for your customers." = "Privacidad para tus clientes."; + /* No comment provided by engineer. */ "Privacy redefined" = "Privacidad redefinida"; @@ -3693,7 +3762,7 @@ "Read" = "Leer"; /* No comment provided by engineer. */ -"Read more" = "Conoce más"; +"Read more" = "Saber más"; /* No comment provided by engineer. */ "Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." = "Conoce más en la [Guía del Usuario](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)."; @@ -3867,6 +3936,9 @@ /* chat item action */ "Reply" = "Responder"; +/* chat list item title */ +"requested to connect" = "solicitado para conectar"; + /* No comment provided by engineer. */ "Required" = "Obligatorio"; @@ -4338,7 +4410,7 @@ "Share profile" = "Comparte perfil"; /* No comment provided by engineer. */ -"Share SimpleX address on social media." = "Compartir dirección SimpleX en redes sociales."; +"Share SimpleX address on social media." = "Comparte tu dirección SimpleX en redes sociales."; /* No comment provided by engineer. */ "Share this 1-time invite link" = "Comparte este enlace de un solo uso"; @@ -4386,10 +4458,10 @@ "SimpleX Address" = "Dirección SimpleX"; /* No comment provided by engineer. */ -"SimpleX address and 1-time links are safe to share via any messenger." = "Compartir enlaces de un uso y direcciones SimpleX es seguro a través de cualquier medio."; +"SimpleX address and 1-time links are safe to share via any messenger." = "Compartir los enlaces de un uso y las direcciones SimpleX es seguro a través de cualquier medio."; /* No comment provided by engineer. */ -"SimpleX address or 1-time link?" = "Dirección SimpleX o enlace de un uso?"; +"SimpleX address or 1-time link?" = "¿Dirección SimpleX o enlace de un uso?"; /* No comment provided by engineer. */ "SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app." = "Simplex Chat y Flux han acordado incluir servidores operados por Flux en la aplicación"; @@ -4574,6 +4646,9 @@ /* No comment provided by engineer. */ "Tap button " = "Pulsa el botón "; +/* No comment provided by engineer. */ +"Tap Create SimpleX address in the menu to create it later." = "Pulsa Crear dirección SimpleX en el menú para crearla más tarde."; + /* No comment provided by engineer. */ "Tap to activate profile." = "Pulsa sobre un perfil para activarlo."; @@ -4695,7 +4770,7 @@ "The same conditions will apply to operator(s): **%@**." = "Las mismas condiciones se aplicarán a el/los operador(es) **%@**."; /* No comment provided by engineer. */ -"The second preset operator in the app!" = "El segundo operador predefinido!"; +"The second preset operator in the app!" = "¡Segundo operador predefinido!"; /* No comment provided by engineer. */ "The second tick we missed! ✅" = "¡El doble check que nos faltaba! ✅"; @@ -4704,7 +4779,7 @@ "The sender will NOT be notified" = "El remitente NO será notificado"; /* No comment provided by engineer. */ -"The servers for new connections of your current chat profile **%@**." = "Lista de servidores para las conexiones nuevas de tu perfil actual **%@**."; +"The servers for new connections of your current chat profile **%@**." = "Lista de servidores para las conexiones nuevas del perfil **%@**."; /* No comment provided by engineer. */ "The servers for new files of your current chat profile **%@**." = "Los servidores para archivos nuevos en tu perfil actual **%@**."; @@ -5280,7 +5355,10 @@ "You already have a chat profile with the same display name. Please choose another name." = "Ya tienes un perfil con este nombre mostrado. Por favor, elige otro nombre."; /* No comment provided by engineer. */ -"You are already connected to %@." = "Ya estás conectado a %@."; +"You are already connected to %@." = "Ya estás conectado con %@."; + +/* No comment provided by engineer. */ +"You are already connected with %@." = "Ya estás conectado con %@."; /* No comment provided by engineer. */ "You are already connecting to %@." = "Ya estás conectando con %@."; @@ -5480,6 +5558,9 @@ /* No comment provided by engineer. */ "You will still receive calls and notifications from muted profiles when they are active." = "Seguirás recibiendo llamadas y notificaciones de los perfiles silenciados cuando estén activos."; +/* No comment provided by engineer. */ +"You will stop receiving messages from this chat. Chat history will be preserved." = "Dejarás de recibir mensajes de este chat. El historial del chat se conserva."; + /* No comment provided by engineer. */ "You will stop receiving messages from this group. Chat history will be preserved." = "Dejarás de recibir mensajes de este grupo. El historial del chat se conservará."; diff --git a/apps/ios/hu.lproj/Localizable.strings b/apps/ios/hu.lproj/Localizable.strings index 58d28cd8ed..594bd3a123 100644 --- a/apps/ios/hu.lproj/Localizable.strings +++ b/apps/ios/hu.lproj/Localizable.strings @@ -344,7 +344,10 @@ "Abort changing address?" = "Címváltoztatás megszakítása??"; /* No comment provided by engineer. */ -"About SimpleX Chat" = "A SimpleX Chatről"; +"About operators" = "Az üzemeltetőkről"; + +/* No comment provided by engineer. */ +"About SimpleX Chat" = "SimpleX Chat névjegye"; /* No comment provided by engineer. */ "above, then choose:" = "gombra fent, majd válassza ki:"; @@ -376,6 +379,9 @@ /* No comment provided by engineer. */ "Accepted conditions" = "Elfogadott feltételek"; +/* chat list item title */ +"accepted invitation" = "elfogadott meghívó"; + /* No comment provided by engineer. */ "Acknowledged" = "Nyugtázva"; @@ -879,7 +885,7 @@ "Change" = "Változtatás"; /* authentication reason */ -"Change chat profiles" = "Felhasználói profilok megváltoztatása"; +"Change chat profiles" = "Csevegési profilok megváltoztatása"; /* No comment provided by engineer. */ "Change database passphrase?" = "Adatbázis-jelmondat megváltoztatása?"; @@ -2344,7 +2350,7 @@ "For console" = "Konzolhoz"; /* No comment provided by engineer. */ -"For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server." = "Ha például az ismerőse a SimpleX Chat kiszolgálón keresztül fogadja az üzeneteket, az Ön alkalmazása a Flux egyik kiszolgálóját használja a kézbesítéshez."; +"For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server." = "Például, ha az Ön ismerőse egy SimpleX Chat-kiszolgálón keresztül fogadja az üzeneteket, az Ön alkalmazása egy Flux-kiszolgálón keresztül fogja azokat kézbesíteni."; /* No comment provided by engineer. */ "For private routing" = "A privát útválasztáshoz"; @@ -3708,7 +3714,7 @@ "Protect app screen" = "Alkalmazás képernyőjének védelme"; /* No comment provided by engineer. */ -"Protect IP address" = "IP-cím védelem"; +"Protect IP address" = "IP-cím védelme"; /* No comment provided by engineer. */ "Protect your chat profiles with a password!" = "Védje meg a csevegési profiljait egy jelszóval!"; @@ -3930,6 +3936,9 @@ /* chat item action */ "Reply" = "Válasz"; +/* chat list item title */ +"requested to connect" = "kérelmezve a kapcsolódáshoz"; + /* No comment provided by engineer. */ "Required" = "Szükséges"; @@ -4454,6 +4463,9 @@ /* No comment provided by engineer. */ "SimpleX address or 1-time link?" = "SimpleX-cím vagy egyszer használható meghívó-hivatkozás?"; +/* No comment provided by engineer. */ +"SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app." = "A SimpleX Chat és a Flux megállapodást kötött arról, hogy a Flux által üzemeltetett kiszolgálókat beépítik az alkalmazásba."; + /* No comment provided by engineer. */ "SimpleX Chat security was audited by Trail of Bits." = "A SimpleX Chat biztonsága a Trail of Bits által lett auditálva."; @@ -4752,7 +4764,7 @@ "The profile is only shared with your contacts." = "A profilja csak az ismerőseivel kerül megosztásra."; /* No comment provided by engineer. */ -"The same conditions will apply to operator **%@**." = "Ugyanezek a feltételek vonatkoznak a következő üzemeltetőre is: **%@**."; +"The same conditions will apply to operator **%@**." = "Ugyanezek a feltételek lesznek elfogadva a következő üzemeltetőre is: **%@**."; /* No comment provided by engineer. */ "The same conditions will apply to operator(s): **%@**." = "Ugyanezek a feltételek lesznek elfogadva a következő üzemeltető(k)re is: **%@**."; @@ -5397,7 +5409,7 @@ "You can configure operators in Network & servers settings." = "Az üzemeltetőket a „Hálózat és kiszolgálók” beállításaban konfigurálhatja."; /* No comment provided by engineer. */ -"You can configure servers via settings." = "A kiszolgálókat a beállításokon keresztül konfigurálhatja."; +"You can configure servers via settings." = "A kiszolgálókat a „Hálózat és kiszolgálók” menüben konfigurálhatja."; /* No comment provided by engineer. */ "You can create it later" = "Létrehozás később"; @@ -5427,7 +5439,7 @@ "You can set connection name, to remember who the link was shared with." = "Beállíthatja az ismerős nevét, hogy emlékezzen arra, hogy kivel osztotta meg a hivatkozást."; /* No comment provided by engineer. */ -"You can set lock screen notification preview via settings." = "A beállításokon keresztül beállíthatja a lezárási képernyő értesítési előnézetét."; +"You can set lock screen notification preview via settings." = "A lezárási képernyő értesítési előnézetét az „Értesítések” menüben állíthatja be."; /* No comment provided by engineer. */ "You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it." = "Megoszthat egy hivatkozást vagy QR-kódot - így bárki csatlakozhat a csoporthoz. Ha a csoport később törlésre kerül, akkor nem fogja elveszíteni annak tagjait."; diff --git a/apps/ios/it.lproj/Localizable.strings b/apps/ios/it.lproj/Localizable.strings index 25a672da26..80122f8535 100644 --- a/apps/ios/it.lproj/Localizable.strings +++ b/apps/ios/it.lproj/Localizable.strings @@ -689,10 +689,10 @@ "Auto-accept" = "Accetta automaticamente"; /* No comment provided by engineer. */ -"Auto-accept contact requests" = "Auto-accetta richieste di contatto"; +"Auto-accept contact requests" = "Auto-accetta le richieste di contatto"; /* No comment provided by engineer. */ -"Auto-accept images" = "Auto-accetta immagini"; +"Auto-accept images" = "Auto-accetta le immagini"; /* alert title */ "Auto-accept settings" = "Accetta automaticamente le impostazioni"; @@ -779,7 +779,7 @@ "Blur for better privacy." = "Sfoca per una privacy maggiore."; /* No comment provided by engineer. */ -"Blur media" = "Sfocatura file multimediali"; +"Blur media" = "Sfocatura dei file multimediali"; /* No comment provided by engineer. */ "bold" = "grassetto"; @@ -3004,7 +3004,7 @@ "Message delivery warning" = "Avviso di consegna del messaggio"; /* No comment provided by engineer. */ -"Message draft" = "Bozza dei messaggi"; +"Message draft" = "Bozza del messaggio"; /* item status text */ "Message forwarded" = "Messaggio inoltrato"; @@ -3982,10 +3982,10 @@ "Reveal" = "Rivela"; /* No comment provided by engineer. */ -"Review conditions" = "Esamina le condizioni"; +"Review conditions" = "Leggi le condizioni"; /* No comment provided by engineer. */ -"Review later" = "Esamina più tardi"; +"Review later" = "Leggi più tardi"; /* No comment provided by engineer. */ "Revoke" = "Revoca"; @@ -4181,7 +4181,7 @@ "Send errors" = "Errori di invio"; /* No comment provided by engineer. */ -"Send link previews" = "Invia anteprime dei link"; +"Send link previews" = "Invia le anteprime dei link"; /* No comment provided by engineer. */ "Send live message" = "Invia messaggio in diretta"; @@ -4401,7 +4401,7 @@ "Share profile" = "Condividi il profilo"; /* No comment provided by engineer. */ -"Share SimpleX address on social media." = "Condividi indirizzo SimpleX sui social media."; +"Share SimpleX address on social media." = "Condividi l'indirizzo SimpleX sui social media."; /* No comment provided by engineer. */ "Share this 1-time invite link" = "Condividi questo link di invito una tantum"; @@ -4635,7 +4635,7 @@ "Tap button " = "Tocca il pulsante "; /* No comment provided by engineer. */ -"Tap Create SimpleX address in the menu to create it later." = "Tocca \"Crea indirizzo SimpleX\" nel menu per crearlo più tardi."; +"Tap Create SimpleX address in the menu to create it later." = "Tocca Crea indirizzo SimpleX nel menu per crearlo più tardi."; /* No comment provided by engineer. */ "Tap to activate profile." = "Tocca per attivare il profilo."; diff --git a/apps/ios/nl.lproj/Localizable.strings b/apps/ios/nl.lproj/Localizable.strings index ba28bd1f59..e5c3520898 100644 --- a/apps/ios/nl.lproj/Localizable.strings +++ b/apps/ios/nl.lproj/Localizable.strings @@ -343,6 +343,9 @@ /* No comment provided by engineer. */ "Abort changing address?" = "Adres wijziging afbreken?"; +/* No comment provided by engineer. */ +"About operators" = "Over operatoren"; + /* No comment provided by engineer. */ "About SimpleX Chat" = "Over SimpleX Chat"; @@ -376,6 +379,9 @@ /* No comment provided by engineer. */ "Accepted conditions" = "Geaccepteerde voorwaarden"; +/* chat list item title */ +"accepted invitation" = "geaccepteerde uitnodiging"; + /* No comment provided by engineer. */ "Acknowledged" = "Erkend"; @@ -3930,6 +3936,9 @@ /* chat item action */ "Reply" = "Antwoord"; +/* chat list item title */ +"requested to connect" = "gevraagd om verbinding te maken"; + /* No comment provided by engineer. */ "Required" = "Vereist"; diff --git a/apps/ios/ru.lproj/Localizable.strings b/apps/ios/ru.lproj/Localizable.strings index 09c95d4203..f22981f80a 100644 --- a/apps/ios/ru.lproj/Localizable.strings +++ b/apps/ios/ru.lproj/Localizable.strings @@ -83,7 +83,7 @@ "**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from." = "**Рекомендовано**: токен устройства и уведомления отправляются на сервер SimpleX Chat, но сервер не получает сами сообщения, их размер или от кого они."; /* No comment provided by engineer. */ -"**Scan / Paste link**: to connect via a link you received." = "**Сканировать / Вставить ссылку**: чтобы соединится через полученную ссылку."; +"**Scan / Paste link**: to connect via a link you received." = "**Сканировать / Вставить ссылку**: чтобы соединиться через полученную ссылку."; /* No comment provided by engineer. */ "**Warning**: Instant push notifications require passphrase saved in Keychain." = "**Внимание**: для работы мгновенных уведомлений пароль должен быть сохранен в Keychain."; @@ -3720,7 +3720,7 @@ "Protect your chat profiles with a password!" = "Защитите Ваши профили чата паролем!"; /* No comment provided by engineer. */ -"Protect your IP address from the messaging relays chosen by your contacts.\nEnable in *Network & servers* settings." = "Защитите ваш IP адрес от серверов сообщений, выбранных Вашими контактами.\nВключите в настройках *Сеть и серверы*."; +"Protect your IP address from the messaging relays chosen by your contacts.\nEnable in *Network & servers* settings." = "Защитите ваш IP адрес от серверов сообщений, выбранных Вашими контактами.\nВключите в настройках *Сети и серверов*."; /* No comment provided by engineer. */ "Protocol timeout" = "Таймаут протокола"; @@ -5406,7 +5406,7 @@ "You can change it in Appearance settings." = "Вы можете изменить это в настройках Интерфейса."; /* No comment provided by engineer. */ -"You can configure operators in Network & servers settings." = "Вы можете настроить операторов в настройках Сеть и серверы."; +"You can configure operators in Network & servers settings." = "Вы можете настроить операторов в настройках Сети и серверов."; /* No comment provided by engineer. */ "You can configure servers via settings." = "Вы можете настроить серверы позже."; diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml index c67b6258a2..4c781f0aab 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml @@ -12,10 +12,10 @@ عن SimpleX أعلاه، ثم: اقبل - لا يمكن التراجع عن هذا الإجراء - سيتم فقد ملف التعريف وجهات الاتصال والرسائل والملفات الخاصة بك بشكل نهائي. + لا يمكن التراجع عن هذا الإجراء - سيتم فقد ملف تعريفك وجهات اتصالك ورسائلك وملفاتك بشكل نهائي. هذه المجموعة لم تعد موجودة. رمز QR هذا ليس رابطًا! - الجيل القادم من \nالرسائل الخاصة + مستقبل المُراسلة لا يمكن التراجع عن هذا الإجراء - سيتم حذف جميع الملفات والوسائط المستلمة والمرسلة. ستبقى الصور منخفضة الدقة. لا يمكن التراجع عن هذا الإجراء - سيتم حذف الرسائل المرسلة والمستلمة قبل التحديد. قد تأخذ عدة دقائق. ينطبق هذا الإعداد على الرسائل الموجودة في ملف تعريف الدردشة الحالي الخاص بك @@ -30,7 +30,7 @@ أضِف خوادم مُعدة مسبقًا أضِف إلى جهاز آخر سيتم حذف جميع الدردشات والرسائل - لا يمكن التراجع عن هذا! - الوصول إلى الخوادم عبر وكيل SOCKS على المنفذ %d؟ يجب بدء تشغيل الوكيل قبل تمكين هذا الخيار. + الوصول إلى الخوادم عبر وكيل SOCKS على المنفذ %d؟ يجب بدء تشغيل الوكيل قبل تفعيل هذا الخيار. أضِف خادم إعدادات الشبكة المتقدمة سيبقى جميع أعضاء المجموعة على اتصال. @@ -42,7 +42,7 @@ قبول التخفي أضِف رسالة ترحيب أضف الخوادم عن طريق مسح رموز QR. - يمكّن للمشرفين إنشاء روابط للانضمام إلى المجموعات. + يمكن للمشرفين إنشاء روابط للانضمام إلى المجموعات. قبول طلب الاتصال؟ سيتم حذف جميع الرسائل - لا يمكن التراجع عن هذا! سيتم حذف الرسائل فقط من أجلك. مكالمة مقبولة @@ -67,17 +67,17 @@ دائِماً مُتاح يمكن للتطبيق استلام الإشعارات فقط عند تشغيله، ولن يتم بدء تشغيل أي خدمة في الخلفية السماح بالرسائل الصوتية؟ - ستبقى جميع جهات الاتصال الخاصة بك متصلة. + ستبقى جميع جهات اتصالك متصلة. استخدم التتابع دائمًا النسخ الاحتياطي لبيانات التطبيق حُذفت جميع بيانات التطبيق. السماح بحذف الرسائل المرسلة بشكل لا رجعة فيه. (24 ساعة) اسمح لجهات اتصالك بإرسال رسائل صوتية. - حول عنوان SimpleX + عن عنوان SimpleX بناء التطبيق: %s المظهر - أضف عنوانًا إلى ملف التعريف الخاص بك ، حتى تتمكن جهات الاتصال الخاصة بك من مشاركته مع أشخاص آخرين. سيتم إرسال تحديث الملف الشخصي إلى جهات الاتصال الخاصة بك. - ستبقى جميع جهات الاتصال الخاصة بك متصلة. سيتم إرسال تحديث الملف الشخصي إلى جهات الاتصال الخاصة بك. + أضف عنوانًا إلى ملف تعريفك، حتى تتمكن جهات اتصالك من مشاركته مع أشخاص آخرين. سيتم إرسال تحديث ملف التعريف إلى جهات اتصالك. + ستبقى جميع جهات اتصالك متصلة. سيتم إرسال تحديث ملف التعريف إلى جهات اتصالك. رمز التطبيق عنوان اسمح لجهات اتصالك بحذف الرسائل المرسلة بشكل لا رجعة فيه. (24 ساعة) @@ -96,7 +96,7 @@ يمكنك أنت وجهة اتصالك إضافة ردود فعل الرسائل. يمكنك أنت وجهة اتصالك إرسال رسائل تختفي. مكالمتك تحت الإجراء - لا يمكّن استلام الملف + لا يمكن استلام الملف جيد للبطارية. يتحقق التطبيق من الرسائل كل 10 دقائق. قد تفوتك مكالمات أو رسائل عاجلة.]]> عريض مكالمات الصوت (ليست مُعمّاة بين الطرفين) @@ -135,7 +135,7 @@ يتم استبدال رمز مرور التطبيق برمز مرور التدمير الذاتي. مكالمات الصوت والفيديو خطأ في الاتصال - تحسين البطارية نشط ، مما يؤدي إلى إيقاف تشغيل خدمة الخلفية والطلبات الدورية للرسائل الجديدة. يمكنك إعادة تمكينها عبر الإعدادات. + تحسين البطارية نشط، مما يؤدي إلى إيقاف تشغيل خدمة الخلفية والطلبات الدورية للرسائل الجديدة. يمكنك إعادة تفعيلها عبر الإعدادات. لا يمكن تهيئة قاعدة البيانات إرفاق طلب لاستلام الصورة @@ -182,7 +182,7 @@ قاعدة البيانات مُعمّاة غيرت دور %s إلى %s تغيير عنوان الاستلام - خطأ في إنشاء الملف الشخصي! + خطأ في إنشاء ملف التعريف! خطأ في الإتصال انتهت مهلة الاتصال جهة الاتصال موجودة بالفعل @@ -303,7 +303,7 @@ أدخل عبارة المرور الدردشات متصل - سيتم حذف جهة الاتصال وجميع الرسائل - لا يمكن التراجع عن هذا الإجراء! + سيتم حذف جهة الاتصال وجميع الرسائل - لا يمكن التراجع عن هذا! الحد الأقصى لحجم الملف المدعوم حاليًا هو %1$s. تواصل عبر الرابط / رمز QR إنشاء رابط دعوة لمرة واحدة @@ -314,7 +314,7 @@ ملون لدى جهة الاتصال التعمية بين الطريفين إنشاء - إنشاء ملف تعريف + أنشئ ملف تعريفك مكالمة جارية... تفعيل التدمير الذاتي الموافقة على التعمية… @@ -409,11 +409,11 @@ توسيع تحديد الدور انتهت صلاحية دعوة المجموعة المجموعة غير موجودة! - تصدير السمة + صدّر السمة الملفات والوسائط قلب الكاميرا سيتم حذف المجموعة لجميع الأعضاء - لا يمكن التراجع عن هذا! - يمكن لأعضاء المجموعة إرسال رسائل مباشرة. + يمكن للأعضاء إرسال رسائل مباشرة. فشل تحميل الدردشات أهلاً! \nتواصل معي عبر SimpleX Chat: %s @@ -422,8 +422,8 @@ الملف حُدّث ملف تعريف المجموعة أدخل اسم المجموعة: - يمكن لأعضاء المجموعة إرسال رسائل صوتية. - الملفات والوسائط ممنوعة في هذه المجموعة. + يمكن للأعضاء إرسال رسائل صوتية. + الملفات والوسائط ممنوعة. رسالة ترحيب المجموعة مزيد من تقليل استخدام البطارية المجموعة @@ -433,16 +433,16 @@ الواجهة الفرنسية المساعدة حُذِفت المجموعة - يمكن لأعضاء المجموعة إرسال رسائل تختفي. + يمكن للأعضاء إرسال رسائل تختفي. إشراف المجموعة أخيرا، لدينا منهم! 🚀 - تصدير قاعدة البيانات + صدّر قاعدة البيانات لوحدة التحكم الميزات التجريبية تجريبي المجموعة غير نشطة الملفات والوسائط - يمكن لأعضاء المجموعة حذف الرسائل المرسلة بشكل لا رجعة فيه. (24 ساعة) + يمكن للأعضاء حذف الرسائل المُرسلة بشكل لا رجعة فيه. (24 ساعة) الإصلاح غير مدعوم من قبل جهة الاتصال يُخزّن ملف تعريف المجموعة على أجهزة الأعضاء، وليس على الخوادم. روابط المجموعة @@ -450,25 +450,25 @@ الاسم الكامل: لم تعد دعوة المجموعة صالحة، تمت أُزيلت بواسطة المرسل. رابط المجموعة - سيتم استلام الملف عندما تكون جهة اتصالك متصلة بالإنترنت، يرجى الانتظار أو التحقق لاحقًا! + سيتم استلام الملف عندما تكون جهة اتصالك متصلة بالإنترنت، يُرجى الانتظار أو التحقق لاحقًا! الاسم الكامل للمجموعة: رابط كامل ملف سيتم حذف المجموعة لك - لا يمكن التراجع عن هذا! فشل تحميل الدردشة - يمكن لأعضاء المجموعة إضافة ردود فعل الرسالة. + يمكن للأعضاء إضافة ردود الفعل على الرسائل. المفضل مخفي حُفظ الملف سيتم حذف الملف من الخوادم. - سيتم استلام الملف عند اكتمال تحميل جهة الاتصال الخاصة بك. + سيتم استلام الملف عندما يكتمل جهة اتصالك من رفعِها. المساعدة الملف: %s إصلاح إصلاح الاتصال إصلاح الاتصال؟ الإصلاح غير مدعوم من قبل أعضاء المجموعة - يمكن لأعضاء المجموعة إرسال الملفات والوسائط. + يمكن للأعضاء إرسال الملفات والوسائط. تفضيلات المجموعة سريع ولا تنتظر حتى يصبح المرسل متصلاً بالإنترنت! إخفاء @@ -483,7 +483,7 @@ استيراد قاعدة بيانات ساعات السجل - سيتم استلام الصورة عند اكتمال تحميل جهة اتصالك. + سيتم استلام الصورة عندما يكتمل جهة اتصالك من رفعِها. اعرض رمز QR في مكالمة الفيديو، أو شارك الرابط.]]> ثبّت SimpleX Chat لطرفية إذا قمت بالتأكيد، فستتمكن خوادم المراسلة من رؤية عنوان IP الخاص بك ومزود الخدمة الخاص بك - أي الخوادم التي تتصل بها. @@ -511,7 +511,7 @@ التخفي عبر رابط لمرة واحدة أرسلت صورة صورة - سيتم استلام الصورة عندما تكون جهة اتصالك متصلة بالإنترنت، يرجى الانتظار أو التحقق لاحقًا! + سيتم استلام الصورة عندما تكون جهة اتصالك متصلة بالإنترنت، يُرجى الانتظار أو التحقق لاحقًا! حُفظت الصورة في المعرض صورة إذا لم تتمكن من الالتقاء شخصيًا، اعرض رمز QR في مكالمة الفيديو، أو شارك الرابط. @@ -526,7 +526,7 @@ فوري المضيف إخفاء - يُرجى السماح لSimpleX للتشغيل في الخلفية في مربع الحوار التالي. وإلا، سيتم تعطيل الإشعارات.]]> + السماح بذلك في مربع الحوار التالي لتلقي الإشعارات على الفور.]]> ردًا على إشعارات فورية خوادم ICE (واحد لكل سطر) @@ -534,7 +534,7 @@ إخفاء ملف التعريف كيفية استخدام ماركداون إذا أدخلت رمز مرور التدمير الذاتي أثناء فتح التطبيق: - يمكن تغييره لاحقًا عبر الإعدادات. + كيف يؤثر على البطارية انضمام فاتح مدعو للتواصل @@ -554,13 +554,13 @@ دعوة الأصدقاء خطأ في Keychain دعوة للمجموعة - يٌمنع حذف الرسائل بشكل لا رجعة فيه في هذه المجموعة. + يٌمنع حذف الرسائل بشكل لا رجعة فيه. تنسيق الرسالة غير صالح البيانات غير صالحة - بيانات الملف الشخصي المحلية فقط + بيانات ملف التعريف المحلية فقط يٌمنع حذف الرسائل بشكل لا رجعة فيه في هذه الدردشة. دعوة الأعضاء - مغادرة المجموعة + غادِر المجموعة الاسم المحلي: غادر يسمح بوجود العديد من الاتصالات المجهولة دون مشاركة أي بيانات بينهم في ملف تعريف دردشة واحد. @@ -603,7 +603,7 @@ نزّل الملف تعطيل قفل SimpleX تحرير - اسم الملف الشخصي: + اسم ملف التعريف: البريد الإلكتروني أدخل أسمك: كرر الرسالة @@ -613,7 +613,7 @@ حُرر الرجوع إلى إصدار سابق وفتح الدردشة رسائل مباشرة - الرسائل المختفية ممنوعة في هذه المجموعة. + الرسائل المختفية ممنوعة. تحرير ملف تعريف المجموعة لا تُظهر مرة أخرى الجهاز @@ -651,7 +651,7 @@ لا تنشئ عنوانًا خطأ في تحديث تضبيط الشبكة خطأ في استلام الملف - خطأ في تبديل الملف الشخصي! + خطأ في تبديل ملف التعريف! حافظ على اتصالاتك تأكد من أن عناوين خادم XFTP بالتنسيق الصحيح، وأن تكون مفصولة بأسطر وليست مكررة. عُلّم محذوف @@ -683,7 +683,7 @@ خطأ في بدء الدردشة خطأ في تصدير قاعدة بيانات الدردشة ستتم إزالة العضو من المجموعة - لا يمكن التراجع عن هذا! - اجعل الملف الشخصي خاصًا! + اجعل ملف التعريف خاصًا! تصفية الدردشات غير المقروءة والمفضلة. البحث عن الدردشات بشكل أسرع تفعيل @@ -734,7 +734,7 @@ \n- و اكثر! حالة الشبكة كتم - ردود الفعل الرسائل ممنوعة في هذه المجموعة. + ردود الفعل الرسائل ممنوعة. المزيد إعدادات متقدّمة مكالمة فائتة @@ -757,9 +757,7 @@ سيتم استخدام مضيفات البصل عند توفرها. لن يتم استخدام مضيفات البصل. لم تٌحدد جهات اتصال - يمكّن للمشرف الآن: -\n- حذف رسائل الأعضاء. -\n- تعطيل الأعضاء (دور "المراقب") + يمكن للمشرف الآن:\n- حذف رسائل الأعضاء.\n- تعطيل الأعضاء (دور المراقب) خدمة الإشعار غير مفعّل` مفعل @@ -793,7 +791,7 @@ تم تعيين كلمة المرور! المالك فقط جهة اتصالك يمكنها إرسال رسائل تختفي. - جهة اتصالك فقط يمكنها إضافة تفاعلات على الرسالة + جهة اتصالك فقط يمكنها إضافة ردود الفعل على الرسالة فقط مالكي المجموعة يمكنهم تغيير تفضيلات المجموعة. جهة اتصالك فقط يمكنها حذف الرسائل بشكل لا رجعة فيه (يمكنك تعليم الرسالة للحذف). (24 ساعة) أنت فقط يمكنك إرسال رسائل صوتية. @@ -806,7 +804,7 @@ كلمة المرور غير موجودة في مخزن المفاتيح، يرجى إدخالها يدوياً. قد يحدث هذا إذا قمت باستعادة ملفات التطبيق باستخدام أداة استرجاع بيانات. إذا لم يكن الأمر كذلك، تواصل مع المبرمجين رجاء افتح الدردشة فتح الرابط في المتصفح قد يقلل خصوصية وحماية اتصالك. الروابط غير الموثوقة من SimpleX ستكون باللون الأحمر - أنت فقط يمكنك إضافة تفاعل على الرسالة. + أنت فقط يمكنك إضافة ردود الفعل على الرسالة. أنت فقط يمكنك حذف الرسائل بشكل لا رجعة فيه (يمكن للمستلم تعليمها للحذف). (24 ساعة) أنت فقط يمكنك إرسال رسائل تختفي أنت فقط يمكنك إجراء المكالمات. @@ -819,7 +817,7 @@ ندّ لِندّ أنت تقرر من يمكنه الاتصال. مكالمة قيد الانتظار - تعمية ثنائية الطبقات من بين الطريفين.]]> + تقوم أجهزة العميل فقط بتخزين ملفات تعريف المستخدمين وجهات الاتصال والمجموعات والرسائل. صفّر الألوان حفظ عنوان الخادم المُعد مسبقًا @@ -829,7 +827,7 @@ الاستلام عبر يُرجى التحقق من استخدامك للرابط الصحيح أو اطلب من جهة اتصالك أن ترسل لك رابطًا آخر. الإشعارات الدورية مُعطَّلة - صورة الملف الشخصي + صورة ملف التعريف الإشعارات خاصة يرجى تخزين عبارة المرور بشكل آمن، فلن تتمكن من الوصول إلى الدردشة إذا فقدتها. يُرجى تحديث التطبيق والتواصل مع المطورين. @@ -862,7 +860,7 @@ الرجاء إدخال كلمة المرور السابقة بعد استعادة نسخة احتياطية لقاعدة البيانات. لا يمكن التراجع عن هذا الإجراء. استعادة النسخة الاحتياطية لقاعدة البيانات؟ حفظ - اتصالات الملف الشخصي والخادم + اتصالات ملف التعريف والخادم منع ردود فعل الرسالة. منع إرسال الرسائل الصوتية. منع ردود فعل الرسائل. @@ -884,7 +882,7 @@ يرى المستلمون التحديثات أثناء كتابتها. استلمت، ممنوع حفظ - سيتم إرسال تحديث الملف الشخصي إلى جهات الاتصال الخاصة بك. + سيتم إرسال تحديث ملف التعريف إلى جهات اتصالك. حفظ وإشعار جهات الاتصال حفظ وتحديث ملف تعريف المجموعة عدد البينج @@ -900,7 +898,7 @@ إزالة صفّر إلى الإعدادات الافتراضية بينج الفاصل الزمني - كلمة مرور الملف الشخصي + كلمة مرور ملف التعريف منع إرسال الرسائل التي تختفي. مهلة البروتوكول مهلة البروتوكول لكل كيلوبايت @@ -924,7 +922,7 @@ سحب وصول الملف؟ رٌفض الإذن! يرجى مطالبة جهة اتصالك بتفعيل إرسال الرسائل الصوتية. - العنصر النائب لصورة الملف الشخصي + العنصر النائب لصورة ملف التعريف رمز QR صفّر المنفذ %d @@ -1016,7 +1014,7 @@ خوادم SMP مشاركة الوسائط… رسائل SimpleX Chat - لم يتم تمكين قفل SimpleX! + قفل SimpleX غير مفعّل! إيقاف الدردشة التوقف عن استلام الملف؟ مشاركة الملف… @@ -1056,7 +1054,7 @@ عرض خيارات المطور simplexmq: v%s (%2s) يتطلب الخادم إذنًا لإنشاء قوائم انتظار، تحقق من كلمة المرور - يتطلب الخادم إذنًا للتحميل، تحقق من كلمة المرور + يتطلب الخادم إذنًا للرفع، تحقق من كلمة المرور عرض جهة الاتصال فقط مكالمات SimpleX Chat خدمة SimpleX Chat @@ -1084,14 +1082,13 @@ سيتم إلغاء الاتصال الذي قبلته! لن تتمكن جهة الاتصال التي شاركت هذا الرابط معها من الاتصال! هذا النص متاح في الإعدادات - لحماية الخصوصية، بدلاً من معرفات المستخدم التي تستخدمها جميع الأنظمة الأساسية الأخرى, يحتوي SimpleX على معرفات لقوائم انتظار الرسائل، منفصلة لكل جهة من جهات اتصالك. - لحماية معلوماتك، قم بتشغيل قفل SimpleX -\nسيُطلب منك إكمال المصادقة قبل تمكين هذه الميزة. + لحماية خصوصيتك، يستخدم SimpleX معرّفات منفصلة لكل جهة اتصال لديك. + لحماية معلوماتك، فعّل قفل SimpleX \nسيُطلب منك إكمال المصادقة قبل تفعيل هذه الميزة. عزل النقل بفضل المستخدمين - المساهمة عبر Weblate! دعم البلوتوث وتحسينات أخرى. بفضل المستخدمين - المساهمة عبر Weblate! - خدمة SimpleX تعمل في الخلفية – يستخدم نسبة قليلة من البطارية يوميًا.]]> + يتم تشغيل SimpleX في الخلفية بدلاً من استخدام إشعارات push.]]> انقر لبدء محادثة جديدة (للمشاركة مع جهة اتصالك) للتواصل عبر الرابط @@ -1103,17 +1100,17 @@ العنوان الرئيسي سيتم وضع علامة على الرسالة على أنها تحت الإشراف لجميع الأعضاء. انقر للانضمام - للكشف عن ملف التعريف المخفي الخاص بك، أدخل كلمة مرور كاملة في حقل البحث في صفحة ملفات تعريف الدردشة الخاصة بك. + للكشف عن ملف تعريفك المخفي، أدخل كلمة مرور كاملة في حقل البحث في صفحة ملفات تعريف الدردشة الخاصة بك. انقر للانضمام إلى وضع التخفي النظام السمات بفضل المستخدمين - المساهمة عبر Weblate! قاعدة البيانات لا تعمل بشكل صحيح. انقر لمعرفة المزيد ألوان الواجهة - انقر لتنشيط الملف الشخصي. + انقر لتنشيط ملف التعريف. عزل النقل هذه السلسلة ليست رابط اتصال! - هذه الإعدادات لملف التعريف الحالي الخاص بك + هذه الإعدادات لملف تعريفك الحالي يمكن تجاوزها في إعدادات الاتصال و المجموعة. انتهت مهلة اتصال TCP لحماية المنطقة الزمنية، تستخدم ملفات الصور / الصوت التوقيت العالمي المنسق (UTC). @@ -1140,7 +1137,7 @@ محاولة الاتصال بالخادم المستخدم لاستلام الرسائل من جهة الاتصال هذه (خطأ: %1$s). تشغيل خوادم WebRTC ICE - أنت تستخدم ملفًا شخصيًا متخفيًا لهذه المجموعة - لمنع مشاركة ملفك الشخصي الرئيسي الذي يدعو جهات الاتصال غير مسموح به + أنت تستخدم ملف تعريف متخفي لهذه المجموعة - لمنع مشاركة ملفك التعريفي الرئيسي الذي يدعو جهات الاتصال غير مسموح به غيّرتَ دور %s إلى %s نعم أنت متصل بالخادم المستخدم لاستلام الرسائل من جهة الاتصال هذه. @@ -1155,23 +1152,23 @@ عبر المُرحل لقد انضممت إلى هذه المجموعة لقد رفضت دعوة المجموعة - عندما تشارك ملفًا شخصيًا متخفيًا مع شخص ما، فسيتم استخدام هذا الملف الشخصي للمجموعات التي يدعوك إليها. + عندما تشارك ملف تعريف متخفي مع شخص ما، فسيتم استخدام هذا الملف التعريفي للمجموعات التي يدعوك إليها. لديك بالفعل ملف تعريف دردشة بنفس اسم العرض. الرجاء اختيار اسم آخر. أنت متصل بالفعل بـ%1$s. في انتظار الفيديو - سيتم استلام الفيديو عند اكتمال تحميل جهة اتصالك. + سيتم استلام الفيديو عند اكتمال رفع جهة اتصالك. تحقق من رمز الأمان رسائل صوتية عندما يطلب الأشخاص الاتصال، يمكنك قبوله أو رفضه. - سوف تكون متصلاً بالمجموعة عندما يكون جهاز مضيف المجموعة متصلاً بالإنترنت، يرجى الانتظار أو التحقق لاحقًا! - سوف تكون متصلاً عندما يتم قبول طلب الاتصال الخاص بك، يرجى الانتظار أو التحقق لاحقًا! + سوف تكون متصلاً بالمجموعة عندما يكون جهاز مضيف المجموعة متصلاً بالإنترنت، يُرجى الانتظار أو التحقق لاحقًا! + سوف تكون متصلاً عندما يتم قبول طلب اتصالك، يُرجى الانتظار أو التحقق لاحقًا! تستخدم خوادم SimpleX Chat. استخدم وكيل SOCKS استخدم مضيفي onion. استخدام وكيل SOCKS؟ عندما تكون متاحة ستبقى جهات اتصالك متصلة. - لا نقوم بتخزين أي من جهات الاتصال أو الرسائل الخاصة بك (بمجرد تسليمها) على الخوادم. + لا نقوم بتخزين أي من جهات اتصالك أو رسائلك (بمجرد تسليمها) على الخوادم. يمكنك استخدام تخفيض السعر لتنسيق الرسائل: استخدم الدردشة أنت @@ -1206,10 +1203,10 @@ عبر رابط لمرة واحدة مكالمة الفيديو ليست مُعمّاة بين الطريفين غيّرتَ العنوان - سوف تكون متصلاً عندما يكون جهاز جهة الاتصال الخاصة بك متصلاً بالإنترنت، يرجى الانتظار أو التحقق لاحقًا! + سوف تكون متصلاً عندما يكون جهاز جهة اتصالك متصلاً بالإنترنت، يُرجى الانتظار أو التحقق لاحقًا! غادرت يجب عليك استخدام أحدث إصدار من قاعدة بيانات الدردشة الخاصة بك على جهاز واحد فقط، وإلا فقد تتوقف عن تلقي الرسائل من بعض جهات الاتصال. - سيتم استلام الفيديو عندما تكون جهة اتصالك متصلة بالإنترنت، يرجى الانتظار أو التحقق لاحقًا! + سيتم استلام الفيديو عندما تكون جهة اتصالك متصلة بالإنترنت، يُرجى الانتظار أو التحقق لاحقًا! يمكنك مشاركة هذا العنوان مع جهات اتصالك للسماح لهم بالاتصال بـ%s. أُزيلت %1$s تحديث @@ -1238,11 +1235,11 @@ سيتم حذف قاعدة بيانات الدردشة الحالية واستبدالها بالقاعدة المستوردة. \nلا يمكن التراجع عن هذا الإجراء - سيتم فقد ملف التعريف وجهات الاتصال والرسائل والملفات الخاصة بك بشكل نهائي. تحديث عبارة مرور قاعدة البيانات - سوف تتوقف عن تلقي الرسائل من هذه المجموعة. سيتم الاحتفاظ سجل الدردشة. + سوف تتوقف عن تلقي الرسائل من هذه المجموعة. سيتم الاحتفاظ بسجل الدردشة. أسابيع يمكنك إخفاء أو كتم ملف تعريف المستخدم - اضغط مطولاً للقائمة. ما هو الجديد - ملفك الشخصي الحالي + ملف تعريفك الحالي عبر %1$s غير مقروءة مرحبًا! @@ -1252,7 +1249,7 @@ فيديو يمكنك مشاركة عنوانك كرابط أو رمز QR - يمكن لأي شخص الاتصال بك. يمكنك إنشاؤه لاحقًا - أنت تحاول دعوة جهة اتصال قمت بمشاركة ملف تعريف متخفي معها إلى المجموعة التي تستخدم فيها ملفك الشخصي الرئيسي + أنت تحاول دعوة جهة اتصال شاركت ملف تعريف متخفي معها إلى المجموعة التي تستخدم فيها ملف تعريفك الرئيسي ألغِ الكتم ألغِ الكتم لقد قبلت الاتصال @@ -1274,7 +1271,7 @@ \n- الوقت المخصص لتختفي. \n- تحرير التاريخ. يمكنك تفعيلة لاحقًا عبر الإعدادات - يمكنك تمكينها لاحقًا عبر إعدادات الخصوصية والأمان للتطبيق. + يمكنك تفعيلها لاحقًا عبر إعدادات الخصوصية والأمان للتطبيق. عبر رابط المجموعة لقد شاركت رابط لمرة واحدة متخفي عبر المتصفح @@ -1287,11 +1284,11 @@ سيتم إرسال ملف تعريف الدردشة الخاص بك \nإلى جهة اتصالك إلغاء الإخفاء - ملفك الشخصي العشوائي - ستستمر في استلام المكالمات والإشعارات من الملفات الشخصية المكتومة عندما تكون نشطة. + ملفك التعريفي العشوائي + ستستمر في استلام المكالمات والإشعارات من الملفات التعريفية المكتومة عندما تكون نشطة. انت تسمح بها مكالمة فيديو - الرسائل الصوتية ممنوعة في هذه الدردشة. + الرسائل الصوتية ممنوعة. فتح القفل رفع الملف لا يمكن التحقق منك؛ الرجاء المحاولة مرة اخرى. @@ -1299,7 +1296,7 @@ رسالة صوتية… أنت مدعو إلى المجموعة لا يمكنك إرسال رسائل! - تحتاج إلى السماح لجهة الاتصال الخاصة بك بإرسال رسائل صوتية لتتمكن من إرسالها. + تحتاج إلى السماح لجهة اتصالك بإرسال رسائل صوتية لتتمكن من إرسالها. أرسلت جهة اتصالك ملفًا أكبر من الحجم الأقصى المعتمد حاليًا (%1$s). الاتصال بمطوري SimpleX Chat لطرح أي أسئلة وتلقي التحديثات.]]> خادمك @@ -1334,7 +1331,7 @@ لا يمكن تشغيل SimpleX في الخلفية. ستستلم الإشعارات فقط عندما يكون التطبيق قيد التشغيل. سيتم مشاركة ملف تعريف عشوائي جديد. ألصق الرابط المُستلَم للتواصل مع جهة اتصالك… - ستتم مشاركة ملفك الشخصي %1$s. + ستتم مشاركة ملفك التعريفي %1$s. قد يغلق التطبيق بعد دقيقة واحدة في الخلفية. سماح لا مكالمات في الخلفية @@ -1388,9 +1385,9 @@ محظور حظر أعضاء المجموعة جهة الاتصال حُذفت - أنشِئ مجموعة باستخدام ملف تعريف عشوائي. - أنشِئ مجموعة - أنشِئ ملف تعريف + أنشئ مجموعة باستخدام ملف تعريف عشوائي. + أنشئ مجموعة + أنشئ ملف تعريف سطح المكتب متصل اتصل تلقائيًا عنوان سطح المكتب @@ -1485,9 +1482,7 @@ تحقق من الرمز مع سطح المكتب مسح رمز QR من سطح المكتب إلغاء الحظر - - إشعار اختياريًا جهات الاتصال المحذوفة. -\n- أسماء الملفات الشخصية بمسافات. -\n- و اكثر! + - إشعار اختياريًا جهات الاتصال المحذوفة. \n- أسماء الملفات التعريفية بمسافات. \n- و اكثر! مسار الملف غير صالح لقد طلبت بالفعل الاتصال عبر هذا العنوان! إظهار وحدة التحكم في نافذة جديدة @@ -1522,7 +1517,7 @@ يمكنك عرض رابط الدعوة مرة أخرى في تفاصيل الاتصال. أبقِ الدعوة غير المستخدمة؟ شارك رابط الدعوة هذا لمرة واحدة - أنشِئ مجموعة: لإنشاء مجموعة جديدة.]]> + أنشئ مجموعة: لإنشاء مجموعة جديدة.]]> التاريخ المرئي رمز مرور التطبيق دردشة جديدة @@ -1550,7 +1545,7 @@ %s غير نشط]]> أظهر مكالمات API البطيئة غير معروف - حدّثت الملف الشخصي + حدّثت ملف التعريف %s مفقود]]> %s لديه إصدار غير مدعوم. يُرجى التأكد من استخدام نفس الإصدار على كلا الجهازين]]> %s في حالة سيئة]]> @@ -1575,9 +1570,9 @@ خيارات المطور تغيّر العضو %1$s إلى %2$s أزلت عنوان الاتصال - أزلت الصورة الشخصية + أزلت صورة ملف التعريف عيّن عنوان جهة اتصال جديد - عيّن صورة شخصية جديدة + عيّن صورة تعريفية جديدة حالة غير معروفة تغيّر جهة الاتصال %1$s إلى %2$s يستغرق تنفيذ الوظيفة وقتًا طويلاً جدًا: %1$d ثانية: %2$s @@ -1624,7 +1619,7 @@ يمكن للمشرفين حظر عضو للجميع. ترحيل بيانات التطبيق جارِ أرشفة قاعدة البيانات - سيتم تعمية جميع جهات الاتصال والمحادثات والملفات الخاصة بك بشكل آمن وتحميلها في أجزاء إلى مُرحلات XFTP التي ضبطت. + سيتم تعمية جميع جهات الاتصال والمحادثات والملفات الخاصة بك بشكل آمن ورفعها في أجزاء إلى مُرحلات XFTP التي ضُبطت. طبّق يُرجى ملاحظة: استخدام نفس قاعدة البيانات على جهازين سيؤدي إلى كسر فك تعمية الرسائل من اتصالاتك، كحماية أمنية.]]> تحذير: سيتم حذف الأرشيف.]]> @@ -1658,7 +1653,7 @@ ألصق رابط الأرشيف يمكنك إعطاء محاولة أخرى. حدث خطأ أثناء تنزيل الأرشيف - الملف المُصدر غير موجود + الملف المُصدّر غير موجود تحقق من عبارة المرور تأكد من أنك تتذكر عبارة مرور قاعدة البيانات لترحيلها. التحقق من عبارة مرور قاعدة البيانات @@ -1718,8 +1713,8 @@ السماح بإرسال روابط SimpleX. منع إرسال روابط SimpleX كل الأعضاء - يمكن لأعضاء المجموعة إرسال روابط SimpleX. - روابط SimpleX محظورة في هذه المجموعة. + يمكن للأعضاء إرسال روابط SimpleX. + روابط SimpleX محظورة. المشرفين مفعّل لـ المالكون @@ -1746,8 +1741,8 @@ عند اتصال بمكالمات الصوت والفيديو. إدارة الشبكة اتصال شبكة أكثر موثوقية. - صور الملف الشخصي - شكل الصور الشخصية + صور ملف التعريف + شكل الصور التعريفية واجهة المستخدم الليتوانية مربع أو دائرة أو أي شيء بينهما. عنوان الخادم غير متوافق مع إعدادات الشبكة. @@ -1814,7 +1809,7 @@ صباح الخير! صورة خلفية الشاشة الوضع الفاتح - السمة الملف الشخصي + سمة ملف التعريف فاتح طبّق لِ ملء @@ -1833,8 +1828,7 @@ \nآخر رسالة تم استلامها: %2$s تسليم التصحيح معلومات قائمة انتظار الرسائل - احمِ عنوان IP الخاص بك من مُرحلات المُراسلة التي اختارتها جهات الاتصال الخاصة بك. -\nفعّل في إعدادات *الشبكة والخوادم*. + احمِ عنوان IP الخاص بك من مُرحلات المُراسلة التي اختارتها جهات اتصالك. \nفعّل في إعدادات *الشبكة والخوادم*. سمات دردشة جديدة حدث خطأ أثناء تهيئة WebView. حدّث نظامك إلى الإصدار الجديد. يُرجى التواصل بالمطورين. \nError: %s @@ -1999,7 +1993,7 @@ بإمكانك إرسال رسائل إلى %1$s من جهات الاتصال المؤرشفة. ألصق الرابط جهات اتصالك - شريط أدوات الدردشة القابل للوصول + شريط أدوات التطبيق القابلة للوصول حُذفت جهة الاتصال. السماح بالمكالمات؟ أرسل رسالة لتفعيل المكالمات. @@ -2040,7 +2034,7 @@ يحمي عنوان IP الخاص بك واتصالاتك. اتصال TCP حفظ وإعادة الاتصال - أنشِئ + أنشئ تجربة دردشة جديدة 🎉 تمويه من أجل خصوصية أفضل. كبّر حجم الخط @@ -2075,9 +2069,9 @@ لا يزال يتم تنزيل %1$d ملفًا. لا تستخدم بيانات الاعتماد مع الوكيل. خطأ في تحويل الرسائل - خطأ في تبديل الملف الشخصي + خطأ في تبديل ملف التعريف حدد ملف تعريف الدردشة - لقد تم نقل اتصالك إلى %s ولكن حدث خطأ غير متوقع أثناء إعادة توجيهك إلى الملف الشخصي. + لقد تم نقل اتصالك إلى %s ولكن حدث خطأ غير متوقع أثناء إعادة توجيهك إلى ملف التعريف. تحويل %1$s رسالة؟ لم يحوّل %1$s من الرسائل جارِ تحويل %1$s رسالة @@ -2117,7 +2111,7 @@ لملف تعريف الدردشة %s: لا يوجد وسائط أو خوادم ملفات. لا يوجد خوادم لإرسال الملفات. - لقد وصل الاتصال إلى الحد الأقصى من الرسائل غير المُسلمة، قد يكون جهة الاتصال الخاصة بك غير متصلة بالإنترنت. + لقد وصل الاتصال إلى الحد الأقصى من الرسائل غير المُسلمة، قد يكون جهة اتصالك غير متصلة بالإنترنت. الرسائل غير المُسلَّمة شارك رابطًا لمرة واحدة مع صديق أمان الاتصال @@ -2182,11 +2176,11 @@ عنوان أو رابط لمرة واحدة؟ مع جهة اتصال واحدة فقط - المشاركة شخصيًا أو عبر أي مُراسل.]]> سيتم قبول الشروط للمُشغلين المفعّلين بعد 30 يومًا. - اختر المُشغلين + مُشغلي الخادم لا يمكن تحميل نص الشروط الحالية، يمكنك مراجعة الشروط عبر هذا الرابط: خطأ في قبول الشروط خطأ في حفظ الخوادم - على سبيل المثال، إذا تلقيت رسائل عبر خادم SimpleX Chat، فسيستخدم التطبيق أحد خوادم Flux للتوجيه الخاص. + على سبيل المثال، إذا تلقى أحد جهات اتصالك رسائل عبر خادم SimpleX Chat، فسوف يقوم تطبيقك بتسليمها عبر خادم Flux. لا يوجد خوادم لتوجيه الرسائل الخاصة. لا يوجد خوادم رسائل. لا يوجد خوادم لاستقبال الملفات. @@ -2202,8 +2196,49 @@ انقر فوق أنشئ عنوان SimpleX في القائمة لإنشائه لاحقًا. حُذفت هذه الرسالة أو لم يتم استلامها بعد. استخدم للرسائل - عندما تفعّل أكثر من مُشغل شبكة واحد، سيستخدم التطبيق خوادم مُشغلين مختلفين لكل مُحادثة. + يحمي التطبيق خصوصيتك من خلال استخدام مُشغلين مختلفين في كل محادثة. %s.]]> %s.]]> %s.]]> + عنوان العمل التجاري + يتم تشغيل التطبيق دائمًا في الخلفية + دردشات العمل التجاري + أضف أعضاء الفريق + أضف أصدقاء + أضف أعضاء فريقك إلى المحادثات. + يُحظر إرسال الرسائل المباشرة بين الأعضاء في هذه الدردشة. + أجهزة Xiaomi: يُرجى تفعيل التشغيل التلقائي (Autostart) في إعدادات النظام لكي تعمل الإشعارات.]]> + مُعمَّاة بين الطرفين، مع أمان ما بعد الكم في الرسائل المباشرة.]]> + تحقق من الرسائل كل 10 دقائق + يُمنع إرسال الرسائل المباشرة بين الأعضاء. + الدردشة + كيف يساعد على الخصوصية + سيتم حذف الدردشة لجميع الأعضاء - لا يمكن التراجع عن هذا! + سيتم حذف الدردشة لديك - لا يمكن التراجع عن هذا! + احذف الدردشة + الدردشة موجودة بالفعل! + حذف الدردشة؟ + %1$s.]]> + أو استورد ملف الأرشيف + لا توجد خدمة خلفية + الإشعارات والبطارية + يمكن فقط لأصحاب الدردشة تغيير التفضيلات. + الخصوصية لعملائك. + الجوالات عن بُعد + ادعُ للدردشة + مغادرة المجموعة؟ + سيتم إزالة العضو من الدردشة - لا يمكن التراجع عن هذا! + غادِر الدردشة + الرسالة كبيرة جدًا! + يُرجى تقليل حجم الرسالة وإرسالها مرة أخرى. + شريط أداة الدردشة القابلة للوصول + الدعوة قُبلت + طلبت الاتصال + يُرجى تقليل حجم الرسالة أو إزالة الوسائط ثم إرسالها مرة أخرى. + يمكنك نسخ الرسالة وتقليل حجمها لإرسالها. + عندما يتم تفعيل أكثر من مُشغل واحد، لن يكون لدى أي منهم بيانات تعريفية لمعرفة من يتواصل مع من. + سيتم تغيير الدور إلى %s. وسيتم إشعار الجميع في الدردشة. + سيتم إرسال ملف تعريفك للدردشة إلى أعضاء الدردشة + سوف تتوقف عن تلقي الرسائل من هذه الدردشة. سيتم حفظ سجل الدردشة. + عن المُشغلين \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml index 5ab956ae85..7c3ecd5ece 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml @@ -62,7 +62,7 @@ Der Absender hat die Dateiübertragung abgebrochen. Fehler beim Empfangen der Datei Fehler beim Erstellen der Adresse - Kontakt ist bereits vorhanden + Kontakt besteht bereits Sie sind bereits mit %1$s verbunden. Ungültiger Verbindungslink Überprüfen Sie bitte, ob Sie den richtigen Link genutzt haben, oder bitten Sie Ihren Kontakt darum, Ihnen nochmal einen Link zuzusenden. @@ -88,9 +88,9 @@ Sofortige Benachrichtigungen Sofortige Benachrichtigungen! Sofortige Benachrichtigungen sind deaktiviert! - SimpleX-Hintergrunddienst genutzt werden – dieser benötigt ein paar Prozent Akkuleistung am Tag.]]> + läuft SimpleX im Hintergrund ab, anstatt Push-Benachrichtigungen zu nutzen.]]> Diese können über die Einstellungen deaktiviert werden – solange die App läuft, werden Benachrichtigungen weiterhin angezeigt.]]> - Erlauben Sie SimpleX im Hintergrund abzulaufen. Ansonsten werden die Benachrichtigungen deaktiviert.]]> + Erlauben Sie es im nächsten Dialog.]]> Die Akkuoptimierung ist aktiv, der Hintergrunddienst und die periodische Nachfrage nach neuen Nachrichten ist abgeschaltet. Sie können diese Funktion in den Einstellungen wieder aktivieren. Periodische Benachrichtigungen Periodische Benachrichtigungen sind deaktiviert! @@ -462,7 +462,7 @@ Verbunden Beendet - Die nächste Generation \ndes privaten Messagings + Die Zukunft des Messagings Datenschutz neu definiert Keine Benutzerkennungen. Immun gegen Spam @@ -474,8 +474,8 @@ Wie es funktioniert Wie SimpleX funktioniert - Zum Schutz Ihrer Privatsphäre verwendet SimpleX anstelle von Benutzerkennungen, die von allen anderen Plattformen verwendet werden, Kennungen für Nachrichtenwarteschlangen, die für jeden Ihrer Kontakte individuell sind. - zweischichtige Ende-zu-Ende-Verschlüsselung gesendet werden.]]> + SimpleX nutzt individuelle Kennungen für jeden Ihrer Kontakte, um Ihre Privatsphäre zu schützen. + Nur die Endgeräte speichern Benutzerprofile, Kontakte, Gruppen und Nachrichten. GitHub-Repository mehr dazu.]]> Fügen Sie den erhaltenen Link ein @@ -683,7 +683,7 @@ Die Einladung ist abgelaufen! Die Gruppeneinladung ist nicht mehr gültig, da sie vom Absender entfernt wurde. Die Gruppe wurde nicht gefunden! - Diese Gruppe existiert nicht mehr. + Diese Gruppe ist nicht mehr vorhanden. Kontakte können nicht eingeladen werden! Sie verwenden ein Inkognito-Profil für diese Gruppe. Um zu verhindern, dass Sie Ihr Hauptprofil teilen, ist in diesem Fall das Einladen von Kontakten nicht erlaubt. @@ -781,8 +781,8 @@ Ändern Wechseln Die Mitgliederrolle ändern? - Die Mitgliederrolle wird auf "%s" geändert. Alle Mitglieder der Gruppe werden benachrichtigt. - Die Mitgliederrolle wird auf "%s" geändert. Das Mitglied wird eine neue Einladung erhalten. + Die Rolle wird auf %s geändert. Alle Mitglieder der Gruppe werden benachrichtigt. + Die Rolle wird auf %s geändert. Das Mitglied wird eine neue Einladung erhalten. Fehler beim Entfernen des Mitglieds Fehler beim Ändern der Rolle Gruppe @@ -872,12 +872,12 @@ Unwiederbringliches Löschen von Nachrichten nicht erlauben. Das Senden von Sprachnachrichten erlauben. Das Senden von Sprachnachrichten nicht erlauben. - Gruppenmitglieder können Direktnachrichten versenden. + Mitglieder können Direktnachrichten versenden. In dieser Gruppe sind Direktnachrichten zwischen Mitgliedern nicht erlaubt. - Gruppenmitglieder können gesendete Nachrichten unwiederbringlich löschen (bis zu 24 Stunden). - In dieser Gruppe ist das unwiederbringliche Löschen von Nachrichten nicht erlaubt. - Gruppenmitglieder können Sprachnachrichten versenden. - In dieser Gruppe sind Sprachnachrichten nicht erlaubt. + Mitglieder können gesendete Nachrichten unwiederbringlich löschen (bis zu 24 Stunden). + Das unwiederbringliche Löschen von Nachrichten ist nicht erlaubt. + Mitglieder können Sprachnachrichten versenden. + Sprachnachrichten sind nicht erlaubt. LIVE Schauen Sie sich den Sicherheitscode an Sofort @@ -887,7 +887,7 @@ %s wurde erfolgreich überprüft Verifikation zurücknehmen Solange die App abläuft - Kann später über die Einstellungen geändert werden. + Auswirkung auf den Akku Löschen nach %d Stunde %d Stunden @@ -922,8 +922,8 @@ Gruppenlink erstellen Erlauben Sie Ihren Kontakten das Senden von verschwindenden Nachrichten. Das Senden von verschwindenden Nachrichten nicht erlauben. - In dieser Gruppe sind verschwindende Nachrichten nicht erlaubt. - Gruppenmitglieder können verschwindende Nachrichten senden. + Verschwindende Nachrichten sind nicht erlaubt. + Mitglieder können verschwindende Nachrichten senden. Fügen Sie Server durch Scannen der QR-Codes hinzu. Verschwindende Nachrichten Übernehmen @@ -978,7 +978,7 @@ Chat-Profil löschen für PING-Zähler Transport-Isolations-Modus aktualisieren\? - Mögliche Server für neue Verbindungen über Ihr aktuelles Chat-Profil + Nachrichten-Server für neue Verbindungen über Ihr aktuelles Chat-Profil Dateien & Medien Transport-Isolation Chat-Profil löschen\? @@ -1248,7 +1248,7 @@ Wenn Sie diesen Zugangscode während des Öffnens der App eingeben, werden alle App-Daten unwiederbringlich gelöscht! Selbstzerstörungs-Zugangscode Zugangscode einstellen - In dieser Gruppe sind Reaktionen auf Nachrichten nicht erlaubt. + Reaktionen auf Nachrichten sind nicht erlaubt. Fehler beim Laden von Details Empfangene Nachricht Information @@ -1279,7 +1279,7 @@ Nur Ihr Kontakt kann Reaktionen auf Nachrichten geben. Reaktionen auf Nachrichten erlauben. Reaktionen auf Nachrichten nicht erlauben. - Gruppenmitglieder können eine Reaktion auf Nachrichten geben. + Mitglieder können eine Reaktion auf Nachrichten geben. Mehr erfahren Endlich haben wir sie! 🚀 Reaktionen auf Nachrichten @@ -1294,9 +1294,7 @@ Farbdesigns anpassen und weitergeben. Tage Stunden - - Bis zu 5 Minuten lange Sprachnachrichten -\n- Zeitdauer für verschwindende Nachrichten anpassen -\n- Nachrichtenverlauf bearbeiten + - Bis zu 5 Minuten lange Sprachnachrichten\n- Zeitdauer für verschwindende Nachrichten anpassen\n- Nachrichtenverlauf bearbeiten benutzerdefiniert Monate Auswählen @@ -1325,9 +1323,9 @@ Wechsel der Empfängeradresse beenden? Dateien und Medien sind nicht erlaubt! Nur Gruppenbesitzer können Dateien und Medien aktivieren. - Gruppenmitglieder können Dateien und Medien senden. + Mitglieder können Dateien und Medien senden. Der Wechsel der Empfängeradresse wird beendet. Die bisherige Adresse wird weiter verwendet. - In dieser Gruppe sind Dateien und Medien nicht erlaubt. + Dateien und Medien sind nicht erlaubt. Favorit entfernen Favorit Keine gefilterten Chats @@ -1385,9 +1383,7 @@ Reparatur der Verschlüsselung nach Wiedereinspielen von Backups. Ein paar weitere Dinge Auch wenn sie in den Unterhaltungen deaktiviert sind. - - stabilere Zustellung von Nachrichten. -\n- ein bisschen verbesserte Gruppen. -\n- und mehr! + - Stabilere Zustellung von Nachrichten.\n- Ein bisschen verbesserte Gruppen.\n- Und mehr! Nicht aktivieren Das Senden von Empfangsbestätigungen an alle Kontakte wird aktiviert. Sie können diese später in den Datenschutz- und Sicherheits-Einstellungen der App aktivieren. @@ -1457,9 +1453,7 @@ Arabisch, Bulgarisch, Finnisch, Hebräisch, Thailändisch und Ukrainisch - Dank der Nutzer und Weblate. Erstellen eines neuen Profils in der Desktop-App. 💻 Inkognito beim Verbinden einschalten. - - Verbindung mit dem Directory-Service (BETA)! -\n- Empfangsbestätigungen (für bis zu 20 Mitglieder). -\n- Schneller und stabiler. + - Verbindung mit dem Directory-Service (BETA)!\n- Empfangsbestätigungen (für bis zu 20 Mitglieder).\n- Schneller und stabiler. Direktnachricht senden Direkt miteinander verbunden Erweitern @@ -1568,9 +1562,7 @@ Desktop-Adresse einfügen Code mit dem Desktop überprüfen Den QR-Code vom Desktop scannen - - Optionale Benachrichtigung von gelöschten Kontakten. -\n- Profilnamen mit Leerzeichen. -\n- Und mehr! + - Optionale Benachrichtigung von gelöschten Kontakten.\n- Profilnamen mit Leerzeichen.\n- Und mehr! Vom Mobiltelefon scannen Verbindungen überprüfen Bitte warten Sie, solange die Datei von dem verknüpften Mobiltelefon geladen wird @@ -1799,12 +1791,12 @@ SimpleX-Links sind nicht erlaubt Sprachnachrichten sind nicht erlaubt SimpleX-Links - Gruppenmitglieder können SimpleX-Links senden. + Mitglieder können SimpleX-Links senden. Administratoren Alle Mitglieder Aktiviert für Eigentümer - In dieser Gruppe sind SimpleX-Links nicht erlaubt. + SimpleX-Links sind nicht erlaubt. Das Senden von SimpleX-Links nicht erlauben. Das Senden von SimpleX-Links erlauben. Lautsprecher @@ -2077,7 +2069,7 @@ Archivierte Kontakte Keine gefilterten Kontakte Ihre Kontakte - Chat-Symbolleiste unten + App-Symbolleiste unten Bitten Sie Ihren Kontakt darum, Anrufe zu aktivieren. Sie müssen Ihrem Kontakt Anrufe zu Ihnen erlauben, bevor Sie ihn selbst anrufen können. Anrufe erlauben? @@ -2157,8 +2149,7 @@ Verwenden Sie für jedes Profil unterschiedliche Proxy-Anmeldeinformationen. Verwenden Sie zufällige Anmeldeinformationen Benutzername - %1$d Datei-Fehler: -\n%2$s + %1$d Datei-Fehler:\n%2$s %1$d Datei(en) wird/werden immer noch heruntergeladen. Bei %1$d Datei(en) ist das Herunterladen fehlgeschlagen. Fehler beim Weiterleiten der Nachrichten @@ -2215,11 +2206,11 @@ Für soziale Medien Oder zum privaten Teilen SimpleX-Adresse oder Einmal-Link? - Betreiber auswählen + Server-Betreiber Netzwerk-Betreiber - Wenn mehr als ein Netzwerk-Betreiber aktiviert ist, verwendet die App für jede Unterhaltung Server der verschiedenen Betreiber. + Die App verwendet für jede Unterhaltung Server von unterschiedlichen Betreibern, um Ihre Privatsphäre zu schützen. Die Nutzungsbedingungen der aktivierten Betreiber werden nach 30 Tagen akzeptiert. - Wenn Sie beispielsweise Nachrichten über einen SimpleX-Chatserver empfangen, verwendet die App einen der Server von Flux für die private Weiterleitung. + Wenn Ihr Kontakt beispielsweise Nachrichten über einen SimpleX-Chat-Server empfängt, wird Ihre App diese über einen Flux-Server versenden. Später einsehen Wählen sie die zu nutzenden Netzwerk-Betreiber aus. Sie können die Betreiber in den Netzwerk- und Servereinstellungen konfigurieren. @@ -2246,7 +2237,7 @@ Für Nachrichten verwenden Nachrichtenserver hinzugefügt Für privates Routing - Die Server Deines aktuellen Chat-Profils für neue Dateien + Medien- und Datei-Server für neue Daten über Ihr aktuelles Chat-Profil Für das Senden Für Dateien verwenden Fehler beim Hinzufügen des Servers @@ -2271,7 +2262,7 @@ nur mit einem Kontakt genutzt werden - teilen Sie in nur persönlich oder über einen beliebigen Messenger.]]> %s.]]> %s.]]> - Die Nutzungsbedingungen werden akzeptiert am: %s. + Die Nutzungsbedingungen wurden akzeptiert am: %s %s.]]> %s zu nutzen, müssen Sie dessen Nutzungsbedingungen akzeptieren.]]> Fehler beim Akzeptieren der Nutzungsbedingungen @@ -2285,10 +2276,49 @@ Diese Verbindung hat das Limit der nicht ausgelieferten Nachrichten erreicht. Ihr Kontakt ist möglicherweise offline. Diese Nachricht wurde gelöscht oder bisher noch nicht empfangen. Zum Schutz vor dem Austausch Ihres Links können Sie die Sicherheitscodes Ihrer Kontakte vergleichen. - %s.]]> + %s.]]> %s.]]> Die Nutzungsbedingungen wurden akzeptiert am: %s. Der Text der aktuellen Nutzungsbedingungen konnte nicht geladen werden. Sie können die Nutzungsbedingungen unter diesem Link einsehen: Ferngesteuerte Mobiltelefone Oder importieren Sie eine Archiv-Datei + Hinweis für Geräte von Xiaomi: Bitte aktivieren Sie in den System-Einstellungen die Option "Autostart", damit Benachrichtigungen funktionieren.]]> + Ende-zu-Ende-verschlüsselt versendet. In Direktnachrichten sogar mit Post-Quantum-Security.]]> + Team-Mitglieder aufnehmen + Freunde aufnehmen + Einladung akzeptiert + Geschäftliche Adresse + Geschäftliche Chats + Nehmen Sie Team-Mitglieder in Ihre Unterhaltungen auf. + Die App läuft immer im Hintergrund ab + In diesem Chat sind Direktnachrichten zwischen Mitgliedern nicht erlaubt. + Kein Hintergrund-Service + Nachrichten alle 10 Minuten überprüfen + Benachrichtigungen und Akku + Zum Chat einladen + Chat besteht bereits! + Chat-Symbolleiste unten + Chat verlassen + Das Mitglied wird aus dem Chat entfernt. Dies kann nicht rückgängig gemacht werden! + Ihr Chat-Profil wird an die Chat-Mitglieder gesendet. + Direktnachrichten zwischen Mitgliedern sind nicht erlaubt. + Wie die Privatsphäre geschützt wird + Chat verlassen? + Chat löschen? + Der Chat wird für alle Mitglieder gelöscht. Dies kann nicht rückgängig gemacht werden! + Schutz der Privatsphäre Ihrer Kunden. + Zur Verbindung aufgefordert + Bitte verkleinern Sie die Nachrichten-Größe oder entfernen Sie Medien und versenden Sie diese erneut. + Nur Chat-Eigentümer können die Präferenzen ändern. + Bitte verkleinern Sie die Nachrichten-Größe und versenden Sie diese erneut. + Die Rolle wird auf %s geändert. Im Chat wird Jeder darüber informiert. + Sie werden von diesem Chat keine Nachrichten mehr erhalten. Der Nachrichtenverlauf wird beibehalten. + Sie können die Nachricht kopieren und verkleinern, um sie zu versenden. + Chat löschen + Der Chat wird für Sie gelöscht. Dies kann nicht rückgängig gemacht werden! + Die Nachricht ist zu umfangreich! + Wenn mehr als ein Betreiber aktiviert ist, hat keiner von ihnen Metadaten, um zu erfahren, wer mit wem kommuniziert. + Chat + %1$s verbunden.]]> + Über Betreiber \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml index 0e53f59c06..434e05c174 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml @@ -94,7 +94,7 @@ Autenticación de dispositivo desactivada. Puedes habilitar Bloqueo SimpleX en Configuración, después de activar la autenticación de dispositivo. Desactivar Los mensajes temporales no están permitidos en este chat. - Los mensajes temporales no están permitidos en este grupo. + Mensajes temporales no permitidos. El nombre mostrado no puede contener espacios en blanco. Videollamada con cifrado de extremo a extremo conexión establecida @@ -337,7 +337,7 @@ Introduce la contraseña… Grupo inactivo grupo eliminado - Los miembros del grupo pueden enviar mensajes temporales. + Los miembros pueden enviar mensajes temporales. Enlaces de grupo Enlace de conexión no válido Error al aceptar solicitud del contacto @@ -357,7 +357,7 @@ Error al eliminar base de datos Base de datos cifrada Error al eliminar miembro - Los miembros del grupo pueden enviar mensajes de voz. + Los miembros pueden enviar mensajes de voz. en modo incógnito mediante enlace de dirección del contacto ¡Error al crear perfil! No se pudo cargar el chat @@ -407,8 +407,8 @@ SERVIDORES Nombre del grupo: Preferencias del grupo - Los miembros del grupo pueden enviar mensajes directos. - Los miembros del grupo pueden eliminar mensajes de forma irreversible. (24 horas) + Los miembros pueden enviar mensajes directos. + Los miembros pueden eliminar mensajes enviados de forma irreversible. (24 horas) Ocultar pantalla de aplicaciones en aplicaciones recientes. Cifrar Ampliar la selección de roles @@ -431,7 +431,7 @@ Cómo funciona El mensaje será eliminado. ¡No podrá deshacerse! El modo incógnito protege tu privacidad creando un perfil aleatorio por cada contacto. - permite que SimpleX se ejecute en segundo plano en el siguiente cuadro de diálogo. De lo contrario las notificaciones se desactivarán.]]> + Da permiso en el siguiente diálogo para recibir notificaciones instantáneas.]]> Instalar terminal de SimpleX Chat invitación al grupo %1$s ha invitado a %1$s @@ -443,7 +443,7 @@ Notificación instantánea Configuración avanzada Sólo los dispositivos cliente almacenan perfiles de usuario, contactos, grupos y mensajes. - Puedes cambiar estos ajustes más tarde en Configuración. + Cómo afecta a la batería Instantánea Unirte Unirte en modo incógnito @@ -451,7 +451,7 @@ Claro Activado La eliminación irreversible de mensajes no está permitida en este chat. - La eliminación irreversible de mensajes no está permitida en este grupo. + Eliminación irreversible no permitida. Configuración del servidor mejorada Esto puede ocurrir cuando: \n1. Los mensajes hayan caducado en el cliente saliente tras 2 días o en el servidor tras 30 días. @@ -555,7 +555,7 @@ has cambiado el servidor para %s ha salido Salir del grupo - Sólo los propietarios pueden modificar las preferencias del grupo. + Sólo los propietarios del grupo pueden cambiar las preferencias. Eliminar sólo el perfil no k @@ -642,7 +642,7 @@ Espacio reservado para la imagen del perfil Código QR Consultas y sugerencias - Dirección del servidor predefinida + Dirección predefinida del servidor Contacta vía email Valora la aplicación Guardar @@ -718,7 +718,7 @@ envío no autorizado Escribe un nombre para el contacto Error desconocido - El rol del miembro cambiará a "%s" y se notificará al grupo. + El rol cambiará a %s. Todos serán notificados. La seguridad de SimpleX Chat ha sido auditada por Trail of Bits. Los mensajes enviados se eliminarán una vez transcurrido el tiempo establecido. Mensajes de chat SimpleX @@ -736,12 +736,12 @@ ¡La conexión que has aceptado se cancelará! La base de datos no funciona correctamente. Pulsa para conocer más El mensaje será marcado como moderado para todos los miembros. - La nueva generación \nde mensajería privada + El futuro de la mensajería Esta acción es irreversible. Se eliminarán todos los archivos y multimedia recibidos y enviados. Las imágenes de baja resolución permanecerán. Esta acción es irreversible. Los mensajes enviados y recibidos anteriores a la selección serán eliminados. Podría tardar varios minutos. Esta configuración se aplica a los mensajes del perfil actual ¡Esta cadena no es un enlace de conexión! - servicio en segundo planoSimpleX, usa un pequeño porcentaje de la batería al día.]]> + SimpleX se ejecuta en segundo plano en lugar de usar notificaciones push.]]> Configuración Altavoz desactivado Inciar chat nuevo @@ -785,7 +785,7 @@ Probar servidor Probar servidores Estrella en GitHub - Lista de servidores para las conexiones nuevas de tu perfil actual + Lista de servidores para las conexiones nuevas del perfil ¿Usar conexión directa a Internet\? El perfil sólo se comparte con tus contactos. inicializando… @@ -804,7 +804,7 @@ Actualizar contraseña base de datos Pulsa para unirte en modo incógnito Cambiar - El rol del miembro cambiará a "%s" y recibirá una invitación nueva. + El rol cambiará a %s y el miembro recibirá una invitación nueva. Actualizar ¿Actualizar la configuración de red\? Intentando conectar con el servidor para recibir mensajes de este contacto. @@ -840,9 +840,9 @@ Mensajes de voz Tus contactos pueden permitir la eliminación completa de mensajes. Mensajes de voz - Los mensajes de voz no están permitidos en este grupo. + Mensajes de voz no permitidos. Comprobar la seguridad de la conexión - ¡Ya estás conectado a %1$s. + ¡Ya estás conectado con %1$s. ¡Bienvenido! Tu perfil será enviado \na tu contacto @@ -1028,7 +1028,7 @@ Servidores XFTP Puerto puerto %d - Usar hosts .onion como No si el proxy SOCKS no los admite.]]> + Usar hosts .onion debe estar a No si el proxy SOCKS no los admite.]]> Descargar archivo Usar proxy SOCKS Host @@ -1208,7 +1208,7 @@ semanas Error al cargar detalles Los miembros pueden añadir reacciones a los mensajes. - Las reacciones a los mensajes no están permitidas en este grupo. + Reacciones a los mensajes no permitidas. Sólo tu contacto puede añadir reacciones a los mensajes. 1 minuto Registro actualiz @@ -1228,12 +1228,12 @@ Personalizar y compartir temas de color. ¡Por fin los tenemos! 🚀 Reacciones a los mensajes - Conoce más + Saber más Interfaz en japonés y portugués sin texto Han ocurrido algunos errores no críticos durante la importación: ¿Cerrar\? - Aplicación + APLICACIÓN Reiniciar Cerrar Las notificaciones dejarán de funcionar hasta que reinicies la aplicación @@ -1248,8 +1248,8 @@ Cancelar cambio de dirección Archivos y multimedia No se permite el envío de archivos y multimedia. - Los archivos y multimedia no están permitidos en este grupo. - Los miembros del grupo pueden enviar archivos y multimedia. + Archivos y multimedia no permitidos. + Los miembros pueden enviar archivos y multimedia. Se permite enviar archivos y multimedia Favorito Sólo los propietarios del grupo pueden activar los archivos y multimedia. @@ -1356,7 +1356,7 @@ La contraseña aleatoria se almacenará en Configuración como texto plano. \nPuedes cambiarlo más tarde. La contraseña para el cifrado de la base de datos se actualizará y almacenará en Configuración - Eliminar contraseña de configuración\? + ¿Eliminar contraseña de configuración? Usar contraseña aleatoria Guardar contraseña en configuración Configuración contraseña base de datos @@ -1715,8 +1715,8 @@ Enlaces SimpleX no permitidos Mensajes de voz no permitidos Enlaces SimpleX - Los miembros del grupo pueden enviar enlaces SimpleX. - Los enlaces SimpleX no se permiten en este grupo. + Los miembros pueden enviar enlaces SimpleX. + Enlaces SimpleX no permitidos. propietarios Móvil Sin conexión de red @@ -1897,7 +1897,7 @@ errores de descifrado Eliminadas Errores de eliminación - desactivado + inactivo Mensaje reenviado El mensaje puede ser entregado más tarde si el miembro vuelve a estar activo. Miembro inactivo @@ -1990,7 +1990,7 @@ Medio Suave Barra de herramientas accesible - llamada + llamar conectar ¿Eliminar %d mensajes de miembros? mensaje @@ -2083,8 +2083,8 @@ Error guardando proxy Contraseña Autenticación proxy - Credenciales proxy diferentes para cada conexión. - Credenciales proxy diferentes para cada perfil. + Se usan credenciales proxy diferentes para cada conexión. + Se usan credenciales proxy diferentes para cada perfil. Credenciales aleatorias Nombre de usuario Tus credenciales podrían ser enviadas sin cifrar. @@ -2125,12 +2125,12 @@ Ningún servidor para enviar archivos. Seguridad de conexión Compartir enlace de un uso con un amigo - Compartir dirección SimpleX en redes sociales. + Comparte tu dirección SimpleX en redes sociales. Configuración de dirección Crear enlace de un uso Para redes sociales - Dirección SimpleX o enlace de un uso? - Selecciona operadores + ¿Dirección SimpleX o enlace de un uso? + Operadores de servidores Operadores de red Las condiciones de los operadores habilitados serán aceptadas después de 30 días. Revisar más tarde @@ -2162,10 +2162,10 @@ Las condiciones serán aceptadas automáticamente para los operadores habilitados el: %s. Continuar El texto con las condiciones actuales no se ha podido cargar, puedes revisar las condiciones en el siguiente enlace: - Habilitar Flux + Habilita Flux Error al aceptar las condiciones Error al actualizar el servidor - para mayor privacidad de los metadatos. + para mejorar la privacidad de los metadatos. Ningún mensaje Servidor nuevo Ningún servidor de archivos y multimedia. @@ -2175,7 +2175,7 @@ O para compartir en privado Selecciona los operadores de red a utilizar Campartir dirección públicamente - Compartir enlaces de un uso y direcciones SimpleX es seguro a través de cualquier medio. + Compartir los enlaces de un uso y las direcciones SimpleX es seguro a través de cualquier medio. Actualizar Sitio web Tus servidores @@ -2191,25 +2191,64 @@ Mensajes no entregados solamente con un contacto - comparte en persona o mediante cualquier aplicación de mensajería.]]> Puedes añadir un nombre a la conexión para recordar a quién corresponde. - Cuando está habilitado más de un operador de red, la aplicación usa servidores de diferentes operadores para cada conversación. + La aplicación protege tu privacidad mediante el uso de diferentes operadores en cada conversación. Puedes configurar los operadores desde Servidores y Redes. %s.]]> %s.]]> %s.]]> - %s.]]> + %s.]]> %s.]]> %s.]]> %s, acepta las condiciones de uso.]]> Los servidores para archivos nuevos en tu perfil actual - El segundo operador predefinido! + ¡Segundo operador predefinido! Puedes configurar los servidores a través de su configuración. Para protegerte contra una sustitución del enlace, puedes comparar los códigos de seguridad con tu contacto. - %s.]]> + %s.]]> %s.]]> - Si por ejemplo recibes los mensajes a través de un servidor de SimpleX Chat, la aplicación usará uno de Flux para el enrutamiento privado. + Por ejemplo, si tu contacto recibe a través de un servidor de SimpleX Chat, tu aplicación enviará a través de un servidor de Flux. Pulsa Crear dirección SimpleX en el menú para crearla más tarde. La conexión ha alcanzado el límite de mensajes no entregados. es posible que tu contacto esté desconectado. El mensaje ha sido borrado o aún no se ha recibido. Móvil remoto O importa desde un archivo + Mensajes directos entre miembros de este chat no permitidos. + En dispositivos Xiaomi: por favor, habilita el Autoinicio en los ajustes del sistema para que las notificaciones funcionen.]]> + Por favor, reduce el tamaño del mensaje y envíalo de nuevo. + Por favor, reduce el tamaño del mensaje o elimina los archivos y envíalo de nuevo. + Puedes copiar y reducir el tamaño del mensaje para enviarlo. + Añade a los miembros de tu equipo a las conversaciones. + Notificaciones y batería + Invitar al chat + Añadir amigos + Añadir miembros del equipo + El chat será eliminado para todos los miembros. ¡No podrá deshacerse! + Eliminar chat + ¿Eliminar chat? + Salir del chat + El chat será eliminado para tí. ¡No podrá deshacerse! + Sólo los propietarios del chat pueden cambiar las preferencias. + El miembro será eliminado del chat. ¡No podrá deshacerse! + El rol cambiará a %s. Todos serán notificados. + Dejarás de recibir mensajes de este chat. El historial del chat se conserva. + Cómo ayuda a la privacidad + Cuando está habilitado más de un operador, ninguno dispone de los metadatos para conocer quién se comunica con quién. + Tu perfil de chat será enviado a los miembros de chat + Chats empresariales + ¿Salir del chat? + Privacidad para tus clientes. + invitación aceptada + solicitado para conectar + Dirección empresarial + Comprobar mensajes cada 10 min. + Sin servicio en segundo plano + Chat + Barra de herramientas accesible + Mensajes directos entre miembros no permitidos. + %1$s.]]> + ¡El chat ya existe! + Acerca de los operadores + La aplicación siempre funciona en segundo plano + cifrados de extremo a extremo y con seguridad postcuántica en mensajes directos.]]> + ¡Mensaje demasiado largo! \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml index 83f408054f..531e29de20 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml @@ -15,7 +15,7 @@ 30 másodperc Egyszer használható meghívó-hivatkozás %1$s szeretne kapcsolatba lépni Önnel ezen keresztül: - A SimpleX Chatről + SimpleX Chat névjegye 1 nap Címváltoztatás megszakítása A SimpleXről @@ -1754,7 +1754,7 @@ Ne küldjön üzeneteket közvetlenül, még akkor sem, ha az Ön kiszolgálója vagy a célkiszolgáló nem támogatja a privát útválasztást. Tor vagy VPN nélkül az IP-címe látható lesz a fájlkiszolgálók számára. FÁJLOK - IP-cím védelem + IP-cím védelme Az alkalmazás kérni fogja az ismeretlen fájlkiszolgálókról történő letöltések megerősítését (kivéve, ha az .onion vagy a SOCKS proxy engedélyezve van). Ismeretlen kiszolgálók! Tor vagy VPN nélkül az IP-címe látható lesz az XFTP-közvetítő-kiszolgálók számára:\n%1$s. @@ -2109,13 +2109,13 @@ Egyszer használható meghívó-hivatkozás létrehozása Kiszolgáló-üzemeltetők Hálózati üzemeltetők - Amikor egynél több hálózati üzemeltető van engedélyezve, akkor az alkalmazás minden egyes beszélgetéshez a különböző üzemeltetők kiszolgálóit használja. - Ha például a SimpleX Chat kiszolgálón keresztül fogadja az üzeneteket, az alkalmazás a Flux egyik kiszolgálóját használja a privát útválasztáshoz. + Az alkalmazás úgy védi az adatait, hogy minden egyes beszélgetésben más-más üzemeltetőt használ. + Például, ha az Ön ismerőse egy SimpleX Chat-kiszolgálón keresztül fogadja az üzeneteket, az Ön alkalmazása egy Flux-kiszolgálón keresztül fogja azokat kézbesíteni. Válassza ki a használni kívánt hálózati üzemeltetőket. Felülvizsgálat később - A kiszolgálókat a beállításokon keresztül konfigurálhatja. + A kiszolgálókat a „Hálózat és kiszolgálók” menüben konfigurálhatja. A feltételek 30 nap elteltével lesznek elfogadva az engedélyezett üzemeltetők számára. - Az üzemeltetőket a „Hálózat és kiszolgálók” beállításaban konfigurálhatja. + Az üzemeltetőket a „Hálózat és kiszolgálók” menüben konfigurálhatja. Frissítés Folytatás Feltételek felülvizsgálata @@ -2133,14 +2133,14 @@ %s használata A jelenlegi feltételek szövegét nem lehetett betölteni, a feltételeket ezen a hivatkozáson keresztül vizsgálhatja felül: %s.]]> - %s.]]> - %s.]]> + %s.]]> + %s.]]> %s.]]> %s.]]> %s.]]> Feltételek elfogadása Használati feltételek - %s kiszolgálóinak használatához fogadja el a használati feltételeket.]]> + %s kiszolgálók használatához fogadja el a használati feltételeket.]]> Használat az üzenetekhez A fogadáshoz A privát útválasztáshoz @@ -2204,4 +2204,9 @@ Az üzenet túl nagy! Csökkentse az üzenet méretét vagy távolítsa el a médiát, és küldje el újra. A tagok közötti közvetlen üzenetek le vannak tiltva ebben a csevegésben. + Amikor egynél több üzemeltető van engedélyezve, akkor egyik sem rendelkezik olyan metaadatokkal, amelyekből megtudható, hogy ki kivel kommunikál. + elfogadott meghívó + kérelmezve a kapcsolódáshoz + Az üzemeltetőkről + A SimpleX Chat és a Flux megállapodást kötött arról, hogy a Flux által üzemeltetett kiszolgálókat beépítik az alkalmazásba. \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml index c342319dbb..11727beaed 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml @@ -363,7 +363,7 @@ Audio spento Audio acceso Chiamate audio e video - Auto-accetta immagini + Auto-accetta le immagini hash del messaggio errato ID messaggio errato Chiamata terminata @@ -470,7 +470,7 @@ attivato per il contatto attivato per te Preferenze del gruppo - Auto-accetta richieste di contatto + Auto-accetta le richieste di contatto %dg %d giorno %d giorni @@ -538,7 +538,7 @@ Codice QR non valido immagine di anteprima link Segna come già letto - Segna come non letto + Segna come non letta Altro Silenzia immagine del profilo @@ -704,7 +704,7 @@ Riavvia l\'app per creare un profilo di chat nuovo. Riavvia l\'app per usare il database della chat importato. AVVIA CHAT - Invia anteprime dei link + Invia le anteprime dei link Imposta la password per esportare IMPOSTAZIONI PROXY SOCKS @@ -918,7 +918,7 @@ Grazie agli utenti – contribuite via Weblate! Interfaccia francese Interfaccia italiana - Bozza dei messaggi + Bozza del messaggio Conserva la bozza dell\'ultimo messaggio, con gli allegati. Nomi di file privati Per profilo di chat (predefinito) o per connessione (BETA). @@ -1348,7 +1348,7 @@ %s e %s si sono connessi/e %s, %s e altri %d membri si sono connessi %s, %s e %s si sono connessi/e - Bozza + Bozza del messaggio Mostra gli ultimi messaggi Il database verrà crittografato e la password conservata nelle impostazioni. La password casuale viene conservata nelle impostazioni come testo normale. @@ -1615,8 +1615,7 @@ hai bloccato %s Il messaggio di benvenuto è troppo lungo Messaggio troppo grande - Migrazione database in corso. -\nPuò richiedere qualche minuto. + Migrazione del database in corso.\nPuò richiedere qualche minuto. Chiamata audio Termina chiamata Videochiamata @@ -1980,7 +1979,7 @@ Errore di connessione al server di inoltro %1$s. Riprova più tardi. La versione server di inoltro è incompatibile con le impostazioni di rete: %1$s. Off - Sfocatura file multimediali + Sfocatura dei file multimediali Leggera Media Forte @@ -2131,18 +2130,18 @@ Per i social media O per condividere in modo privato Operatori di rete - Quando più di un operatore di rete è attivato, l\'app userà i server di diversi operatori per ogni conversazione. + L\'app protegge la tua privacy usando diversi operatori per ogni conversazione. Puoi configurare gli operatori nelle impostazioni di rete e server. Operatori del server Seleziona gli operatori di rete da usare. Continua Aggiorna - Esamina più tardi + Leggi più tardi Server preimpostati Condizioni accettate Le condizioni verranno accettate automaticamente per gli operatori attivati il: %s. I tuoi server - Esamina le condizioni + Leggi le condizioni %s.]]> %s.]]> Il testo delle condizioni attuali testo non è stato caricato, puoi consultare le condizioni tramite questo link: @@ -2192,13 +2191,13 @@ Errore di accettazione delle condizioni Errore di salvataggio dei server per una migliore privacy dei metadati. - Ad esempio, se ricevi messaggi tramite il server di SimpleX Chat, l\'app userà uno dei server Flux per l\'instradamento privato. + Ad esempio, se il tuo contatto riceve i messaggi tramite un server di SimpleX Chat, la tua app li consegnerà tramite un server di Flux. Navigazione della chat migliorata Nuovo server Usa per i file Indirizzo SimpleX o link una tantum? Questo messaggio è stato eliminato o non ancora ricevuto. - Tocca \"Crea indirizzo SimpleX\" nel menu per crearlo più tardi. + Tocca Crea indirizzo SimpleX nel menu per crearlo più tardi. La connessione ha raggiunto il limite di messaggi non consegnati, il contatto potrebbe essere offline. Usa i server Puoi configurare i server nelle impostazioni. @@ -2208,7 +2207,7 @@ Nessun server per inviare file. - Apri la chat sul primo messaggio non letto.\n- Salta ai messaggi citati. Condividi indirizzo pubblicamente - Condividi indirizzo SimpleX sui social media. + Condividi l\'indirizzo SimpleX sui social media. O importa file archivio Telefoni remoti I messaggi diretti tra i membri sono vietati in questa chat. @@ -2246,4 +2245,7 @@ Il ruolo verrà cambiato in %s. Verrà notificato a tutti nella chat. Il tuo profilo di chat verrà inviato ai membri della chat Non riceverai più messaggi da questa chat. La cronologia della chat verrà conservata. + Quando più di un operatore è attivato, nessuno di essi ha metadati per capire chi comunica con chi. + invito accettato + richiesto di connettersi \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml index f00182b469..57a6a18e90 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml @@ -2147,7 +2147,7 @@ Later beoordelen Selecteer welke netwerkoperators u wilt gebruiken. Update - Wanneer er meer dan één netwerkoperator is ingeschakeld, gebruikt de app voor elk gesprek de servers van verschillende operators. + De app beschermt uw privacy door in elk gesprek verschillende operators te gebruiken. U kunt operators configureren in Netwerk- en serverinstellingen. Doorgaan Voorwaarden bekijken @@ -2183,7 +2183,7 @@ Verbeterde chatnavigatie Netwerk decentralisatie De tweede vooraf ingestelde operator in de app! - Als u bijvoorbeeld berichten ontvangt via de SimpleX Chat-server, gebruikt de app een van de Flux-servers voor privéroutering. + Als uw contactpersoon bijvoorbeeld berichten ontvangt via een SimpleX Chat-server, worden deze door uw app via een Flux-server verzonden. Flux inschakelen Geen bericht App-werkbalken @@ -2242,4 +2242,8 @@ Geen achtergrondservice Alleen chateigenaren kunnen voorkeuren wijzigen. Verklein de berichtgrootte of verwijder de media en verzend het bericht opnieuw. + geaccepteerde uitnodiging + Wanneer er meer dan één operator is ingeschakeld, beschikt geen enkele operator over metagegevens om te achterhalen wie met wie communiceert. + gevraagd om verbinding te maken + Over operatoren \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml index a0c5ccf01b..f234a2cf0a 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml @@ -1882,8 +1882,7 @@ Информация об очереди сообщений Персидский интерфейс Защитить IP адрес - Защитите ваш IP адрес от серверов сообщений, выбранных Вашими контактами. -\nВключите в настройках Сеть и серверы. + Защитите ваш IP адрес от серверов сообщений, выбранных Вашими контактами. \nВключите в настройках Сети и серверов. Отправьте сообщения напрямую, когда Ваш сервер или сервер получателя не поддерживает конфиденциальную доставку. Конфиденциальная доставка Использовать конфиденциальную доставку с неизвестными серверами. @@ -2265,7 +2264,7 @@ Как это улучшает конфиденциальность Операторы серверов Выберите операторов сети. - Вы можете настроить операторов в настройках Сеть и серверы. + Вы можете настроить операторов в настройках Сети и серверов. Продолжить Посмотреть позже Обновить @@ -2305,7 +2304,7 @@ Ошибка сохранения сервера Для доставки сообщений Открыть изменения - Оператор серверов изменен. + Оператор сервера изменен. Протокол сервера изменен. Серверы для новых файлов Вашего текущего профиля Для получения diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml index 548e29f836..eff112717e 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml @@ -219,7 +219,7 @@ OK Скопійовано в буфер обміну Для підключення через посилання - Відкрити у мобільному додатку, а потім торкніться Підключити в додатку.]]> + Відкрити у мобільному додатку, а потім торкніться Підключити в додатку.]]> Приглушити Скасувати приглушення Ви запросили контакт @@ -233,7 +233,7 @@ Одноразове запрошення Невірний код безпеки! Для перевірки end-to-end шифрування порівняйте (або скануйте) код на своїх пристроях. - Ваші налаштування + Налаштування Ваша SimpleX-адреса Допомога з Markdown Блокування SimpleX @@ -268,7 +268,7 @@ Ніяких ідентифікаторів користувачів. Децентралізована Використовувати чат - Це можна змінити пізніше в налаштуваннях. + Як це впливає на батарею Миттєво Виклик вже завершено! Ваші виклики @@ -496,7 +496,7 @@ Тільки ваш контакт може надсилати голосові повідомлення. Забороняйте надсилання повідомлень, які зникають. Забороняйте невідворотне видалення повідомлень. - Учасники групи можуть надсилати голосові повідомлення. + Учасники можуть надсилати голосові повідомлення. %dm Нове в %s Самознищуючий пароль @@ -544,7 +544,7 @@ Створити файл Помилка видалення користувача Помилка оновлення конфіденційності користувача - фоновий сервіс SimpleX – він використовує кілька відсотків батареї щодня.]]> + SimpleX працює у фоновому режимі замість використання пуш-повідомлень.]]> Періодичні сповіщення Служба чату SimpleX Перевіряє нові повідомлення кожні 10 хвилин протягом 1 хвилини @@ -649,7 +649,7 @@ Ви можете використовувати markdown для форматування повідомлень: Створіть свій профіль Створіть приватне підключення - шифрування на двох рівнях.]]> + Тільки клієнтські пристрої зберігають профілі, контакти, групи та повідомлення. Приватні сповіщення Споживає більше акумулятора! Додаток завжди працює у фоновому режимі – сповіщення відображаються миттєво.]]> Вставте отримане посилання @@ -746,14 +746,14 @@ Тільки ви можете надсилати голосові повідомлення. Тільки ви можете додавати реакції на повідомлення. Заборонити реакції на повідомлення. - Самознищувальні повідомлення заборонені в цій групі. - Учасники групи можуть надсилати приватні повідомлення. + Повідомлення, що зникають, заборонені. + Учасники можуть надсилати прямі повідомлення. Приватні повідомлення між учасниками заборонені в цій групі. - Учасники групи можуть назавжди видаляти відправлені повідомлення. (24 години) - Назавжди видалення повідомлень заборонене в цій групі. - Голосові повідомлення заборонені в цій групі. - Учасники групи можуть додавати реакції на повідомлення. - Реакції на повідомлення заборонені в цій групі. + Учасники можуть необоротно видаляти надіслані повідомлення (протягом 24 годин). + Заборонено необоротне видалення повідомлень. + Голосові повідомлення заборонені + Учасники можуть додавати реакції на повідомлення. + Реакції на повідомлення заборонені. %d година %d тиждень %d тижні @@ -805,7 +805,7 @@ Безпечна черга Видалити чергу Будь ласка, перевірте, що ви використали правильне посилання або попросіть вашого контакту вислати інше. - дозвольте SimpleX працювати в фоновому режимі в наступному діалозі. В іншому випадку сповіщення будуть вимкнені.]]> + Дозвольте це в наступному діалозі, щоб отримувати сповіщення миттєво.]]> Миттєві сповіщення Контакт прихований: нове повідомлення @@ -892,7 +892,7 @@ Дякуємо користувачам – приєднуйтеся через Weblate! Режим блокування SimpleX Системна аутентифікація - Для захисту приватності, замість ідентифікаторів користувачів, які використовуються всіма іншими платформами, у SimpleX є ідентифікатори черг повідомлень, окремі для кожного з ваших контактів. + Для захисту вашої конфіденційності SimpleX використовує окремі ID для кожного вашого контакту. Коли додаток запущено Періодично контакт не має зашифрування e2e @@ -919,7 +919,7 @@ ви видалили %1$s Торкніться для активації профілю. Забороняйте надсилання голосових повідомлень. - Учасники групи можуть надсилати самознищувальні повідомлення. + Учасники можуть надсилати повідомлення, що зникають. %d хв Зменшене споживання енергії батареї Редагувати зображення @@ -1012,7 +1012,7 @@ Очистити перевірку %s перевірено %s не перевірено - Надішліть нам електронного листа + Написати нам ел. листа Тестовий сервер Зберегти сервери\? Ваші сервери ICE @@ -1089,7 +1089,7 @@ ні вимк Встановити налаштування групи - Ваші налаштування + Налаштування Прямі повідомлення Помилка Одноразове запрошення @@ -1161,7 +1161,7 @@ кольоровий дзвінок завершено %1$s помилка дзвінка - Наступне покоління \nприватних повідомлень + Майбутнє обміну повідомленнями Кожен може хостити сервери. Інструменти розробника Експериментальні функції @@ -1178,7 +1178,7 @@ (щоб поділитися з вашим контактом) (сканувати або вставити з буферу обміну) підключитися до розробників SimpleX Chat, щоб задати будь-які питання і отримувати оновлення.]]> - Сканувати QR-код.]]> + Сканувати QR-код.]]> Адреса SimpleX Показати QR-код Приєднання до групи @@ -1249,7 +1249,7 @@ Помилка відміни зміни адреси Перервати зміну адреси Дозволити надсилання файлів та медіафайлів. - Файли та медіафайли заборонені в цій групі. + Файли та медіа заборонені. Підключити інкогніто Використовувати поточний профіль Дозволити @@ -1347,7 +1347,7 @@ Зміна адреси буде скасована. Буде використовуватися стара адреса для отримання. Повторно узгодити шифрування? Шифрування працює і нова угода про шифрування не потрібна. Це може призвести до помилок підключення! - Учасники групи можуть надсилати файли та медіафайли. + Учасники можуть надсилати файли та медіа. База даних буде зашифрована, і ключова фраза буде збережена в налаштуваннях. Розгорнути Повторити запит на підключення? @@ -1441,7 +1441,7 @@ Підключати автоматично Адреса робочого столу Одночасно може працювати лише один пристрій - Посилання на мобільний та комп\'ютерний додатки! 🔗 + Підключіть мобільний і десктопний додатки! 🔗 Через безпечний квантовостійкий протокол. Використовувати з робочого столу у мобільному додатку і скануйте QR-код.]]> Щоб приховати небажані повідомлення. @@ -1734,7 +1734,7 @@ Переслати Переслано Переслано з - Учасники групи можуть надсилати посилання SimpleX. + Учасники можуть надсилати посилання SimpleX. Звуки вхідного дзвінка Світлий режим Запасний варіант маршрутизації повідомлень @@ -1828,7 +1828,7 @@ Коли IP приховано Так Отримання паралелізму - У цій групі заборонені посилання на SimpleX. + Посилання SimpleX заборонені. Сформуйте зображення профілю При підключенні аудіо та відеодзвінків. Скинути колір @@ -2011,7 +2011,7 @@ Завантажити %s (%s) Відкрити розташування файлу Пропустити цю версію - Доступна панель чату + Доступні панелі додатка Не можна зателефонувати контакту Підключення до контакту, будь ласка, зачекайте або перевірте пізніше! Дзвінки заборонені! @@ -2129,10 +2129,10 @@ Щоб захиститися від заміни вашого посилання, ви можете порівняти коди безпеки контактів. Для соціальних мереж Або поділитися приватно - Обирайте операторів + Оператори серверів Мережеві оператори Умови будуть прийняті для ввімкнених операторів через 30 днів. - Наприклад, якщо ви отримуєте повідомлення через сервер SimpleX Chat, програма використовуватиме один із серверів Flux для приватної маршрутизації. + Наприклад, якщо ваш контакт отримує повідомлення через сервер SimpleX Chat, ваш додаток доставлятиме їх через сервер Flux. Виберіть мережевих операторів для використання. Ви можете налаштувати сервери за допомогою налаштувань. Перегляньте пізніше @@ -2145,7 +2145,7 @@ Ваші сервери Використовуйте %s Використовуйте сервери - %s.]]> + %s.]]> %s.]]> Прийняти умови Умови перегляду @@ -2177,7 +2177,7 @@ %s.]]> %s.]]> %s.]]> - %s.]]> + %s.]]> %s.]]> %s, прийміть умови використання.]]> Текст поточних умов не вдалося завантажити, ви можете переглянути умови за цим посиланням: @@ -2203,9 +2203,48 @@ SimpleX-адреси та одноразові посилання можна безпечно ділитися через будь-який месенджер. З\'єднання досягло ліміту недоставлених повідомлень, ваш контакт може бути офлайн. Натисніть Створити адресу SimpleX у меню, щоб створити її пізніше. - Якщо увімкнено більше одного оператора, програма використовуватиме сервери різних операторів для кожної розмови. + Додаток захищає вашу конфіденційність, використовуючи різних операторів у кожній розмові. Використовуйте для повідомлень Ви можете налаштувати операторів у налаштуваннях Мережі та серверів. Або імпортуйте архівний файл Віддалені мобільні + Пристрої Xiaomi: будь ласка, увімкніть Автозапуск у налаштуваннях системи, щоб сповіщення працювали.]]> + Повідомлення занадто велике! + Будь ласка, зменшіть розмір повідомлення або видаліть медіа та надішліть знову. + Додайте учасників команди до розмов. + Бізнес адреса + Перевіряти повідомлення кожні 10 хвилин. + Без фонової служби + Сповіщення та батарея + Додаток завжди працює у фоні. + зашифрованими end-to-end, з пост-квантовою безпекою в особистих повідомленнях.]]> + Покинути чат? + Учасник буде видалений з чату — це неможливо скасувати! + Бізнес чати + Конфіденційність для ваших клієнтів. + Доступна панель чату + Додати друзів + Додати учасників команди + Запросити до чату + Чат буде видалений для всіх учасників — це неможливо скасувати! + Видалити чат + Видалити чат? + Тільки власники чату можуть змінювати налаштування. + Роль буде змінена на %s. Усі учасники чату отримають повідомлення. + Прямі повідомлення між учасниками заборонені. + %1$s.]]> + Чат вже існує! + Як це допомагає зберігати конфіденційність + Прямі повідомлення між учасниками заборонені в цьому чаті. + Покинути чат + Чат + Чат буде видалений для вас — це неможливо скасувати! + Будь ласка, зменшіть розмір повідомлення та надішліть знову. + Скопіюйте та зменшіть розмір повідомлення для відправки. + Ви припините отримувати повідомлення з цього чату. Історія чату буде збережена. + Ваш профіль чату буде надіслано учасникам чату. + Коли увімкнено більше ніж одного оператора, жоден з них не має метаданих, щоб дізнатися, хто спілкується з ким. + прийнято запрошення + запит на підключення + Про операторів \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml index ca42ccc902..0477307343 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml @@ -2157,8 +2157,8 @@ 应用中的第二个预设运营者! 改进了聊天导航 查看更新后的条款 - 比如,如果你通过 SimpleX 服务器收到消息,应用会使用 Flux 服务器中的一台进行私密路由。 - 启用了多于一个网络运营者时,应用会为每个对话使用不同运营者的服务器。 + 比如,如果你通过 SimpleX 服务器收到消息,应用会通过 Flux 服务器传送它们。 + 应用通过在每个对话中使用不同运营者保护你的隐私。 接受条款 模糊 地址或一次性链接? @@ -2229,4 +2229,8 @@ 聊天 将从聊天中删除成员 - 此操作无法撤销! 请减小消息尺寸并再次发送。 + 当启用了超过一个运营者时,没有一个运营者拥有了解谁和谁联络的元数据。 + 已接受邀请 + 被请求连接 + 关于运营者 \ No newline at end of file From 307211a47fee8fe7cdf0a7828219077615579caa Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Sun, 8 Dec 2024 00:09:00 +0700 Subject: [PATCH 162/167] android, desktop: landscape calls on Android and better local camera ratio management (#5124) * android, desktop: landscape calls on Android and better local camera ratio management The main thing is that now when exiting from CallActivity while in call audio devices are not reset to default. It allows to have landscape mode enabled * styles * fix changing calls --- .../android/src/main/AndroidManifest.xml | 1 - .../main/java/chat/simplex/app/SimplexApp.kt | 2 + .../views/call/CallAudioDeviceManager.kt | 8 +- .../common/views/call/CallView.android.kt | 113 ++++++++++-------- .../chat/simplex/common/platform/Platform.kt | 2 + .../simplex/common/views/call/CallManager.kt | 3 + .../chat/simplex/common/views/call/WebRTC.kt | 5 +- .../simplex/common/views/chat/ChatView.kt | 4 +- .../resources/assets/www/android/style.css | 105 +++++++++++----- .../commonMain/resources/assets/www/call.js | 31 +++-- .../resources/assets/www/desktop/style.css | 4 +- .../simplex-chat-webrtc/src/android/style.css | 105 +++++++++++----- packages/simplex-chat-webrtc/src/call.ts | 33 +++-- .../simplex-chat-webrtc/src/desktop/style.css | 4 +- 14 files changed, 283 insertions(+), 137 deletions(-) diff --git a/apps/multiplatform/android/src/main/AndroidManifest.xml b/apps/multiplatform/android/src/main/AndroidManifest.xml index deb5d83e5f..67bc0d70c8 100644 --- a/apps/multiplatform/android/src/main/AndroidManifest.xml +++ b/apps/multiplatform/android/src/main/AndroidManifest.xml @@ -115,7 +115,6 @@ android:launchMode="singleInstance" android:supportsPictureInPicture="true" android:autoRemoveFromRecents="true" - android:screenOrientation="portrait" android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"/> = Build.VERSION_CODES.S) { + callAudioDeviceManager.start() + } + } + + override fun close() { + if (closed) return + closed = true + CallSoundsPlayer.stop() + if (wasConnected) { + CallSoundsPlayer.vibrate() + } + callAudioDeviceManager.stop() + dropAudioManagerOverrides() + if (proximityLock?.isHeld == true) { + proximityLock.release() + } + } + + private fun screenOffWakeLock(): WakeLock? { val pm = (androidAppContext.getSystemService(Context.POWER_SERVICE) as PowerManager) - if (pm.isWakeLockLevelSupported(PROXIMITY_SCREEN_OFF_WAKE_LOCK)) { + return if (pm.isWakeLockLevelSupported(PROXIMITY_SCREEN_OFF_WAKE_LOCK)) { pm.newWakeLock(PROXIMITY_SCREEN_OFF_WAKE_LOCK, androidAppContext.packageName + ":proximityLock") } else { null } } - val wasConnected = rememberSaveable { mutableStateOf(false) } +} + + +@SuppressLint("SourceLockedOrientationActivity") +@Composable +actual fun ActiveCallView() { + val call = remember { chatModel.activeCall }.value + val callState = call?.androidCallState as ActiveCallState? + val scope = rememberCoroutineScope() LaunchedEffect(call) { - if (call?.callState == CallState.Connected && !wasConnected.value) { + if (call?.callState == CallState.Connected && callState != null && !callState.wasConnected) { CallSoundsPlayer.vibrate(2) - wasConnected.value = true + callState.wasConnected = true } } - val callAudioDeviceManager = remember { CallAudioDeviceManagerInterface.new() } - DisposableEffect(Unit) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - callAudioDeviceManager.start() - } - onDispose { - CallSoundsPlayer.stop() - if (wasConnected.value) { - CallSoundsPlayer.vibrate() - } - callAudioDeviceManager.stop() - dropAudioManagerOverrides() - if (proximityLock?.isHeld == true) { - proximityLock.release() - } - } - } - LaunchedEffect(chatModel.activeCallViewIsCollapsed.value) { + LaunchedEffect(callState, chatModel.activeCallViewIsCollapsed.value) { + callState ?: return@LaunchedEffect if (chatModel.activeCallViewIsCollapsed.value) { - if (proximityLock?.isHeld == true) proximityLock.release() + if (callState.proximityLock?.isHeld == true) callState.proximityLock.release() } else { delay(1000) - if (proximityLock?.isHeld == false) proximityLock.acquire() + if (callState.proximityLock?.isHeld == false) callState.proximityLock.acquire() } } Box(Modifier.fillMaxSize()) { @@ -122,6 +134,7 @@ actual fun ActiveCallView() { Log.d(TAG, "received from WebRTCView: $apiMsg") val call = chatModel.activeCall.value if (call != null) { + val callState = call.androidCallState as ActiveCallState Log.d(TAG, "has active call $call") val callRh = call.remoteHostId when (val r = apiMsg.resp) { @@ -131,9 +144,9 @@ actual fun ActiveCallView() { updateActiveCall(call) { it.copy(callState = CallState.InvitationSent, localCapabilities = r.capabilities) } if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { // Starting is delayed to make Android <= 11 working good with Bluetooth - callAudioDeviceManager.start() + callState.callAudioDeviceManager.start() } else { - callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.hasVideo, true) + callState.callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.hasVideo, true) } CallSoundsPlayer.startConnectingCallSound(scope) activeCallWaitDeliveryReceipt(scope) @@ -143,9 +156,9 @@ actual fun ActiveCallView() { updateActiveCall(call) { it.copy(callState = CallState.OfferSent, localCapabilities = r.capabilities) } if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { // Starting is delayed to make Android <= 11 working good with Bluetooth - callAudioDeviceManager.start() + callState.callAudioDeviceManager.start() } else { - callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.hasVideo, true) + callState.callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.hasVideo, true) } } is WCallResponse.Answer -> withBGApi { @@ -228,14 +241,14 @@ actual fun ActiveCallView() { !chatModel.activeCallViewIsCollapsed.value -> true else -> false } - if (call != null && showOverlay) { - ActiveCallOverlay(call, chatModel, callAudioDeviceManager) + if (call != null && showOverlay && callState != null) { + ActiveCallOverlay(call, chatModel, callState.callAudioDeviceManager) } } - KeyChangeEffect(call?.localMediaSources?.hasVideo) { - if (call != null && call.hasVideo && callAudioDeviceManager.currentDevice.value?.type == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE) { + KeyChangeEffect(callState, call?.localMediaSources?.hasVideo) { + if (call != null && call.hasVideo && callState != null && callState.callAudioDeviceManager.currentDevice.value?.type == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE) { // enabling speaker on user action (peer action ignored) and not disabling it again - callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.hasVideo, true) + callState.callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.hasVideo, true) } } val context = LocalContext.current @@ -243,16 +256,12 @@ actual fun ActiveCallView() { val activity = context as? Activity ?: return@DisposableEffect onDispose {} val prevVolumeControlStream = activity.volumeControlStream activity.volumeControlStream = AudioManager.STREAM_VOICE_CALL - // Lock orientation to portrait in order to have good experience with calls - activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT chatModel.activeCallViewIsVisible.value = true // After the first call, End command gets added to the list which prevents making another calls chatModel.callCommand.removeAll { it is WCallCommand.End } keepScreenOn(true) onDispose { activity.volumeControlStream = prevVolumeControlStream - // Unlock orientation - activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED chatModel.activeCallViewIsVisible.value = false chatModel.callCommand.clear() keepScreenOn(false) @@ -264,8 +273,8 @@ actual fun ActiveCallView() { private fun ActiveCallOverlay(call: Call, chatModel: ChatModel, callAudioDeviceManager: CallAudioDeviceManagerInterface) { ActiveCallOverlayLayout( call = call, - devices = remember { callAudioDeviceManager.devices }.value, - currentDevice = remember { callAudioDeviceManager.currentDevice }, + devices = remember(callAudioDeviceManager) { callAudioDeviceManager.devices }.value, + currentDevice = remember(callAudioDeviceManager) { callAudioDeviceManager.currentDevice }, dismiss = { withBGApi { chatModel.callManager.endCall(call) } }, toggleAudio = { chatModel.callCommand.add(WCallCommand.Media(CallMediaSource.Mic, enable = !call.localMediaSources.mic)) }, selectDevice = { callAudioDeviceManager.selectDevice(it.id) }, @@ -832,7 +841,8 @@ fun PreviewActiveCallOverlayVideo() { connectionInfo = ConnectionInfo( RTCIceCandidate(RTCIceCandidateType.Host, "tcp"), RTCIceCandidate(RTCIceCandidateType.Host, "tcp") - ) + ), + androidCallState = {} ), devices = emptyList(), currentDevice = remember { mutableStateOf(null) }, @@ -841,7 +851,7 @@ fun PreviewActiveCallOverlayVideo() { selectDevice = {}, toggleVideo = {}, toggleSound = {}, - flipCamera = {} + flipCamera = {}, ) } } @@ -862,7 +872,8 @@ fun PreviewActiveCallOverlayAudio() { connectionInfo = ConnectionInfo( RTCIceCandidate(RTCIceCandidateType.Host, "udp"), RTCIceCandidate(RTCIceCandidateType.Host, "udp") - ) + ), + androidCallState = {} ), devices = emptyList(), currentDevice = remember { mutableStateOf(null) }, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt index e0a9e22f71..448100bc17 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt @@ -10,6 +10,7 @@ import chat.simplex.common.model.ChatId import chat.simplex.common.model.NotificationsMode import chat.simplex.common.ui.theme.CurrentColors import kotlinx.coroutines.Job +import java.io.Closeable interface PlatformInterface { suspend fun androidServiceStart() {} @@ -26,6 +27,7 @@ interface PlatformInterface { fun androidPictureInPictureAllowed(): Boolean = true fun androidCallEnded() {} fun androidRestartNetworkObserver() {} + fun androidCreateActiveCallState(): Closeable = Closeable { } fun androidIsXiaomiDevice(): Boolean = false val androidApiLevel: Int? get() = null @Composable fun androidLockPortraitOrientation() {} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/CallManager.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/CallManager.kt index 405094f72a..d6ab57a70d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/CallManager.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/CallManager.kt @@ -43,6 +43,7 @@ class CallManager(val chatModel: ChatModel) { private fun justAcceptIncomingCall(invitation: RcvCallInvitation, userProfile: Profile) { with (chatModel) { + activeCall.value?.androidCallState?.close() activeCall.value = Call( remoteHostId = invitation.remoteHostId, userProfile = userProfile, @@ -51,6 +52,7 @@ class CallManager(val chatModel: ChatModel) { callState = CallState.InvitationAccepted, initialCallType = invitation.callType.media, sharedKey = invitation.sharedKey, + androidCallState = platform.androidCreateActiveCallState() ) showCallView.value = true val useRelay = controller.appPrefs.webrtcPolicyRelay.get() @@ -78,6 +80,7 @@ class CallManager(val chatModel: ChatModel) { // Don't destroy WebView if you plan to accept next call right after this one if (!switchingCall.value) { showCallView.value = false + activeCall.value?.androidCallState?.close() activeCall.value = null activeCallViewIsCollapsed.value = false platform.androidCallEnded() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/WebRTC.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/WebRTC.kt index bbf860b39c..705fc6a28f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/WebRTC.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/WebRTC.kt @@ -7,6 +7,7 @@ import chat.simplex.res.MR import kotlinx.datetime.Instant import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import java.io.Closeable import java.net.URI import kotlin.collections.ArrayList @@ -27,7 +28,9 @@ data class Call( // When a user has audio call, and then he wants to enable camera but didn't grant permissions for using camera yet, // we show permissions view without enabling camera before permissions are granted. After they are granted, enabling camera - val wantsToEnableCamera: Boolean = false + val wantsToEnableCamera: Boolean = false, + + val androidCallState: Closeable ) { val encrypted: Boolean get() = localEncrypted && sharedKey != null private val localEncrypted: Boolean get() = localCapabilities?.encryption ?: false diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index ddf25a6e3b..913ea87c98 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.unit.* import chat.simplex.common.model.* import chat.simplex.common.model.CIDirection.GroupRcv import chat.simplex.common.model.ChatController.appPrefs +import chat.simplex.common.model.ChatModel.activeCall import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.ui.theme.* @@ -573,7 +574,8 @@ 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, callUUID = null, callState = CallState.WaitCapabilities, initialCallType = media, userProfile = profile) + activeCall.value?.androidCallState?.close() + chatModel.activeCall.value = Call(remoteHostId = remoteHostId, contact = chatInfo.contact, callUUID = null, callState = CallState.WaitCapabilities, initialCallType = media, userProfile = profile, androidCallState = platform.androidCreateActiveCallState()) chatModel.showCallView.value = true chatModel.callCommand.add(WCallCommand.Capabilities(media)) } diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/www/android/style.css b/apps/multiplatform/common/src/commonMain/resources/assets/www/android/style.css index a9d1c3785a..377458c184 100644 --- a/apps/multiplatform/common/src/commonMain/resources/assets/www/android/style.css +++ b/apps/multiplatform/common/src/commonMain/resources/assets/www/android/style.css @@ -12,26 +12,60 @@ body { object-fit: cover; } -#remote-video-stream.collapsed { - position: absolute; - max-width: 30%; - max-height: 30%; - object-fit: cover; - margin: 16px; - border-radius: 16px; - bottom: 80px; - right: 0; +@media (orientation: portrait) { + #remote-video-stream.collapsed { + position: absolute; + width: 30%; + max-width: 30%; + height: 39.9vw; + object-fit: cover; + margin: 16px; + border-radius: 16px; + bottom: 80px; + right: 0; + } } -#remote-video-stream.collapsed-pip { - position: absolute; - max-width: 50%; - max-height: 50%; - object-fit: cover; - margin: 8px; - border-radius: 8px; - bottom: 0; - right: 0; +@media (orientation: landscape) { + #remote-video-stream.collapsed { + position: absolute; + width: 20%; + max-width: 20%; + height: 15.03vw; + object-fit: cover; + margin: 16px; + border-radius: 16px; + bottom: 80px; + right: 0; + } +} + +@media (orientation: portrait) { + #remote-video-stream.collapsed-pip { + position: absolute; + width: 50%; + max-width: 50%; + height: 66.5vw; + object-fit: cover; + margin: 8px; + border-radius: 8px; + bottom: 0; + right: 0; + } +} + +@media (orientation: landscape) { + #remote-video-stream.collapsed-pip { + position: absolute; + width: 50%; + max-width: 50%; + height: 37.59vw; + object-fit: cover; + margin: 8px; + border-radius: 8px; + bottom: 0; + right: 0; + } } #remote-screen-video-stream.inline { @@ -41,15 +75,32 @@ body { object-fit: cover; } -#local-video-stream.inline { - position: absolute; - width: 30%; - max-width: 30%; - object-fit: cover; - margin: 16px; - border-radius: 16px; - top: 0; - right: 0; +@media (orientation: portrait) { + #local-video-stream.inline { + position: absolute; + width: 30%; + max-width: 30%; + height: 39.9vw; + object-fit: cover; + margin: 16px; + border-radius: 16px; + top: 0; + right: 0; + } +} + +@media (orientation: landscape) { + #local-video-stream.inline { + position: absolute; + width: 20%; + max-width: 20%; + height: 15.03vw; + object-fit: cover; + margin: 16px; + border-radius: 16px; + top: 0; + right: 0; + } } #local-screen-video-stream.inline { diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/www/call.js b/apps/multiplatform/common/src/commonMain/resources/assets/www/call.js index 4dae487d03..7ab8d6fdd6 100644 --- a/apps/multiplatform/common/src/commonMain/resources/assets/www/call.js +++ b/apps/multiplatform/common/src/commonMain/resources/assets/www/call.js @@ -301,6 +301,7 @@ const processCommand = (function () { localStream = await getLocalMediaStream(true, command.media == CallMediaType.Video && (await browserHasCamera()), VideoCamera.User); const videos = getVideoElements(); if (videos) { + setupLocalVideoRatio(videos.local); videos.local.srcObject = localStream; videos.local.play().catch((e) => console.log(e)); } @@ -330,9 +331,12 @@ const processCommand = (function () { console.log("starting incoming call - create webrtc session"); if (activeCall) endCall(); + // It can be already defined on Android when switching calls (if the previous call was outgoing) + notConnectedCall = undefined; inactiveCallMediaSources.mic = true; inactiveCallMediaSources.camera = command.media == CallMediaType.Video; inactiveCallMediaSourcesChanged(inactiveCallMediaSources); + setupLocalVideoRatio(getVideoElements().local); const { media, iceServers, relay } = command; const encryption = supportsInsertableStreams(useWorker); const aesKey = encryption ? command.aesKey : undefined; @@ -547,13 +551,13 @@ const processCommand = (function () { } function endCall() { var _a; + shutdownCameraAndMic(); try { (_a = activeCall === null || activeCall === void 0 ? void 0 : activeCall.connection) === null || _a === void 0 ? void 0 : _a.close(); } catch (e) { console.log(e); } - shutdownCameraAndMic(); activeCall = undefined; resetVideoElements(); } @@ -642,27 +646,21 @@ const processCommand = (function () { } // Without doing it manually Firefox shows black screen but video can be played in Picture-in-Picture videos.local.play().catch((e) => console.log(e)); - setupLocalVideoRatio(videos.local); } function setupLocalVideoRatio(local) { - const ratio = isDesktop ? 1.33 : 1 / 1.33; - const currentRect = local.getBoundingClientRect(); - // better to get percents from here than to hardcode values from styles (the styles can be changed) - const screenWidth = currentRect.left + currentRect.width; - const percents = currentRect.width / screenWidth; - local.style.width = `${percents * 100}%`; - local.style.height = `${(percents / ratio) * 100}vw`; local.addEventListener("loadedmetadata", function () { console.log("Local video videoWidth: " + local.videoWidth + "px, videoHeight: " + local.videoHeight + "px"); if (local.videoWidth == 0 || local.videoHeight == 0) return; - local.style.height = `${(percents / (local.videoWidth / local.videoHeight)) * 100}vw`; + const ratio = local.videoWidth > local.videoHeight ? 0.2 : 0.3; + local.style.height = `${(ratio / (local.videoWidth / local.videoHeight)) * 100}vw`; }); local.onresize = function () { console.log("Local video size changed to " + local.videoWidth + "x" + local.videoHeight); if (local.videoWidth == 0 || local.videoHeight == 0) return; - local.style.height = `${(percents / (local.videoWidth / local.videoHeight)) * 100}vw`; + const ratio = local.videoWidth > local.videoHeight ? 0.2 : 0.3; + local.style.height = `${(ratio / (local.videoWidth / local.videoHeight)) * 100}vw`; }; } function setupEncryptionForLocalStream(call) { @@ -1128,8 +1126,9 @@ const processCommand = (function () { (!!useWorker && "RTCRtpScriptTransform" in window)); } function shutdownCameraAndMic() { - if (activeCall === null || activeCall === void 0 ? void 0 : activeCall.localStream) { + if (activeCall) { activeCall.localStream.getTracks().forEach((track) => track.stop()); + activeCall.localScreenStream.getTracks().forEach((track) => track.stop()); } } function resetVideoElements() { @@ -1295,6 +1294,9 @@ function changeLayout(layout) { break; } videos.localScreen.style.visibility = localSources.screenVideo ? "visible" : "hidden"; + if (!isDesktop && !localSources.camera) { + resetLocalVideoElementHeight(videos.local); + } } function getVideoElements() { const local = document.getElementById("local-video-stream"); @@ -1312,6 +1314,11 @@ function getVideoElements() { return; return { local, localScreen, remote, remoteScreen }; } +// Allow CSS to figure out the size of view by itself on Android because rotating to different orientation +// without dropping override will cause the view to have not normal proportion while no video is present +function resetLocalVideoElementHeight(local) { + local.style.height = ""; +} function desktopShowPermissionsAlert(mediaType) { if (!isDesktop) return; diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/www/desktop/style.css b/apps/multiplatform/common/src/commonMain/resources/assets/www/desktop/style.css index 99050bc94f..5110c7c7d6 100644 --- a/apps/multiplatform/common/src/commonMain/resources/assets/www/desktop/style.css +++ b/apps/multiplatform/common/src/commonMain/resources/assets/www/desktop/style.css @@ -15,8 +15,9 @@ body { #remote-video-stream.collapsed { position: absolute; + width: 20%; max-width: 20%; - max-height: 20%; + height: 15.03vw; object-fit: cover; margin: 16px; border-radius: 16px; @@ -47,6 +48,7 @@ body { position: absolute; width: 20%; max-width: 20%; + height: 15.03vw; object-fit: cover; margin: 16px; border-radius: 16px; diff --git a/packages/simplex-chat-webrtc/src/android/style.css b/packages/simplex-chat-webrtc/src/android/style.css index a9d1c3785a..377458c184 100644 --- a/packages/simplex-chat-webrtc/src/android/style.css +++ b/packages/simplex-chat-webrtc/src/android/style.css @@ -12,26 +12,60 @@ body { object-fit: cover; } -#remote-video-stream.collapsed { - position: absolute; - max-width: 30%; - max-height: 30%; - object-fit: cover; - margin: 16px; - border-radius: 16px; - bottom: 80px; - right: 0; +@media (orientation: portrait) { + #remote-video-stream.collapsed { + position: absolute; + width: 30%; + max-width: 30%; + height: 39.9vw; + object-fit: cover; + margin: 16px; + border-radius: 16px; + bottom: 80px; + right: 0; + } } -#remote-video-stream.collapsed-pip { - position: absolute; - max-width: 50%; - max-height: 50%; - object-fit: cover; - margin: 8px; - border-radius: 8px; - bottom: 0; - right: 0; +@media (orientation: landscape) { + #remote-video-stream.collapsed { + position: absolute; + width: 20%; + max-width: 20%; + height: 15.03vw; + object-fit: cover; + margin: 16px; + border-radius: 16px; + bottom: 80px; + right: 0; + } +} + +@media (orientation: portrait) { + #remote-video-stream.collapsed-pip { + position: absolute; + width: 50%; + max-width: 50%; + height: 66.5vw; + object-fit: cover; + margin: 8px; + border-radius: 8px; + bottom: 0; + right: 0; + } +} + +@media (orientation: landscape) { + #remote-video-stream.collapsed-pip { + position: absolute; + width: 50%; + max-width: 50%; + height: 37.59vw; + object-fit: cover; + margin: 8px; + border-radius: 8px; + bottom: 0; + right: 0; + } } #remote-screen-video-stream.inline { @@ -41,15 +75,32 @@ body { object-fit: cover; } -#local-video-stream.inline { - position: absolute; - width: 30%; - max-width: 30%; - object-fit: cover; - margin: 16px; - border-radius: 16px; - top: 0; - right: 0; +@media (orientation: portrait) { + #local-video-stream.inline { + position: absolute; + width: 30%; + max-width: 30%; + height: 39.9vw; + object-fit: cover; + margin: 16px; + border-radius: 16px; + top: 0; + right: 0; + } +} + +@media (orientation: landscape) { + #local-video-stream.inline { + position: absolute; + width: 20%; + max-width: 20%; + height: 15.03vw; + object-fit: cover; + margin: 16px; + border-radius: 16px; + top: 0; + right: 0; + } } #local-screen-video-stream.inline { diff --git a/packages/simplex-chat-webrtc/src/call.ts b/packages/simplex-chat-webrtc/src/call.ts index 693ad6bbe5..5f3d2bf332 100644 --- a/packages/simplex-chat-webrtc/src/call.ts +++ b/packages/simplex-chat-webrtc/src/call.ts @@ -593,6 +593,7 @@ const processCommand = (function () { ) const videos = getVideoElements() if (videos) { + setupLocalVideoRatio(videos.local) videos.local.srcObject = localStream videos.local.play().catch((e) => console.log(e)) } @@ -621,9 +622,12 @@ const processCommand = (function () { console.log("starting incoming call - create webrtc session") if (activeCall) endCall() + // It can be already defined on Android when switching calls (if the previous call was outgoing) + notConnectedCall = undefined inactiveCallMediaSources.mic = true inactiveCallMediaSources.camera = command.media == CallMediaType.Video inactiveCallMediaSourcesChanged(inactiveCallMediaSources) + setupLocalVideoRatio(getVideoElements()!.local) const {media, iceServers, relay} = command const encryption = supportsInsertableStreams(useWorker) @@ -827,12 +831,12 @@ const processCommand = (function () { } function endCall() { + shutdownCameraAndMic() try { activeCall?.connection?.close() } catch (e) { console.log(e) } - shutdownCameraAndMic() activeCall = undefined resetVideoElements() } @@ -925,28 +929,21 @@ const processCommand = (function () { } // Without doing it manually Firefox shows black screen but video can be played in Picture-in-Picture videos.local.play().catch((e) => console.log(e)) - setupLocalVideoRatio(videos.local) } function setupLocalVideoRatio(local: HTMLVideoElement) { - const ratio = isDesktop ? 1.33 : 1 / 1.33 - const currentRect = local.getBoundingClientRect() - // better to get percents from here than to hardcode values from styles (the styles can be changed) - const screenWidth = currentRect.left + currentRect.width - const percents = currentRect.width / screenWidth - local.style.width = `${percents * 100}%` - local.style.height = `${(percents / ratio) * 100}vw` - local.addEventListener("loadedmetadata", function () { console.log("Local video videoWidth: " + local.videoWidth + "px, videoHeight: " + local.videoHeight + "px") if (local.videoWidth == 0 || local.videoHeight == 0) return - local.style.height = `${(percents / (local.videoWidth / local.videoHeight)) * 100}vw` + const ratio = local.videoWidth > local.videoHeight ? 0.2 : 0.3 + local.style.height = `${(ratio / (local.videoWidth / local.videoHeight)) * 100}vw` }) local.onresize = function () { console.log("Local video size changed to " + local.videoWidth + "x" + local.videoHeight) if (local.videoWidth == 0 || local.videoHeight == 0) return - local.style.height = `${(percents / (local.videoWidth / local.videoHeight)) * 100}vw` + const ratio = local.videoWidth > local.videoHeight ? 0.2 : 0.3 + local.style.height = `${(ratio / (local.videoWidth / local.videoHeight)) * 100}vw` } } @@ -1441,8 +1438,9 @@ const processCommand = (function () { } function shutdownCameraAndMic() { - if (activeCall?.localStream) { + if (activeCall) { activeCall.localStream.getTracks().forEach((track) => track.stop()) + activeCall.localScreenStream.getTracks().forEach((track) => track.stop()) } } @@ -1614,6 +1612,9 @@ function changeLayout(layout: LayoutType) { break } videos.localScreen.style.visibility = localSources.screenVideo ? "visible" : "hidden" + if (!isDesktop && !localSources.camera) { + resetLocalVideoElementHeight(videos.local) + } } function getVideoElements(): VideoElements | undefined { @@ -1637,6 +1638,12 @@ function getVideoElements(): VideoElements | undefined { return {local, localScreen, remote, remoteScreen} } +// Allow CSS to figure out the size of view by itself on Android because rotating to different orientation +// without dropping override will cause the view to have not normal proportion while no video is present +function resetLocalVideoElementHeight(local: HTMLVideoElement) { + local.style.height = "" +} + function desktopShowPermissionsAlert(mediaType: CallMediaType) { if (!isDesktop) return diff --git a/packages/simplex-chat-webrtc/src/desktop/style.css b/packages/simplex-chat-webrtc/src/desktop/style.css index 99050bc94f..5110c7c7d6 100644 --- a/packages/simplex-chat-webrtc/src/desktop/style.css +++ b/packages/simplex-chat-webrtc/src/desktop/style.css @@ -15,8 +15,9 @@ body { #remote-video-stream.collapsed { position: absolute; + width: 20%; max-width: 20%; - max-height: 20%; + height: 15.03vw; object-fit: cover; margin: 16px; border-radius: 16px; @@ -47,6 +48,7 @@ body { position: absolute; width: 20%; max-width: 20%; + height: 15.03vw; object-fit: cover; margin: 16px; border-radius: 16px; From 7c86484978fd03e99964f6004cee78ff73ce98ee Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sat, 7 Dec 2024 17:22:14 +0000 Subject: [PATCH 163/167] ios: update library --- apps/ios/SimpleX.xcodeproj/project.pbxproj | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 22d5ba971b..ac9993e367 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -167,9 +167,9 @@ 648010AB281ADD15009009B9 /* CIFileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648010AA281ADD15009009B9 /* CIFileView.swift */; }; 648679AB2BC96A74006456E7 /* ChatItemForwardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648679AA2BC96A74006456E7 /* ChatItemForwardingView.swift */; }; 649B28DD2CFE07CF00536B68 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 649B28D82CFE07CF00536B68 /* libffi.a */; }; - 649B28DE2CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.6-5lGV6gtq9gSDlEsE8DHXYo-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 649B28D92CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.6-5lGV6gtq9gSDlEsE8DHXYo-ghc9.6.3.a */; }; + 649B28DE2CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.7-3p784Fmu4gOAiEiFcsHj1E-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 649B28D92CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.7-3p784Fmu4gOAiEiFcsHj1E-ghc9.6.3.a */; }; 649B28DF2CFE07CF00536B68 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 649B28DA2CFE07CF00536B68 /* libgmpxx.a */; }; - 649B28E02CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.6-5lGV6gtq9gSDlEsE8DHXYo.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 649B28DB2CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.6-5lGV6gtq9gSDlEsE8DHXYo.a */; }; + 649B28E02CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.7-3p784Fmu4gOAiEiFcsHj1E.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 649B28DB2CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.7-3p784Fmu4gOAiEiFcsHj1E.a */; }; 649B28E12CFE07CF00536B68 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 649B28DC2CFE07CF00536B68 /* libgmp.a */; }; 649BCDA0280460FD00C3A862 /* ComposeImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649BCD9F280460FD00C3A862 /* ComposeImageView.swift */; }; 649BCDA22805D6EF00C3A862 /* CIImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649BCDA12805D6EF00C3A862 /* CIImageView.swift */; }; @@ -516,9 +516,9 @@ 648679AA2BC96A74006456E7 /* ChatItemForwardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemForwardingView.swift; sourceTree = ""; }; 6493D667280ED77F007A76FB /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; 649B28D82CFE07CF00536B68 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 649B28D92CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.6-5lGV6gtq9gSDlEsE8DHXYo-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.2.0.6-5lGV6gtq9gSDlEsE8DHXYo-ghc9.6.3.a"; sourceTree = ""; }; + 649B28D92CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.7-3p784Fmu4gOAiEiFcsHj1E-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.2.0.7-3p784Fmu4gOAiEiFcsHj1E-ghc9.6.3.a"; sourceTree = ""; }; 649B28DA2CFE07CF00536B68 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; - 649B28DB2CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.6-5lGV6gtq9gSDlEsE8DHXYo.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.2.0.6-5lGV6gtq9gSDlEsE8DHXYo.a"; sourceTree = ""; }; + 649B28DB2CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.7-3p784Fmu4gOAiEiFcsHj1E.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.2.0.7-3p784Fmu4gOAiEiFcsHj1E.a"; sourceTree = ""; }; 649B28DC2CFE07CF00536B68 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; 649BCD9F280460FD00C3A862 /* ComposeImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeImageView.swift; sourceTree = ""; }; 649BCDA12805D6EF00C3A862 /* CIImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIImageView.swift; sourceTree = ""; }; @@ -671,9 +671,9 @@ 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, 649B28E12CFE07CF00536B68 /* libgmp.a in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, - 649B28E02CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.6-5lGV6gtq9gSDlEsE8DHXYo.a in Frameworks */, + 649B28E02CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.7-3p784Fmu4gOAiEiFcsHj1E.a in Frameworks */, CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */, - 649B28DE2CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.6-5lGV6gtq9gSDlEsE8DHXYo-ghc9.6.3.a in Frameworks */, + 649B28DE2CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.7-3p784Fmu4gOAiEiFcsHj1E-ghc9.6.3.a in Frameworks */, 649B28DD2CFE07CF00536B68 /* libffi.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -754,8 +754,8 @@ 649B28D82CFE07CF00536B68 /* libffi.a */, 649B28DC2CFE07CF00536B68 /* libgmp.a */, 649B28DA2CFE07CF00536B68 /* libgmpxx.a */, - 649B28D92CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.6-5lGV6gtq9gSDlEsE8DHXYo-ghc9.6.3.a */, - 649B28DB2CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.6-5lGV6gtq9gSDlEsE8DHXYo.a */, + 649B28D92CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.7-3p784Fmu4gOAiEiFcsHj1E-ghc9.6.3.a */, + 649B28DB2CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.7-3p784Fmu4gOAiEiFcsHj1E.a */, ); path = Libraries; sourceTree = ""; From df30bb99cb0ed722f82665bba608d1d18f3a54b7 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sat, 7 Dec 2024 17:51:53 +0000 Subject: [PATCH 164/167] ui: add translations --- .../SimpleX Localizations/it.xcloc/Localized Contents/it.xliff | 1 + apps/ios/it.lproj/Localizable.strings | 3 +++ .../common/src/commonMain/resources/MR/de/strings.xml | 1 + .../common/src/commonMain/resources/MR/es/strings.xml | 1 + .../common/src/commonMain/resources/MR/it/strings.xml | 1 + .../common/src/commonMain/resources/MR/nl/strings.xml | 1 + 6 files changed, 8 insertions(+) diff --git a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff index a3aa28580c..8a32fd3277 100644 --- a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff +++ b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff @@ -6715,6 +6715,7 @@ Attivalo nelle impostazioni *Rete e server*. SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app. + SimpleX Chat e Flux hanno concluso un accordo per includere server gestiti da Flux nell'app No comment provided by engineer. diff --git a/apps/ios/it.lproj/Localizable.strings b/apps/ios/it.lproj/Localizable.strings index 80122f8535..31ee4e9e18 100644 --- a/apps/ios/it.lproj/Localizable.strings +++ b/apps/ios/it.lproj/Localizable.strings @@ -4454,6 +4454,9 @@ /* No comment provided by engineer. */ "SimpleX address or 1-time link?" = "Indirizzo SimpleX o link una tantum?"; +/* No comment provided by engineer. */ +"SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app." = "SimpleX Chat e Flux hanno concluso un accordo per includere server gestiti da Flux nell'app"; + /* No comment provided by engineer. */ "SimpleX Chat security was audited by Trail of Bits." = "La sicurezza di SimpleX Chat è stata verificata da Trail of Bits."; diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml index 7c3ecd5ece..be6896d932 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml @@ -2321,4 +2321,5 @@ Chat %1$s verbunden.]]> Über Betreiber + SimpleX-Chat und Flux haben vereinbart, die von Flux betriebenen Server in die App aufzunehmen. \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml index 434e05c174..6163d7e873 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml @@ -2251,4 +2251,5 @@ La aplicación siempre funciona en segundo plano cifrados de extremo a extremo y con seguridad postcuántica en mensajes directos.]]> ¡Mensaje demasiado largo! + Simplex Chat y Flux han acordado incluir servidores operados por Flux en la aplicación. \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml index 11727beaed..ffdc377ceb 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml @@ -2248,4 +2248,5 @@ Quando più di un operatore è attivato, nessuno di essi ha metadati per capire chi comunica con chi. invito accettato richiesto di connettersi + SimpleX Chat e Flux hanno concluso un accordo per includere server gestiti da Flux nell\'app \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml index 57a6a18e90..ced3b9a3b0 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml @@ -2246,4 +2246,5 @@ Wanneer er meer dan één operator is ingeschakeld, beschikt geen enkele operator over metagegevens om te achterhalen wie met wie communiceert. gevraagd om verbinding te maken Over operatoren + Simplex-chat en flux hebben een overeenkomst gemaakt om door flux geëxploiteerde servers in de app op te nemen. \ No newline at end of file From f0781adbd3d4ec3449dd14a248f636950246688f Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Sat, 7 Dec 2024 22:51:23 +0400 Subject: [PATCH 165/167] desktop: fix opening operators on onboarding (#5351) --- .../views/onboarding/ChooseServerOperators.kt | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt index dcb7d7e133..2f84166362 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt @@ -14,10 +14,8 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import chat.simplex.common.model.* import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.platform.* @@ -55,7 +53,7 @@ fun ModalData.ChooseServerOperators( Column(Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING), horizontalAlignment = Alignment.CenterHorizontally) { OnboardingInformationButton( stringResource(MR.strings.how_it_helps_privacy), - onClick = { modalManager.showModal { ChooseServerOperatorsInfoView() } } + onClick = { modalManager.showModal { ChooseServerOperatorsInfoView(modalManager) } } ) } @@ -346,7 +344,9 @@ private fun enabledOperators(operators: List, selectedOperatorId } @Composable -private fun ChooseServerOperatorsInfoView() { +private fun ChooseServerOperatorsInfoView( + modalManager: ModalManager +) { ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.onboarding_network_operators)) @@ -362,7 +362,7 @@ private fun ChooseServerOperatorsInfoView() { SectionView(title = stringResource(MR.strings.onboarding_network_about_operators).uppercase()) { chatModel.conditions.value.serverOperators.forEach { op -> - ServerOperatorRow(op) + ServerOperatorRow(op, modalManager) } } SectionBottomSpacer() @@ -371,11 +371,12 @@ private fun ChooseServerOperatorsInfoView() { @Composable() private fun ServerOperatorRow( - operator: ServerOperator + operator: ServerOperator, + modalManager: ModalManager ) { SectionItemView( { - ModalManager.start.showModalCloseable { close -> + modalManager.showModalCloseable { close -> OperatorInfoView(operator) } } From febea096db302330543fc5a88253687b4dfa5f37 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sat, 7 Dec 2024 18:53:18 +0000 Subject: [PATCH 166/167] android, desktop: remove duplicate translation key --- .../common/src/commonMain/resources/MR/base/strings.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 82bf5bc8dc..cfe56f88ee 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -1752,7 +1752,6 @@ %s.]]> %s.]]> %s.]]> - %s.]]> %s.]]> %s.]]> View conditions From 33bc539e16004ac1126fca70e4ab8f2be41e192f Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sat, 7 Dec 2024 20:53:01 +0000 Subject: [PATCH 167/167] 6.2: ios 254, android 259, desktop 82 --- apps/ios/SimpleX.xcodeproj/project.pbxproj | 20 ++++++++++---------- apps/multiplatform/gradle.properties | 8 ++++---- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index ac9993e367..30b9a27e0e 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -1931,7 +1931,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 253; + CURRENT_PROJECT_VERSION = 254; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -1980,7 +1980,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 253; + CURRENT_PROJECT_VERSION = 254; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -2021,7 +2021,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 253; + CURRENT_PROJECT_VERSION = 254; DEVELOPMENT_TEAM = 5NN7GUYB6T; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; @@ -2041,7 +2041,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 253; + CURRENT_PROJECT_VERSION = 254; DEVELOPMENT_TEAM = 5NN7GUYB6T; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; @@ -2066,7 +2066,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 253; + CURRENT_PROJECT_VERSION = 254; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GCC_OPTIMIZATION_LEVEL = s; @@ -2103,7 +2103,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 253; + CURRENT_PROJECT_VERSION = 254; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_CODE_COVERAGE = NO; @@ -2140,7 +2140,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 253; + CURRENT_PROJECT_VERSION = 254; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -2191,7 +2191,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 253; + CURRENT_PROJECT_VERSION = 254; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -2242,7 +2242,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 253; + CURRENT_PROJECT_VERSION = 254; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -2276,7 +2276,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 253; + CURRENT_PROJECT_VERSION = 254; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; diff --git a/apps/multiplatform/gradle.properties b/apps/multiplatform/gradle.properties index e620b4992d..c392be6b0e 100644 --- a/apps/multiplatform/gradle.properties +++ b/apps/multiplatform/gradle.properties @@ -24,11 +24,11 @@ android.nonTransitiveRClass=true kotlin.mpp.androidSourceSetLayoutVersion=2 kotlin.jvm.target=11 -android.version_name=6.2-beta.6 -android.version_code=258 +android.version_name=6.2 +android.version_code=259 -desktop.version_name=6.2-beta.6 -desktop.version_code=81 +desktop.version_name=6.2 +desktop.version_code=82 kotlin.version=1.9.23 gradle.plugin.version=8.2.0