diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c41fb4646a..6ad4f12ef9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,12 +5,22 @@ on: branches: - master - stable - - users tags: - "v*" - "!*-fdroid" - "!*-armv7a" pull_request: + paths-ignore: + - "apps/ios" + - "apps/multiplatform" + - "blog" + - "docs" + - "fastlane" + - "images" + - "packages" + - "website" + - "README.md" + - "PRIVACY.md" jobs: prepare-release: diff --git a/.gitignore b/.gitignore index e3ea5d267b..645b55ec9d 100644 --- a/.gitignore +++ b/.gitignore @@ -61,6 +61,7 @@ website/package/generated* # Ignore build tool output, e.g. code coverage website/.nyc_output/ website/coverage/ +result # Ignore API documentation website/api-docs/ diff --git a/apps/ios/Shared/Assets.xcassets/checkmark.2.symbolset/Contents.json b/apps/ios/Shared/Assets.xcassets/checkmark.2.symbolset/Contents.json new file mode 100644 index 0000000000..8e38b499dd --- /dev/null +++ b/apps/ios/Shared/Assets.xcassets/checkmark.2.symbolset/Contents.json @@ -0,0 +1,12 @@ +{ + "info": { + "author": "xcode", + "version": 1 + }, + "symbols": [ + { + "filename": "checkmark.2.svg", + "idiom": "universal" + } + ] +} \ No newline at end of file diff --git a/apps/ios/Shared/Assets.xcassets/checkmark.2.symbolset/checkmark.2.svg b/apps/ios/Shared/Assets.xcassets/checkmark.2.symbolset/checkmark.2.svg new file mode 100644 index 0000000000..577fa1db76 --- /dev/null +++ b/apps/ios/Shared/Assets.xcassets/checkmark.2.symbolset/checkmark.2.svg @@ -0,0 +1,227 @@ + + + checkmark.2 + + + + + + + Weight/Scale Variations + + + Ultralight + + + Thin + + + Light + + + Regular + + + Medium + + + Semibold + + + Bold + + + Heavy + + + Black + + + + + + + + + + + + + Design Variations + + + Symbols are supported in up to nine weights and three scales. + + + For optimal layout with text and other symbols, vertically align + + + symbols with the adjacent text. + + + + + + + + + Margins + + + Leading and trailing margins on the left and right side of each symbol + + + + can be adjusted by modifying the x-location of the margin guidelines. + + + + Modifications are automatically applied proportionally to all + + + scales and weights. + + + + + + Exporting + + + Symbols should be outlined when exporting to ensure the + + + design is preserved when submitting to Xcode. + + + Template v.5.0 + + + Requires Xcode 15 or greater + + + Generated from double.checkmark + + + Typeset at 100.0 points + + + Small + + + Medium + + + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/ios/Shared/Assets.xcassets/checkmark.wide.symbolset/Contents.json b/apps/ios/Shared/Assets.xcassets/checkmark.wide.symbolset/Contents.json new file mode 100644 index 0000000000..11a91cb811 --- /dev/null +++ b/apps/ios/Shared/Assets.xcassets/checkmark.wide.symbolset/Contents.json @@ -0,0 +1,12 @@ +{ + "info": { + "author": "xcode", + "version": 1 + }, + "symbols": [ + { + "filename": "checkmark.wide.svg", + "idiom": "universal" + } + ] +} \ No newline at end of file diff --git a/apps/ios/Shared/Assets.xcassets/checkmark.wide.symbolset/checkmark.wide.svg b/apps/ios/Shared/Assets.xcassets/checkmark.wide.symbolset/checkmark.wide.svg new file mode 100644 index 0000000000..b5dfc6b3de --- /dev/null +++ b/apps/ios/Shared/Assets.xcassets/checkmark.wide.symbolset/checkmark.wide.svg @@ -0,0 +1,218 @@ + + + checkmark.wide + + + + + + + Weight/Scale Variations + + + Ultralight + + + Thin + + + Light + + + Regular + + + Medium + + + Semibold + + + Bold + + + Heavy + + + Black + + + + + + + + + + + + + Design Variations + + + Symbols are supported in up to nine weights and three scales. + + + For optimal layout with text and other symbols, vertically align + + + symbols with the adjacent text. + + + + + + + + + Margins + + + Leading and trailing margins on the left and right side of each symbol + + + + can be adjusted by modifying the x-location of the margin guidelines. + + + + Modifications are automatically applied proportionally to all + + + scales and weights. + + + + + + Exporting + + + Symbols should be outlined when exporting to ensure the + + + design is preserved when submitting to Xcode. + + + Template v.5.0 + + + Requires Xcode 15 or greater + + + Generated from double.checkmark + + + Typeset at 100.0 points + + + Small + + + Medium + + + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index caa54887f7..c03a0083ec 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -61,7 +61,7 @@ class ItemsModel: ObservableObject { init() { publisher - .throttle(for: 0.25, scheduler: DispatchQueue.main, latest: true) + .throttle(for: 0.2, scheduler: DispatchQueue.main, latest: true) .sink { self.objectWillChange.send() } .store(in: &bag) } @@ -123,6 +123,14 @@ class NetworkModel: ObservableObject { } } +/// ChatItemWithMenu can depend on previous or next item for it's appearance +/// This dummy model is used to force an update of all chat items, +/// when they might have changed appearance. +class ChatItemDummyModel: ObservableObject { + static let shared = ChatItemDummyModel() + func sendUpdate() { objectWillChange.send() } +} + final class ChatModel: ObservableObject { @Published var onboardingStage: OnboardingStage? @Published var setDeliveryReceipts = false @@ -427,19 +435,17 @@ final class ChatModel: ObservableObject { private func _upsertChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) -> Bool { if let i = getChatItemIndex(cItem) { - withConditionalAnimation { - _updateChatItem(at: i, with: cItem) - } + _updateChatItem(at: i, with: cItem) + ChatItemDummyModel.shared.sendUpdate() return false } else { - withConditionalAnimation(itemAnimation()) { - var ci = cItem - if let status = chatItemStatuses.removeValue(forKey: ci.id), case .sndNew = ci.meta.itemStatus { - ci.meta.itemStatus = status - } - im.reversedChatItems.insert(ci, at: hasLiveDummy ? 1 : 0) - im.itemAdded = true + var ci = cItem + if let status = chatItemStatuses.removeValue(forKey: ci.id), case .sndNew = ci.meta.itemStatus { + ci.meta.itemStatus = status } + im.reversedChatItems.insert(ci, at: hasLiveDummy ? 1 : 0) + im.itemAdded = true + ChatItemDummyModel.shared.sendUpdate() return true } @@ -556,6 +562,7 @@ final class ChatModel: ObservableObject { // update preview _updateChat(cInfo.id) { chat in self.decreaseUnreadCounter(user: self.currentUser!, by: chat.chatStats.unreadCount) + self.updateFloatingButtons(unreadCount: 0) chat.chatStats = ChatStats() } // update current chat @@ -572,6 +579,12 @@ final class ChatModel: ObservableObject { } } + private func updateFloatingButtons(unreadCount: Int) { + let fbm = ChatView.FloatingButtonModel.shared + fbm.totalUnread = unreadCount + fbm.objectWillChange.send() + } + func markChatItemsRead(_ cInfo: ChatInfo, aboveItem: ChatItem? = nil) { if let cItem = aboveItem { if chatId == cInfo.id, let i = getChatItemIndex(cItem) { @@ -590,6 +603,7 @@ final class ChatModel: ObservableObject { if markedCount > 0 { chat.chatStats.unreadCount -= markedCount self.decreaseUnreadCounter(user: self.currentUser!, by: markedCount) + self.updateFloatingButtons(unreadCount: chat.chatStats.unreadCount) } } } @@ -619,19 +633,15 @@ final class ChatModel: ObservableObject { } } - func markChatItemRead(_ cInfo: ChatInfo, _ cItem: ChatItem) async { - if chatId == cInfo.id, - let itemIndex = getChatItemIndex(cItem), - im.reversedChatItems[itemIndex].isRcvNew { - await MainActor.run { - withTransaction(Transaction()) { - // update current chat - markChatItemRead_(itemIndex) - // update preview - unreadCollector.changeUnreadCounter(cInfo.id, by: -1) + func markChatItemsRead(_ cInfo: ChatInfo, _ itemIds: [ChatItem.ID]) { + if self.chatId == cInfo.id { + for itemId in itemIds { + if let i = im.reversedChatItems.firstIndex(where: { $0.id == itemId }) { + markChatItemRead_(i) } } } + self.unreadCollector.changeUnreadCounter(cInfo.id, by: -itemIds.count) } private let unreadCollector = UnreadCollector() @@ -657,9 +667,10 @@ final class ChatModel: ObservableObject { } func changeUnreadCounter(_ chatId: ChatId, by count: Int) { - DispatchQueue.main.async { - self.unreadCounts[chatId] = (self.unreadCounts[chatId] ?? 0) + count + if chatId == ChatModel.shared.chatId { + ChatView.FloatingButtonModel.shared.totalUnread += count } + self.unreadCounts[chatId] = (self.unreadCounts[chatId] ?? 0) + count subject.send() } } @@ -881,35 +892,6 @@ final class ChatModel: ObservableObject { _ = upsertGroupMember(groupInfo, updatedMember) } } - - func unreadChatItemCounts(itemsInView: Set) -> UnreadChatItemCounts { - var i = 0 - var totalBelow = 0 - var unreadBelow = 0 - while i < im.reversedChatItems.count - 1 && !itemsInView.contains(im.reversedChatItems[i].viewId) { - totalBelow += 1 - if im.reversedChatItems[i].isRcvNew { - unreadBelow += 1 - } - i += 1 - } - return UnreadChatItemCounts( - // TODO these thresholds account for the fact that items are still "visible" while - // covered by compose area, they should be replaced with the actual height in pixels below the screen. - isNearBottom: totalBelow < 15, - isReallyNearBottom: totalBelow < 2, - unreadBelow: unreadBelow - ) - } - - func topItemInView(itemsInView: Set) -> ChatItem? { - let maxIx = im.reversedChatItems.count - 1 - var i = 0 - let inView = { itemsInView.contains(self.im.reversedChatItems[$0].viewId) } - while i < maxIx && !inView(i) { i += 1 } - while i < maxIx && inView(i) { i += 1 } - return im.reversedChatItems[min(i - 1, maxIx)] - } } struct ShowingInvitation { @@ -922,12 +904,6 @@ struct NTFContactRequest { var chatId: String } -struct UnreadChatItemCounts: Equatable { - var isNearBottom: Bool - var isReallyNearBottom: Bool - var unreadBelow: Int -} - final class Chat: ObservableObject, Identifiable, ChatLike { @Published var chatInfo: ChatInfo @Published var chatItems: [ChatItem] diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 6c5d5504e6..7312b42b1b 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -357,17 +357,17 @@ func apiGetChatItemInfo(type: ChatType, id: Int64, itemId: Int64) async throws - throw r } -func apiForwardChatItem(toChatType: ChatType, toChatId: Int64, fromChatType: ChatType, fromChatId: Int64, itemId: Int64, ttl: Int?) async -> ChatItem? { - let cmd: ChatCommand = .apiForwardChatItem(toChatType: toChatType, toChatId: toChatId, fromChatType: fromChatType, fromChatId: fromChatId, itemId: itemId, ttl: ttl) +func apiForwardChatItems(toChatType: ChatType, toChatId: Int64, fromChatType: ChatType, fromChatId: Int64, itemIds: [Int64], ttl: Int?) async -> [ChatItem]? { + let cmd: ChatCommand = .apiForwardChatItems(toChatType: toChatType, toChatId: toChatId, fromChatType: fromChatType, fromChatId: fromChatId, itemIds: itemIds, ttl: ttl) return await processSendMessageCmd(toChatType: toChatType, cmd: cmd) } -func apiSendMessage(type: ChatType, id: Int64, file: CryptoFile?, quotedItemId: Int64?, msg: MsgContent, live: Bool = false, ttl: Int? = nil) async -> ChatItem? { - let cmd: ChatCommand = .apiSendMessage(type: type, id: id, file: file, quotedItemId: quotedItemId, msg: msg, live: live, ttl: ttl) +func apiSendMessages(type: ChatType, id: Int64, live: Bool = false, ttl: Int? = nil, composedMessages: [ComposedMessage]) async -> [ChatItem]? { + let cmd: ChatCommand = .apiSendMessages(type: type, id: id, live: live, ttl: ttl, composedMessages: composedMessages) return await processSendMessageCmd(toChatType: type, cmd: cmd) } -private func processSendMessageCmd(toChatType: ChatType, cmd: ChatCommand) async -> ChatItem? { +private func processSendMessageCmd(toChatType: ChatType, cmd: ChatCommand) async -> [ChatItem]? { let chatModel = ChatModel.shared let r: ChatResponse if toChatType == .direct { @@ -380,10 +380,13 @@ private func processSendMessageCmd(toChatType: ChatType, cmd: ChatCommand) async } }) r = await chatSendCmd(cmd, bgTask: false) - if case let .newChatItem(_, aChatItem) = r { - cItem = aChatItem.chatItem - chatModel.messageDelivery[aChatItem.chatItem.id] = endTask - return cItem + if case let .newChatItems(_, aChatItems) = r { + let cItems = aChatItems.map { $0.chatItem } + if let cItemLast = cItems.last { + cItem = cItemLast + chatModel.messageDelivery[cItemLast.id] = endTask + } + return cItems } if let networkErrorAlert = networkErrorAlert(r) { AlertManager.shared.showAlert(networkErrorAlert) @@ -394,18 +397,18 @@ private func processSendMessageCmd(toChatType: ChatType, cmd: ChatCommand) async return nil } else { r = await chatSendCmd(cmd, bgDelay: msgDelay) - if case let .newChatItem(_, aChatItem) = r { - return aChatItem.chatItem + if case let .newChatItems(_, aChatItems) = r { + return aChatItems.map { $0.chatItem } } sendMessageErrorAlert(r) return nil } } -func apiCreateChatItem(noteFolderId: Int64, file: CryptoFile?, msg: MsgContent) async -> ChatItem? { - let r = await chatSendCmd(.apiCreateChatItem(noteFolderId: noteFolderId, file: file, msg: msg)) - if case let .newChatItem(_, aChatItem) = r { return aChatItem.chatItem } - createChatItemErrorAlert(r) +func apiCreateChatItems(noteFolderId: Int64, composedMessages: [ComposedMessage]) async -> [ChatItem]? { + let r = await chatSendCmd(.apiCreateChatItems(noteFolderId: noteFolderId, composedMessages: composedMessages)) + if case let .newChatItems(_, aChatItems) = r { return aChatItems.map { $0.chatItem } } + createChatItemsErrorAlert(r) return nil } @@ -417,8 +420,8 @@ private func sendMessageErrorAlert(_ r: ChatResponse) { ) } -private func createChatItemErrorAlert(_ r: ChatResponse) { - logger.error("apiCreateChatItem error: \(String(describing: r))") +private func createChatItemsErrorAlert(_ r: ChatResponse) { + logger.error("apiCreateChatItems error: \(String(describing: r))") AlertManager.shared.showAlertMsg( title: "Error creating message", message: "Error: \(responseError(r))" @@ -673,6 +676,13 @@ func apiSetConnectionIncognito(connId: Int64, incognito: Bool) async throws -> P throw r } +func apiChangeConnectionUser(connId: Int64, userId: Int64) async throws -> PendingContactConnection? { + let r = await chatSendCmd(.apiChangeConnectionUser(connId: connId, userId: userId)) + + if case let .connectionUserChanged(_, _, toConnection, _) = r {return toConnection} + throw r +} + func apiConnectPlan(connReq: String) async throws -> ConnectionPlan { let userId = try currentUserId("apiConnectPlan") let r = await chatSendCmd(.apiConnectPlan(userId: userId, connReq: connReq)) @@ -990,6 +1000,10 @@ func apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64)) async thr try await sendCommandOkResp(.apiChatRead(type: type, id: id, itemRange: itemRange)) } +func apiChatItemsRead(type: ChatType, id: Int64, itemIds: [Int64]) async throws { + try await sendCommandOkResp(.apiChatItemsRead(type: type, id: id, itemIds: itemIds)) +} + func apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool) async throws { try await sendCommandOkResp(.apiChatUnread(type: type, id: id, unreadChat: unreadChat)) } @@ -1277,11 +1291,23 @@ func markChatUnread(_ chat: Chat, unreadChat: Bool = true) async { func apiMarkChatItemRead(_ cInfo: ChatInfo, _ cItem: ChatItem) async { do { - logger.debug("apiMarkChatItemRead: \(cItem.id)") try await apiChatRead(type: cInfo.chatType, id: cInfo.apiId, itemRange: (cItem.id, cItem.id)) - await ChatModel.shared.markChatItemRead(cInfo, cItem) + DispatchQueue.main.async { + ChatModel.shared.markChatItemsRead(cInfo, [cItem.id]) + } } catch { - logger.error("apiMarkChatItemRead apiChatRead error: \(responseError(error))") + 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) + DispatchQueue.main.async { + ChatModel.shared.markChatItemsRead(cInfo, itemIds) + } + } catch { + logger.error("apiChatItemsRead error: \(responseError(error))") } } @@ -1775,23 +1801,25 @@ func processReceivedMsg(_ res: ChatResponse) async { n.networkStatuses = ns } } - case let .newChatItem(user, aChatItem): - let cInfo = aChatItem.chatInfo - let cItem = aChatItem.chatItem - await MainActor.run { - if active(user) { - m.addChatItem(cInfo, cItem) - } else if cItem.isRcvNew && cInfo.ntfsEnabled { - m.increaseUnreadCounter(user: user) + case let .newChatItems(user, chatItems): + for chatItem in chatItems { + let cInfo = chatItem.chatInfo + let cItem = chatItem.chatItem + await MainActor.run { + if active(user) { + m.addChatItem(cInfo, cItem) + } else if cItem.isRcvNew && cInfo.ntfsEnabled { + m.increaseUnreadCounter(user: user) + } } - } - if let file = cItem.autoReceiveFile() { - Task { - await receiveFile(user: user, fileId: file.fileId, auto: true) + if let file = cItem.autoReceiveFile() { + Task { + await receiveFile(user: user, fileId: file.fileId, auto: true) + } + } + if cItem.showNotification { + NtfManager.shared.notifyMessageReceived(user, cInfo, cItem) } - } - if cItem.showNotification { - NtfManager.shared.notifyMessageReceived(user, cInfo, cItem) } case let .chatItemStatusUpdated(user, aChatItem): let cInfo = aChatItem.chatInfo @@ -1801,10 +1829,15 @@ func processReceivedMsg(_ res: ChatResponse) async { } if let endTask = m.messageDelivery[cItem.id] { switch cItem.meta.itemStatus { + case .sndNew: () case .sndSent: endTask() + case .sndRcvd: endTask() case .sndErrorAuth: endTask() case .sndError: endTask() - default: () + case .sndWarning: endTask() + case .rcvNew: () + case .rcvRead: () + case .invalid: () } } case let .chatItemUpdated(user, aChatItem): diff --git a/apps/ios/Shared/Theme/Theme.swift b/apps/ios/Shared/Theme/Theme.swift index e2641eb8dd..53f2931d16 100644 --- a/apps/ios/Shared/Theme/Theme.swift +++ b/apps/ios/Shared/Theme/Theme.swift @@ -102,7 +102,7 @@ extension ThemeWallpaper { public func importFromString() -> ThemeWallpaper { if preset == nil, let image { // Need to save image from string and to save its path - if let parsed = UIImage(base64Encoded: image), + if let parsed = imageFromBase64(image), let filename = saveWallpaperFile(image: parsed) { var copy = self copy.image = nil diff --git a/apps/ios/Shared/Views/Call/IncomingCallView.swift b/apps/ios/Shared/Views/Call/IncomingCallView.swift index 4960281d72..5479a9fada 100644 --- a/apps/ios/Shared/Views/Call/IncomingCallView.swift +++ b/apps/ios/Shared/Views/Call/IncomingCallView.swift @@ -38,6 +38,7 @@ struct IncomingCallView: View { } HStack { ProfilePreview(profileOf: invitation.contact, color: .white) + .padding(.vertical, 6) Spacer() callButton("Reject", "phone.down.fill", .red) { diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift index ef0fec5dfe..1a77b36d6f 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift @@ -12,6 +12,7 @@ import SimpleXChat struct CIGroupInvitationView: View { @EnvironmentObject var chatModel: ChatModel @EnvironmentObject var theme: AppTheme + @Environment(\.showTimestamp) var showTimestamp: Bool @ObservedObject var chat: Chat var chatItem: ChatItem var groupInvitation: CIGroupInvitation @@ -45,7 +46,7 @@ struct CIGroupInvitationView: View { .foregroundColor(inProgress ? theme.colors.secondary : chatIncognito ? .indigo : theme.colors.primary) .font(.callout) + Text(" ") - + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy) + + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, colorMode: .transparent, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp) ) .overlay(DetermineWidth()) } @@ -53,7 +54,7 @@ struct CIGroupInvitationView: View { ( groupInvitationText() + Text(" ") - + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy) + + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, colorMode: .transparent, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp) ) .overlay(DetermineWidth()) } @@ -69,7 +70,7 @@ struct CIGroupInvitationView: View { } .padding(.horizontal, 12) .padding(.vertical, 6) - .background(chatItemFrameColor(chatItem, theme)) + .background { chatItemFrameColor(chatItem, theme).modifier(ChatTailPadding()) } .textSelection(.disabled) .onPreferenceChange(DetermineWidth.Key.self) { frameWidth = $0 } .onChange(of: inProgress) { inProgress in diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift index 3966d7e258..b06c6df48c 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift @@ -165,9 +165,9 @@ struct CIImageView: View { private func fileIcon(_ icon: String, _ size: CGFloat, _ padding: CGFloat) -> some View { Image(systemName: icon) .resizable() + .invertedForegroundStyle() .aspectRatio(contentMode: .fit) .frame(width: size, height: size) - .foregroundColor(.white) .padding(padding) } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CILinkView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CILinkView.swift index 3c864ab172..692e6bb8a6 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CILinkView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CILinkView.swift @@ -16,7 +16,7 @@ struct CILinkView: View { var body: some View { VStack(alignment: .center, spacing: 6) { - if let uiImage = UIImage(base64Encoded: linkPreview.image) { + if let uiImage = imageFromBase64(linkPreview.image) { Image(uiImage: uiImage) .resizable() .scaledToFit() diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift index 66b810cf2f..9840b22fc8 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift @@ -12,11 +12,13 @@ import SimpleXChat struct CIMetaView: View { @ObservedObject var chat: Chat @EnvironmentObject var theme: AppTheme + @Environment(\.showTimestamp) var showTimestamp: Bool var chatItem: ChatItem var metaColor: Color var paleMetaColor = Color(UIColor.tertiaryLabel) var showStatus = true var showEdited = true + var invertedMaterial = false @AppStorage(DEFAULT_SHOW_SENT_VIA_RPOXY) private var showSentViaProxy = false @@ -24,93 +26,138 @@ struct CIMetaView: View { if chatItem.isDeletedContent { chatItem.timestampText.font(.caption).foregroundColor(metaColor) } else { - let meta = chatItem.meta - let ttl = chat.chatInfo.timedMessagesTTL - let encrypted = chatItem.encryptedFile - switch meta.itemStatus { - case let .sndSent(sndProgress): - switch sndProgress { - case .complete: ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .sent, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy) - case .partial: ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .sent, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy) + ZStack { + ciMetaText( + chatItem.meta, + chatTTL: chat.chatInfo.timedMessagesTTL, + encrypted: chatItem.encryptedFile, + color: metaColor, + paleColor: paleMetaColor, + colorMode: invertedMaterial + ? .invertedMaterial + : .normal, + showStatus: showStatus, + showEdited: showEdited, + showViaProxy: showSentViaProxy, + showTimesamp: showTimestamp + ).invertedForegroundStyle(enabled: invertedMaterial) + if invertedMaterial { + ciMetaText( + chatItem.meta, + chatTTL: chat.chatInfo.timedMessagesTTL, + encrypted: chatItem.encryptedFile, + colorMode: .normal, + onlyOverrides: true, + showStatus: showStatus, + showEdited: showEdited, + showViaProxy: showSentViaProxy, + showTimesamp: showTimestamp + ) } - case let .sndRcvd(_, sndProgress): - switch sndProgress { - case .complete: - ZStack { - ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .rcvd1, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy) - ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .rcvd2, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy) - } - case .partial: - ZStack { - ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .rcvd1, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy) - ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .rcvd2, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy) - } - } - default: - ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy) } } } } -enum SentCheckmark { - case sent - case rcvd1 - case rcvd2 +enum MetaColorMode { + // Renders provided colours + case normal + // Fully transparent meta - used for reserving space + case transparent + // Renders white on dark backgrounds and black on light ones + case invertedMaterial + + func resolve(_ c: Color?) -> Color? { + switch self { + case .normal: c + case .transparent: .clear + case .invertedMaterial: nil + } + } + + var statusSpacer: Text { + switch self { + case .normal, .transparent: Text(Image(systemName: "circlebadge.fill")).foregroundColor(.clear) + case .invertedMaterial: Text(" ").kerning(13) + } + } } func ciMetaText( _ meta: CIMeta, chatTTL: Int?, encrypted: Bool?, - color: Color = .clear, + color: Color = .clear, // we use this function to reserve space without rendering meta + paleColor: Color? = nil, primaryColor: Color = .accentColor, - transparent: Bool = false, - sent: SentCheckmark? = nil, + colorMode: MetaColorMode = .normal, + onlyOverrides: Bool = false, // only render colors that differ from base showStatus: Bool = true, showEdited: Bool = true, - showViaProxy: Bool + showViaProxy: Bool, + showTimesamp: Bool ) -> Text { var r = Text("") + var space: Text? = nil + let appendSpace = { + if let sp = space { + r = r + sp + space = nil + } + } + let resolved = colorMode.resolve(color) if showEdited, meta.itemEdited { - r = r + statusIconText("pencil", color) + r = r + statusIconText("pencil", resolved) } if meta.disappearing { - r = r + statusIconText("timer", color).font(.caption2) + r = r + statusIconText("timer", resolved).font(.caption2) let ttl = meta.itemTimed?.ttl if ttl != chatTTL { - r = r + Text(shortTimeText(ttl)).foregroundColor(color) + r = r + colored(Text(shortTimeText(ttl)), resolved) } - r = r + Text(" ") + space = Text(" ") } if showViaProxy, meta.sentViaProxy == true { - r = r + statusIconText("arrow.forward", color.opacity(0.67)).font(.caption2) + appendSpace() + r = r + statusIconText("arrow.forward", resolved?.opacity(0.67)).font(.caption2) } if showStatus { - if let (icon, statusColor) = meta.statusIcon(color, primaryColor) { - let t = Text(Image(systemName: icon)).font(.caption2) - let gap = Text(" ").kerning(-1.25) - let t1 = t.foregroundColor(transparent ? .clear : statusColor.opacity(0.67)) - switch sent { - case nil: r = r + t1 - case .sent: r = r + t1 + gap - case .rcvd1: r = r + t.foregroundColor(transparent ? .clear : statusColor.opacity(0.67)) + gap - case .rcvd2: r = r + gap + t1 + appendSpace() + if let (image, statusColor) = meta.itemStatus.statusIcon(color, paleColor ?? color, primaryColor) { + let metaColor = if onlyOverrides && statusColor == color { + Color.clear + } else { + colorMode.resolve(statusColor) } - r = r + Text(" ") + r = r + colored(Text(image), metaColor) + space = Text(" ") } else if !meta.disappearing { - r = r + statusIconText("circlebadge.fill", .clear) + Text(" ") + space = colorMode.statusSpacer + Text(" ") } } if let enc = encrypted { - r = r + statusIconText(enc ? "lock" : "lock.open", color) + Text(" ") + appendSpace() + r = r + statusIconText(enc ? "lock" : "lock.open", resolved) + space = Text(" ") + } + if showTimesamp { + appendSpace() + r = r + colored(meta.timestampText, resolved) } - r = r + meta.timestampText.foregroundColor(color) return r.font(.caption) } -private func statusIconText(_ icon: String, _ color: Color) -> Text { - Text(Image(systemName: icon)).foregroundColor(color) +private func statusIconText(_ icon: String, _ color: Color?) -> Text { + colored(Text(Image(systemName: icon)), color) +} + +// Applying `foregroundColor(nil)` breaks `.invertedForegroundStyle` modifier +private func colored(_ t: Text, _ color: Color?) -> Text { + if let color { + t.foregroundColor(color) + } else { + t + } } struct CIMetaView_Previews: PreviewProvider { diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift index 1f2e16448d..c76ffe8c05 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift @@ -15,6 +15,7 @@ struct CIRcvDecryptionError: View { @EnvironmentObject var m: ChatModel @EnvironmentObject var theme: AppTheme @ObservedObject var chat: Chat + @Environment(\.showTimestamp) var showTimestamp: Bool var msgDecryptError: MsgDecryptError var msgCount: UInt32 var chatItem: ChatItem @@ -68,37 +69,40 @@ struct CIRcvDecryptionError: View { } @ViewBuilder private func viewBody() -> some View { - if case let .direct(contact) = chat.chatInfo, - let contactStats = contact.activeConn?.connectionStats { - if contactStats.ratchetSyncAllowed { - decryptionErrorItemFixButton(syncSupported: true) { - alert = .syncAllowedAlert { syncContactConnection(contact) } + Group { + if case let .direct(contact) = chat.chatInfo, + let contactStats = contact.activeConn?.connectionStats { + if contactStats.ratchetSyncAllowed { + decryptionErrorItemFixButton(syncSupported: true) { + alert = .syncAllowedAlert { syncContactConnection(contact) } + } + } else if !contactStats.ratchetSyncSupported { + decryptionErrorItemFixButton(syncSupported: false) { + alert = .syncNotSupportedContactAlert + } + } else { + basicDecryptionErrorItem() } - } else if !contactStats.ratchetSyncSupported { - decryptionErrorItemFixButton(syncSupported: false) { - alert = .syncNotSupportedContactAlert + } else if case let .group(groupInfo) = chat.chatInfo, + case let .groupRcv(groupMember) = chatItem.chatDir, + let mem = m.getGroupMember(groupMember.groupMemberId), + let memberStats = mem.wrapped.activeConn?.connectionStats { + if memberStats.ratchetSyncAllowed { + decryptionErrorItemFixButton(syncSupported: true) { + alert = .syncAllowedAlert { syncMemberConnection(groupInfo, groupMember) } + } + } else if !memberStats.ratchetSyncSupported { + decryptionErrorItemFixButton(syncSupported: false) { + alert = .syncNotSupportedMemberAlert + } + } else { + basicDecryptionErrorItem() } } else { basicDecryptionErrorItem() } - } else if case let .group(groupInfo) = chat.chatInfo, - case let .groupRcv(groupMember) = chatItem.chatDir, - let mem = m.getGroupMember(groupMember.groupMemberId), - let memberStats = mem.wrapped.activeConn?.connectionStats { - if memberStats.ratchetSyncAllowed { - decryptionErrorItemFixButton(syncSupported: true) { - alert = .syncAllowedAlert { syncMemberConnection(groupInfo, groupMember) } - } - } else if !memberStats.ratchetSyncSupported { - decryptionErrorItemFixButton(syncSupported: false) { - alert = .syncNotSupportedMemberAlert - } - } else { - basicDecryptionErrorItem() - } - } else { - basicDecryptionErrorItem() } + .background { chatItemFrameColor(chatItem, theme).modifier(ChatTailPadding()) } } private func basicDecryptionErrorItem() -> some View { @@ -122,7 +126,7 @@ struct CIRcvDecryptionError: View { .foregroundColor(syncSupported ? theme.colors.primary : theme.colors.secondary) .font(.callout) + Text(" ") - + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy) + + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, colorMode: .transparent, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp) ) } .padding(.horizontal, 12) @@ -131,7 +135,6 @@ struct CIRcvDecryptionError: View { } .onTapGesture(perform: { onClick() }) .padding(.vertical, 6) - .background(Color(uiColor: .tertiarySystemGroupedBackground)) .textSelection(.disabled) } @@ -142,7 +145,7 @@ struct CIRcvDecryptionError: View { .foregroundColor(.red) .italic() + Text(" ") - + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy) + + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, colorMode: .transparent, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp) } .padding(.horizontal, 12) CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary) @@ -150,7 +153,6 @@ struct CIRcvDecryptionError: View { } .onTapGesture(perform: { onClick() }) .padding(.vertical, 6) - .background(Color(uiColor: .tertiarySystemGroupedBackground)) .textSelection(.disabled) } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift index 4670fc685f..851b90bc3d 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift @@ -292,30 +292,22 @@ struct CIVideoView: View { .clipShape(Circle()) } - private func durationProgress() -> some View { - HStack { - Text("\(durationText(videoPlaying ? progress : duration))") - .foregroundColor(.white) - .font(.caption) - .padding(.vertical, 3) - .padding(.horizontal, 6) - .background(Color.black.opacity(0.35)) - .cornerRadius(10) - .padding([.top, .leading], 6) - - if let file = chatItem.file, !videoPlaying { - Text("\(ByteCountFormatter.string(fromByteCount: file.fileSize, countStyle: .binary))") - .foregroundColor(.white) - .font(.caption) - .padding(.vertical, 3) - .padding(.horizontal, 6) - .background(Color.black.opacity(0.35)) - .cornerRadius(10) - .padding(.top, 6) - } + private var fileSizeString: String { + if let file = chatItem.file, !videoPlaying { + " " + ByteCountFormatter.string(fromByteCount: file.fileSize, countStyle: .binary) + } else { + "" } } + private func durationProgress() -> some View { + Text((durationText(videoPlaying ? progress : duration)) + fileSizeString) + .invertedForegroundStyle() + .font(.caption) + .padding(.vertical, 6) + .padding(.horizontal, 12) + } + private func imageView(_ img: UIImage) -> some View { let w = img.size.width <= img.size.height ? maxWidth * 0.75 : maxWidth return ZStack(alignment: .topTrailing) { @@ -411,9 +403,9 @@ struct CIVideoView: View { private func fileIcon(_ icon: String, _ size: CGFloat, _ padding: CGFloat) -> some View { Image(systemName: icon) .resizable() + .invertedForegroundStyle() .aspectRatio(contentMode: .fit) .frame(width: size, height: size) - .foregroundColor(.white) .padding(smallView ? 0 : padding) } @@ -428,10 +420,8 @@ struct CIVideoView: View { private func progressCircle(_ progress: Int64, _ total: Int64) -> some View { Circle() .trim(from: 0, to: Double(progress) / Double(total)) - .stroke( - Color(uiColor: .white), - style: StrokeStyle(lineWidth: 2) - ) + .stroke(style: StrokeStyle(lineWidth: 2)) + .invertedForegroundStyle() .rotationEffect(.degrees(-90)) .frame(width: 16, height: 16) .padding([.trailing, .top], smallView ? 0 : 11) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift index 313ec0d419..5f2930951f 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift @@ -12,7 +12,7 @@ import SimpleXChat struct FramedItemView: View { @EnvironmentObject var m: ChatModel @EnvironmentObject var theme: AppTheme - @EnvironmentObject var scrollModel: ReverseListScrollModel + @EnvironmentObject var scrollModel: ReverseListScrollModel @ObservedObject var chat: Chat var chatItem: ChatItem var preview: UIImage? @@ -64,15 +64,20 @@ struct FramedItemView: View { .overlay(DetermineWidth()) } - if chatItem.content.msgContent != nil { - CIMetaView(chat: chat, chatItem: chatItem, metaColor: useWhiteMetaColor ? Color.white : theme.colors.secondary) - .padding(.horizontal, 12) - .padding(.bottom, 6) - .overlay(DetermineWidth()) - .accessibilityLabel("") + if let content = chatItem.content.msgContent { + CIMetaView( + chat: chat, + chatItem: chatItem, + metaColor: theme.colors.secondary, + invertedMaterial: useWhiteMetaColor + ) + .padding(.horizontal, 12) + .padding(.bottom, 6) + .overlay(DetermineWidth()) + .accessibilityLabel("") } - } - .background(chatItemFrameColorMaybeImageOrVideo(chatItem, theme)) + } + .background { chatItemFrameColorMaybeImageOrVideo(chatItem, theme).modifier(ChatTailPadding()) } .onPreferenceChange(DetermineWidth.Key.self) { msgWidth = $0 } if let (title, text) = chatItem.meta.itemStatus.statusInfo { @@ -185,7 +190,7 @@ struct FramedItemView: View { let v = ZStack(alignment: .topTrailing) { switch (qi.content) { case let .image(_, image): - if let uiImage = UIImage(base64Encoded: image) { + if let uiImage = imageFromBase64(image) { ciQuotedMsgView(qi) .padding(.trailing, 70).frame(minWidth: msgWidth, alignment: .leading) Image(uiImage: uiImage) @@ -197,7 +202,7 @@ struct FramedItemView: View { ciQuotedMsgView(qi) } case let .video(_, image, _): - if let uiImage = UIImage(base64Encoded: image) { + if let uiImage = imageFromBase64(image) { ciQuotedMsgView(qi) .padding(.trailing, 70).frame(minWidth: msgWidth, alignment: .leading) Image(uiImage: uiImage) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/FullScreenMediaView.swift b/apps/ios/Shared/Views/Chat/ChatItem/FullScreenMediaView.swift index a80c5412b6..044ee2a26d 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/FullScreenMediaView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/FullScreenMediaView.swift @@ -13,7 +13,7 @@ import AVKit struct FullScreenMediaView: View { @EnvironmentObject var m: ChatModel - @EnvironmentObject var scrollModel: ReverseListScrollModel + @EnvironmentObject var scrollModel: ReverseListScrollModel @State var chatItem: ChatItem @State var image: UIImage? @State var player: AVPlayer? = nil diff --git a/apps/ios/Shared/Views/Chat/ChatItem/IntegrityErrorItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/IntegrityErrorItemView.swift index 822dda4d06..afeb88b05d 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/IntegrityErrorItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/IntegrityErrorItemView.swift @@ -69,7 +69,7 @@ struct CIMsgError: View { } .padding(.leading, 12) .padding(.vertical, 6) - .background(Color(uiColor: .tertiarySystemGroupedBackground)) + .background { chatItemFrameColor(chatItem, theme).modifier(ChatTailPadding()) } .textSelection(.disabled) .onTapGesture(perform: onTap) } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift index 25e06b9ea4..afd817357c 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift @@ -22,7 +22,7 @@ struct MarkedDeletedItemView: View { .foregroundColor(theme.colors.secondary) .padding(.horizontal, 12) .padding(.vertical, 6) - .background(chatItemFrameColor(chatItem, theme)) + .background { chatItemFrameColor(chatItem, theme).modifier(ChatTailPadding()) } .textSelection(.disabled) } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift index 999f99b294..63d5dc30dc 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift @@ -26,6 +26,7 @@ private func typing(_ w: Font.Weight = .light) -> Text { struct MsgContentView: View { @ObservedObject var chat: Chat + @Environment(\.showTimestamp) var showTimestamp: Bool @EnvironmentObject var theme: AppTheme var text: String var formattedText: [FormattedText]? = nil @@ -84,7 +85,7 @@ struct MsgContentView: View { } private func reserveSpaceForMeta(_ mt: CIMeta) -> Text { - (rightToLeft ? Text("\n") : Text(" ")) + ciMetaText(mt, chatTTL: chat.chatInfo.timedMessagesTTL, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy) + (rightToLeft ? Text("\n") : Text(" ")) + ciMetaText(mt, chatTTL: chat.chatInfo.timedMessagesTTL, encrypted: nil, colorMode: .transparent, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp) } } diff --git a/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift b/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift index f6a856dad1..62ea607d27 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift @@ -450,20 +450,8 @@ struct ChatItemInfoView: View { .foregroundColor(theme.colors.secondary).opacity(0.67) } let v = Group { - let (icon, statusColor) = status.statusIcon(theme.colors.secondary, theme.colors.primary) - switch status { - case .rcvd: - ZStack(alignment: .trailing) { - Image(systemName: icon) - .foregroundColor(statusColor.opacity(0.67)) - .padding(.trailing, 6) - Image(systemName: icon) - .foregroundColor(statusColor.opacity(0.67)) - } - default: - Image(systemName: icon) - .foregroundColor(statusColor) - } + let (image, statusColor) = status.statusIcon(theme.colors.secondary, theme.colors.primary) + image.foregroundColor(statusColor) } if let (title, text) = status.statusInfo { diff --git a/apps/ios/Shared/Views/Chat/ChatItemView.swift b/apps/ios/Shared/Views/Chat/ChatItemView.swift index 870fe30108..bf09d15ff1 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemView.swift @@ -9,9 +9,21 @@ import SwiftUI import SimpleXChat +extension EnvironmentValues { + struct ShowTimestamp: EnvironmentKey { + static let defaultValue: Bool = true + } + + var showTimestamp: Bool { + get { self[ShowTimestamp.self] } + set { self[ShowTimestamp.self] = newValue } + } +} + struct ChatItemView: View { @ObservedObject var chat: Chat @EnvironmentObject var theme: AppTheme + @Environment(\.showTimestamp) var showTimestamp: Bool var chatItem: ChatItem var maxWidth: CGFloat = .infinity @Binding var revealed: Bool @@ -60,7 +72,7 @@ struct ChatItemView: View { default: nil } } - .flatMap { UIImage(base64Encoded: $0) } + .flatMap { imageFromBase64($0) } let adjustedMaxWidth = { if let preview, preview.size.width <= preview.size.height { maxWidth * 0.75 diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index d65fbc1ed6..5c11cdc3df 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -22,8 +22,7 @@ struct ChatView: View { @Environment(\.presentationMode) var presentationMode @Environment(\.scenePhase) var scenePhase @State @ObservedObject var chat: Chat - @StateObject private var scrollModel = ReverseListScrollModel() - @StateObject private var floatingButtonModel = FloatingButtonModel() + @StateObject private var scrollModel = ReverseListScrollModel() @State private var showChatInfoSheet: Bool = false @State private var showAddMembersSheet: Bool = false @State private var composeState = ComposeState() @@ -76,7 +75,7 @@ struct ChatView: View { VStack(spacing: 0) { ZStack(alignment: .bottomTrailing) { chatItemsList() - floatingButtons(counts: floatingButtonModel.unreadChatItemCounts) + FloatingButtons(theme: theme, scrollModel: scrollModel, chat: chat) } connectingText() if selectedChatItems == nil { @@ -340,6 +339,7 @@ struct ChatView: View { await markChatUnread(chat, unreadChat: false) } } + ChatView.FloatingButtonModel.shared.totalUnread = chat.chatStats.unreadCount } private func searchToolbar() -> some View { @@ -413,12 +413,6 @@ struct ChatView: View { revealedChatItem: $revealedChatItem, selectedChatItems: $selectedChatItems ) - .onAppear { - floatingButtonModel.appeared(viewId: ci.viewId) - } - .onDisappear { - floatingButtonModel.disappeared(viewId: ci.viewId) - } .id(ci.id) // Required to trigger `onAppear` on iOS15 } loadPage: { loadChatItems(cInfo) @@ -429,13 +423,10 @@ struct ChatView: View { .onChange(of: searchText) { _ in Task { await loadChat(chat: chat, search: searchText) } } - .onChange(of: im.reversedChatItems) { _ in - floatingButtonModel.chatItemsChanged() - } .onChange(of: im.itemAdded) { added in if added { im.itemAdded = false - if floatingButtonModel.unreadChatItemCounts.isReallyNearBottom { + if FloatingButtonModel.shared.isReallyNearBottom { scrollModel.scrollToBottom() } } @@ -458,103 +449,165 @@ struct ChatView: View { } class FloatingButtonModel: ObservableObject { - private enum Event { - case appeared(String) - case disappeared(String) - case chatItemsChanged - } + static let shared = FloatingButtonModel() + @Published var unreadBelow: Int = 0 + @Published var isNearBottom: Bool = true + @Published var date: Date? + @Published var isDateVisible: Bool = false + var totalUnread: Int = 0 + var isReallyNearBottom: Bool = true + var hideDateWorkItem: DispatchWorkItem? - @Published var unreadChatItemCounts: UnreadChatItemCounts - - private let events = PassthroughSubject() - private var bag = Set() - - init() { - unreadChatItemCounts = UnreadChatItemCounts( - isNearBottom: true, - isReallyNearBottom: true, - unreadBelow: 0 - ) - events - .receive(on: DispatchQueue.global(qos: .background)) - .scan(Set()) { itemsInView, event in - var updated = itemsInView - switch event { - case let .appeared(viewId): updated.insert(viewId) - case let .disappeared(viewId): updated.remove(viewId) - case .chatItemsChanged: () - } - return updated + func updateOnListChange(_ listState: ListState) { + let im = ItemsModel.shared + let unreadBelow = + if let id = listState.bottomItemId, + let index = im.reversedChatItems.firstIndex(where: { $0.id == id }) + { + im.reversedChatItems[.. 0 && listState.scrollOffset < 500 + } + + // set floating button indication mode + let nearBottom = listState.scrollOffset < 800 + if nearBottom != self.isNearBottom { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) { [weak self] in + self?.isNearBottom = nearBottom + } + } + + // hide Date indicator after 1 second of no scrolling + hideDateWorkItem?.cancel() + let workItem = DispatchWorkItem { [weak self] in + guard let it = self else { return } + it.setDate(visibility: false) + it.hideDateWorkItem = nil + } + DispatchQueue.main.async { [weak self] in + self?.hideDateWorkItem = workItem + DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: workItem) + } } - func appeared(viewId: String) { - events.send(.appeared(viewId)) + func resetDate() { + date = nil + isDateVisible = false } - func disappeared(viewId: String) { - events.send(.disappeared(viewId)) + private func setDate(visibility isVisible: Bool) { + if isVisible { + if !isNearBottom, + !isDateVisible, + let date, !Calendar.current.isDateInToday(date) { + withAnimation { self.isDateVisible = true } + } + } else if isDateVisible { + withAnimation { self.isDateVisible = false } + } } - func chatItemsChanged() { - events.send(.chatItemsChanged) - } } - private func floatingButtons(counts: UnreadChatItemCounts) -> some View { - VStack { - let unreadAbove = chat.chatStats.unreadCount - counts.unreadBelow - if unreadAbove > 0 { - circleButton { - unreadCountText(unreadAbove) - .font(.callout) - .foregroundColor(theme.colors.primary) + private struct FloatingButtons: View { + let theme: AppTheme + let scrollModel: ReverseListScrollModel + let chat: Chat + @ObservedObject var model = FloatingButtonModel.shared + + var body: some View { + ZStack(alignment: .top) { + if let date = model.date { + DateSeparator(date: date) + .padding(.vertical, 4).padding(.horizontal, 8) + .background(.thinMaterial) + .clipShape(Capsule()) + .opacity(model.isDateVisible ? 1 : 0) } - .onTapGesture { - scrollModel.scrollToNextPage() - } - .contextMenu { - Button { - Task { - await markChatRead(chat) + VStack { + let unreadAbove = model.totalUnread - model.unreadBelow + if unreadAbove > 0 { + circleButton { + unreadCountText(unreadAbove) + .font(.callout) + .foregroundColor(theme.colors.primary) } - } label: { - Label("Mark read", systemImage: "checkmark") + .onTapGesture { + scrollModel.scrollToNextPage() + } + .contextMenu { + Button { + Task { + await markChatRead(chat) + } + } label: { + Label("Mark read", systemImage: "checkmark") + } + } + } + Spacer() + if model.unreadBelow > 0 { + circleButton { + unreadCountText(model.unreadBelow) + .font(.callout) + .foregroundColor(theme.colors.primary) + } + .onTapGesture { + scrollModel.scrollToBottom() + } + } else if !model.isNearBottom { + circleButton { + Image(systemName: "chevron.down") + .foregroundColor(theme.colors.primary) + } + .onTapGesture { scrollModel.scrollToBottom() } } } + .padding() + .frame(maxWidth: .infinity, alignment: .trailing) } - Spacer() - if counts.unreadBelow > 0 { - circleButton { - unreadCountText(counts.unreadBelow) - .font(.callout) - .foregroundColor(theme.colors.primary) - } - .onTapGesture { - scrollModel.scrollToBottom() - } - } else if !counts.isNearBottom { - circleButton { - Image(systemName: "chevron.down") - .foregroundColor(theme.colors.primary) - } - .onTapGesture { scrollModel.scrollToBottom() } + .onDisappear(perform: model.resetDate) + } + + private func circleButton(_ content: @escaping () -> Content) -> some View { + ZStack { + Circle() + .foregroundColor(Color(uiColor: .tertiarySystemGroupedBackground)) + .frame(width: 44, height: 44) + content() } } - .padding() } - private func circleButton(_ content: @escaping () -> Content) -> some View { - ZStack { - Circle() - .foregroundColor(Color(uiColor: .tertiarySystemGroupedBackground)) - .frame(width: 44, height: 44) - content() + private struct DateSeparator: View { + let date: Date + + var body: some View { + Text(String.localizedStringWithFormat( + NSLocalizedString("%@, %@", comment: "format for date separator in chat"), + date.formatted(.dateTime.weekday(.abbreviated)), + date.formatted(.dateTime.day().month(.abbreviated)) + )) + .font(.callout) + .fontWeight(.medium) + .foregroundStyle(.secondary) } } @@ -696,6 +749,7 @@ struct ChatView: View { @EnvironmentObject var m: ChatModel @EnvironmentObject var theme: AppTheme @Binding @ObservedObject var chat: Chat + @ObservedObject var dummyModel: ChatItemDummyModel = .shared let chatItem: ChatItem let maxWidth: CGFloat @Binding var composeState: ComposeState @@ -709,38 +763,65 @@ struct ChatView: View { @State private var showChatItemInfoSheet: Bool = false @State private var chatItemInfo: ChatItemInfo? @State private var showForwardingSheet: Bool = false - + @State private var msgWidth: CGFloat = 0 + @Binding var selectedChatItems: Set? @State private var allowMenu: Bool = true + @State private var markedRead = false var revealed: Bool { chatItem == revealedChatItem } + typealias ItemSeparation = (timestamp: Bool, largeGap: Bool, date: Date?) + + func getItemSeparation(_ chatItem: ChatItem, at i: Int?) -> ItemSeparation { + let im = ItemsModel.shared + if let i, i > 0 && im.reversedChatItems.count >= i { + let nextItem = im.reversedChatItems[i - 1] + let largeGap = !nextItem.chatDir.sameDirection(chatItem.chatDir) || nextItem.meta.itemTs.timeIntervalSince(chatItem.meta.itemTs) > 60 + return ( + timestamp: largeGap || formatTimestampMeta(chatItem.meta.itemTs) != formatTimestampMeta(nextItem.meta.itemTs), + largeGap: largeGap, + date: Calendar.current.isDate(chatItem.meta.itemTs, inSameDayAs: nextItem.meta.itemTs) ? nil : nextItem.meta.itemTs + ) + } else { + return (timestamp: true, largeGap: true, date: nil) + } + } + var body: some View { - let (currIndex, _) = m.getNextChatItem(chatItem) + let currIndex = m.getChatItemIndex(chatItem) let ciCategory = chatItem.mergeCategory let (prevHidden, prevItem) = m.getPrevShownChatItem(currIndex, ciCategory) let range = itemsRange(currIndex, prevHidden) + let timeSeparation = getItemSeparation(chatItem, at: currIndex) let im = ItemsModel.shared Group { if revealed, let range = range { let items = Array(zip(Array(range), im.reversedChatItems[range])) - ForEach(items.reversed(), id: \.1.viewId) { (i, ci) in - let prev = i == prevHidden ? prevItem : im.reversedChatItems[i + 1] - chatItemView(ci, nil, prev) - .overlay { - if let selected = selectedChatItems, ci.canBeDeletedForSelf { - Color.clear - .contentShape(Rectangle()) - .onTapGesture { - let checked = selected.contains(ci.id) - selectUnselectChatItem(select: !checked, ci) + VStack(spacing: 0) { + ForEach(items.reversed(), id: \.1.viewId) { (i: Int, ci: ChatItem) in + let prev = i == prevHidden ? prevItem : im.reversedChatItems[i + 1] + chatItemView(ci, nil, prev, getItemSeparation(ci, at: i)) + .overlay { + if let selected = selectedChatItems, ci.canBeDeletedForSelf { + Color.clear + .contentShape(Rectangle()) + .onTapGesture { + let checked = selected.contains(ci.id) + selectUnselectChatItem(select: !checked, ci) + } } - } + } } } } else { - chatItemView(chatItem, range, prevItem) + VStack(spacing: 0) { + chatItemView(chatItem, range, prevItem, timeSeparation) + if let date = timeSeparation.date { + DateSeparator(date: date).padding(8) + } + } .overlay { if let selected = selectedChatItems, chatItem.canBeDeletedForSelf { Color.clear @@ -754,12 +835,16 @@ struct ChatView: View { } } .onAppear { + if markedRead { + return + } else { + markedRead = true + } if let range { - if let items = unreadItems(range) { + let itemIds = unreadItemIds(range) + if !itemIds.isEmpty { waitToMarkRead { - for ci in items { - await apiMarkChatItemRead(chat.chatInfo, ci) - } + await apiMarkChatItemsRead(chat.chatInfo, itemIds) } } } else if chatItem.isRcvNew { @@ -769,29 +854,75 @@ struct ChatView: View { } } } - - private func unreadItems(_ range: ClosedRange) -> [ChatItem]? { + + private func unreadItemIds(_ range: ClosedRange) -> [ChatItem.ID] { let im = ItemsModel.shared - let items = range.compactMap { i in + return range.compactMap { i in if i >= 0 && i < im.reversedChatItems.count { let ci = im.reversedChatItems[i] - return if ci.isRcvNew { ci } else { nil } + return if ci.isRcvNew { ci.id } else { nil } } else { return nil } } - return if items.isEmpty { nil } else { items } } private func waitToMarkRead(_ op: @Sendable @escaping () async -> Void) { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) { + Task { + _ = try? await Task.sleep(nanoseconds: 600_000000) if m.chatId == chat.chatInfo.id { - Task(operation: op) + await op() } } } + - @ViewBuilder func chatItemView(_ ci: ChatItem, _ range: ClosedRange?, _ prevItem: ChatItem?) -> some View { + @available(iOS 16.0, *) + struct MemberLayout: Layout { + let spacing: Double + let msgWidth: Double + + private func sizes(subviews: Subviews, proposal: ProposedViewSize) -> (CGSize, CGSize) { + assert(subviews.count == 2, "member layout must contain exactly two subviews") + let roleSize = subviews[1].sizeThatFits(proposal) + let memberSize = subviews[0].sizeThatFits( + ProposedViewSize( + width: (proposal.width ?? msgWidth) - roleSize.width, + height: proposal.height + ) + ) + return (memberSize, roleSize) + } + + func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout Void) -> CGSize { + let (memberSize, roleSize) = sizes(subviews: subviews, proposal: proposal) + return CGSize( + width: min( + proposal.width ?? msgWidth, + max(msgWidth, roleSize.width + spacing + memberSize.width) + ), + height: max(memberSize.height, roleSize.height) + ) + } + + func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout Void) { + let (memberSize, roleSize) = sizes(subviews: subviews, proposal: proposal) + subviews[0].place( + at: CGPoint(x: bounds.minX, y: bounds.midY - memberSize.height / 2), + proposal: ProposedViewSize(memberSize) + ) + subviews[1].place( + at: CGPoint( + x: bounds.minX + max(memberSize.width + spacing, msgWidth - roleSize.width), + y: bounds.midY - roleSize.height / 2 + ), + proposal: ProposedViewSize(roleSize) + ) + } + } + + @ViewBuilder func chatItemView(_ ci: ChatItem, _ range: ClosedRange?, _ prevItem: ChatItem?, _ itemSeparation: ItemSeparation) -> some View { + let bottomPadding: Double = itemSeparation.largeGap ? 10 : 2 if case let .groupRcv(member) = ci.chatDir, case let .group(groupInfo) = chat.chatInfo { let (prevMember, memCount): (GroupMember?, Int) = @@ -803,24 +934,49 @@ struct ChatView: View { if prevItem == nil || showMemberImage(member, prevItem) || prevMember != nil { VStack(alignment: .leading, spacing: 4) { if ci.content.showMemberName { - let t = if memCount == 1 && member.memberRole > .member { - Text(member.memberRole.text + " ").fontWeight(.semibold) + Text(member.displayName) - } else { - Text(memberNames(member, prevMember, memCount)) + Group { + if memCount == 1 && member.memberRole > .member { + Group { + if #available(iOS 16.0, *) { + MemberLayout(spacing: 16, msgWidth: msgWidth) { + Text(member.chatViewName) + .lineLimit(1) + Text(member.memberRole.text) + .fontWeight(.semibold) + .lineLimit(1) + .padding(.trailing, 8) + } + } else { + HStack(spacing: 16) { + Text(member.chatViewName) + .lineLimit(1) + Text(member.memberRole.text) + .fontWeight(.semibold) + .lineLimit(1) + .layoutPriority(1) + } + } + } + .frame( + maxWidth: maxWidth, + alignment: chatItem.chatDir.sent ? .trailing : .leading + ) + } else { + Text(memberNames(member, prevMember, memCount)) + .lineLimit(2) + } } - t .font(.caption) .foregroundStyle(.secondary) - .lineLimit(2) .padding(.leading, memberImageSize + 14 + (selectedChatItems != nil && ci.canBeDeletedForSelf ? 12 + 24 : 0)) - .padding(.top, 7) + .padding(.top, 3) // this is in addition to message sequence gap } HStack(alignment: .center, spacing: 0) { if selectedChatItems != nil && ci.canBeDeletedForSelf { SelectedChatItem(ciId: ci.id, selectedChatItems: $selectedChatItems) .padding(.trailing, 12) } - HStack(alignment: .top, spacing: 8) { + HStack(alignment: .top, spacing: 10) { MemberProfileImage(member, size: memberImageSize, backgroundColor: theme.colors.background) .onTapGesture { if let member = m.getGroupMember(member.groupMemberId) { @@ -833,11 +989,12 @@ struct ChatView: View { } } } - chatItemWithMenu(ci, range, maxWidth) + chatItemWithMenu(ci, range, maxWidth, itemSeparation) + .onPreferenceChange(DetermineWidth.Key.self) { msgWidth = $0 } } } } - .padding(.bottom, 5) + .padding(.bottom, bottomPadding) .padding(.trailing) .padding(.leading, 12) } else { @@ -846,11 +1003,11 @@ struct ChatView: View { SelectedChatItem(ciId: ci.id, selectedChatItems: $selectedChatItems) .padding(.leading, 12) } - chatItemWithMenu(ci, range, maxWidth) + chatItemWithMenu(ci, range, maxWidth, itemSeparation) .padding(.trailing) - .padding(.leading, memberImageSize + 8 + 12) + .padding(.leading, 10 + memberImageSize + 12) } - .padding(.bottom, 5) + .padding(.bottom, bottomPadding) } } else { HStack(alignment: .center, spacing: 0) { @@ -863,10 +1020,10 @@ struct ChatView: View { .padding(.leading) } } - chatItemWithMenu(ci, range, maxWidth) + chatItemWithMenu(ci, range, maxWidth, itemSeparation) .padding(.horizontal) } - .padding(.bottom, 5) + .padding(.bottom, bottomPadding) } } @@ -881,7 +1038,7 @@ struct ChatView: View { } } - @ViewBuilder func chatItemWithMenu(_ ci: ChatItem, _ range: ClosedRange?, _ maxWidth: CGFloat) -> some View { + @ViewBuilder func chatItemWithMenu(_ ci: ChatItem, _ range: ClosedRange?, _ maxWidth: CGFloat, _ itemSeparation: ItemSeparation) -> some View { let alignment: Alignment = ci.chatDir.sent ? .trailing : .leading VStack(alignment: alignment.horizontal, spacing: 3) { ChatItemView( @@ -891,7 +1048,8 @@ struct ChatView: View { revealed: .constant(revealed), allowMenu: $allowMenu ) - .modifier(ChatItemClipped(ci)) + .environment(\.showTimestamp, itemSeparation.timestamp) + .modifier(ChatItemClipped(ci, tailVisible: itemSeparation.largeGap)) .contextMenu { menu(ci, range, live: composeState.liveMessage != nil) } .accessibilityLabel("") if ci.content.msgContent != nil && (ci.meta.itemDeleted == nil || revealed) && ci.reactions.count > 0 { diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeImageView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeImageView.swift index df3a8caf55..14026d79d1 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeImageView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeImageView.swift @@ -18,7 +18,7 @@ struct ComposeImageView: View { var body: some View { HStack(alignment: .center, spacing: 8) { let imgs: [UIImage] = images.compactMap { image in - UIImage(base64Encoded: image) + imageFromBase64(image) } if imgs.count == 0 { ProgressView() diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeLinkView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeLinkView.swift index f7f1a89299..6c44aeea83 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeLinkView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeLinkView.swift @@ -40,7 +40,7 @@ struct ComposeLinkView: View { private func linkPreviewView(_ linkPreview: LinkPreview) -> some View { HStack(alignment: .center, spacing: 8) { - if let uiImage = UIImage(base64Encoded: linkPreview.image) { + if let uiImage = imageFromBase64(linkPreview.image) { Image(uiImage: uiImage) .resizable() .aspectRatio(contentMode: .fit) diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift index 78cae78cf5..99ab778a0e 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift @@ -751,6 +751,7 @@ struct ComposeView: View { case .linkPreview: sent = await send(checkLinkPreview(), quoted: quoted, live: live, ttl: ttl) case let .mediaPreviews(mediaPreviews: media): + // TODO batch send: batch media previews let last = media.count - 1 if last >= 0 { for i in 0.. ChatItem? { - if let chatItem = chat.chatInfo.chatType == .local - ? await apiCreateChatItem(noteFolderId: chat.chatInfo.apiId, file: file, msg: mc) - : await apiSendMessage( + if let chatItems = chat.chatInfo.chatType == .local + ? await apiCreateChatItems( + noteFolderId: chat.chatInfo.apiId, + composedMessages: [ComposedMessage(fileSource: file, msgContent: mc)] + ) + : await apiSendMessages( type: chat.chatInfo.chatType, id: chat.chatInfo.apiId, - file: file, - quotedItemId: quoted, - msg: mc, live: live, - ttl: ttl + ttl: ttl, + composedMessages: [ComposedMessage(fileSource: file, quotedItemId: quoted, msgContent: mc)] ) { await MainActor.run { chatModel.removeLiveDummy(animated: false) - chatModel.addChatItem(chat.chatInfo, chatItem) + for chatItem in chatItems { + chatModel.addChatItem(chat.chatInfo, chatItem) + } } - return chatItem + // UI only supports sending one item at a time + return chatItems.first } if let file = file { removeFile(file.filePath) @@ -911,18 +916,21 @@ struct ComposeView: View { } func forwardItem(_ forwardedItem: ChatItem, _ fromChatInfo: ChatInfo, _ ttl: Int?) async -> ChatItem? { - if let chatItem = await apiForwardChatItem( + if let chatItems = await apiForwardChatItems( toChatType: chat.chatInfo.chatType, toChatId: chat.chatInfo.apiId, fromChatType: fromChatInfo.chatType, fromChatId: fromChatInfo.apiId, - itemId: forwardedItem.id, + itemIds: [forwardedItem.id], ttl: ttl ) { await MainActor.run { - chatModel.addChatItem(chat.chatInfo, chatItem) + for chatItem in chatItems { + chatModel.addChatItem(chat.chatInfo, chatItem) + } } - return chatItem + // TODO batch send: forward multiple messages + return chatItems.first } return nil } diff --git a/apps/ios/Shared/Views/Chat/ReverseList.swift b/apps/ios/Shared/Views/Chat/ReverseList.swift index 94d160e1b4..2e09909c5e 100644 --- a/apps/ios/Shared/Views/Chat/ReverseList.swift +++ b/apps/ios/Shared/Views/Chat/ReverseList.swift @@ -8,15 +8,16 @@ import SwiftUI import Combine +import SimpleXChat /// A List, which displays it's items in reverse order - from bottom to top -struct ReverseList: UIViewControllerRepresentable { - let items: Array +struct ReverseList: UIViewControllerRepresentable { + let items: Array - @Binding var scrollState: ReverseListScrollModel.State + @Binding var scrollState: ReverseListScrollModel.State /// Closure, that returns user interface for a given item - let content: (Item) -> Content + let content: (ChatItem) -> Content let loadPage: () -> Void @@ -25,7 +26,9 @@ struct ReverseList: UIV } func updateUIViewController(_ controller: Controller, context: Context) { + controller.representer = self if case let .scrollingTo(destination) = scrollState, !items.isEmpty { + controller.view.layer.removeAllAnimations() switch destination { case .nextPage: controller.scrollToNextPage() @@ -42,9 +45,10 @@ struct ReverseList: UIV /// Controller, which hosts SwiftUI cells class Controller: UITableViewController { private enum Section { case main } - private let representer: ReverseList - private var dataSource: UITableViewDiffableDataSource! + var representer: ReverseList + private var dataSource: UITableViewDiffableDataSource! private var itemCount: Int = 0 + private let updateFloatingButtons = PassthroughSubject() private var bag = Set() init(representer: ReverseList) { @@ -71,7 +75,7 @@ struct ReverseList: UIV } // 3. Configure data source - self.dataSource = UITableViewDiffableDataSource( + self.dataSource = UITableViewDiffableDataSource( tableView: tableView ) { (tableView, indexPath, item) -> UITableViewCell? in if indexPath.item > self.itemCount - 8, self.itemCount > 8 { @@ -103,6 +107,15 @@ struct ReverseList: UIV name: notificationName, object: nil ) + + updateFloatingButtons + .throttle(for: 0.2, scheduler: DispatchQueue.global(qos: .background), latest: true) + .sink { + if let listState = DispatchQueue.main.sync(execute: { [weak self] in self?.getListState() }) { + ChatView.FloatingButtonModel.shared.updateOnListChange(listState) + } + } + .store(in: &bag) } @available(*, unavailable) @@ -171,8 +184,8 @@ struct ReverseList: UIV Task { representer.scrollState = .atDestination } } - func update(items: Array) { - var snapshot = NSDiffableDataSourceSnapshot() + func update(items: [ChatItem]) { + var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([.main]) snapshot.appendItems(items) dataSource.defaultRowAnimation = .none @@ -188,6 +201,42 @@ struct ReverseList: UIV ) } itemCount = items.count + updateFloatingButtons.send() + } + + override func scrollViewDidScroll(_ scrollView: UIScrollView) { + updateFloatingButtons.send() + } + + func getListState() -> ListState? { + if let visibleRows = tableView.indexPathsForVisibleRows, + visibleRows.last?.item ?? 0 < representer.items.count { + let scrollOffset: Double = tableView.contentOffset.y + InvertedTableView.inset + let topItemDate: Date? = + if let lastVisible = visibleRows.last(where: { isVisible(indexPath: $0) }) { + representer.items[lastVisible.item].meta.itemTs + } else { + nil + } + let bottomItemId: ChatItem.ID? = + if let firstVisible = visibleRows.first(where: { isVisible(indexPath: $0) }) { + representer.items[firstVisible.item].id + } else { + nil + } + return (scrollOffset: scrollOffset, topItemDate: topItemDate, bottomItemId: bottomItemId) + } + return nil + } + + private func isVisible(indexPath: IndexPath) -> Bool { + if let relativeFrame = tableView.superview?.convert( + tableView.rectForRow(at: indexPath), + from: tableView + ) { + relativeFrame.maxY > InvertedTableView.inset && + relativeFrame.minY < tableView.frame.height - InvertedTableView.inset + } else { false } } } @@ -231,13 +280,19 @@ struct ReverseList: UIV } } +typealias ListState = ( + scrollOffset: Double, + topItemDate: Date?, + bottomItemId: ChatItem.ID? +) + /// Manages ``ReverseList`` scrolling -class ReverseListScrollModel: ObservableObject { +class ReverseListScrollModel: ObservableObject { /// Represents Scroll State of ``ReverseList`` enum State: Equatable { enum Destination: Equatable { case nextPage - case item(Item.ID) + case item(ChatItem.ID) case bottom } @@ -255,7 +310,7 @@ class ReverseListScrollModel: ObservableObject { state = .scrollingTo(.bottom) } - func scrollToItem(id: Item.ID) { + func scrollToItem(id: ChatItem.ID) { state = .scrollingTo(.item(id)) } } diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift index 8ad03236f1..4d1c182554 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift @@ -9,6 +9,17 @@ import SwiftUI import SimpleXChat +enum UserPickerSheet: Identifiable { + case address + case chatPreferences + case chatProfiles + case currentProfile + case useFromDesktop + case settings + + var id: Self { self } +} + struct ChatListView: View { @EnvironmentObject var chatModel: ChatModel @EnvironmentObject var theme: AppTheme @@ -18,9 +29,9 @@ struct ChatListView: View { @State private var searchText = "" @State private var searchShowingSimplexLink = false @State private var searchChatFilteredBySimplexLink: String? = nil - @State private var userPickerVisible = false - @State private var showConnectDesktop = false @State private var scrollToSearchBar = false + @State private var activeUserPickerSheet: UserPickerSheet? = nil + @State private var userPickerShown: Bool = false @AppStorage(DEFAULT_SHOW_UNREAD_AND_FAVORITES) private var showUnreadAndFavorites = false @AppStorage(GROUP_DEFAULT_ONE_HAND_UI, store: groupDefaults) private var oneHandUI = true @@ -46,21 +57,44 @@ struct ChatListView: View { ), destination: chatView ) { chatListView } - if userPickerVisible { - Rectangle().fill(.white.opacity(0.001)).onTapGesture { - withAnimation { - userPickerVisible.toggle() + } + .sheet(isPresented: $userPickerShown) { + UserPicker(activeSheet: $activeUserPickerSheet) + .sheet(item: $activeUserPickerSheet) { sheet in + if let currentUser = chatModel.currentUser { + switch sheet { + case .address: + NavigationView { + UserAddressView(shareViaProfile: currentUser.addressShared) + .navigationTitle("SimpleX address") + .navigationBarTitleDisplayMode(.large) + .modifier(ThemedBackground(grouped: true)) + } + case .chatProfiles: + NavigationView { + UserProfilesView() + } + case .currentProfile: + NavigationView { + UserProfile() + .navigationTitle("Your current profile") + .modifier(ThemedBackground(grouped: true)) + } + case .chatPreferences: + NavigationView { + PreferencesView(profile: currentUser.profile, preferences: currentUser.fullPreferences, currentPreferences: currentUser.fullPreferences) + .navigationTitle("Your preferences") + .navigationBarTitleDisplayMode(.large) + .modifier(ThemedBackground(grouped: true)) + } + case .useFromDesktop: + ConnectDesktopView(viaSettings: false) + case .settings: + SettingsView(showSettings: $showSettings) + .navigationBarTitleDisplayMode(.large) + } } } - } - UserPicker( - showSettings: $showSettings, - showConnectDesktop: $showConnectDesktop, - userPickerVisible: $userPickerVisible - ) - } - .sheet(isPresented: $showConnectDesktop) { - ConnectDesktopView() } } @@ -73,7 +107,7 @@ struct ChatListView: View { .navigationBarHidden(searchMode || oneHandUI) } .scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center) - .onDisappear() { withAnimation { userPickerVisible = false } } + .onDisappear() { activeUserPickerSheet = nil } .refreshable { AlertManager.shared.showAlert(Alert( title: Text("Reconnect servers?"), @@ -164,7 +198,7 @@ struct ChatListView: View { let user = chatModel.currentUser ?? User.sampleData ZStack(alignment: .topTrailing) { ProfileImage(imageStr: user.image, size: 32, color: Color(uiColor: .quaternaryLabel)) - .padding(.trailing, 4) + .padding([.top, .trailing], 3) let allRead = chatModel.users .filter { u in !u.user.activeUser && !u.user.hidden } .allSatisfy { u in u.unreadCount == 0 } @@ -173,13 +207,7 @@ struct ChatListView: View { } } .onTapGesture { - if chatModel.users.filter({ u in u.user.activeUser || !u.user.hidden }).count > 1 { - withAnimation { - userPickerVisible.toggle() - } - } else { - showSettings = true - } + userPickerShown = true } } @@ -269,7 +297,7 @@ struct ChatListView: View { } } - private func unreadBadge(_ text: Text? = Text(" "), size: CGFloat = 18) -> some View { + private func unreadBadge(size: CGFloat = 18) -> some View { Circle() .frame(width: size, height: size) .foregroundColor(theme.colors.primary) diff --git a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift index 9e6d3005b6..cf9977860d 100644 --- a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift @@ -302,7 +302,7 @@ struct ChatPreviewView: View { case let .link(_, preview): smallContentPreview(size: dynamicMediaSize) { ZStack(alignment: .topTrailing) { - Image(uiImage: UIImage(base64Encoded: preview.image) ?? UIImage(systemName: "arrow.up.right")!) + Image(uiImage: imageFromBase64(preview.image) ?? UIImage(systemName: "arrow.up.right")!) .resizable() .aspectRatio(contentMode: .fill) .frame(width: dynamicMediaSize, height: dynamicMediaSize) @@ -323,13 +323,13 @@ struct ChatPreviewView: View { } case let .image(_, image): smallContentPreview(size: dynamicMediaSize) { - CIImageView(chatItem: ci, preview: UIImage(base64Encoded: image), maxWidth: dynamicMediaSize, smallView: true, showFullScreenImage: $showFullscreenGallery) - .environmentObject(ReverseListScrollModel()) + CIImageView(chatItem: ci, preview: imageFromBase64(image), maxWidth: dynamicMediaSize, smallView: true, showFullScreenImage: $showFullscreenGallery) + .environmentObject(ReverseListScrollModel()) } case let .video(_,image, duration): smallContentPreview(size: dynamicMediaSize) { - CIVideoView(chatItem: ci, preview: UIImage(base64Encoded: image), duration: duration, maxWidth: dynamicMediaSize, videoWidth: nil, smallView: true, showFullscreenPlayer: $showFullscreenGallery) - .environmentObject(ReverseListScrollModel()) + CIVideoView(chatItem: ci, preview: imageFromBase64(image), duration: duration, maxWidth: dynamicMediaSize, videoWidth: nil, smallView: true, showFullscreenPlayer: $showFullscreenGallery) + .environmentObject(ReverseListScrollModel()) } case let .voice(_, duration): smallContentPreviewVoice(size: dynamicMediaSize) { diff --git a/apps/ios/Shared/Views/ChatList/UserPicker.swift b/apps/ios/Shared/Views/ChatList/UserPicker.swift index 5041e093db..efe54cb036 100644 --- a/apps/ios/Shared/Views/ChatList/UserPicker.swift +++ b/apps/ios/Shared/Views/ChatList/UserPicker.swift @@ -8,179 +8,228 @@ import SimpleXChat struct UserPicker: View { @EnvironmentObject var m: ChatModel - @Environment(\.scenePhase) var scenePhase @EnvironmentObject var theme: AppTheme - @Binding var showSettings: Bool - @Binding var showConnectDesktop: Bool - @Binding var userPickerVisible: Bool - @State var scrollViewContentSize: CGSize = .zero - @State var disableScrolling: Bool = true - private let menuButtonHeight: CGFloat = 68 - @State var chatViewNameWidth: CGFloat = 0 - + @Environment(\.dynamicTypeSize) private var userFont: DynamicTypeSize + @Environment(\.scenePhase) private var scenePhase: ScenePhase + @Environment(\.colorScheme) private var colorScheme: ColorScheme + @Environment(\.dismiss) private var dismiss: DismissAction + @Binding var activeSheet: UserPickerSheet? + @State private var switchingProfile = false var body: some View { - VStack { - Spacer().frame(height: 1) - VStack(spacing: 0) { - ScrollView { - ScrollViewReader { sp in - let users = m.users - .filter({ u in u.user.activeUser || !u.user.hidden }) - .sorted { u, _ in u.user.activeUser } - VStack(spacing: 0) { - ForEach(users) { u in - userView(u) - Divider() - if u.user.activeUser { Divider() } - } - } - .overlay { - GeometryReader { geo -> Color in - DispatchQueue.main.async { - scrollViewContentSize = geo.size - let scenes = UIApplication.shared.connectedScenes - if let windowScene = scenes.first as? UIWindowScene { - let layoutFrame = windowScene.windows[0].safeAreaLayoutGuide.layoutFrame - disableScrolling = scrollViewContentSize.height + menuButtonHeight + 10 < layoutFrame.height - } - } - return Color.clear - } - } - .onChange(of: userPickerVisible) { visible in - if visible, let u = users.first { - sp.scrollTo(u.id) + if #available(iOS 16.0, *) { + let v = viewBody.presentationDetents([.height(420)]) + if #available(iOS 16.4, *) { + v.scrollBounceBehavior(.basedOnSize) + } else { + v + } + } else { + viewBody + } + } + + private var viewBody: some View { + let otherUsers = m.users.filter { u in !u.user.hidden && u.user.userId != m.currentUser?.userId } + return List { + Section(header: Text("You").foregroundColor(theme.colors.secondary)) { + if let user = m.currentUser { + openSheetOnTap(label: { + ZStack { + let v = ProfilePreview(profileOf: user) + .foregroundColor(.primary) + .padding(.leading, -8) + if #available(iOS 16.0, *) { + v + } else { + v.padding(.vertical, 4) } } + }) { + activeSheet = .currentProfile } - } - .simultaneousGesture(DragGesture(minimumDistance: disableScrolling ? 0 : 10000000)) - .frame(maxHeight: scrollViewContentSize.height) - menuButton("Use from desktop", icon: "desktopcomputer") { - showConnectDesktop = true - withAnimation { - userPickerVisible.toggle() + openSheetOnTap(title: m.userAddress == nil ? "Create SimpleX address" : "Your SimpleX address", icon: "qrcode") { + activeSheet = .address + } + + openSheetOnTap(title: "Chat preferences", icon: "switch.2") { + activeSheet = .chatPreferences } } - Divider() - menuButton("Settings", icon: "gearshape") { - showSettings = true - withAnimation { - userPickerVisible.toggle() + } + + Section { + if otherUsers.isEmpty { + openSheetOnTap(title: "Your chat profiles", icon: "person.crop.rectangle.stack") { + activeSheet = .chatProfiles + } + } else { + let v = userPickerRow(otherUsers, size: 44) + .padding(.leading, -11) + if #available(iOS 16.0, *) { + v + } else { + v.padding(.vertical, 4) + } + } + + openSheetOnTap(title: "Use from desktop", icon: "desktopcomputer") { + activeSheet = .useFromDesktop + } + + ZStack(alignment: .trailing) { + openSheetOnTap(title: "Settings", icon: "gearshape") { + activeSheet = .settings + } + Label {} icon: { + Image(systemName: colorScheme == .light ? "sun.max" : "moon.fill") + .resizable() + .symbolRenderingMode(.monochrome) + .foregroundColor(theme.colors.secondary) + .frame(maxWidth: 20, maxHeight: 20) + } + .onTapGesture { + if (colorScheme == .light) { + ThemeManager.applyTheme(systemDarkThemeDefault.get()) + } else { + ThemeManager.applyTheme(DefaultTheme.LIGHT.themeName) + } + } + .onLongPressGesture { + ThemeManager.applyTheme(DefaultTheme.SYSTEM_THEME_NAME) } } } } - .clipShape(RoundedRectangle(cornerRadius: 16)) - .background( - Rectangle() - .fill(theme.colors.surface) - .cornerRadius(16) - .shadow(color: .black.opacity(0.12), radius: 24, x: 0, y: 0) - ) - .onPreferenceChange(DetermineWidth.Key.self) { chatViewNameWidth = $0 } - .frame(maxWidth: chatViewNameWidth > 0 ? min(300, chatViewNameWidth + 130) : 300) - .padding(8) - .opacity(userPickerVisible ? 1.0 : 0.0) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) .onAppear { - // This check prevents the call of listUsers after the app is suspended, and the database is closed. - if case .active = scenePhase { - Task { - do { - let users = try await listUsersAsync() - await MainActor.run { m.users = users } - } catch { - logger.error("Error loading users \(responseError(error))") - } - } - } - } - } - - private func userView(_ u: UserInfo) -> some View { - let user = u.user - return Button(action: { - if user.activeUser { - showSettings = true - withAnimation { - userPickerVisible.toggle() - } - } else { + // This check prevents the call of listUsers after the app is suspended, and the database is closed. + if case .active = scenePhase { Task { do { - try await changeActiveUserAsync_(user.userId, viewPwd: nil) - await MainActor.run { userPickerVisible = false } + let users = try await listUsersAsync() + await MainActor.run { m.users = users } } catch { - await MainActor.run { - AlertManager.shared.showAlertMsg( - title: "Error switching profile!", - message: "Error: \(responseError(error))" - ) - } + logger.error("Error loading users \(responseError(error))") } } } - }, label: { - HStack(spacing: 0) { - ProfileImage(imageStr: user.image, size: 44, color: Color(uiColor: .tertiarySystemFill)) - .padding(.trailing, 12) - Text(user.chatViewName) - .fontWeight(user.activeUser ? .medium : .regular) - .foregroundColor(theme.colors.onBackground) - .overlay(DetermineWidth()) - Spacer() - if user.activeUser { - Image(systemName: "checkmark") - } else if u.unreadCount > 0 { - unreadCounter(u.unreadCount, color: user.showNtfs ? theme.colors.primary : theme.colors.secondary) - } else if !user.showNtfs { - Image(systemName: "speaker.slash") + } + .modifier(ThemedBackground(grouped: true)) + .disabled(switchingProfile) + } + + private func userPickerRow(_ users: [UserInfo], size: CGFloat) -> some View { + HStack(spacing: 6) { + let s = ScrollView(.horizontal) { + HStack(spacing: 27) { + ForEach(users) { u in + if !u.user.hidden && u.user.userId != m.currentUser?.userId { + userView(u, size: size) + } + } + } + .padding(.leading, 4) + .padding(.trailing, 22) + } + ZStack(alignment: .trailing) { + if #available(iOS 16.0, *) { + s.scrollIndicators(.hidden) + } else { + s + } + LinearGradient( + colors: [.clear, .black], + startPoint: .leading, + endPoint: .trailing + ) + .frame(width: size, height: size + 3) + .blendMode(.destinationOut) + .allowsHitTesting(false) + } + .compositingGroup() + .padding(.top, -3) // to fit unread badge + Spacer() + Image(systemName: "chevron.right") + .foregroundColor(theme.colors.secondary) + .padding(.trailing, 4) + .onTapGesture { + activeSheet = .chatProfiles + } + } + } + + private func userView(_ u: UserInfo, size: CGFloat) -> some View { + ZStack(alignment: .topTrailing) { + ProfileImage(imageStr: u.user.image, size: size, color: Color(uiColor: .tertiarySystemGroupedBackground)) + .padding([.top, .trailing], 3) + if (u.unreadCount > 0) { + unreadBadge(u) + } + } + .frame(width: size) + .onTapGesture { + switchingProfile = true + Task { + do { + try await changeActiveUserAsync_(u.user.userId, viewPwd: nil) + await MainActor.run { + switchingProfile = false + dismiss() + } + } catch { + await MainActor.run { + switchingProfile = false + AlertManager.shared.showAlertMsg( + title: "Error switching profile!", + message: "Error: \(responseError(error))" + ) + } } } - .padding(.trailing) - .padding([.leading, .vertical], 12) - }) - .buttonStyle(PressedButtonStyle(defaultColor: theme.colors.surface, pressedColor: Color(uiColor: .secondarySystemFill))) + } } - - private func menuButton(_ title: LocalizedStringKey, icon: String, action: @escaping () -> Void) -> some View { - Button(action: action) { - HStack(spacing: 0) { - Text(title) - .overlay(DetermineWidth()) - Spacer() - Image(systemName: icon) + + private func openSheetOnTap(title: LocalizedStringKey, icon: String, action: @escaping () -> Void) -> some View { + openSheetOnTap(label: { + ZStack(alignment: .leading) { + Image(systemName: icon).frame(maxWidth: 24, maxHeight: 24, alignment: .center) .symbolRenderingMode(.monochrome) .foregroundColor(theme.colors.secondary) + Text(title) + .foregroundColor(.primary) + .padding(.leading, 36) } - .padding(.horizontal) - .padding(.vertical, 22) - .frame(height: menuButtonHeight) - } - .buttonStyle(PressedButtonStyle(defaultColor: theme.colors.surface, pressedColor: Color(uiColor: .secondarySystemFill))) + }, action: action) + } + + private func openSheetOnTap(label: () -> V, action: @escaping () -> Void) -> some View { + Button(action: action, label: label) + .frame(maxWidth: .infinity, alignment: .leading) + .contentShape(Rectangle()) + } + + private func unreadBadge(_ u: UserInfo) -> some View { + let size = dynamicSize(userFont).chatInfoSize + return unreadCountText(u.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) + .cornerRadius(dynamicSize(userFont).unreadCorner) } -} - -private func unreadCounter(_ unread: Int, color: Color) -> some View { - unreadCountText(unread) - .font(.caption) - .foregroundColor(.white) - .padding(.horizontal, 4) - .frame(minWidth: 18, minHeight: 18) - .background(color) - .cornerRadius(10) } struct UserPicker_Previews: PreviewProvider { static var previews: some View { + @State var activeSheet: UserPickerSheet? + let m = ChatModel() m.users = [UserInfo.sampleData, UserInfo.sampleData] return UserPicker( - showSettings: Binding.constant(false), - showConnectDesktop: Binding.constant(false), - userPickerVisible: Binding.constant(true) + activeSheet: $activeSheet ) .environmentObject(m) } diff --git a/apps/ios/Shared/Views/Database/DatabaseView.swift b/apps/ios/Shared/Views/Database/DatabaseView.swift index f5b5287971..d0de0e1bd3 100644 --- a/apps/ios/Shared/Views/Database/DatabaseView.swift +++ b/apps/ios/Shared/Views/Database/DatabaseView.swift @@ -270,12 +270,12 @@ struct DatabaseView: View { case let .archiveImportedWithErrors(errs): return Alert( title: Text("Chat database imported"), - message: Text("Restart the app to use imported chat database") + Text(verbatim: "\n\n") + Text("Some non-fatal errors occurred during import:") + archiveErrorsText(errs) + message: Text("Restart the app to use imported chat database") + Text(verbatim: "\n") + Text("Some non-fatal errors occurred during import:") + archiveErrorsText(errs) ) case let .archiveExportedWithErrors(archivePath, errs): return Alert( title: Text("Chat database exported"), - message: Text("You may save the exported archive.") + Text(verbatim: "\n\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:") + archiveErrorsText(errs), dismissButton: .default(Text("Continue")) { showShareSheet(items: [archivePath]) } diff --git a/apps/ios/Shared/Views/Helpers/ChatItemClipShape.swift b/apps/ios/Shared/Views/Helpers/ChatItemClipShape.swift index 477dc567eb..e1e0911e4d 100644 --- a/apps/ios/Shared/Views/Helpers/ChatItemClipShape.swift +++ b/apps/ios/Shared/Views/Helpers/ChatItemClipShape.swift @@ -14,50 +14,60 @@ import SimpleXChat /// Supports [Dynamic Type](https://developer.apple.com/documentation/uikit/uifont/scaling_fonts_automatically) /// by retaining pill shape, even when ``ChatItem``'s height is less that twice its corner radius struct ChatItemClipped: ViewModifier { - struct ClipShape: Shape { - let maxCornerRadius: Double - - func path(in rect: CGRect) -> Path { - Path( - roundedRect: rect, - cornerRadius: min((rect.height / 2), maxCornerRadius), - style: .circular - ) - } - } - + @AppStorage(DEFAULT_CHAT_ITEM_ROUNDNESS) private var roundness = defaultChatItemRoundness + @AppStorage(DEFAULT_CHAT_ITEM_TAIL) private var tailEnabled = true + private let chatItem: (content: CIContent, chatDir: CIDirection)? + private let tailVisible: Bool + init() { - clipShape = ClipShape( - maxCornerRadius: 18 - ) + self.chatItem = nil + self.tailVisible = false + } + + init(_ ci: ChatItem, tailVisible: Bool) { + self.chatItem = (ci.content, ci.chatDir) + self.tailVisible = tailVisible } - init(_ chatItem: ChatItem) { - clipShape = ClipShape( - maxCornerRadius: { - switch chatItem.content { - case - .sndMsgContent, + private func shapeStyle() -> ChatItemShape.Style { + if let ci = chatItem { + switch ci.content { + case + .sndMsgContent, .rcvMsgContent, .rcvDecryptionError, - .rcvGroupInvitation, - .sndGroupInvitation, - .sndDeleted, + .sndDeleted, .rcvDeleted, .rcvIntegrityError, - .sndModerated, - .rcvModerated, + .sndModerated, + .rcvModerated, .rcvBlocked, - .invalidJSON: 18 - default: 8 + .invalidJSON: + let tail = if let mc = ci.content.msgContent, mc.isImageOrVideo && mc.text.isEmpty { + false + } else { + tailVisible } - }() - ) + return tailEnabled + ? .bubble( + padding: ci.chatDir.sent ? .trailing : .leading, + tailVisible: tail + ) + : .roundRect(radius: msgRectMaxRadius) + case .rcvGroupInvitation, .sndGroupInvitation: + return .roundRect(radius: msgRectMaxRadius) + default: return .roundRect(radius: 8) + } + } else { + return .roundRect(radius: msgRectMaxRadius) + } } - - private let clipShape: ClipShape - + func body(content: Content) -> some View { + let clipShape = ChatItemShape( + roundness: roundness, + style: shapeStyle() + ) content .contentShape(.dragPreview, clipShape) .contentShape(.contextMenuPreview, clipShape) @@ -65,4 +75,106 @@ struct ChatItemClipped: ViewModifier { } } +struct ChatTailPadding: ViewModifier { + func body(content: Content) -> some View { + content.padding(.horizontal, -msgTailWidth) + } +} +private let msgRectMaxRadius: Double = 18 +private let msgBubbleMaxRadius: Double = msgRectMaxRadius * 1.2 +private let msgTailWidth: Double = 9 +private let msgTailMinHeight: Double = msgTailWidth * 1.254 // ~56deg +private let msgTailMaxHeight: Double = msgTailWidth * 1.732 // 60deg + +struct ChatItemShape: Shape { + fileprivate enum Style { + case bubble(padding: HorizontalEdge, tailVisible: Bool) + case roundRect(radius: Double) + } + + fileprivate let roundness: Double + fileprivate let style: Style + + func path(in rect: CGRect) -> Path { + switch style { + case let .bubble(padding, tailVisible): + let w = rect.width + let h = rect.height + let rxMax = min(msgBubbleMaxRadius, w / 2) + let ryMax = min(msgBubbleMaxRadius, h / 2) + let rx = roundness * rxMax + let ry = roundness * ryMax + let tailHeight = min(msgTailMinHeight + roundness * (msgTailMaxHeight - msgTailMinHeight), h / 2) + var path = Path() + // top side + path.move(to: CGPoint(x: rx, y: 0)) + path.addLine(to: CGPoint(x: w - rx, y: 0)) + if roundness > 0 { + // top-right corner + path.addQuadCurve(to: CGPoint(x: w, y: ry), control: CGPoint(x: w, y: 0)) + } + if rect.height > 2 * ry { + // right side + path.addLine(to: CGPoint(x: w, y: h - ry)) + } + if roundness > 0 { + // bottom-right corner + path.addQuadCurve(to: CGPoint(x: w - rx, y: h), control: CGPoint(x: w, y: h)) + } + // bottom side + if tailVisible { + path.addLine(to: CGPoint(x: -msgTailWidth, y: h)) + if roundness > 0 { + // bottom-left tail + // distance of control point from touch point, calculated via ratios + let d = tailHeight - msgTailWidth * msgTailWidth / tailHeight + // tail control point + let tc = CGPoint(x: 0, y: h - tailHeight + d * sqrt(roundness)) + // bottom-left tail curve + path.addQuadCurve(to: CGPoint(x: 0, y: h - tailHeight), control: tc) + } else { + path.addLine(to: CGPoint(x: 0, y: h - tailHeight)) + } + if rect.height > ry + tailHeight { + // left side + path.addLine(to: CGPoint(x: 0, y: ry)) + } + } else { + path.addLine(to: CGPoint(x: rx, y: h)) + path.addQuadCurve(to: CGPoint(x: 0, y: h - ry), control: CGPoint(x: 0 , y: h)) + if rect.height > 2 * ry { + // left side + path.addLine(to: CGPoint(x: 0, y: ry)) + } + } + if roundness > 0 { + // top-left corner + path.addQuadCurve(to: CGPoint(x: rx, y: 0), control: CGPoint(x: 0, y: 0)) + } + path.closeSubpath() + return switch padding { + case .leading: path + case .trailing: path + .scale(x: -1, y: 1, anchor: .center) + .path(in: rect) + } + case let .roundRect(radius): + return Path(roundedRect: rect, cornerRadius: radius * roundness) + } + } + + var offset: Double? { + switch style { + case let .bubble(padding, isTailVisible): + if isTailVisible { + switch padding { + case .leading: -msgTailWidth + case .trailing: msgTailWidth + } + } else { 0 } + case .roundRect: 0 + } + } + +} diff --git a/apps/ios/Shared/Views/Helpers/InvertedForegroundStyle.swift b/apps/ios/Shared/Views/Helpers/InvertedForegroundStyle.swift new file mode 100644 index 0000000000..dca413dafe --- /dev/null +++ b/apps/ios/Shared/Views/Helpers/InvertedForegroundStyle.swift @@ -0,0 +1,21 @@ +// +// Test.swift +// SimpleX (iOS) +// +// Created by Levitating Pineapple on 31/08/2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import SwiftUI + +extension View { + @ViewBuilder + func invertedForegroundStyle(enabled: Bool = true) -> some View { + if enabled { + foregroundStyle(Material.ultraThin) + .environment(\.colorScheme, .dark) + .grayscale(1) + .contrast(-20) + } else { self } + } +} diff --git a/apps/ios/Shared/Views/Helpers/ProfileImage.swift b/apps/ios/Shared/Views/Helpers/ProfileImage.swift index 248504c59b..3eedd56441 100644 --- a/apps/ios/Shared/Views/Helpers/ProfileImage.swift +++ b/apps/ios/Shared/Views/Helpers/ProfileImage.swift @@ -20,7 +20,7 @@ struct ProfileImage: View { @AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var radius = defaultProfileImageCorner var body: some View { - if let uiImage = UIImage(base64Encoded: imageStr) { + if let uiImage = imageFromBase64(imageStr) { clipProfileImage(Image(uiImage: uiImage), size: size, radius: radius, blurred: blurred) } else { let c = color.asAnotherColorFromSecondaryVariant(theme) diff --git a/apps/ios/Shared/Views/Helpers/ShareSheet.swift b/apps/ios/Shared/Views/Helpers/ShareSheet.swift index 936c6cb3ab..7b80dd1544 100644 --- a/apps/ios/Shared/Views/Helpers/ShareSheet.swift +++ b/apps/ios/Shared/Views/Helpers/ShareSheet.swift @@ -8,15 +8,47 @@ import SwiftUI -func showShareSheet(items: [Any], completed: (() -> Void)? = nil) { +func getTopViewController() -> UIViewController? { let keyWindowScene = UIApplication.shared.connectedScenes.first { $0.activationState == .foregroundActive } as? UIWindowScene if let keyWindow = keyWindowScene?.windows.filter(\.isKeyWindow).first, - let presentedViewController = keyWindow.rootViewController?.presentedViewController ?? keyWindow.rootViewController { + let rootViewController = keyWindow.rootViewController { + // Find the top-most presented view controller + var topController = rootViewController + while let presentedViewController = topController.presentedViewController { + topController = presentedViewController + } + return topController + } + return nil +} + +func showShareSheet(items: [Any], completed: (() -> Void)? = nil) { + if let topController = getTopViewController() { let activityViewController = UIActivityViewController(activityItems: items, applicationActivities: nil) if let completed = completed { - let handler: UIActivityViewController.CompletionWithItemsHandler = { _,_,_,_ in completed() } - activityViewController.completionWithItemsHandler = handler - } - presentedViewController.present(activityViewController, animated: true) + activityViewController.completionWithItemsHandler = { _, _, _, _ in + completed() + } + } + topController.present(activityViewController, animated: true) + } +} + +func showAlert( + title: String, + message: String? = nil, + buttonTitle: String, + buttonAction: @escaping () -> Void, + cancelButton: Bool +) -> Void { + if let topController = getTopViewController() { + let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) + alert.addAction(UIAlertAction(title: buttonTitle, style: .default) { _ in + buttonAction() + }) + if cancelButton { + alert.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: "alert button"), style: .cancel)) + } + topController.present(alert, animated: true) } } diff --git a/apps/ios/Shared/Views/Migration/MigrateFromDevice.swift b/apps/ios/Shared/Views/Migration/MigrateFromDevice.swift index 1303a1247f..73e5b97057 100644 --- a/apps/ios/Shared/Views/Migration/MigrateFromDevice.swift +++ b/apps/ios/Shared/Views/Migration/MigrateFromDevice.swift @@ -58,8 +58,6 @@ private enum MigrateFromDeviceViewAlert: Identifiable { struct MigrateFromDevice: View { @EnvironmentObject var m: ChatModel @EnvironmentObject var theme: AppTheme - @Environment(\.dismiss) var dismiss: DismissAction - @Binding var showSettings: Bool @Binding var showProgressOnSettings: Bool @State private var migrationState: MigrationFromState = .chatStopInProgress @State private var useKeychain = storeDBPassphraseGroupDefault.get() @@ -108,9 +106,6 @@ struct MigrateFromDevice: View { finishedView(chatDeletion) } } - .modifier(BackButton(label: "Back", disabled: $backDisabled) { - dismiss() - }) .onChange(of: migrationState) { state in backDisabled = switch migrationState { case .chatStopInProgress, .archiving, .linkShown, .finished: true @@ -182,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\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:") + archiveErrorsText(errs), dismissButton: .default(Text("Continue")) { Task { await uploadArchive(path: archivePath) } } @@ -601,7 +596,7 @@ struct MigrateFromDevice: View { } catch let error { fatalError("Error starting chat \(responseError(error))") } - showSettings = false + dismissAllSheets(animated: true) } } catch let error { alert = .error(title: "Error deleting database", error: responseError(error)) @@ -624,9 +619,7 @@ struct MigrateFromDevice: View { } // Hide settings anyway if chatDbStatus is not ok, probably passphrase needs to be entered if dismiss || m.chatDbStatus != .ok { - await MainActor.run { - showSettings = false - } + dismissAllSheets(animated: true) } } @@ -778,6 +771,6 @@ private class MigrationChatReceiver { struct MigrateFromDevice_Previews: PreviewProvider { static var previews: some View { - MigrateFromDevice(showSettings: Binding.constant(true), showProgressOnSettings: Binding.constant(false)) + MigrateFromDevice(showProgressOnSettings: Binding.constant(false)) } } diff --git a/apps/ios/Shared/Views/Migration/MigrateToDevice.swift b/apps/ios/Shared/Views/Migration/MigrateToDevice.swift index e2df68a0e4..fe0eec609b 100644 --- a/apps/ios/Shared/Views/Migration/MigrateToDevice.swift +++ b/apps/ios/Shared/Views/Migration/MigrateToDevice.swift @@ -571,7 +571,7 @@ struct MigrateToDevice: View { AlertManager.shared.showAlert( Alert( title: Text("Error migrating settings"), - message: Text ("Not all settings were migrated. Repeat migration if you need them.") + Text("\n\n") + Text(responseError(error))) + message: Text ("Some app settings were not migrated.") + Text("\n") + Text(responseError(error))) ) } hideView() diff --git a/apps/ios/Shared/Views/NewChat/AddContactLearnMore.swift b/apps/ios/Shared/Views/NewChat/AddContactLearnMore.swift index 6001dff790..3a64a955c5 100644 --- a/apps/ios/Shared/Views/NewChat/AddContactLearnMore.swift +++ b/apps/ios/Shared/Views/NewChat/AddContactLearnMore.swift @@ -28,7 +28,9 @@ struct AddContactLearnMore: View { Text("If you can't meet in person, show QR code in a video call, or share the link.") Text("Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends).") } + .frame(maxWidth: .infinity, alignment: .leading) .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) } .modifier(ThemedBackground(grouped: true)) } diff --git a/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift b/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift index 764c2b47b3..051b1158ec 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift @@ -14,9 +14,10 @@ enum ContactType: Int { } struct NewChatMenuButton: View { + @EnvironmentObject var chatModel: ChatModel @State private var showNewChatSheet = false @State private var alert: SomeAlert? = nil - @State private var globalAlert: SomeAlert? = nil + @State private var pendingConnection: PendingContactConnection? = nil var body: some View { Button { @@ -28,22 +29,14 @@ struct NewChatMenuButton: View { .frame(width: 24, height: 24) } .appSheet(isPresented: $showNewChatSheet) { - NewChatSheet(alert: $alert) + NewChatSheet(pendingConnection: $pendingConnection) .environment(\EnvironmentValues.refresh as! WritableKeyPath, nil) - .alert(item: $alert) { a in - return a.alert + .onDisappear { + alert = cleanupPendingConnection(chatModel: chatModel, contactConnection: pendingConnection) + pendingConnection = nil } } - // This is a workaround to show "Keep unused invitation" alert in both following cases: - // - on going back from NewChatView to NewChatSheet, - // - on dismissing NewChatMenuButton sheet while on NewChatView (skipping NewChatSheet) - .onChange(of: alert?.id) { a in - if !showNewChatSheet && alert != nil { - globalAlert = alert - alert = nil - } - } - .alert(item: $globalAlert) { a in + .alert(item: $alert) { a in return a.alert } } @@ -60,7 +53,8 @@ struct NewChatSheet: View { @State private var searchText = "" @State private var searchShowingSimplexLink = false @State private var searchChatFilteredBySimplexLink: String? = nil - @Binding var alert: SomeAlert? + @State private var alert: SomeAlert? + @Binding var pendingConnection: PendingContactConnection? // Sheet height management @State private var isAddContactActive = false @@ -78,6 +72,9 @@ struct NewChatSheet: View { .navigationBarTitleDisplayMode(.large) .navigationBarHidden(searchMode) .modifier(ThemedBackground(grouped: true)) + .alert(item: $alert) { a in + return a.alert + } } if #available(iOS 16.0, *), oneHandUI { let sheetHeight: CGFloat = showArchive ? 575 : 500 @@ -112,7 +109,7 @@ struct NewChatSheet: View { if (searchText.isEmpty) { Section { NavigationLink(isActive: $isAddContactActive) { - NewChatView(selection: .invite, parentAlert: $alert) + NewChatView(selection: .invite, parentAlert: $alert, contactConnection: $pendingConnection) .navigationTitle("New chat") .modifier(ThemedBackground(grouped: true)) .navigationBarTitleDisplayMode(.large) @@ -122,7 +119,7 @@ struct NewChatSheet: View { } } NavigationLink(isActive: $isScanPasteLinkActive) { - NewChatView(selection: .connect, showQRCodeScanner: true, parentAlert: $alert) + NewChatView(selection: .connect, showQRCodeScanner: true, parentAlert: $alert, contactConnection: $pendingConnection) .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 c9670aa44a..f07ddf1420 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatView.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatView.swift @@ -45,18 +45,47 @@ enum NewChatOption: Identifiable { var id: Self { self } } +func cleanupPendingConnection(chatModel: ChatModel, contactConnection: PendingContactConnection?) -> SomeAlert? { + var alert: SomeAlert? = nil + + if !(chatModel.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: [] + )) + } + } + ), + id: "keepUnusedInvitation" + ) + } + + chatModel.showingInvitation = nil + + return alert +} + struct NewChatView: View { @EnvironmentObject var m: ChatModel @EnvironmentObject var theme: AppTheme @State var selection: NewChatOption @State var showQRCodeScanner = false @State private var invitationUsed: Bool = false - @State private var contactConnection: PendingContactConnection? = nil @State private var connReqInvitation: String = "" @State private var creatingConnReq = false + @State var choosingProfile = false @State private var pastedLink: String = "" @State private var alert: NewChatViewAlert? @Binding var parentAlert: SomeAlert? + @Binding var contactConnection: PendingContactConnection? var body: some View { VStack(alignment: .leading) { @@ -122,26 +151,10 @@ struct NewChatView: View { } } .onDisappear { - if !(m.showingInvitation?.connChatUsed ?? true), - let conn = contactConnection { - parentAlert = 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: [] - )) - } - } - ), - id: "keepUnusedInvitation" - ) + if !choosingProfile { + parentAlert = cleanupPendingConnection(chatModel: m, contactConnection: contactConnection) + contactConnection = nil } - m.showingInvitation = nil } .alert(item: $alert) { a in switch(a) { @@ -159,7 +172,8 @@ struct NewChatView: View { InviteView( invitationUsed: $invitationUsed, contactConnection: $contactConnection, - connReqInvitation: connReqInvitation + connReqInvitation: $connReqInvitation, + choosingProfile: $choosingProfile ) } else if creatingConnReq { creatingLinkProgressView() @@ -210,13 +224,24 @@ struct NewChatView: View { } } +private func incognitoProfileImage() -> some View { + Image(systemName: "theatermasks.fill") + .resizable() + .scaledToFit() + .frame(width: 30) + .foregroundColor(.indigo) +} + private struct InviteView: View { @EnvironmentObject var chatModel: ChatModel @EnvironmentObject var theme: AppTheme @Binding var invitationUsed: Bool @Binding var contactConnection: PendingContactConnection? - var connReqInvitation: String + @Binding var connReqInvitation: String + @Binding var choosingProfile: Bool + @AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false + @State private var showSettings: Bool = false var body: some View { List { @@ -226,28 +251,40 @@ private struct InviteView: View { .listRowInsets(EdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 10)) qrCodeView() - - Section { - IncognitoToggle(incognitoEnabled: $incognitoDefault) - } footer: { - sharedProfileInfo(incognitoDefault) - .foregroundColor(theme.colors.secondary) + if let selectedProfile = chatModel.currentUser { + Section { + NavigationLink { + ActiveProfilePicker( + contactConnection: $contactConnection, + connReqInvitation: $connReqInvitation, + incognitoEnabled: $incognitoDefault, + choosingProfile: $choosingProfile, + selectedProfile: selectedProfile + ) + } label: { + HStack { + if incognitoDefault { + incognitoProfileImage() + Text("Incognito") + } else { + ProfileImage(imageStr: chatModel.currentUser?.image, size: 30) + Text(chatModel.currentUser?.chatViewName ?? "") + } + } + } + } header: { + Text("Share profile").foregroundColor(theme.colors.secondary) + } footer: { + if incognitoDefault { + Text("A new random profile will be shared.") + } + } } } .onChange(of: incognitoDefault) { incognito in - Task { - do { - if let contactConn = contactConnection, - let conn = try await apiSetConnectionIncognito(connId: contactConn.pccConnId, incognito: incognito) { - await MainActor.run { - contactConnection = conn - chatModel.updateContactConnection(conn) - } - } - } catch { - logger.error("apiSetConnectionIncognito error: \(responseError(error))") - } - } + setInvitationUsed() + } + .onChange(of: chatModel.currentUser) { u in setInvitationUsed() } } @@ -270,6 +307,7 @@ private struct InviteView: View { private func qrCodeView() -> some View { Section(header: Text("Or show this code").foregroundColor(theme.colors.secondary)) { SimpleXLinkQRCode(uri: connReqInvitation, onShare: setInvitationUsed) + .id("simplex-qrcode-view-for-\(connReqInvitation)") .padding() .background( RoundedRectangle(cornerRadius: 12, style: .continuous) @@ -289,6 +327,257 @@ private struct InviteView: View { } } +private enum ProfileSwitchStatus { + case switchingUser + case switchingIncognito + case idle +} + +private struct ActiveProfilePicker: View { + @Environment(\.dismiss) var dismiss + @EnvironmentObject var chatModel: ChatModel + @EnvironmentObject var theme: AppTheme + @Binding var contactConnection: PendingContactConnection? + @Binding var connReqInvitation: String + @Binding var incognitoEnabled: Bool + @Binding var choosingProfile: Bool + @State private var alert: SomeAlert? + @State private var profileSwitchStatus: ProfileSwitchStatus = .idle + @State private var switchingProfileByTimeout = false + @State private var lastSwitchingProfileByTimeoutCall: Double? + @State private var profiles: [User] = [] + @State private var searchTextOrPassword = "" + @State private var showIncognitoSheet = false + @State private var incognitoFirst: Bool = false + @State var selectedProfile: User + var trimmedSearchTextOrPassword: String { searchTextOrPassword.trimmingCharacters(in: .whitespaces)} + + var body: some View { + viewBody() + .navigationTitle("Select chat profile") + .searchable(text: $searchTextOrPassword, placement: .navigationBarDrawer(displayMode: .always)) + .autocorrectionDisabled(true) + .navigationBarTitleDisplayMode(.large) + .onAppear { + profiles = chatModel.users + .map { $0.user } + .sorted { u, _ in u.activeUser } + } + .onChange(of: incognitoEnabled) { incognito in + if profileSwitchStatus != .switchingIncognito { + return + } + + Task { + do { + if let contactConn = contactConnection, + let conn = try await apiSetConnectionIncognito(connId: contactConn.pccConnId, incognito: incognito) { + await MainActor.run { + contactConnection = conn + chatModel.updateContactConnection(conn) + profileSwitchStatus = .idle + dismiss() + } + } + } catch { + profileSwitchStatus = .idle + incognitoEnabled = !incognito + logger.error("apiSetConnectionIncognito error: \(responseError(error))") + let err = getErrorAlert(error, "Error changing to incognito!") + + alert = SomeAlert( + alert: Alert( + title: Text(err.title), + message: Text(err.message ?? "Error: \(responseError(error))") + ), + id: "setConnectionIncognitoError" + ) + } + } + } + .onChange(of: profileSwitchStatus) { sp in + if sp != .idle { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + switchingProfileByTimeout = profileSwitchStatus != .idle + } + } else { + switchingProfileByTimeout = false + } + } + .onChange(of: selectedProfile) { profile in + if (profileSwitchStatus != .switchingUser) { + return + } + Task { + do { + if let contactConn = contactConnection, + let conn = try await apiChangeConnectionUser(connId: contactConn.pccConnId, userId: profile.userId) { + + await MainActor.run { + contactConnection = conn + connReqInvitation = conn.connReqInv ?? "" + incognitoEnabled = false + chatModel.updateContactConnection(conn) + } + do { + try await changeActiveUserAsync_(profile.userId, viewPwd: profile.hidden ? trimmedSearchTextOrPassword : nil ) + await MainActor.run { + profileSwitchStatus = .idle + dismiss() + } + } catch { + await MainActor.run { + profileSwitchStatus = .idle + alert = SomeAlert( + alert: Alert( + title: Text("Error switching profile"), + message: Text("Your connection was moved to \(profile.chatViewName) but an unexpected error occurred while redirecting you to the profile.") + ), + id: "switchingProfileError" + ) + } + } + } + } catch { + await MainActor.run { + profileSwitchStatus = .idle + if let currentUser = chatModel.currentUser { + selectedProfile = currentUser + } + let err = getErrorAlert(error, "Error changing connection profile") + alert = SomeAlert( + alert: Alert( + title: Text(err.title), + message: Text(err.message ?? "Error: \(responseError(error))") + ), + id: "changeConnectionUserError" + ) + } + } + } + } + .alert(item: $alert) { a in + a.alert + } + .onAppear { + incognitoFirst = incognitoEnabled + choosingProfile = true + } + .onDisappear { + choosingProfile = false + } + .sheet(isPresented: $showIncognitoSheet) { + IncognitoHelp() + } + } + + + @ViewBuilder private func viewBody() -> some View { + profilePicker() + .allowsHitTesting(!switchingProfileByTimeout) + .modifier(ThemedBackground(grouped: true)) + .overlay { + if switchingProfileByTimeout { + ProgressView() + .scaleEffect(2) + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + } + } + + private func filteredProfiles() -> [User] { + let s = trimmedSearchTextOrPassword + let lower = s.localizedLowercase + + return profiles.filter { u in + if (u.activeUser || !u.hidden) && (s == "" || u.chatViewName.localizedLowercase.contains(lower)) { + return true + } + return correctPassword(u, s) + } + } + + @ViewBuilder private func profilerPickerUserOption(_ user: User) -> some View { + Button { + if selectedProfile == user && incognitoEnabled { + incognitoEnabled = false + profileSwitchStatus = .switchingIncognito + } else if selectedProfile != user { + selectedProfile = user + profileSwitchStatus = .switchingUser + } + } label: { + HStack { + ProfileImage(imageStr: user.image, size: 30) + .padding(.trailing, 2) + Text(user.chatViewName) + .foregroundColor(theme.colors.onBackground) + .lineLimit(1) + Spacer() + if selectedProfile == user, !incognitoEnabled { + Image(systemName: "checkmark") + .resizable().scaledToFit().frame(width: 16) + .foregroundColor(theme.colors.primary) + } + } + } + } + + @ViewBuilder private func profilePicker() -> some View { + let incognitoOption = Button { + if !incognitoEnabled { + incognitoEnabled = true + profileSwitchStatus = .switchingIncognito + } + } label : { + HStack { + incognitoProfileImage() + Text("Incognito") + .foregroundColor(theme.colors.onBackground) + Image(systemName: "info.circle") + .foregroundColor(theme.colors.primary) + .font(.system(size: 14)) + .onTapGesture { + showIncognitoSheet = true + } + Spacer() + if incognitoEnabled { + Image(systemName: "checkmark") + .resizable().scaledToFit().frame(width: 16) + .foregroundColor(theme.colors.primary) + } + } + } + + List { + let filteredProfiles = filteredProfiles() + let activeProfile = filteredProfiles.first { u in u.activeUser } + + if let selectedProfile = activeProfile { + let otherProfiles = filteredProfiles.filter { u in u.userId != activeProfile?.userId } + + if incognitoFirst { + incognitoOption + profilerPickerUserOption(selectedProfile) + } else { + profilerPickerUserOption(selectedProfile) + incognitoOption + } + + ForEach(otherProfiles) { p in + profilerPickerUserOption(p) + } + } else { + incognitoOption + ForEach(filteredProfiles) { p in + profilerPickerUserOption(p) + } + } + } + .opacity(switchingProfileByTimeout ? 0.4 : 1) + } +} + private struct ConnectView: View { @Environment(\.dismiss) var dismiss: DismissAction @EnvironmentObject var theme: AppTheme @@ -976,10 +1265,12 @@ func connReqSentAlert(_ type: ConnReqType) -> Alert { struct NewChatView_Previews: PreviewProvider { static var previews: some View { @State var parentAlert: SomeAlert? + @State var contactConnection: PendingContactConnection? = nil NewChatView( selection: .invite, - parentAlert: $parentAlert + parentAlert: $parentAlert, + contactConnection: $contactConnection ) } } diff --git a/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift b/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift index be063334d3..b1f68c09f4 100644 --- a/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift +++ b/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift @@ -59,13 +59,6 @@ struct ConnectDesktopView: View { var body: some View { if viaSettings { viewBody - .modifier(BackButton(label: "Back", disabled: Binding.constant(false)) { - if m.activeRemoteCtrl { - alert = .disconnectDesktop(action: .back) - } else { - dismiss() - } - }) } else { NavigationView { viewBody diff --git a/apps/ios/Shared/Views/TerminalView.swift b/apps/ios/Shared/Views/TerminalView.swift index d209ced128..36c05ed43d 100644 --- a/apps/ios/Shared/Views/TerminalView.swift +++ b/apps/ios/Shared/Views/TerminalView.swift @@ -160,7 +160,7 @@ struct TerminalView_Previews: PreviewProvider { let chatModel = ChatModel() chatModel.terminalItems = [ .resp(.now, ChatResponse.response(type: "contactSubscribed", json: "{}")), - .resp(.now, ChatResponse.response(type: "newChatItem", json: "{}")) + .resp(.now, ChatResponse.response(type: "newChatItems", json: "{}")) ] return NavigationView { TerminalView() diff --git a/apps/ios/Shared/Views/UserSettings/AppearanceSettings.swift b/apps/ios/Shared/Views/UserSettings/AppearanceSettings.swift index 73a789f108..70c33329b1 100644 --- a/apps/ios/Shared/Views/UserSettings/AppearanceSettings.swift +++ b/apps/ios/Shared/Views/UserSettings/AppearanceSettings.swift @@ -33,6 +33,8 @@ struct AppearanceSettings: View { }() @State private var darkModeTheme: String = UserDefaults.standard.string(forKey: DEFAULT_SYSTEM_DARK_THEME) ?? DefaultTheme.DARK.themeName @AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var profileImageCornerRadius = defaultProfileImageCorner + @AppStorage(DEFAULT_CHAT_ITEM_ROUNDNESS) private var chatItemRoundness = defaultChatItemRoundness + @AppStorage(DEFAULT_CHAT_ITEM_TAIL) private var chatItemTail = true @AppStorage(GROUP_DEFAULT_ONE_HAND_UI, store: groupDefaults) private var oneHandUI = true @AppStorage(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial @@ -179,6 +181,14 @@ struct AppearanceSettings: View { } } + Section(header: Text("Message shape").foregroundColor(theme.colors.secondary)) { + HStack { + Text("Corner") + Slider(value: $chatItemRoundness, in: 0...1, step: 0.05) + } + Toggle("Tail", isOn: $chatItemTail) + } + Section(header: Text("Profile images").foregroundColor(theme.colors.secondary)) { HStack(spacing: 16) { if let img = m.currentUser?.image, img != "" { @@ -358,20 +368,21 @@ struct ChatThemePreview: View { let bob = ChatItem.getSample(2, CIDirection.directSnd, Date.now, NSLocalizedString("Good morning!", comment: "message preview"), quotedItem: CIQuote.getSample(alice.id, alice.meta.itemTs, alice.content.text, chatDir: alice.chatDir)) HStack { ChatItemView(chat: Chat.sampleData, chatItem: alice, revealed: Binding.constant(false)) - .modifier(ChatItemClipped()) + .modifier(ChatItemClipped(alice, tailVisible: true)) Spacer() } HStack { Spacer() ChatItemView(chat: Chat.sampleData, chatItem: bob, revealed: Binding.constant(false)) - .modifier(ChatItemClipped()) + .modifier(ChatItemClipped(bob, tailVisible: true)) .frame(alignment: .trailing) } } else { Rectangle().fill(.clear) } } - .padding(10) + .padding(.vertical, 10) + .padding(.horizontal, 16) .frame(maxWidth: .infinity) if let wallpaperType, let wallpaperImage = wallpaperType.image, let backgroundColor, let tintColor { diff --git a/apps/ios/Shared/Views/UserSettings/IncognitoHelp.swift b/apps/ios/Shared/Views/UserSettings/IncognitoHelp.swift index a0250afddf..d9862aaac8 100644 --- a/apps/ios/Shared/Views/UserSettings/IncognitoHelp.swift +++ b/apps/ios/Shared/Views/UserSettings/IncognitoHelp.swift @@ -26,6 +26,7 @@ struct IncognitoHelp: View { Text("Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).") } .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) } .modifier(ThemedBackground()) } diff --git a/apps/ios/Shared/Views/UserSettings/PreferencesView.swift b/apps/ios/Shared/Views/UserSettings/PreferencesView.swift index 0c10da2103..bd8171623a 100644 --- a/apps/ios/Shared/Views/UserSettings/PreferencesView.swift +++ b/apps/ios/Shared/Views/UserSettings/PreferencesView.swift @@ -32,6 +32,17 @@ struct PreferencesView: View { .disabled(currentPreferences == preferences) } } + .onDisappear { + if currentPreferences != preferences { + showAlert( + title: NSLocalizedString("Your chat preferences", comment: "alert title"), + message: NSLocalizedString("Chat preferences were changed.", comment: "alert message"), + buttonTitle: NSLocalizedString("Save", comment: "alert button"), + buttonAction: savePreferences, + cancelButton: true + ) + } + } } private func featureSection(_ feature: ChatFeature, _ allowFeature: Binding) -> some View { diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift index a4908f628f..463ac4ae07 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift @@ -47,6 +47,8 @@ let DEFAULT_ACCENT_COLOR_GREEN = "accentColorGreen" // deprecated, only used for let DEFAULT_ACCENT_COLOR_BLUE = "accentColorBlue" // deprecated, only used for migration let DEFAULT_USER_INTERFACE_STYLE = "userInterfaceStyle" // deprecated, only used for migration 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_TOOLBAR_MATERIAL = "toolbarMaterial" let DEFAULT_CONNECT_VIA_LINK_TAB = "connectViaLinkTab" @@ -75,6 +77,8 @@ let DEFAULT_THEME_OVERRIDES = "themeOverrides" let ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN = "androidCallOnLockScreen" +let defaultChatItemRoundness: Double = 0.75 + let appDefaults: [String: Any] = [ DEFAULT_SHOW_LA_NOTICE: false, DEFAULT_LA_NOTICE_SHOWN: false, @@ -98,6 +102,8 @@ let appDefaults: [String: Any] = [ DEFAULT_DEVELOPER_TOOLS: false, DEFAULT_ENCRYPTION_STARTED: false, DEFAULT_PROFILE_IMAGE_CORNER_RADIUS: defaultProfileImageCorner, + DEFAULT_CHAT_ITEM_ROUNDNESS: defaultChatItemRoundness, + DEFAULT_CHAT_ITEM_TAIL: true, DEFAULT_ONE_HAND_UI_CARD_SHOWN: false, DEFAULT_TOOLBAR_MATERIAL: ToolbarMaterial.defaultMaterial, DEFAULT_CONNECT_VIA_LINK_TAB: ConnectViaLinkTab.scan.rawValue, @@ -256,7 +262,9 @@ struct SettingsView: View { var body: some View { ZStack { - settingsView() + NavigationView { + settingsView() + } if showProgress { progressView() } @@ -268,63 +276,7 @@ struct SettingsView: View { @ViewBuilder func settingsView() -> some View { let user = chatModel.currentUser - NavigationView { List { - Section(header: Text("You").foregroundColor(theme.colors.secondary)) { - if let user = user { - NavigationLink { - UserProfile() - .navigationTitle("Your current profile") - .modifier(ThemedBackground()) - } label: { - ProfilePreview(profileOf: user) - .padding(.leading, -8) - } - } - - NavigationLink { - UserProfilesView(showSettings: $showSettings) - } label: { - settingsRow("person.crop.rectangle.stack", color: theme.colors.secondary) { Text("Your chat profiles") } - } - - - if let user = user { - NavigationLink { - UserAddressView(shareViaProfile: user.addressShared) - .navigationTitle("SimpleX address") - .modifier(ThemedBackground(grouped: true)) - .navigationBarTitleDisplayMode(.large) - } label: { - settingsRow("qrcode", color: theme.colors.secondary) { Text("Your SimpleX address") } - } - - NavigationLink { - PreferencesView(profile: user.profile, preferences: user.fullPreferences, currentPreferences: user.fullPreferences) - .navigationTitle("Your preferences") - .modifier(ThemedBackground(grouped: true)) - } label: { - settingsRow("switch.2", color: theme.colors.secondary) { Text("Chat preferences") } - } - } - - NavigationLink { - ConnectDesktopView(viaSettings: true) - } label: { - settingsRow("desktopcomputer", color: theme.colors.secondary) { Text("Use from desktop") } - } - - NavigationLink { - MigrateFromDevice(showSettings: $showSettings, showProgressOnSettings: $showProgress) - .navigationTitle("Migrate device") - .modifier(ThemedBackground(grouped: true)) - .navigationBarTitleDisplayMode(.large) - } label: { - settingsRow("tray.and.arrow.up", color: theme.colors.secondary) { Text("Migrate to another device") } - } - } - .disabled(chatModel.chatRunning != true) - Section(header: Text("Settings").foregroundColor(theme.colors.secondary)) { NavigationLink { NotificationsView() @@ -375,10 +327,20 @@ struct SettingsView: View { } .disabled(chatModel.chatRunning != true) } - - chatDatabaseRow() } + Section(header: Text("Chat database").foregroundColor(theme.colors.secondary)) { + chatDatabaseRow() + NavigationLink { + MigrateFromDevice(showProgressOnSettings: $showProgress) + .navigationTitle("Migrate device") + .modifier(ThemedBackground(grouped: true)) + .navigationBarTitleDisplayMode(.large) + } label: { + settingsRow("tray.and.arrow.up", color: theme.colors.secondary) { Text("Migrate to another device") } + } + } + Section(header: Text("Help").foregroundColor(theme.colors.secondary)) { if let user = user { NavigationLink { @@ -456,11 +418,10 @@ struct SettingsView: View { } .navigationTitle("Your settings") .modifier(ThemedBackground(grouped: true)) - } - .onDisappear { - chatModel.showingTerminal = false - chatModel.terminalItems = [] - } + .onDisappear { + chatModel.showingTerminal = false + chatModel.terminalItems = [] + } } private func chatDatabaseRow() -> some View { @@ -543,17 +504,18 @@ struct ProfilePreview: View { HStack { ProfileImage(imageStr: profileOf.image, size: 44, color: color) .padding(.trailing, 6) - .padding(.vertical, 6) - VStack(alignment: .leading) { - Text(profileOf.displayName) - .fontWeight(.bold) - .font(.title2) - if profileOf.fullName != "" && profileOf.fullName != profileOf.displayName { - Text(profileOf.fullName) - } - } + profileName().lineLimit(1) } } + + private func profileName() -> Text { + var t = Text(profileOf.displayName).fontWeight(.semibold).font(.title2) + if profileOf.fullName != "" && profileOf.fullName != profileOf.displayName { + t = t + Text(" (" + profileOf.fullName + ")") +// .font(.callout) + } + return t + } } struct SettingsView_Previews: PreviewProvider { diff --git a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift index fa95c51d36..2469dc59db 100644 --- a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift +++ b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift @@ -14,7 +14,6 @@ struct UserAddressView: View { @Environment(\.dismiss) var dismiss: DismissAction @EnvironmentObject private var chatModel: ChatModel @EnvironmentObject var theme: AppTheme - @State var viaCreateLinkView = false @State var shareViaProfile = false @State private var aas = AutoAcceptState() @State private var savedAAS = AutoAcceptState() @@ -22,7 +21,6 @@ struct UserAddressView: View { @State private var showMailView = false @State private var mailViewResult: Result? = nil @State private var alert: UserAddressAlert? - @State private var showSaveDialogue = false @State private var progressIndicator = false @FocusState private var keyboardVisible: Bool @@ -44,26 +42,19 @@ struct UserAddressView: View { var body: some View { ZStack { - if viaCreateLinkView { - userAddressScrollView() - } else { - userAddressScrollView() - .modifier(BackButton(disabled: Binding.constant(false)) { - if savedAAS == aas { - dismiss() - } else { - keyboardVisible = false - showSaveDialogue = true - } - }) - .confirmationDialog("Save settings?", isPresented: $showSaveDialogue) { - Button("Save auto-accept settings") { - saveAAS() - dismiss() - } - Button("Exit without saving") { dismiss() } + 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 + ) } - } + } + if progressIndicator { ZStack { if chatModel.userAddress != nil { @@ -342,7 +333,7 @@ struct UserAddressView: View { } } } - + private struct AutoAcceptState: Equatable { var enable = false var incognito = false @@ -447,6 +438,8 @@ struct UserAddressView_Previews: PreviewProvider { static var previews: some View { let chatModel = ChatModel() chatModel.userAddress = UserContactLink(connReqContact: "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D") + + return Group { UserAddressView() .environmentObject(chatModel) diff --git a/apps/ios/Shared/Views/UserSettings/UserProfile.swift b/apps/ios/Shared/Views/UserSettings/UserProfile.swift index 198fd495bd..6ca661ae48 100644 --- a/apps/ios/Shared/Views/UserSettings/UserProfile.swift +++ b/apps/ios/Shared/Views/UserSettings/UserProfile.swift @@ -11,8 +11,11 @@ import SimpleXChat struct UserProfile: View { @EnvironmentObject var chatModel: ChatModel + @EnvironmentObject var theme: AppTheme + @AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var radius = defaultProfileImageCorner @State private var profile = Profile(displayName: "", fullName: "") - @State private var editProfile = false + @State private var currentProfileHash: Int? + // Modals @State private var showChooseSource = false @State private var showImagePicker = false @State private var showTakePhoto = false @@ -21,85 +24,83 @@ struct UserProfile: View { @FocusState private var focusDisplayName var body: some View { - let user: User = chatModel.currentUser! - - return VStack(alignment: .leading) { - Text("Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile.") - .padding(.bottom) - - if editProfile { - ZStack(alignment: .center) { - ZStack(alignment: .topTrailing) { + List { + Group { + if profile.image != nil { + ZStack(alignment: .bottomTrailing) { + ZStack(alignment: .topTrailing) { + profileImageView(profile.image) + .onTapGesture { showChooseSource = true } + overlayButton("multiply", edge: .top) { profile.image = nil } + } + overlayButton("camera", edge: .bottom) { showChooseSource = true } + } + } else { + ZStack(alignment: .center) { profileImageView(profile.image) - if user.image != nil { - Button { - profile.image = nil - } label: { - Image(systemName: "multiply") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 12) - } + editImageButton { showChooseSource = true } + } + } + } + .frame(maxWidth: .infinity, alignment: .center) + .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) + .padding(.top) + .contentShape(Rectangle()) + + Section { + HStack { + TextField("Enter your name…", text: $profile.displayName) + .focused($focusDisplayName) + if !validDisplayName(profile.displayName) { + Button { + alert = .invalidNameError(validName: mkValidName(profile.displayName)) + } label: { + Image(systemName: "exclamationmark.circle").foregroundColor(.red) } } - - editImageButton { showChooseSource = true } } - .frame(maxWidth: .infinity, alignment: .center) - - VStack(alignment: .leading) { - ZStack(alignment: .leading) { - if !validNewProfileName(user) { - Button { - alert = .invalidNameError(validName: mkValidName(profile.displayName)) - } label: { - Image(systemName: "exclamationmark.circle").foregroundColor(.red) - } - } else { - Image(systemName: "exclamationmark.circle").foregroundColor(.clear) - } - profileNameTextEdit("Profile name", $profile.displayName) - .focused($focusDisplayName) - } - .padding(.bottom) - if showFullName(user) { - profileNameTextEdit("Full name (optional)", $profile.fullName) - .padding(.bottom) - } - HStack(spacing: 20) { - Button("Cancel") { editProfile = false } - Button("Save (and notify contacts)") { saveProfile() } - .disabled(!canSaveProfile(user)) - } + if let user = chatModel.currentUser, showFullName(user) { + TextField("Full name (optional)", text: $profile.fullName) } - .frame(maxWidth: .infinity, minHeight: 120, alignment: .leading) - } else { - ZStack(alignment: .center) { - profileImageView(user.image) - .onTapGesture { startEditingImage(user) } + } footer: { + Text("Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.") + } - if user.image == nil { - editImageButton { startEditingImage(user) } - } + Section { + Button(action: getCurrentProfile) { + Text("Reset") } - .frame(maxWidth: .infinity, alignment: .center) - - VStack(alignment: .leading) { - profileNameView("Profile name:", user.profile.displayName) - if showFullName(user) { - profileNameView("Full name:", user.profile.fullName) - } - Button("Edit") { - profile = fromLocalProfile(user.profile) - editProfile = true - focusDisplayName = true - } + .disabled(currentProfileHash == profile.hashValue) + Button(action: saveProfile) { + Text("Save (and notify contacts)") } - .frame(maxWidth: .infinity, minHeight: 120, alignment: .leading) + .disabled(!canSaveProfile) } } - .padding() - .frame(maxHeight: .infinity, alignment: .top) + // Lifecycle + .onAppear { + getCurrentProfile() + } + .onDisappear { + if canSaveProfile { + showAlert( + title: NSLocalizedString("Save your profile?", comment: "alert title"), + message: NSLocalizedString("Your profile was changed. If you save it, the updated profile will be sent to all your contacts.", comment: "alert message"), + buttonTitle: NSLocalizedString("Save (and notify contacts)", comment: "alert button"), + buttonAction: saveProfile, + cancelButton: true + ) + } + } + .onChange(of: chosenImage) { image in + if let image { + profile.image = resizeImageToStrSize(cropToSquare(image), maxDataSize: 12500) + } else { + profile.image = nil + } + } + // Modals .confirmationDialog("Profile image", isPresented: $showChooseSource, titleVisibility: .visible) { Button("Take picture") { showTakePhoto = true @@ -126,57 +127,49 @@ struct UserProfile: View { } } } - .onChange(of: chosenImage) { image in - if let image = image { - profile.image = resizeImageToStrSize(cropToSquare(image), maxDataSize: 12500) - } else { - profile.image = nil - } - } .alert(item: $alert) { a in userProfileAlert(a, $profile.displayName) } } - func profileNameTextEdit(_ label: LocalizedStringKey, _ name: Binding) -> some View { - TextField(label, text: name) - .padding(.leading, 32) - } - - func profileNameView(_ label: LocalizedStringKey, _ name: String) -> some View { - HStack { - Text(label) - Text(name).fontWeight(.bold) - } - .padding(.bottom) - } - - func startEditingImage(_ user: User) { - profile = fromLocalProfile(user.profile) - editProfile = true - showChooseSource = true - } - - private func validNewProfileName(_ user: User) -> Bool { - profile.displayName == user.profile.displayName || validDisplayName(profile.displayName.trimmingCharacters(in: .whitespaces)) + @ViewBuilder + private func overlayButton( + _ systemName: String, + edge: Edge.Set, + action: @escaping () -> Void + ) -> some View { + Image(systemName: systemName) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(height: 12) + .foregroundColor(theme.colors.primary) + .padding(6) + .frame(width: 36, height: 36, alignment: .center) + .background(radius >= 20 ? Color.clear : theme.colors.background.opacity(0.5)) + .clipShape(Circle()) + .contentShape(Circle()) + .padding([.trailing, edge], -12) + .onTapGesture(perform: action) } private func showFullName(_ user: User) -> Bool { user.profile.fullName != "" && user.profile.fullName != user.profile.displayName } - - private func canSaveProfile(_ user: User) -> Bool { - profile.displayName.trimmingCharacters(in: .whitespaces) != "" && validNewProfileName(user) + + private var canSaveProfile: Bool { + currentProfileHash != profile.hashValue && + profile.displayName.trimmingCharacters(in: .whitespaces) != "" && + validDisplayName(profile.displayName) } - func saveProfile() { + private func saveProfile() { + focusDisplayName = false Task { do { profile.displayName = profile.displayName.trimmingCharacters(in: .whitespaces) if let (newProfile, _) = try await apiUpdateProfile(profile: profile) { - DispatchQueue.main.async { + await MainActor.run { chatModel.updateCurrentUser(newProfile) - profile = newProfile + getCurrentProfile() } - editProfile = false } else { alert = .duplicateUserError } @@ -185,6 +178,13 @@ struct UserProfile: View { } } } + + private func getCurrentProfile() { + if let user = chatModel.currentUser { + profile = fromLocalProfile(user.profile) + currentProfileHash = profile.hashValue + } + } } func profileImageView(_ imageStr: String?) -> some View { @@ -201,19 +201,3 @@ func editImageButton(action: @escaping () -> Void) -> some View { .frame(width: 48) } } - -struct UserProfile_Previews: PreviewProvider { - static var previews: some View { - let chatModel1 = ChatModel() - chatModel1.currentUser = User.sampleData - let chatModel2 = ChatModel() - chatModel2.currentUser = User.sampleData - chatModel2.currentUser?.profile.image = "data:image/jpg;base64,/9j/4AAQSkZJRgABAQAASABIAAD/4QBMRXhpZgAATU0AKgAAAAgAAgESAAMAAAABAAEAAIdpAAQAAAABAAAAJgAAAAAAAqACAAQAAAABAAAAgKADAAQAAAABAAAAgAAAAAD/7QA4UGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAAAA4QklNBCUAAAAAABDUHYzZjwCyBOmACZjs+EJ+/+ICNElDQ19QUk9GSUxFAAEBAAACJGFwcGwEAAAAbW50clJHQiBYWVogB+EABwAHAA0AFgAgYWNzcEFQUEwAAAAAQVBQTAAAAAAAAAAAAAAAAAAAAAAAAPbWAAEAAAAA0y1hcHBsyhqVgiV/EE04mRPV0eoVggAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKZGVzYwAAAPwAAABlY3BydAAAAWQAAAAjd3RwdAAAAYgAAAAUclhZWgAAAZwAAAAUZ1hZWgAAAbAAAAAUYlhZWgAAAcQAAAAUclRSQwAAAdgAAAAgY2hhZAAAAfgAAAAsYlRSQwAAAdgAAAAgZ1RSQwAAAdgAAAAgZGVzYwAAAAAAAAALRGlzcGxheSBQMwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB0ZXh0AAAAAENvcHlyaWdodCBBcHBsZSBJbmMuLCAyMDE3AABYWVogAAAAAAAA81EAAQAAAAEWzFhZWiAAAAAAAACD3wAAPb////+7WFlaIAAAAAAAAEq/AACxNwAACrlYWVogAAAAAAAAKDgAABELAADIuXBhcmEAAAAAAAMAAAACZmYAAPKnAAANWQAAE9AAAApbc2YzMgAAAAAAAQxCAAAF3v//8yYAAAeTAAD9kP//+6L///2jAAAD3AAAwG7/wAARCACAAIADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9sAQwABAQEBAQECAQECAwICAgMEAwMDAwQGBAQEBAQGBwYGBgYGBgcHBwcHBwcHCAgICAgICQkJCQkLCwsLCwsLCwsL/9sAQwECAgIDAwMFAwMFCwgGCAsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsL/90ABAAI/9oADAMBAAIRAxEAPwD4N1TV59SxpunRtBb/APPP/lo+eMsf4R+uKyxNa6Y32a3UTzjoi8Ip9/8AOfYV0tx4d1a8VlsojaWo6uThj+Pb6Cs2CCGyP2LQ4xPIMBpGIVVz7ngV+Ap31P2C1iSDQbnWXRtVYyMT8kSDkZ9B29zXXReD7ZVOkX0QlLgg2ycjBH8ZHXPoOK9O8L6LpljZidWMjyqMzAdc/wB3PJ+p4qjrPiuxs1a38LwLJIn35ScoP94jlm9hxW8ZKJm1fY/Gv4yeA/E37L3xf07xz4GuH0260+7i1bRLpDkwzQOHVfQ+WwAI7r1zmv7fv2Nv2nfCv7YH7PHh346+FwkD6nEYtRs1OTZ6jBhbiA98K/zJnrGynvX8u3x3+G6fFvwXcadcOZNTQebZyN1EgH3QB91W6H657VD/AMEYP2qdQ/Zb/aRuPgN8RpjZeFviJcJabJztWy1tPkgkOeFE3+ok9zGTwtfY5Nj1Vjyt6nzuZ4XlfMj+zamH5TupVYnhhgjsaRyMYNe8eEMC7jxxU+1SMYFQFyaevPWgRqaeuSVFb0SDgAZI/SsLS9w4kxux1HTNdTEAMDvQJst20UitvA4rotMh8ycbuAv6k1Rs3UgcHjrXc6Xb2iTKVIJPQEcZ96qKMW7nWabpNmzRyEE9wOlegtplzFCLiMbEcfKw5/XP51l6ZPK6b2SJsdd64A/Kr0t5fyRsqsPLU5baNo49P0q2I//Q8iuPD17eeTpVy32u2ufls5lAC5P8MmOA2O/Q/XIrHl+GWn+CGN7qyC9ugxkSID92nvz1+pwK/TKb9j34t3Pw/PjXXrpdR165L3F7pkiDz5RISzHzFIUzliXKBQCTgMGwD8P6zompRzR2V2xuLWV9sE7ggo4yPLlBxhgRgE8k8cHivyPPMl9g3iMMrw6r+X/gH6PlmZ+1tRrP3uj7/wDBPnjXdR1rXWDao5jtm4S3h43gf3jwSPyH1rW0Xw9f6uyw2MYSNAAT/Ag/qa9ii+GTWEv2nV8nfztH3m/+t/nirMsVtMPscGIYYuCqjj8fWvmo+9qz227aI5O38NeH/DeJIGE079ZW9fQf/W/Ovyx/ba+C1x/aR+K/h6FoLa5dUvDH8rRzj7kgI+7ux253DPev1yuINKtF3XriOMDlm+83+6O1eNePZoPH2h3ngWC032N7E0UhI7HuPcdQfWvQweJdKakjkxFFTjZn6+f8Eu/2yE/a+/Zss9R8TXCyeMvCpTSfECZ+eSZF/dXWPS5jG4n/AJ6Bx2r9JGbd0r+GX9jD476z/wAE5v20IL3xPM7eGdUZdK8QBeUewmYGO6A7tbviT127171/cfaXdve28d1aSJNFKqukiHcjqwyGUjggggg9xX6Dhq6q01JM+NxVF05tdCyRQCOvakY4GRTFYd66DmN2xk2sK6eE5+YVxlo5EwB4rrLZiTyePWgmSOmsAThCcZPFdxZ5KruJyprgrWQ5G3tXS21+FABzVrYyZ6ZZTTSqCR8vQ4rUudWgW1e3QMrBScj1/D+tcpp1+UXaOn09fWtKP7OAzNjK+tNiP//R/oYjkSW9NgqsWVA7HHyrk4AJ9Tzx6CvjL9qz4M+FrbRrn4q2s0Fjcs6R3ttKdsd+ZCFBUf8APx0xj/WAYOCA1fVF58Y/hbb/AAwPxlXWIH8OCHzhdKc57bAv3vM3fLsxu3cYzX58eGdH8f8A7b/xIHi/xOs2k+DNGkK28AOCgPVQejXMg++/IiU7RyefmI+Z79+qPl++0JpR/wATG7Z9M4WOQfeVv7srdT/snp+NeWa9bfZXez8KxCZQcGVhiJT/AOzH6fnX7K/Fn9mfwzf6N9r+GmnwWV3DF5UlmBiC8iAxtbPAkx0c/e6N/eH5s+IvDcuj2jWcUTJYwsYXDrtktHXgxuvBxngE9Oh9/is6yVUr4nDL3Oq7enl+R9Plmac9qNZ+90ff/gnybLoheT7XrM3nMo5JH8h2HtXJa9/aGoMYbAC0gTqwH7x1H8hXsHiWGDRUboqr/Eeck+nrXj9/d3twWmlzbQHnn77e/tXzaqXXuntuNtz4z/ay+Eul+NPAf9u+H4TLq2kqzEAfNLAeXU/T7w/Ed6/XL/giD+2n/wALr+Ck37Nnjq78zxV8PYkW0Z2+a60VjthbJ5LWzfuW/wBjyz3NfCGuJLLm30tSsT8OT/U1+b1v4w8VfsE/tXeHf2kfhqjz2Vvcl5rdDiO4tZflu7Q+zoSUz0baeq19RkWMUZexk/Q8LNMLzx51uf3yIxPXvTQuTkVw3wz+IfhH4seBNG+JngS7W+0XX7OG/sp1P34ZlDLn0Izhh2YEGu+LAHFfXo+XJ4P9cp6YNdbCWHFcerFSCK6OGcMBk0wOmtZMVswurDNcnHKB7VqxXbDGKaZEoncRXpt4iy8fWlN44XdM5+bGPauWbUAI9p5NeH/E39oTwF8OAdO1W6+06kfuWVuQ0vtvOcIPdiPalOrGC5pOyHToym7RV2f/0nXmiaPrF/ceJvC1hrUnhC11EyFGZsIN2Mtg+QLjy+A5GQcZI6V/QP8ABrWvhd4i+GmnXXwZeI6DAnkxRxgq0LL95JFb5hJnO7dyTz3qt4f8EeCPC3g5Pht4csYItKt4fKNngMpjfOd4PJLckk8k18FeKvBXj79kHxu/xW+ECte+F711XUtNdiVC54VvQj/lnL2+63FfNNqWh7rVtT9JdItdaitpV8QSxyy+a5VowVURE/KDnuB1PQ9a/OD4yfEbwv8AEP4rx6F8JNIfXb4QyQXMlqAwvmQgEBThSkQBUysQpyFBOBjE+NH7WWu/HtrH4QfACxvYpNZHl3bSr5M7kjLQqc/JGo5ml/u8DrX2X+z38A9C+B3hzyQUvNbvVX7dehcA7ekUQ/hiT+Fe/U81m1bVj1Px/wDiX4FXQ4b7WNItJXitXZLq3nU+fpzjqpQ87PQ88eowa+JdanuvP+03JzG3Kk87voP8a/pi+NPwStfiAo8V+GDHaeI7aPYsjj91dxj/AJYzjuOyv1X6V+Mfxk+By6eL7xPodhLE9kzDUNJYfvbSXqWUd4z147cjivjc3ybkviMMtOq7eaPo8tzXmtRrvXo/8z4aaC/1a3drrbDbr6nCgepPc+36V4T8Z/A/h7xz4KvPB8uGmcb4LhhxHKv3WUeh6HPY17TrMuo3dysUA3p0VUGEArCudFt7aH7bqjguOQP6V89SquLUk9T26lNNWZ7L/wAEJv2vNQ8L6xq/7BPxZma3ureafUPDHnHvy93Zg/X9/EO+XA7Cv6fFwRnNfwWftIWHi/wL4u0T9pX4Vu2ma74buobpJY+GEkDBo5CO4B+Vx3U4PFf2VfshftPeFf2tv2e/Dvx18LbYhq0G29tQcm0vovluID/uPkr6oVPev0TLsWq9FT69T43MMN7KpdbM+q1kA+WtuF8qCa5H7SD0qvrnjbw34L0KTxD4qvobCyhBLzTuFUY7DPU+wya7nNJXZwxu3ZHoqyqq5JxXnPxL+Nvw3+EemjUPHmqxWIbPlxcvNIR2WNcsfrjFflz8cf8AgpDJMZ/DvwKgwOVOq3S/rFGf0LV8MaZp/jf4j603ibxTdT3U053PdXRLu+eflB7fkK8PFZ5TheNHV/h/wT2cLlFSfvVNF+J+hnxI/ba8cfEa5fQfhnG+h6e5KCY/NeTD6jIjH0yfcV514W8HX2plrjUiWLEtIWbcSSOS7dST/k1x2g2PhrwdZhpyFbHzEnLk+5/oK6eDxRq2soYdPH2S0xjjh2H9K+erY+pVlzTdz3aWEhSjaCsf/9P+gafwFajxovjGKeVJSqrJEPuOVUoD7ZBGR32ivgn9pz9pHUfGOvP+zb8BIDrGr6kZLO/nhwUXH34UY/LwP9bJ91BxndxXyp41/ab/AGivht4c1D9mf+0La7vrOY6f/asUpe4WP7vlRzEhRnIHmMNyAkcEcfpB+zB+zBo37O/hQ3moBL3xLfxA312gyFA5EEOeRGp79Xb5j2x8wfQHyHZ/CP41fsg6lZ/GHT3tvEVvDC0WqxwIU8uGUqXXnnaCoIlHQj5vlOR+lPwv+Lngv4v+Gk8UeC7oTRBvLnib5ZYJcZKSL1B9D0YcgkU/QfEkXitbuzuLR7S5tGCTwS4bAfO3kcEEA5B/lg1+Yn7Qdtbfsd/E/TPiT8IdShs21jzDc6HIf3TRIQWyB0hYnCE8xt9044Ckr7k7H7AiUEf4V438U/hZa+O0TXNGkWy120XbDcEfJKn/ADxmA+8h7Hqp5HpWN8Efjv4N+OvhFfFHhOTy5otqXlnIR51tKRnaw7g9VccMOnOQPXZ71Yo2mdgiqMsWOAAOufasXoyrXPw++NX7P9zHdX174Q0wWOqW/wC81DSjjMe7J86HHDxtgnC5zzjkEV+Z3iOS20u7PlZupiT+9YYQH/ZWv6hvjRp3grXPAJ8c3t6lldabGZLC/j5be3KxY/jSUgAp+IwRkfzs/tYan4Vi+LM8nhzyo5bq2gnu4Iukd04PmDI6ZGGIHc18hnmW06K+s09LvVefkfRZTjZ1H7Cetlo/8z5d1bQk1m1ng1OMTRXCGOVX+7tbg5+tQf8ABPL9o/xV/wAE9vi/r3gDxhYahrPw18WSrMJbGMzvZXcYwkyxjn5k/dyr1OFI6VqBpJ8LdPiM9gOv0FWFTzJBFbJtzgADliT0H515uAzKphpNxV0z0sVhIVo8sj9rviP/AMFJPhxpuhJ/wqm2n1rUbhcqbmJreKLP95T8zEeg/GvzP8Y/Eb4vftA+Ije+Kb2XUWU/JCDstoAewH3Rj8TXmOi+HrJYTd63MII1OPLB+d8diev4DtXtWjeIrPTNNENtD9mjx8kY+V2H0/hH60YzNK2IdpPTsthYXL6VHWK17s2/C3gHQvDCLqPiKRZ7hei/wKfYdz7mu9/4TGa5lEGjREA8Z7/5+lec2Ntf65KLm+IjhXkZ4UCunt9X0zTONN56gu39K4k2dtlueh6Xpdxcz/a9UfMi84J4X+grv7fxNaaehi0oCWUDDSH7o+leNW99f30fls3l2+eT0z61oDVFgiEOngtgY3Y/kP61pEln/9T74+Ff/BPn4e6R8MnsPieWvfFF+haS+gkbbZM3RIQeHA/jLjMhznAwBufCz42+Mf2bPEsHwM/aNlMmiONmj6+cmIRg4Cuxz+7GQMn5oicNlcGvWf2ffiB418d/Dfwn4tvR9st9StTb3IVVUxSw8NK7E5O4qRgeo46msH9tXx78JfAfwS1CL4oQx30l8ki6XZ5Ama7VTtkQ9UWPIMjdNvynO4A/NHvnqP7Rn7Q/gX9nLwY3iXVGiudR1BS2n2aOA102PvkjpEowWfpjgcmviz9nH9njxT8afFEn7SX7TkJvJL8+bp+mXSfIUP3JJIyPljUf6qI9vmPOK+DfgboFl4V+LfhHxt+1DpWoW/he7iL6bJfRt9mLpgwOwbOYIyd23sSrFdvX+iZ7n7bY+fpkqHzU3RSj50IYZVuDhh34PIqG7bBufnr8Zv2fvF3wa8Vf8L8/ZgQ20sAJ1DR4lLRPF1fbGPvRHGWjHKn5kxjFe8fDD9qX4Q/FL4cXni/V7uHS2sIv+JpYXLgyQE/3RwZEc8Rso+bpwcive/E/irQPBOgXfizxTeJYafp8ZmnnkOFRR+pJPAA5J4GTX8uP7Uf7R3hHWPilqfjDwNpo02HVZ8wWqL84jAAaVlHAeUguVHAY/Unnq1oU6bnVdkuv6GtOlKclCmtWfQn7X37bl7qEqaB4HRbaCyXytOssgiBTgedL281hzg9Onrn8xl1eNpJNQ1C4M00zGSSV23M7HqST1Oa5K7Np44uf7Psmkubp3M0hCjcG9ZGzjn1r3fwR8LrDRokvNaIlmABw3IU/l1/yBXwWZY+eJnzS0itl/XU+tweEjh4WW73ZmaHpev8AiNhJCjW9vjh2+8w9hXqVnpukeGoFe4cqVIJdjyT2/X86W+8U2ljG1rpCiRxxu6jNeO+IrbX9amEzuwERy3rz9eB/M15jdztSPQhr7ahrEt/b/Ky8bXHIz0bn1HPP4CvW/CsEUKNqOqybQ3zZb77n2z/OvnvS2khv4r5wZLiLAUADbx6jvjtmvWNGinvbn7TqjlyRnGcjNNR0DmPTZtYuNSxb2KlY+w7fX3rd063toHDTAzSj+H/H0+lYulwz3Moislx2yOD+n9KzvF3xX8C/DCIwXbi+1NvuWsJzhj/fPRRxVRRV7ntNlp91eRm61F1hgUZOTtVawtT+JGiaQDYeF4hf3J+Uyn/VqT6dya+GNb+M3j74i339n3rx2ttG2PItwwT2yxALH6ce9e3eGLXyLFcofN24wf6nsPYU9gP/1fof9kb9uf4LeBf2QYLjxVctDrujNcIdJAImuJHkYoIiRjaejFsbMHI6Zf8As+/BTxt+1l4/X9qT9pSPdpW4NoukOCIpI0OYyUPS3Q8qDzK3zNkdfkv/AIJ4/s0ah+0xZWv7Q3xmjik8PCZvstqgwuoSQnYC3cwJtwSeZmBz8uc/vtp3iPQrm+k0LT50M9oMNCo27QuFIXgAheAdudp4ODXzeyPfbIviJ4C8I/FLwnceCPHFmLvTrkdOjxOPuyRt/A69iPocgkV+dehfEbxr+wf4ot/hz8W5ZtZ+Hd+7DS9VRCz2h67CvoM/PFnK/eTK5FfpHrviHR/DejXXiDxBdRWNhYxNPcXEzBI4o0GWZieAAK/mw/bP/bF1n9pvxTH4a8DxvD4X0mZjYRSAo88pBQ3Uw6jKkiOP+FSc/MxxhUqQpwc6jtFFU6cqk1GCu2W/26f269Y+Nutnwv4KElv4cs5M2ds/ytcOOPtE2O/9xP4R7kmvz00L4e614kvTqniKR087qf429h/dH616Zofg/S/D+dW16Xz7k/MXbr9AO3+ea2W1q8v/AN1pqeTE3AYj5iPb/P4V8DmWZzxU9NILZfq/M+uwWCjh495dWa2jWPh7wZaC10+FFfsqD5ifUnrn3/WpbibUtVI+0Psj/uA449z/AErPjtrTTI/tepybc8kE5Ymse78UXV0fL0hPIjHG89fw9K8u3c7W7Grd38WjOEt0Blx95v4c+i/41iW5ur+VmvHIG7IHTmqscK2ymaY5dhnLck/Qf41sWlqyqZp3EWevrRZCu2bdgoUiCIYOeT3zXp2hrp+nRfb9VmWCFerP1PsB3NeNz+K9O0eApYr58q/xN0B9f/1VzZ1q/wBQv/td07Mw6lvT2HRR+pockhpHp3jv4q6pdwnR/CObKBxgyf8ALZx7dxXz5p+i6tPqryW8WXYHLSgso7/Oe59s16Np9rNdXTG0Uh24Z++Pr2H5n6V6LZ22k+HoFudVcBs/LHjv7L1J9z+lRzGyiM8IeCI7fZfXKguFUGRjkcDnaD/WvQrrxNYaQo0rSYzLMR25wfUn/P0rift2ueJG2RB7S3PRV/1jD3PRRj/9ddh4b0C1iJKAY/MZPv8AxH9KhS1Lt3P/1v0M/YPkRP2ZNBhiARY3uVCqMAAStwAOwr6budO8L6Fe3PjW/dbUQRySzTSSlII12jzJGBIRTtQbnwOBya+Lf+CevizRdf8A2VNH1vS7lJbQT3hMmcBQshJ3Z+7t75xivy7/AG6/27G+OWpy/CP4WXTL4OgfE9wmQ2qyIeG7H7MrfcU48w4Y8bRXy9ScYRc5uyW59BGEpT5YrUs/tq/tm6r+0x4gPw3+G9xJa+CdPmDM/KNqMiHiVxwfKB5ijPX77c4C/GVlc2eip9h0SLz5z94noD/tH/J9hXJaTZXUkGxT5MA5YZxnPdm9/QV1j3WmeHoFkuPk4+Vf4mHsP4R7n8q+DzTMpYufLHSC2/zZ9XgcFHDxu/iZaj0i6uZDqGtThtvJzwoqrdeJY7RzbaYuSRw7Dt7f5xXE6h4kvNamG/5YgcqmcLj1Pc/X8qtLAwQGPDyPzk9B/n0ryuXsdzkW5LyS4k8+/kLsx4X/AB/wFdFYxXVwyxW6gMe55Ix6Cm6Z4et7JTqevzCJj1Zu/wBBUepeNba3t2svDcflL/FPJyT9BSsuormlcPYaJGHuGM0zcjJrk7vUbvUZwJD8vO1Rwo/Dv+Ncvda3AP3s7FpHOSzHLE+w7Utm+q6uTFZDyo8/Mx6/WomWkb+baDDTPlj0ReSPqRnFdBpukXeptv2iK3Xl3Y4RQPU1mWkFhpOQF+0XAwCO+TnAJ6L9OvtViJNV8RShdTcC2j5ESfLEvufU/Xn0rNstRPQI9QtwgsfCyiYr/wAvLjEQP+yv8X1P610mj+H0WcXWpO1xeMOWbl8fyQU3RbbMSiyG1EH+sbjgf3R2+tdbamytrc3KnbErANM3OWPOAP4iR0qGzdGotg2xbNBktjKJk/p1P48fSuziOn6DBtuj5twekYP3Sf7xH8q8/ttbvriUw6eGgSTv/wAtZB65/hH0P49qll1PS9FJF0RLP2jU5xn1qLiP/9k=" - return Group { - UserProfile() - .environmentObject(chatModel1) - UserProfile() - .environmentObject(chatModel2) - } - } -} diff --git a/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift b/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift index 160130bccc..330ce56e0b 100644 --- a/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift +++ b/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift @@ -9,7 +9,6 @@ import SimpleXChat struct UserProfilesView: View { @EnvironmentObject private var m: ChatModel @EnvironmentObject private var theme: AppTheme - @Binding var showSettings: Bool @Environment(\.editMode) private var editMode @AppStorage(DEFAULT_SHOW_HIDDEN_PROFILES_NOTICE) private var showHiddenProfilesNotice = true @AppStorage(DEFAULT_SHOW_MUTE_PROFILE_ALERT) private var showMuteProfileAlert = true @@ -96,8 +95,7 @@ struct UserProfilesView: View { } label: { Label("Add profile", systemImage: "plus") } - .frame(height: 44) - .padding(.vertical, 4) + .frame(height: 38) } } footer: { Text("Tap to activate profile.") @@ -285,7 +283,7 @@ struct UserProfilesView: View { await MainActor.run { onboardingStageDefault.set(.step1_SimpleXInfo) m.onboardingStage = .step1_SimpleXInfo - showSettings = false + dismissAllSheets() } } } else { @@ -308,14 +306,14 @@ struct UserProfilesView: View { Task { do { try await changeActiveUserAsync_(user.userId, viewPwd: userViewPassword(user)) + dismissAllSheets() } catch { await MainActor.run { alert = .activateUserError(error: responseError(error)) } } } } label: { HStack { - ProfileImage(imageStr: user.image, size: 44) - .padding(.vertical, 4) + ProfileImage(imageStr: user.image, size: 38) .padding(.trailing, 12) Text(user.chatViewName) Spacer() @@ -406,8 +404,15 @@ public func chatPasswordHash(_ pwd: String, _ salt: String) -> String { return hash } +public func correctPassword(_ user: User, _ pwd: String) -> Bool { + if let ph = user.viewPwdHash { + return pwd != "" && chatPasswordHash(pwd, ph.salt) == ph.hash + } + return false +} + struct UserProfilesView_Previews: PreviewProvider { static var previews: some View { - UserProfilesView(showSettings: Binding.constant(true)) + UserProfilesView() } } 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 419f0ae864..f55c87b1b8 100644 --- a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff @@ -137,6 +137,10 @@ %@ иска да се свърже! notification title + + %1$@, %2$@ + format for date separator in chat + %@, %@ and %lld members %@, %@ и %lld членове @@ -1003,6 +1007,10 @@ Автоматично приемане на изображения No comment provided by engineer. + + Auto-accept settings + alert title + Back Назад @@ -1167,7 +1175,7 @@ Cancel Отказ - No comment provided by engineer. + alert button Cancel migration @@ -1310,6 +1318,10 @@ Чат настройки No comment provided by engineer. + + Chat preferences were changed. + alert message + Chat theme No comment provided by engineer. @@ -1685,6 +1697,10 @@ This is your own one-time link! Версия на ядрото: v%@ No comment provided by engineer. + + Corner + No comment provided by engineer. + Correct name to %@? Поправи име на %@? @@ -2645,6 +2661,10 @@ This is your own one-time link! Грешка при промяна на адреса No comment provided by engineer. + + Error changing connection profile + No comment provided by engineer. + Error changing role Грешка при промяна на ролята @@ -2655,6 +2675,10 @@ This is your own one-time link! Грешка при промяна на настройката No comment provided by engineer. + + Error changing to incognito! + No comment provided by engineer. + Error connecting to forwarding server %@. Please try later. No comment provided by engineer. @@ -2773,6 +2797,10 @@ This is your own one-time link! Грешка при зареждане на %@ сървъри No comment provided by engineer. + + Error migrating settings + No comment provided by engineer. + Error opening chat Грешка при отваряне на чата @@ -2870,6 +2898,10 @@ This is your own one-time link! Грешка при спиране на чата No comment provided by engineer. + + Error switching profile + No comment provided by engineer. + Error switching profile! Грешка при смяна на профил! @@ -3190,11 +3222,6 @@ Error: %2$@ Пълно име (незадължително) No comment provided by engineer. - - Full name: - Пълно име: - No comment provided by engineer. - Fully decentralized – visible only to members. Напълно децентрализирана – видима е само за членовете. @@ -4069,6 +4096,10 @@ This is your link for group %@! Message servers No comment provided by engineer. + + Message shape + No comment provided by engineer. + Message source remains private. Източникът на съобщението остава скрит. @@ -4883,16 +4914,6 @@ Error: %@ Профилни изображения No comment provided by engineer. - - Profile name - Име на профила - No comment provided by engineer. - - - Profile name: - Име на профила: - No comment provided by engineer. - Profile password Профилна парола @@ -5200,6 +5221,10 @@ Enable in *Network & servers* settings. Премахване No comment provided by engineer. + + Remove archive? + No comment provided by engineer. + Remove image No comment provided by engineer. @@ -5385,12 +5410,13 @@ Enable in *Network & servers* settings. Save Запази - chat item action + alert button + chat item action Save (and notify contacts) Запази (и уведоми контактите) - No comment provided by engineer. + alert button Save and notify contact @@ -5416,11 +5442,6 @@ Enable in *Network & servers* settings. Запази архив No comment provided by engineer. - - Save auto-accept settings - Запази настройките за автоматично приемане - No comment provided by engineer. - Save group profile Запази профила на групата @@ -5456,16 +5477,15 @@ Enable in *Network & servers* settings. Запази сървърите? No comment provided by engineer. - - Save settings? - Запази настройките? - No comment provided by engineer. - Save welcome message? Запази съобщението при посрещане? No comment provided by engineer. + + Save your profile? + alert title + Saved Запазено @@ -5562,6 +5582,10 @@ Enable in *Network & servers* settings. Избери chat item action + + Select chat profile + No comment provided by engineer. + Selected %lld No comment provided by engineer. @@ -5877,6 +5901,10 @@ Enable in *Network & servers* settings. Настройки No comment provided by engineer. + + Settings were changed. + alert message + Shape profile images Променете формата на профилните изображения @@ -5911,6 +5939,10 @@ Enable in *Network & servers* settings. Сподели линк No comment provided by engineer. + + Share profile + No comment provided by engineer. + Share this 1-time invite link Сподели този еднократен линк за връзка @@ -6069,6 +6101,10 @@ Enable in *Network & servers* settings. Soft blur media + + Some app settings were not migrated. + No comment provided by engineer. + Some file(s) were not exported: No comment provided by engineer. @@ -6235,6 +6271,10 @@ Enable in *Network & servers* settings. TCP_KEEPINTVL No comment provided by engineer. + + Tail + No comment provided by engineer. + Take picture Направи снимка @@ -6423,6 +6463,10 @@ It can happen because of some bug or when the connection is compromised.Текстът, който поставихте, не е 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. @@ -7475,11 +7519,19 @@ Repeat connection request? Вашата чат база данни не е криптирана - задайте парола, за да я криптирате. No comment provided by engineer. + + Your chat preferences + alert title + 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 (%@). Вашият контакт изпрати файл, който е по-голям от поддържания в момента максимален размер (%@). @@ -7525,13 +7577,15 @@ Repeat connection request? Вашият профил **%@** ще бъде споделен. 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 сървърите не могат да видят вашия профил. + + Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile. + Вашият профил се съхранява на вашето устройство и се споделя само с вашите контакти. SimpleX сървърите не могат да видят вашия профил. No comment provided by engineer. + + Your profile was changed. If you save it, the updated profile will be sent to all your contacts. + alert message + Your profile, contacts and delivered messages are stored on your device. Вашият профил, контакти и доставени съобщения се съхраняват на вашето устройство. 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 a02203e630..3b29a1e51f 100644 --- a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff @@ -135,6 +135,10 @@ %@ se chce připojit! notification title + + %1$@, %2$@ + format for date separator in chat + %@, %@ and %lld members No comment provided by engineer. @@ -973,6 +977,10 @@ Automaticky přijímat obrázky No comment provided by engineer. + + Auto-accept settings + alert title + Back Zpět @@ -1127,7 +1135,7 @@ Cancel Zrušit - No comment provided by engineer. + alert button Cancel migration @@ -1266,6 +1274,10 @@ Předvolby chatu No comment provided by engineer. + + Chat preferences were changed. + alert message + Chat theme No comment provided by engineer. @@ -1623,6 +1635,10 @@ This is your own one-time link! Verze jádra: v%@ No comment provided by engineer. + + Corner + No comment provided by engineer. + Correct name to %@? No comment provided by engineer. @@ -2552,6 +2568,10 @@ This is your own one-time link! Chuba změny adresy No comment provided by engineer. + + Error changing connection profile + No comment provided by engineer. + Error changing role Chyba při změně role @@ -2562,6 +2582,10 @@ This is your own one-time link! Chyba změny nastavení No comment provided by engineer. + + Error changing to incognito! + No comment provided by engineer. + Error connecting to forwarding server %@. Please try later. No comment provided by engineer. @@ -2678,6 +2702,10 @@ This is your own one-time link! Chyba načítání %@ serverů No comment provided by engineer. + + Error migrating settings + No comment provided by engineer. + Error opening chat No comment provided by engineer. @@ -2772,6 +2800,10 @@ This is your own one-time link! Chyba při zastavení chatu No comment provided by engineer. + + Error switching profile + No comment provided by engineer. + Error switching profile! Chyba při přepínání profilu! @@ -3079,11 +3111,6 @@ Error: %2$@ Celé jméno (volitelně) No comment provided by engineer. - - Full name: - Celé jméno: - No comment provided by engineer. - Fully decentralized – visible only to members. No comment provided by engineer. @@ -3928,6 +3955,10 @@ This is your link for group %@! Message servers No comment provided by engineer. + + Message shape + No comment provided by engineer. + Message source remains private. No comment provided by engineer. @@ -4705,14 +4736,6 @@ Error: %@ Profile images No comment provided by engineer. - - Profile name - No comment provided by engineer. - - - Profile name: - No comment provided by engineer. - Profile password Heslo profilu @@ -5014,6 +5037,10 @@ Enable in *Network & servers* settings. Odstranit No comment provided by engineer. + + Remove archive? + No comment provided by engineer. + Remove image No comment provided by engineer. @@ -5192,12 +5219,13 @@ Enable in *Network & servers* settings. Save Uložit - chat item action + alert button + chat item action Save (and notify contacts) Uložit (a informovat kontakty) - No comment provided by engineer. + alert button Save and notify contact @@ -5223,11 +5251,6 @@ Enable in *Network & servers* settings. Uložit archiv No comment provided by engineer. - - Save auto-accept settings - Uložit nastavení automatického přijímání - No comment provided by engineer. - Save group profile Uložení profilu skupiny @@ -5263,16 +5286,15 @@ Enable in *Network & servers* settings. Uložit servery? No comment provided by engineer. - - Save settings? - Uložit nastavení? - No comment provided by engineer. - Save welcome message? Uložit uvítací zprávu? No comment provided by engineer. + + Save your profile? + alert title + Saved No comment provided by engineer. @@ -5363,6 +5385,10 @@ Enable in *Network & servers* settings. Vybrat chat item action + + Select chat profile + No comment provided by engineer. + Selected %lld No comment provided by engineer. @@ -5675,6 +5701,10 @@ Enable in *Network & servers* settings. Nastavení No comment provided by engineer. + + Settings were changed. + alert message + Shape profile images No comment provided by engineer. @@ -5708,6 +5738,10 @@ Enable in *Network & servers* settings. Sdílet odkaz No comment provided by engineer. + + Share profile + No comment provided by engineer. + Share this 1-time invite link No comment provided by engineer. @@ -5862,6 +5896,10 @@ Enable in *Network & servers* settings. Soft blur media + + Some app settings were not migrated. + No comment provided by engineer. + Some file(s) were not exported: No comment provided by engineer. @@ -6024,6 +6062,10 @@ Enable in *Network & servers* settings. TCP_KEEPINTVL No comment provided by engineer. + + Tail + No comment provided by engineer. + Take picture Vyfotit @@ -6207,6 +6249,10 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován The text you pasted is not a SimpleX link. 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. @@ -7204,11 +7250,19 @@ Repeat connection request? Vaše chat databáze není šifrována – nastavte přístupovou frázi pro její šifrování. No comment provided by engineer. + + Your chat preferences + alert title + Your chat profiles Vaše chat profily 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 (%@). Kontakt odeslal soubor, který je větší než aktuálně podporovaná maximální velikost (%@). @@ -7253,13 +7307,15 @@ Repeat connection request? Váš profil **%@** bude sdílen. No comment provided by engineer. - - Your profile is stored on your device and shared only with your contacts. -SimpleX servers cannot see your profile. - Váš profil je uložen ve vašem zařízení a sdílen pouze s vašimi kontakty. -Servery SimpleX nevidí váš profil. + + Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile. + Váš profil je uložen ve vašem zařízení a sdílen pouze s vašimi kontakty. Servery SimpleX nevidí váš profil. No comment provided by engineer. + + Your profile was changed. If you save it, the updated profile will be sent to all your contacts. + alert message + Your profile, contacts and delivered messages are stored on your device. Váš profil, kontakty a doručené zprávy jsou uloženy ve vašem zařízení. 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 29adfbabf0..884c322dae 100644 --- a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff +++ b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff @@ -137,6 +137,10 @@ %@ will sich mit Ihnen verbinden! notification title + + %1$@, %2$@ + format for date separator in chat + %@, %@ and %lld members %@, %@ und %lld Mitglieder @@ -1020,6 +1024,10 @@ Bilder automatisch akzeptieren No comment provided by engineer. + + Auto-accept settings + alert title + Back Zurück @@ -1193,7 +1201,7 @@ Cancel Abbrechen - No comment provided by engineer. + alert button Cancel migration @@ -1341,6 +1349,10 @@ Chat-Präferenzen No comment provided by engineer. + + Chat preferences were changed. + alert message + Chat theme Chat-Design @@ -1393,22 +1405,22 @@ Clear - Löschen + Entfernen swipe action Clear conversation - Chatinhalte löschen + Chat-Inhalte entfernen No comment provided by engineer. Clear conversation? - Unterhaltung löschen? + Chat-Inhalte entfernen? No comment provided by engineer. Clear private notes? - Private Notizen löschen? + Private Notizen entfernen? No comment provided by engineer. @@ -1722,7 +1734,7 @@ Das ist Ihr eigener Einmal-Link! Conversation deleted! - Unterhaltung gelöscht! + Chat-Inhalte entfernt! No comment provided by engineer. @@ -1740,6 +1752,10 @@ Das ist Ihr eigener Einmal-Link! Core Version: v%@ No comment provided by engineer. + + Corner + No comment provided by engineer. + Correct name to %@? Richtiger Name für %@? @@ -2724,6 +2740,10 @@ Das ist Ihr eigener Einmal-Link! Fehler beim Wechseln der Empfängeradresse No comment provided by engineer. + + Error changing connection profile + No comment provided by engineer. + Error changing role Fehler beim Ändern der Rolle @@ -2734,6 +2754,10 @@ Das ist Ihr eigener Einmal-Link! Fehler beim Ändern der Einstellung No comment provided by engineer. + + Error changing to incognito! + No comment provided by engineer. + Error connecting to forwarding server %@. Please try later. Fehler beim Verbinden mit dem Weiterleitungsserver %@. Bitte versuchen Sie es später erneut. @@ -2854,6 +2878,10 @@ Das ist Ihr eigener Einmal-Link! Fehler beim Laden von %@ Servern No comment provided by engineer. + + Error migrating settings + No comment provided by engineer. + Error opening chat Fehler beim Öffnen des Chats @@ -2954,6 +2982,10 @@ Das ist Ihr eigener Einmal-Link! Fehler beim Beenden des Chats No comment provided by engineer. + + Error switching profile + No comment provided by engineer. + Error switching profile! Fehler beim Umschalten des Profils! @@ -3289,11 +3321,6 @@ Fehler: %2$@ Vollständiger Name (optional) No comment provided by engineer. - - Full name: - Vollständiger Name: - No comment provided by engineer. - Fully decentralized – visible only to members. Vollständig dezentralisiert – nur für Mitglieder sichtbar. @@ -3451,7 +3478,7 @@ Fehler: %2$@ Group will be deleted for you - this cannot be undone! - Die Gruppe wird für Sie gelöscht. Dies kann nicht rückgängig gemacht werden! + Die Gruppe wird nur bei Ihnen gelöscht. Dies kann nicht rückgängig gemacht werden! No comment provided by engineer. @@ -3906,7 +3933,7 @@ Das ist Ihr Link für die Gruppe %@! Keep conversation - Unterhaltung behalten + Chat-Inhalte beibehalten No comment provided by engineer. @@ -4184,6 +4211,10 @@ Das ist Ihr Link für die Gruppe %@! Nachrichten-Server No comment provided by engineer. + + Message shape + No comment provided by engineer. + Message source remains private. Die Nachrichtenquelle bleibt privat. @@ -4604,7 +4635,7 @@ Dies erfordert die Aktivierung eines VPNs. Only delete conversation - Nur die Unterhaltung löschen + Nur die Chat-Inhalte löschen No comment provided by engineer. @@ -5021,16 +5052,6 @@ Fehler: %@ Profil-Bilder No comment provided by engineer. - - Profile name - Profilname - No comment provided by engineer. - - - Profile name: - Profilname: - No comment provided by engineer. - Profile password Passwort für Profil @@ -5155,7 +5176,7 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Reachable chat toolbar - Erreichbare Chat-Symbolleiste + Chat-Symbolleiste unten No comment provided by engineer. @@ -5354,6 +5375,10 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Entfernen No comment provided by engineer. + + Remove archive? + No comment provided by engineer. + Remove image Bild entfernen @@ -5547,12 +5572,13 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Save Speichern - chat item action + alert button + chat item action Save (and notify contacts) Speichern (und Kontakte benachrichtigen) - No comment provided by engineer. + alert button Save and notify contact @@ -5579,11 +5605,6 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Archiv speichern No comment provided by engineer. - - Save auto-accept settings - Einstellungen von "Automatisch akzeptieren" speichern - No comment provided by engineer. - Save group profile Gruppenprofil speichern @@ -5619,16 +5640,15 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Alle Server speichern? No comment provided by engineer. - - Save settings? - Einstellungen speichern? - No comment provided by engineer. - Save welcome message? Begrüßungsmeldung speichern? No comment provided by engineer. + + Save your profile? + alert title + Saved Abgespeichert @@ -5696,7 +5716,7 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Search or paste SimpleX link - Suchen oder fügen Sie den SimpleX-Link ein + Suchen oder SimpleX-Link einfügen No comment provided by engineer. @@ -5729,6 +5749,10 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Auswählen chat item action + + Select chat profile + No comment provided by engineer. + Selected %lld %lld ausgewählt @@ -6064,6 +6088,10 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Einstellungen No comment provided by engineer. + + Settings were changed. + alert message + Shape profile images Form der Profil-Bilder @@ -6099,6 +6127,10 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Link teilen No comment provided by engineer. + + Share profile + No comment provided by engineer. + Share this 1-time invite link Teilen Sie diesen Einmal-Einladungslink @@ -6264,6 +6296,10 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Weich blur media + + Some app settings were not migrated. + No comment provided by engineer. + Some file(s) were not exported: Einzelne Datei(en) wurde(n) nicht exportiert: @@ -6439,6 +6475,10 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. TCP_KEEPINTVL No comment provided by engineer. + + Tail + No comment provided by engineer. + Take picture Machen Sie ein Foto @@ -6588,7 +6628,7 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro The messages will be deleted for all members. - Die Nachrichten werden für alle Mitglieder gelöscht werden. + Die Nachrichten werden für alle Gruppenmitglieder gelöscht. No comment provided by engineer. @@ -6631,6 +6671,10 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro Der von Ihnen eingefügte Text ist kein SimpleX-Link. No comment provided by engineer. + + The uploaded database archive will be permanently removed from the servers. + No comment provided by engineer. + Themes Design @@ -7087,7 +7131,7 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Use the app with one hand. - Die App mit einer Hand nutzen. + Die App mit einer Hand bedienen. No comment provided by engineer. @@ -7519,7 +7563,7 @@ Verbindungsanfrage wiederholen? You can still view conversation with %@ in the list of chats. - Sie können in der Chatliste weiterhin die Unterhaltung mit %@ einsehen. + Sie können in der Chat-Liste weiterhin die Unterhaltung mit %@ einsehen. No comment provided by engineer. @@ -7714,11 +7758,19 @@ Verbindungsanfrage wiederholen? Ihre Chat-Datenbank ist nicht verschlüsselt. Bitte legen Sie ein Passwort fest, um sie zu schützen. No comment provided by engineer. + + Your chat preferences + alert title + Your chat profiles Ihre Chat-Profile 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 (%@). Ihr Kontakt hat eine Datei gesendet, die größer ist als die derzeit unterstützte maximale Größe (%@). @@ -7764,13 +7816,15 @@ Verbindungsanfrage wiederholen? Ihr Profil **%@** wird geteilt. No comment provided by engineer. - - Your profile is stored on your device and shared only with your contacts. -SimpleX servers cannot see your profile. - Ihr Profil wird auf Ihrem Gerät gespeichert und nur mit Ihren Kontakten geteilt. -SimpleX-Server können Ihr Profil nicht einsehen. + + Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile. + Ihr Profil wird auf Ihrem Gerät gespeichert und nur mit Ihren Kontakten geteilt. SimpleX-Server können Ihr Profil nicht einsehen. No comment provided by engineer. + + Your profile was changed. If you save it, the updated profile will be sent to all your contacts. + alert message + Your profile, contacts and delivered messages are stored on your device. Ihr Profil, Ihre Kontakte und zugestellten Nachrichten werden auf Ihrem Gerät gespeichert. 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 c217793f03..6eb935222f 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff +++ b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff @@ -137,6 +137,11 @@ %@ wants to connect! notification title + + %1$@, %2$@ + %1$@, %2$@ + format for date separator in chat + %@, %@ and %lld members %@, %@ and %lld members @@ -1020,6 +1025,11 @@ Auto-accept images No comment provided by engineer. + + Auto-accept settings + Auto-accept settings + alert title + Back Back @@ -1193,7 +1203,7 @@ Cancel Cancel - No comment provided by engineer. + alert button Cancel migration @@ -1341,6 +1351,11 @@ Chat preferences No comment provided by engineer. + + Chat preferences were changed. + Chat preferences were changed. + alert message + Chat theme Chat theme @@ -1740,6 +1755,11 @@ This is your own one-time link! Core version: v%@ No comment provided by engineer. + + Corner + Corner + No comment provided by engineer. + Correct name to %@? Correct name to %@? @@ -2724,6 +2744,11 @@ This is your own one-time link! Error changing address No comment provided by engineer. + + Error changing connection profile + Error changing connection profile + No comment provided by engineer. + Error changing role Error changing role @@ -2734,6 +2759,11 @@ This is your own one-time link! Error changing setting No comment provided by engineer. + + Error changing to incognito! + Error changing to incognito! + No comment provided by engineer. + Error connecting to forwarding server %@. Please try later. Error connecting to forwarding server %@. Please try later. @@ -2854,6 +2884,11 @@ This is your own one-time link! Error loading %@ servers No comment provided by engineer. + + Error migrating settings + Error migrating settings + No comment provided by engineer. + Error opening chat Error opening chat @@ -2954,6 +2989,11 @@ This is your own one-time link! Error stopping chat No comment provided by engineer. + + Error switching profile + Error switching profile + No comment provided by engineer. + Error switching profile! Error switching profile! @@ -3289,11 +3329,6 @@ Error: %2$@ Full name (optional) No comment provided by engineer. - - Full name: - Full name: - No comment provided by engineer. - Fully decentralized – visible only to members. Fully decentralized – visible only to members. @@ -4184,6 +4219,11 @@ This is your link for group %@! Message servers No comment provided by engineer. + + Message shape + Message shape + No comment provided by engineer. + Message source remains private. Message source remains private. @@ -5021,16 +5061,6 @@ Error: %@ Profile images No comment provided by engineer. - - Profile name - Profile name - No comment provided by engineer. - - - Profile name: - Profile name: - No comment provided by engineer. - Profile password Profile password @@ -5354,6 +5384,11 @@ Enable in *Network & servers* settings. Remove No comment provided by engineer. + + Remove archive? + Remove archive? + No comment provided by engineer. + Remove image Remove image @@ -5547,12 +5582,13 @@ Enable in *Network & servers* settings. Save Save - chat item action + alert button + chat item action Save (and notify contacts) Save (and notify contacts) - No comment provided by engineer. + alert button Save and notify contact @@ -5579,11 +5615,6 @@ Enable in *Network & servers* settings. Save archive No comment provided by engineer. - - Save auto-accept settings - Save auto-accept settings - No comment provided by engineer. - Save group profile Save group profile @@ -5619,16 +5650,16 @@ Enable in *Network & servers* settings. Save servers? No comment provided by engineer. - - Save settings? - Save settings? - No comment provided by engineer. - Save welcome message? Save welcome message? No comment provided by engineer. + + Save your profile? + Save your profile? + alert title + Saved Saved @@ -5729,6 +5760,11 @@ Enable in *Network & servers* settings. Select chat item action + + Select chat profile + Select chat profile + No comment provided by engineer. + Selected %lld Selected %lld @@ -6064,6 +6100,11 @@ Enable in *Network & servers* settings. Settings No comment provided by engineer. + + Settings were changed. + Settings were changed. + alert message + Shape profile images Shape profile images @@ -6099,6 +6140,11 @@ Enable in *Network & servers* settings. Share link No comment provided by engineer. + + Share profile + Share profile + No comment provided by engineer. + Share this 1-time invite link Share this 1-time invite link @@ -6264,6 +6310,11 @@ Enable in *Network & servers* settings. Soft blur media + + Some app settings were not migrated. + Some app settings were not migrated. + No comment provided by engineer. + Some file(s) were not exported: Some file(s) were not exported: @@ -6439,6 +6490,11 @@ Enable in *Network & servers* settings. TCP_KEEPINTVL No comment provided by engineer. + + Tail + Tail + No comment provided by engineer. + Take picture Take picture @@ -6631,6 +6687,11 @@ It can happen because of some bug or when the connection is compromised.The text you pasted is not a SimpleX link. No comment provided by engineer. + + The uploaded database archive will be permanently removed from the servers. + The uploaded database archive will be permanently removed from the servers. + No comment provided by engineer. + Themes Themes @@ -7714,11 +7775,21 @@ Repeat connection request? Your chat database is not encrypted - set passphrase to encrypt it. No comment provided by engineer. + + Your chat preferences + Your chat preferences + alert title + Your chat profiles Your chat profiles No comment provided by engineer. + + Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile. + 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 (%@). Your contact sent a file that is larger than currently supported maximum size (%@). @@ -7764,13 +7835,16 @@ Repeat connection request? Your profile **%@** will be shared. No comment provided by engineer. - - Your profile is stored on your device and shared only with your contacts. -SimpleX servers cannot see your profile. - Your profile is stored on your device and shared only with your contacts. -SimpleX servers cannot see your profile. + + Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile. + Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile. No comment provided by engineer. + + Your profile was changed. If you save it, the updated profile will be sent to all your contacts. + Your profile was changed. If you save it, the updated profile will be sent to all your contacts. + alert message + Your profile, contacts and delivered messages are stored on your device. Your profile, contacts and delivered messages are stored on your device. 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 6e5ec0e85a..a10a1594de 100644 --- a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff +++ b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff @@ -137,6 +137,10 @@ ¡ %@ quiere contactar! notification title + + %1$@, %2$@ + format for date separator in chat + %@, %@ and %lld members %@, %@ y %lld miembro(s) más @@ -1020,6 +1024,10 @@ Aceptar imágenes automáticamente No comment provided by engineer. + + Auto-accept settings + alert title + Back Volver @@ -1193,7 +1201,7 @@ Cancel Cancelar - No comment provided by engineer. + alert button Cancel migration @@ -1341,6 +1349,10 @@ Preferencias de Chat No comment provided by engineer. + + Chat preferences were changed. + alert message + Chat theme Tema de chat @@ -1740,6 +1752,10 @@ This is your own one-time link! Versión Core: v%@ No comment provided by engineer. + + Corner + No comment provided by engineer. + Correct name to %@? ¿Corregir el nombre a %@? @@ -2724,6 +2740,10 @@ This is your own one-time link! Error al cambiar servidor No comment provided by engineer. + + Error changing connection profile + No comment provided by engineer. + Error changing role Error al cambiar rol @@ -2734,6 +2754,10 @@ This is your own one-time link! Error cambiando configuración No comment provided by engineer. + + Error changing to incognito! + No comment provided by engineer. + Error connecting to forwarding server %@. Please try later. Error al conectar con el servidor de reenvío %@. Por favor, inténtalo más tarde. @@ -2854,6 +2878,10 @@ This is your own one-time link! Error al cargar servidores %@ No comment provided by engineer. + + Error migrating settings + No comment provided by engineer. + Error opening chat Error al abrir chat @@ -2954,6 +2982,10 @@ This is your own one-time link! Error al parar SimpleX No comment provided by engineer. + + Error switching profile + No comment provided by engineer. + Error switching profile! ¡Error al cambiar perfil! @@ -3289,11 +3321,6 @@ Error: %2$@ Nombre completo (opcional) No comment provided by engineer. - - Full name: - Nombre completo: - No comment provided by engineer. - Fully decentralized – visible only to members. Completamente descentralizado y sólo visible para los miembros. @@ -4184,6 +4211,10 @@ This is your link for group %@! Servidores de mensajes No comment provided by engineer. + + Message shape + No comment provided by engineer. + Message source remains private. El autor del mensaje se mantiene privado. @@ -5021,16 +5052,6 @@ Error: %@ Forma de los perfiles No comment provided by engineer. - - Profile name - Nombre del perfil - No comment provided by engineer. - - - Profile name: - Nombre del perfil: - No comment provided by engineer. - Profile password Contraseña del perfil @@ -5354,6 +5375,10 @@ Actívalo en ajustes de *Servidores y Redes*. Eliminar No comment provided by engineer. + + Remove archive? + No comment provided by engineer. + Remove image Eliminar imagen @@ -5547,12 +5572,13 @@ Actívalo en ajustes de *Servidores y Redes*. Save Guardar - chat item action + alert button + chat item action Save (and notify contacts) Guardar (y notificar contactos) - No comment provided by engineer. + alert button Save and notify contact @@ -5579,11 +5605,6 @@ Actívalo en ajustes de *Servidores y Redes*. Guardar archivo No comment provided by engineer. - - Save auto-accept settings - Guardar configuración de auto aceptar - No comment provided by engineer. - Save group profile Guardar perfil de grupo @@ -5619,16 +5640,15 @@ Actívalo en ajustes de *Servidores y Redes*. ¿Guardar servidores? No comment provided by engineer. - - Save settings? - ¿Guardar configuración? - No comment provided by engineer. - Save welcome message? ¿Guardar mensaje de bienvenida? No comment provided by engineer. + + Save your profile? + alert title + Saved Guardado @@ -5729,6 +5749,10 @@ Actívalo en ajustes de *Servidores y Redes*. Seleccionar chat item action + + Select chat profile + No comment provided by engineer. + Selected %lld Seleccionados %lld @@ -6064,6 +6088,10 @@ Actívalo en ajustes de *Servidores y Redes*. Configuración No comment provided by engineer. + + Settings were changed. + alert message + Shape profile images Dar forma a las imágenes de perfil @@ -6099,6 +6127,10 @@ Actívalo en ajustes de *Servidores y Redes*. Compartir enlace No comment provided by engineer. + + Share profile + No comment provided by engineer. + Share this 1-time invite link Comparte este enlace de un solo uso @@ -6264,6 +6296,10 @@ Actívalo en ajustes de *Servidores y Redes*. Suave blur media + + Some app settings were not migrated. + No comment provided by engineer. + Some file(s) were not exported: Algunos archivos no han sido exportados: @@ -6439,6 +6475,10 @@ Actívalo en ajustes de *Servidores y Redes*. TCP_KEEPINTVL No comment provided by engineer. + + Tail + No comment provided by engineer. + Take picture Tomar foto @@ -6631,6 +6671,10 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. El texto pegado no es un enlace SimpleX. No comment provided by engineer. + + The uploaded database archive will be permanently removed from the servers. + No comment provided by engineer. + Themes Temas @@ -7714,11 +7758,19 @@ Repeat connection request? La base de datos no está cifrada - establece una contraseña para cifrarla. No comment provided by engineer. + + Your chat preferences + alert title + Your chat profiles Mis perfiles 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 (%@). El contacto ha enviado un archivo mayor al máximo admitido (%@). @@ -7764,13 +7816,15 @@ Repeat connection request? El perfil **%@** será compartido. No comment provided by engineer. - - Your profile is stored on your device and shared only with your contacts. -SimpleX servers cannot see your profile. - Tu perfil es almacenado en tu dispositivo y solamente se comparte con tus contactos. -Los servidores SimpleX no pueden ver tu perfil. + + Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile. + Tu perfil es almacenado en tu dispositivo y solamente se comparte con tus contactos. Los servidores SimpleX no pueden ver tu perfil. No comment provided by engineer. + + Your profile was changed. If you save it, the updated profile will be sent to all your contacts. + alert message + Your profile, contacts and delivered messages are stored on your device. Tu perfil, contactos y mensajes se almacenan en tu dispositivo. 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 211e512a1e..cf2ea3c36d 100644 --- a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff @@ -133,6 +133,10 @@ %@ haluaa muodostaa yhteyden! notification title + + %1$@, %2$@ + format for date separator in chat + %@, %@ and %lld members No comment provided by engineer. @@ -967,6 +971,10 @@ Hyväksy kuvat automaattisesti No comment provided by engineer. + + Auto-accept settings + alert title + Back Takaisin @@ -1120,7 +1128,7 @@ Cancel Peruuta - No comment provided by engineer. + alert button Cancel migration @@ -1259,6 +1267,10 @@ Chat-asetukset No comment provided by engineer. + + Chat preferences were changed. + alert message + Chat theme No comment provided by engineer. @@ -1616,6 +1628,10 @@ This is your own one-time link! Ydinversio: v%@ No comment provided by engineer. + + Corner + No comment provided by engineer. + Correct name to %@? No comment provided by engineer. @@ -2544,6 +2560,10 @@ This is your own one-time link! Virhe osoitteenvaihdossa No comment provided by engineer. + + Error changing connection profile + No comment provided by engineer. + Error changing role Virhe roolin vaihdossa @@ -2554,6 +2574,10 @@ This is your own one-time link! Virhe asetuksen muuttamisessa No comment provided by engineer. + + Error changing to incognito! + No comment provided by engineer. + Error connecting to forwarding server %@. Please try later. No comment provided by engineer. @@ -2669,6 +2693,10 @@ This is your own one-time link! Virhe %@-palvelimien lataamisessa No comment provided by engineer. + + Error migrating settings + No comment provided by engineer. + Error opening chat No comment provided by engineer. @@ -2762,6 +2790,10 @@ This is your own one-time link! Virhe keskustelun lopettamisessa No comment provided by engineer. + + Error switching profile + No comment provided by engineer. + Error switching profile! Virhe profiilin vaihdossa! @@ -3069,11 +3101,6 @@ Error: %2$@ Koko nimi (valinnainen) No comment provided by engineer. - - Full name: - Koko nimi: - No comment provided by engineer. - Fully decentralized – visible only to members. No comment provided by engineer. @@ -3918,6 +3945,10 @@ This is your link for group %@! Message servers No comment provided by engineer. + + Message shape + No comment provided by engineer. + Message source remains private. No comment provided by engineer. @@ -4693,14 +4724,6 @@ Error: %@ Profile images No comment provided by engineer. - - Profile name - No comment provided by engineer. - - - Profile name: - No comment provided by engineer. - Profile password Profiilin salasana @@ -5002,6 +5025,10 @@ Enable in *Network & servers* settings. Poista No comment provided by engineer. + + Remove archive? + No comment provided by engineer. + Remove image No comment provided by engineer. @@ -5180,12 +5207,13 @@ Enable in *Network & servers* settings. Save Tallenna - chat item action + alert button + chat item action Save (and notify contacts) Tallenna (ja ilmoita kontakteille) - No comment provided by engineer. + alert button Save and notify contact @@ -5211,11 +5239,6 @@ Enable in *Network & servers* settings. Tallenna arkisto No comment provided by engineer. - - Save auto-accept settings - Tallenna automaattisen hyväksynnän asetukset - No comment provided by engineer. - Save group profile Tallenna ryhmäprofiili @@ -5251,16 +5274,15 @@ Enable in *Network & servers* settings. Tallenna palvelimet? No comment provided by engineer. - - Save settings? - Tallenna asetukset? - No comment provided by engineer. - Save welcome message? Tallenna tervetuloviesti? No comment provided by engineer. + + Save your profile? + alert title + Saved No comment provided by engineer. @@ -5351,6 +5373,10 @@ Enable in *Network & servers* settings. Valitse chat item action + + Select chat profile + No comment provided by engineer. + Selected %lld No comment provided by engineer. @@ -5662,6 +5688,10 @@ Enable in *Network & servers* settings. Asetukset No comment provided by engineer. + + Settings were changed. + alert message + Shape profile images No comment provided by engineer. @@ -5695,6 +5725,10 @@ Enable in *Network & servers* settings. Jaa linkki No comment provided by engineer. + + Share profile + No comment provided by engineer. + Share this 1-time invite link No comment provided by engineer. @@ -5848,6 +5882,10 @@ Enable in *Network & servers* settings. Soft blur media + + Some app settings were not migrated. + No comment provided by engineer. + Some file(s) were not exported: No comment provided by engineer. @@ -6010,6 +6048,10 @@ Enable in *Network & servers* settings. TCP_KEEPINTVL No comment provided by engineer. + + Tail + No comment provided by engineer. + Take picture Ota kuva @@ -6193,6 +6235,10 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.The text you pasted is not a SimpleX link. 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. @@ -7189,11 +7235,19 @@ Repeat connection request? Keskustelut-tietokantasi ei ole salattu - aseta tunnuslause sen salaamiseksi. No comment provided by engineer. + + Your chat preferences + alert title + Your chat profiles Keskusteluprofiilisi 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 (%@). Yhteyshenkilösi lähetti tiedoston, joka on suurempi kuin tällä hetkellä tuettu enimmäiskoko (%@). @@ -7238,13 +7292,15 @@ Repeat connection request? Profiilisi **%@** jaetaan. No comment provided by engineer. - - Your profile is stored on your device and shared only with your contacts. -SimpleX servers cannot see your profile. - Profiilisi tallennetaan laitteeseesi ja jaetaan vain yhteystietojesi kanssa. -SimpleX-palvelimet eivät näe profiiliasi. + + Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile. + Profiilisi tallennetaan laitteeseesi ja jaetaan vain yhteystietojesi kanssa. SimpleX-palvelimet eivät näe profiiliasi. No comment provided by engineer. + + Your profile was changed. If you save it, the updated profile will be sent to all your contacts. + alert message + Your profile, contacts and delivered messages are stored on your device. Profiilisi, kontaktisi ja toimitetut viestit tallennetaan laitteellesi. 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 c05098980e..547d0f3674 100644 --- a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff @@ -137,6 +137,10 @@ %@ veut se connecter ! notification title + + %1$@, %2$@ + format for date separator in chat + %@, %@ and %lld members %@, %@ et %lld membres @@ -1020,6 +1024,10 @@ Images auto-acceptées No comment provided by engineer. + + Auto-accept settings + alert title + Back Retour @@ -1193,7 +1201,7 @@ Cancel Annuler - No comment provided by engineer. + alert button Cancel migration @@ -1341,6 +1349,10 @@ Préférences de chat No comment provided by engineer. + + Chat preferences were changed. + alert message + Chat theme Thème de chat @@ -1740,6 +1752,10 @@ Il s'agit de votre propre lien unique ! Version du cœur : v%@ No comment provided by engineer. + + Corner + No comment provided by engineer. + Correct name to %@? Corriger le nom pour %@ ? @@ -2724,6 +2740,10 @@ Il s'agit de votre propre lien unique ! Erreur de changement d'adresse No comment provided by engineer. + + Error changing connection profile + No comment provided by engineer. + Error changing role Erreur lors du changement de rôle @@ -2734,6 +2754,10 @@ Il s'agit de votre propre lien unique ! Erreur de changement de paramètre No comment provided by engineer. + + Error changing to 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. @@ -2854,6 +2878,10 @@ Il s'agit de votre propre lien unique ! Erreur lors du chargement des serveurs %@ No comment provided by engineer. + + Error migrating settings + No comment provided by engineer. + Error opening chat Erreur lors de l'ouverture du chat @@ -2954,6 +2982,10 @@ Il s'agit de votre propre lien unique ! Erreur lors de l'arrêt du chat No comment provided by engineer. + + Error switching profile + No comment provided by engineer. + Error switching profile! Erreur lors du changement de profil ! @@ -3289,11 +3321,6 @@ Erreur : %2$@ Nom complet (optionnel) No comment provided by engineer. - - Full name: - Nom complet : - No comment provided by engineer. - Fully decentralized – visible only to members. Entièrement décentralisé – visible que par ses membres. @@ -4184,6 +4211,10 @@ Voici votre lien pour le groupe %@ ! Serveurs de messages No comment provided by engineer. + + Message shape + No comment provided by engineer. + Message source remains private. La source du message reste privée. @@ -5021,16 +5052,6 @@ Erreur : %@ Images de profil No comment provided by engineer. - - Profile name - Nom du profil - No comment provided by engineer. - - - Profile name: - Nom du profil : - No comment provided by engineer. - Profile password Mot de passe de profil @@ -5354,6 +5375,10 @@ Activez-le dans les paramètres *Réseau et serveurs*. Supprimer No comment provided by engineer. + + Remove archive? + No comment provided by engineer. + Remove image Enlever l'image @@ -5547,12 +5572,13 @@ Activez-le dans les paramètres *Réseau et serveurs*. Save Enregistrer - chat item action + alert button + chat item action Save (and notify contacts) Enregistrer (et en informer les contacts) - No comment provided by engineer. + alert button Save and notify contact @@ -5579,11 +5605,6 @@ Activez-le dans les paramètres *Réseau et serveurs*. Enregistrer l'archive No comment provided by engineer. - - Save auto-accept settings - Enregistrer les paramètres de validation automatique - No comment provided by engineer. - Save group profile Enregistrer le profil du groupe @@ -5619,16 +5640,15 @@ Activez-le dans les paramètres *Réseau et serveurs*. Enregistrer les serveurs ? No comment provided by engineer. - - Save settings? - Enregistrer les paramètres ? - No comment provided by engineer. - Save welcome message? Enregistrer le message d'accueil ? No comment provided by engineer. + + Save your profile? + alert title + Saved Enregistré @@ -5729,6 +5749,10 @@ Activez-le dans les paramètres *Réseau et serveurs*. Choisir chat item action + + Select chat profile + No comment provided by engineer. + Selected %lld %lld sélectionné(s) @@ -6064,6 +6088,10 @@ Activez-le dans les paramètres *Réseau et serveurs*. Paramètres No comment provided by engineer. + + Settings were changed. + alert message + Shape profile images Images de profil modelable @@ -6099,6 +6127,10 @@ Activez-le dans les paramètres *Réseau et serveurs*. Partager le lien No comment provided by engineer. + + Share profile + No comment provided by engineer. + Share this 1-time invite link Partager ce lien d'invitation unique @@ -6264,6 +6296,10 @@ Activez-le dans les paramètres *Réseau et serveurs*. Léger blur media + + Some app settings were not migrated. + No comment provided by engineer. + Some file(s) were not exported: Certains fichiers n'ont pas été exportés : @@ -6439,6 +6475,10 @@ Activez-le dans les paramètres *Réseau et serveurs*. TCP_KEEPINTVL No comment provided by engineer. + + Tail + No comment provided by engineer. + Take picture Prendre une photo @@ -6631,6 +6671,10 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. 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. + No comment provided by engineer. + Themes Thèmes @@ -7714,11 +7758,19 @@ Répéter la demande de connexion ? Votre base de données de chat n'est pas chiffrée - définisez une phrase secrète. No comment provided by engineer. + + Your chat preferences + alert title + 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. + 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(%@). @@ -7764,13 +7816,15 @@ Répéter la demande de connexion ? Votre profil **%@** sera partagé. 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. + + 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. No comment provided by engineer. + + Your profile was changed. If you save it, the updated profile will be sent to all your contacts. + alert message + 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. 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 f7328eed91..063dd3d14a 100644 --- a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff @@ -137,6 +137,10 @@ %@ kapcsolódni szeretne! notification title + + %1$@, %2$@ + format for date separator in chat + %@, %@ and %lld members %@, %@ és további %lld tag @@ -423,8 +427,8 @@ - voice messages up to 5 minutes. - custom time to disappear. - editing history. - - hangüzenetek legfeljebb 5 perces időtartamig. -- egyedi eltűnési időhatár megadása. + - 5 perc hosszúságú hangüzenetek. +- egyedi üzenet-eltűnési időkorlát. - előzmények szerkesztése. No comment provided by engineer. @@ -492,7 +496,7 @@ <p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p> <p>Üdvözlöm!</p> -<p><a href=„%@”>Csatlakozzon hozzám a SimpleX Chaten</a></p> +<p><a href=„%@”>Csatlakozzon hozzám a SimpleX Chaten keresztül</a></p> email text @@ -566,7 +570,7 @@ Accept connection request? - Kapcsolódási kérelem elfogadása? + Ismerőskérelem elfogadása? No comment provided by engineer. @@ -1020,6 +1024,10 @@ Képek automatikus elfogadása No comment provided by engineer. + + Auto-accept settings + alert title + Back Vissza @@ -1042,7 +1050,7 @@ Bad message hash - Hibás az üzenet ellenőrzőösszege + Hibás az üzenet hasító értéke No comment provided by engineer. @@ -1193,7 +1201,7 @@ Cancel Mégse - No comment provided by engineer. + alert button Cancel migration @@ -1341,6 +1349,10 @@ Csevegési beállítások No comment provided by engineer. + + Chat preferences were changed. + alert message + Chat theme Csevegés témája @@ -1518,7 +1530,7 @@ Connect to desktop - Kapcsolódás számítógéphez + Társítás számítógéppel No comment provided by engineer. @@ -1542,7 +1554,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 egyszer használatos hivatkozása! +Ez az ön egyszer használatos hivatkozása! No comment provided by engineer. @@ -1572,7 +1584,7 @@ Ez az egyszer használatos hivatkozása! Connected desktop - Csatlakoztatott számítógép + Társított számítógép No comment provided by engineer. @@ -1667,7 +1679,7 @@ Ez az egyszer használatos hivatkozása! Contact already exists - Létező ismerős + Az ismerős már létezik No comment provided by engineer. @@ -1740,6 +1752,10 @@ Ez az egyszer használatos hivatkozása! Alapverziószám: v%@ No comment provided by engineer. + + Corner + No comment provided by engineer. + Correct name to %@? Név javítása erre: %@? @@ -1777,7 +1793,7 @@ Ez az egyszer használatos hivatkozása! Create group link - Csoportos hivatkozás létrehozása + Csoporthivatkozás létrehozása No comment provided by engineer. @@ -1832,7 +1848,7 @@ Ez az egyszer használatos hivatkozása! Creating archive link - Archív hivatkozás létrehozása + Archívum hivatkozás létrehozása No comment provided by engineer. @@ -2161,7 +2177,7 @@ Ez az egyszer használatos hivatkozása! Delete pending connection? - Függő kapcsolatfelvételi kérések törlése? + Függőben lévő ismerőskérelem törlése? No comment provided by engineer. @@ -2471,7 +2487,7 @@ Ez az egyszer használatos hivatkozása! Duplicate display name! - Duplikált megjelenítési név! + Duplikált megjelenített név! No comment provided by engineer. @@ -2716,7 +2732,7 @@ Ez az egyszer használatos hivatkozása! Error adding member(s) - Hiba a tag(-ok) hozzáadásakor + Hiba a tag(ok) hozzáadásakor No comment provided by engineer. @@ -2724,6 +2740,10 @@ Ez az egyszer használatos hivatkozása! Hiba a cím megváltoztatásakor No comment provided by engineer. + + Error changing connection profile + No comment provided by engineer. + Error changing role Hiba a szerepkör megváltoztatásakor @@ -2734,6 +2754,10 @@ Ez az egyszer használatos hivatkozása! Hiba a beállítás megváltoztatásakor No comment provided by engineer. + + Error changing to incognito! + No comment provided by engineer. + Error connecting to forwarding server %@. Please try later. Hiba a(z) %@ továbbító kiszolgálóhoz való kapcsolódáskor. Próbálja meg később. @@ -2751,7 +2775,7 @@ Ez az egyszer használatos hivatkozása! Error creating group link - Hiba a csoport hivatkozásának létrehozásakor + Hiba a csoporthivatkozás létrehozásakor No comment provided by engineer. @@ -2854,6 +2878,10 @@ Ez az egyszer használatos hivatkozása! Hiba a %@ kiszolgálók betöltésekor No comment provided by engineer. + + Error migrating settings + No comment provided by engineer. + Error opening chat Hiba a csevegés megnyitásakor @@ -2954,6 +2982,10 @@ Ez az egyszer használatos hivatkozása! Hiba a csevegés megállításakor No comment provided by engineer. + + Error switching profile + No comment provided by engineer. + Error switching profile! Hiba a profil váltásakor! @@ -2966,7 +2998,7 @@ Ez az egyszer használatos hivatkozása! Error updating group link - Hiba a csoport hivatkozás frissítésekor + Hiba a csoporthivatkozás frissítésekor No comment provided by engineer. @@ -3289,11 +3321,6 @@ Hiba: %2$@ Teljes név (opcionális) No comment provided by engineer. - - Full name: - Teljes név: - No comment provided by engineer. - Fully decentralized – visible only to members. Teljesen decentralizált - kizárólag tagok számára látható. @@ -3371,12 +3398,12 @@ Hiba: %2$@ Group link - Csoport hivatkozás + Csoporthivatkozás No comment provided by engineer. Group links - Csoport hivatkozások + Csoporthivatkozások No comment provided by engineer. @@ -3446,7 +3473,7 @@ Hiba: %2$@ Group will be deleted for all members - this cannot be undone! - 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 minden tag számára - ez a művelet nem vonható vissza! No comment provided by engineer. @@ -3743,12 +3770,12 @@ Hiba: %2$@ Invalid connection link - Érvénytelen kapcsolati hivatkozás + Érvénytelen kapcsolattartási hivatkozás No comment provided by engineer. Invalid display name! - Érvénytelen megjelenítendő felhaszálónév! + Érvénytelen megjelenítendő név! No comment provided by engineer. @@ -3966,7 +3993,7 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Let's talk in SimpleX Chat - Beszélgessünk a SimpleX Chat-ben + Beszélgessünk a SimpleX Chatben email subject @@ -3981,17 +4008,17 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Link mobile and desktop apps! 🔗 - Társítsa össze a mobil és az asztali alkalmazásokat! 🔗 + Társítsa össze a mobil és asztali alkalmazásokat! 🔗 No comment provided by engineer. Linked desktop options - Összekapcsolt számítógép beállítások + Társított számítógép beállítások No comment provided by engineer. Linked desktops - Összekapcsolt számítógépek + Társított számítógépek No comment provided by engineer. @@ -4056,7 +4083,7 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?* - Sokan kérdezték: *ha a SimpleX-nek nincsenek felhasználói azonosítói, akkor hogyan tud üzeneteket kézbesíteni?* + Sokan kérdezték: *ha a SimpleX Chatnek nincsenek felhasználói azonosítói, akkor hogyan tud üzeneteket kézbesíteni?* No comment provided by engineer. @@ -4184,6 +4211,10 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Üzenetkiszolgálók No comment provided by engineer. + + Message shape + No comment provided by engineer. + Message source remains private. Az üzenet forrása titokban marad. @@ -4311,12 +4342,12 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Moderated at - Moderálva lett ekkor: + Moderálva ekkor: No comment provided by engineer. Moderated at: %@ - Moderálva lett ekkor: %@ + Moderálva ekkor: %@ copied message info @@ -4401,7 +4432,7 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! New contact request - Új kapcsolattartási kérelem + Új ismerőskérelem notification @@ -4461,7 +4492,7 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! No contacts selected - Nem kerültek ismerősök kiválasztásra + Nincs kiválasztva ismerős No comment provided by engineer. @@ -4526,7 +4557,7 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Nothing selected - Semmi sincs kiválasztva + Nincs kiválasztva semmi No comment provided by engineer. @@ -4570,7 +4601,7 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Old database archive - Régi adatbázis archívum + Régi adatbázis-archívum No comment provided by engineer. @@ -4599,7 +4630,7 @@ VPN engedélyezése szükséges. Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**. - Csak a klienseszközök tárolják a felhasználói profilokat, névjegyeket, csoportokat és a **2 rétegű végponttól-végpontig titkosítással** küldött üzeneteket. + Csak a klienseszközök tárolják a felhasználói 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. @@ -4734,7 +4765,7 @@ VPN engedélyezése szükséges. Or securely share this file link - Vagy a fájl hivítkozásának biztonságos megosztása + Vagy ossza meg biztonságosan ezt a fájlhivatkozást No comment provided by engineer. @@ -4794,7 +4825,7 @@ VPN engedélyezése szükséges. Past member %@ - Már nem tag - %@ + %@ (már nem tag) past/unknown group member @@ -4814,12 +4845,12 @@ VPN engedélyezése szükséges. Paste the link you received - Fogadott hivatkozás beillesztése + Kapott hivatkozás beillesztése No comment provided by engineer. Pending - Függő + Függőben No comment provided by engineer. @@ -4866,7 +4897,7 @@ Minden további problémát osszon meg a fejlesztőkkel. 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 ismerősét, hogy küldjön egy másikat. + 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. @@ -5021,16 +5052,6 @@ Hiba: %@ Profilképek No comment provided by engineer. - - Profile name - Profilnév - No comment provided by engineer. - - - Profile name: - Profil neve: - No comment provided by engineer. - Profile password Profiljelszó @@ -5354,6 +5375,10 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben. Eltávolítás No comment provided by engineer. + + Remove archive? + No comment provided by engineer. + Remove image Kép eltávolítása @@ -5426,7 +5451,7 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben. Reset - Alaphelyzetbe állítás + Visszaállítás No comment provided by engineer. @@ -5446,7 +5471,7 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben. Reset colors - Színek alaphelyzetbe állítása + Színek visszaállítása No comment provided by engineer. @@ -5456,7 +5481,7 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben. Reset to defaults - Alaphelyzetbe állítás + Visszaállítás alaphelyzetbe No comment provided by engineer. @@ -5547,12 +5572,13 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben. Save Mentés - chat item action + alert button + chat item action 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 @@ -5579,11 +5605,6 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben. Archívum mentése No comment provided by engineer. - - Save auto-accept settings - Automatikus elfogadási beállítások mentése - No comment provided by engineer. - Save group profile Csoportprofil elmentése @@ -5619,16 +5640,15 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben. Kiszolgálók mentése? No comment provided by engineer. - - Save settings? - Beállítások mentése? - No comment provided by engineer. - Save welcome message? Üdvözlőszöveg mentése? No comment provided by engineer. + + Save your profile? + alert title + Saved Mentett @@ -5729,6 +5749,10 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben. Választás chat item action + + Select chat profile + No comment provided by engineer. + Selected %lld %lld kiválasztva @@ -5996,12 +6020,12 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben. Servers info - információk a kiszolgálókról + Információk a kiszolgálókról No comment provided by engineer. Servers statistics will be reset - this cannot be undone! - A kiszolgálók statisztikái visszaállnak - ez nem vonható vissza! + A kiszolgálók statisztikái visszaállnak - ez a művelet nem vonható vissza! No comment provided by engineer. @@ -6064,6 +6088,10 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben. Beállítások No comment provided by engineer. + + Settings were changed. + alert message + Shape profile images Profilkép alakzat @@ -6099,6 +6127,10 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben. Hivatkozás megosztása No comment provided by engineer. + + Share profile + No comment provided by engineer. + Share this 1-time invite link Egyszer használatos meghívó hivatkozás megosztása @@ -6211,7 +6243,7 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben. SimpleX group link - SimpleX csoport hivatkozás + SimpleX csoporthivatkozás simplex link type @@ -6264,6 +6296,10 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben. Enyhe blur media + + Some app settings were not migrated. + No comment provided by engineer. + Some file(s) were not exported: Néhány fájl nem került exportálásra: @@ -6401,7 +6437,7 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben. Support SimpleX Chat - Támogassa a SimpleX Chatet + SimpleX Chat támogatása No comment provided by engineer. @@ -6439,6 +6475,10 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben. TCP_KEEPINTVL No comment provided by engineer. + + Tail + No comment provided by engineer. + Take picture Kép készítése @@ -6516,7 +6556,7 @@ Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben. Thanks to the users – contribute via Weblate! - Köszönet a felhasználóknak - hozzájárulás a Weblaten! + Köszönet a felhasználóknak - hozzájárulás a Weblate-en! No comment provided by engineer. @@ -6548,12 +6588,12 @@ 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 kód nem egy SimpleX hivatkozás QR-kód. + 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 kapcsolat vissza lesz vonva! + Az ön által elfogadott kérelem vissza lesz vonva! No comment provided by engineer. @@ -6573,7 +6613,7 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. The hash of the previous message is different. - Az előző üzenet ellenőrzőösszege különbözik. + Az előző üzenet hasító értéke különbözik. No comment provided by engineer. @@ -6631,6 +6671,10 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. A beillesztett szöveg nem egy SimpleX hivatkozás. No comment provided by engineer. + + The uploaded database archive will be permanently removed from the servers. + No comment provided by engineer. + Themes Témák @@ -6678,7 +6722,7 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. This display name is invalid. Please choose another name. - Ez a megjelenített felhasználónév érvénytelen. Válasszon egy másik nevet. + Ez a megjelenített név érvénytelen. Válasszon egy másik nevet. No comment provided by engineer. @@ -6698,12 +6742,12 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. This is your own one-time link! - Ez az egyszer használatos hivatkozása! + Ez az ön egyszer használatos hivatkozása! No comment provided by engineer. This link was used with another mobile device, please create a new link on the desktop. - Ezt a hivatkozást egy másik mobilleszközön már használták, hozzon létre egy új hivatkozást az asztali számítógépén. + Ezt a hivatkozást egy másik mobileszközön már használták, hozzon létre egy új hivatkozást az asztali számítógépén. No comment provided by engineer. @@ -6810,12 +6854,12 @@ A funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beáll Trying to connect to the server used to receive messages from this contact (error: %@). - Kapcsolódási kísérlet ahhoz a kiszolgálóhoz, amely az adott ismerőstől érkező üzenetek fogadására szolgál (hiba: %@). + 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: %@). No comment provided by engineer. Trying to connect to the server used to receive messages from this contact. - Kapcsolódási kísérlet ahhoz a kiszolgálóhoz, amely az adott ismerőstől érkező üzenetek fogadására szolgál. + 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. No comment provided by engineer. @@ -6921,8 +6965,8 @@ A funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beáll Unless your contact deleted the connection or this link was already used, it might be a bug - please report it. To connect, please ask your contact to create another connection link and check that you have a stable network connection. - Hacsak az ismerőse nem törölte a kapcsolatot, vagy ez a hivatkozás már használatban volt, lehet hogy ez egy hiba – jelentse a problémát. -A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsolati 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. +A 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. No comment provided by engineer. @@ -7047,7 +7091,7 @@ A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsol Use from desktop - Használat számítógépről + Társítás számítógéppel No comment provided by engineer. @@ -7387,7 +7431,7 @@ A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsol You are already connected to %@. - Már kapcsolódva van hozzá: %@. + Ön már kapcsolódva van ehhez: %@. No comment provided by engineer. @@ -7429,7 +7473,7 @@ Csatlakozási ké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őstől érkező üzenetek fogadására szolgál. + 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. @@ -7479,7 +7523,7 @@ Csatlakozási kérés megismétlése? You can make it visible to your SimpleX contacts via Settings. - Láthatóvá teheti SimpleX ismerősök számára a Beállításokban. + Láthatóvá teheti a SimpleXbeli ismerősei számára a „Beállításokban”. No comment provided by engineer. @@ -7626,7 +7670,7 @@ Kapcsolódási kérés megismétlése? You will be connected when group link host's device is online, please wait or check later! - Akkor lesz kapcsolódva, amikor a csoportos hivatkozás tulajdonosának eszköze online lesz, várjon, vagy ellenőrizze később! + Akkor lesz kapcsolódva, amikor a csoporthivatkozás tulajdonosának eszköze online lesz, várjon, vagy ellenőrizze később! No comment provided by engineer. @@ -7691,7 +7735,7 @@ Kapcsolódási kérés megismétlése? Your SimpleX address - Az ön SimpleX címe + Profil SimpleX címe No comment provided by engineer. @@ -7714,11 +7758,19 @@ Kapcsolódási kérés megismétlése? A csevegési adatbázis nincs titkosítva – adjon meg egy jelmondatot a titkosításhoz. No comment provided by engineer. + + Your chat preferences + alert title + Your chat profiles Csevegési profilok 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 (%@). Ismerőse olyan fájlt küldött, amely meghaladja a jelenleg támogatott maximális méretet (%@). @@ -7764,13 +7816,15 @@ Kapcsolódási kérés megismétlése? A(z) **%@** nevű profilja megosztásra fog kerülni. No comment provided by engineer. - - Your profile is stored on your device and shared only with your contacts. -SimpleX servers cannot see your profile. - Profilja az eszközön van tárolva, és csak az ismerősökkel kerül megosztásra. -A SimpleX kiszolgálók nem látjhatják profilját. + + Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile. + Profilja az eszközön van tárolva, és csak az ismerősökkel kerül megosztásra. A SimpleX kiszolgálók nem látjhatják profilját. No comment provided by engineer. + + Your profile was changed. If you save it, the updated profile will be sent to all your contacts. + alert message + Your profile, contacts and delivered messages are stored on your device. Profilja, ismerősei és az elküldött üzenetei az eszközön kerülnek tárolásra. @@ -7808,7 +7862,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. [Star on GitHub](https://github.com/simplex-chat/simplex-chat) - [Csillag a GitHubon](https://github.com/simplex-chat/simplex-chat) + [Csillagozás a GitHubon](https://github.com/simplex-chat/simplex-chat) No comment provided by engineer. @@ -7888,7 +7942,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. bad message hash - hibás az üzenet ellenőrzőösszege + hibás az üzenet hasító értéke integrity error chat item @@ -7918,7 +7972,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. call error - hiba a hívásban + híváshiba call status @@ -8013,7 +8067,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. connecting call… - hívás kapcsolódik… + kapcsolódási hívás… call status @@ -8253,12 +8307,12 @@ A SimpleX kiszolgálók nem látjhatják profilját. incognito via group link - inkognitó a csoportos hivatkozáson keresztül + inkognitó a csoporthivatkozáson keresztül chat list item description incognito via one-time link - inkognitó az egyszer használatos hivatkozáson keresztül + inkognitó egy egyszer használatos hivatkozáson keresztül chat list item description @@ -8308,7 +8362,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. invited via your group link - meghíva az ön csoport hivatkozásán keresztül + meghíva az ön csoporthivatkozásán keresztül rcv group event chat item @@ -8328,7 +8382,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. marked deleted - töröltnek jelölve + törlésre jelölve marked deleted chat item preview text @@ -8470,7 +8524,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. received answer… - fogadott válasz… + válasz fogadása… No comment provided by engineer. @@ -8644,12 +8698,12 @@ utoljára fogadott üzenet: %2$@ via group link - csoport hivatkozáson keresztül + a csoporthivatkozáson keresztül chat list item description via one-time link - egyszer használatos hivatkozáson keresztül + egy egyszer használatos hivatkozáson keresztül chat list item description @@ -8714,7 +8768,7 @@ utoljára fogadott üzenet: %2$@ you blocked %@ - ön letiltotta %@-t + ön letiltotta őt: %@ snd group event chat item 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 72eb3561e3..a5e013fec2 100644 --- a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff +++ b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff @@ -137,6 +137,10 @@ %@ si vuole connettere! notification title + + %1$@, %2$@ + format for date separator in chat + %@, %@ and %lld members %@, %@ e %lld membri @@ -1020,6 +1024,10 @@ Auto-accetta immagini No comment provided by engineer. + + Auto-accept settings + alert title + Back Indietro @@ -1193,7 +1201,7 @@ Cancel Annulla - No comment provided by engineer. + alert button Cancel migration @@ -1341,6 +1349,10 @@ Preferenze della chat No comment provided by engineer. + + Chat preferences were changed. + alert message + Chat theme Tema della chat @@ -1740,6 +1752,10 @@ Questo è il tuo link una tantum! Versione core: v%@ No comment provided by engineer. + + Corner + No comment provided by engineer. + Correct name to %@? Correggere il nome a %@? @@ -2724,6 +2740,10 @@ Questo è il tuo link una tantum! Errore nella modifica dell'indirizzo No comment provided by engineer. + + Error changing connection profile + No comment provided by engineer. + Error changing role Errore nel cambio di ruolo @@ -2734,6 +2754,10 @@ Questo è il tuo link una tantum! Errore nella modifica dell'impostazione No comment provided by engineer. + + Error changing to incognito! + No comment provided by engineer. + Error connecting to forwarding server %@. Please try later. Errore di connessione al server di inoltro %@. Riprova più tardi. @@ -2854,6 +2878,10 @@ Questo è il tuo link una tantum! Errore nel caricamento dei server %@ No comment provided by engineer. + + Error migrating settings + No comment provided by engineer. + Error opening chat Errore di apertura della chat @@ -2954,6 +2982,10 @@ Questo è il tuo link una tantum! Errore nell'interruzione della chat No comment provided by engineer. + + Error switching profile + No comment provided by engineer. + Error switching profile! Errore nel cambio di profilo! @@ -3289,11 +3321,6 @@ Errore: %2$@ Nome completo (facoltativo) No comment provided by engineer. - - Full name: - Nome completo: - No comment provided by engineer. - Fully decentralized – visible only to members. Completamente decentralizzato: visibile solo ai membri. @@ -4184,6 +4211,10 @@ Questo è il tuo link per il gruppo %@! Server dei messaggi No comment provided by engineer. + + Message shape + No comment provided by engineer. + Message source remains private. La fonte del messaggio resta privata. @@ -5021,16 +5052,6 @@ Errore: %@ Immagini del profilo No comment provided by engineer. - - Profile name - Nome del profilo - No comment provided by engineer. - - - Profile name: - Nome del profilo: - No comment provided by engineer. - Profile password Password del profilo @@ -5354,6 +5375,10 @@ Attivalo nelle impostazioni *Rete e server*. Rimuovi No comment provided by engineer. + + Remove archive? + No comment provided by engineer. + Remove image Rimuovi immagine @@ -5547,12 +5572,13 @@ Attivalo nelle impostazioni *Rete e server*. Save Salva - chat item action + alert button + chat item action Save (and notify contacts) Salva (e avvisa i contatti) - No comment provided by engineer. + alert button Save and notify contact @@ -5579,11 +5605,6 @@ Attivalo nelle impostazioni *Rete e server*. Salva archivio No comment provided by engineer. - - Save auto-accept settings - Salva le impostazioni di accettazione automatica - No comment provided by engineer. - Save group profile Salva il profilo del gruppo @@ -5619,16 +5640,15 @@ Attivalo nelle impostazioni *Rete e server*. Salvare i server? No comment provided by engineer. - - Save settings? - Salvare le impostazioni? - No comment provided by engineer. - Save welcome message? Salvare il messaggio di benvenuto? No comment provided by engineer. + + Save your profile? + alert title + Saved Salvato @@ -5729,6 +5749,10 @@ Attivalo nelle impostazioni *Rete e server*. Seleziona chat item action + + Select chat profile + No comment provided by engineer. + Selected %lld %lld selezionato @@ -6064,6 +6088,10 @@ Attivalo nelle impostazioni *Rete e server*. Impostazioni No comment provided by engineer. + + Settings were changed. + alert message + Shape profile images Forma delle immagini del profilo @@ -6099,6 +6127,10 @@ Attivalo nelle impostazioni *Rete e server*. Condividi link No comment provided by engineer. + + Share profile + No comment provided by engineer. + Share this 1-time invite link Condividi questo link di invito una tantum @@ -6264,6 +6296,10 @@ Attivalo nelle impostazioni *Rete e server*. Leggera blur media + + Some app settings were not migrated. + No comment provided by engineer. + Some file(s) were not exported: Alcuni file non sono stati esportati: @@ -6439,6 +6475,10 @@ Attivalo nelle impostazioni *Rete e server*. TCP_KEEPINTVL No comment provided by engineer. + + Tail + No comment provided by engineer. + Take picture Scatta foto @@ -6631,6 +6671,10 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa.Il testo che hai incollato non è un link SimpleX. No comment provided by engineer. + + The uploaded database archive will be permanently removed from the servers. + No comment provided by engineer. + Themes Temi @@ -7714,11 +7758,19 @@ Ripetere la richiesta di connessione? Il tuo database della chat non è crittografato: imposta la password per crittografarlo. No comment provided by engineer. + + Your chat preferences + alert title + Your chat profiles I tuoi profili di chat 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 (%@). Il tuo contatto ha inviato un file più grande della dimensione massima attualmente supportata (%@). @@ -7764,13 +7816,15 @@ Ripetere la richiesta di connessione? Verrà condiviso il tuo profilo **%@**. No comment provided by engineer. - - Your profile is stored on your device and shared only with your contacts. -SimpleX servers cannot see your profile. - Il tuo profilo è memorizzato sul tuo dispositivo e condiviso solo con i tuoi contatti. -I server di SimpleX non possono vedere il tuo profilo. + + Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile. + Il tuo profilo è memorizzato sul tuo dispositivo e condiviso solo con i tuoi contatti. I server di SimpleX non possono vedere il tuo profilo. No comment provided by engineer. + + Your profile was changed. If you save it, the updated profile will be sent to all your contacts. + alert message + Your profile, contacts and delivered messages are stored on your device. Il tuo profilo, i contatti e i messaggi recapitati sono memorizzati sul tuo dispositivo. 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 a545f3ba05..3edfb59c57 100644 --- a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff +++ b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff @@ -137,6 +137,10 @@ %@ が接続を希望しています! notification title + + %1$@, %2$@ + format for date separator in chat + %@, %@ and %lld members %@や%@など%lld人のメンバー @@ -990,6 +994,10 @@ 画像を自動的に受信 No comment provided by engineer. + + Auto-accept settings + alert title + Back 戻る @@ -1144,7 +1152,7 @@ Cancel 中止 - No comment provided by engineer. + alert button Cancel migration @@ -1283,6 +1291,10 @@ チャット設定 No comment provided by engineer. + + Chat preferences were changed. + alert message + Chat theme No comment provided by engineer. @@ -1640,6 +1652,10 @@ This is your own one-time link! コアのバージョン: v%@ No comment provided by engineer. + + Corner + No comment provided by engineer. + Correct name to %@? No comment provided by engineer. @@ -2569,6 +2585,10 @@ This is your own one-time link! アドレス変更にエラー発生 No comment provided by engineer. + + Error changing connection profile + No comment provided by engineer. + Error changing role 役割変更にエラー発生 @@ -2579,6 +2599,10 @@ This is your own one-time link! 設定変更にエラー発生 No comment provided by engineer. + + Error changing to incognito! + No comment provided by engineer. + Error connecting to forwarding server %@. Please try later. No comment provided by engineer. @@ -2694,6 +2718,10 @@ This is your own one-time link! %@ サーバーのロード中にエラーが発生 No comment provided by engineer. + + Error migrating settings + No comment provided by engineer. + Error opening chat No comment provided by engineer. @@ -2787,6 +2815,10 @@ This is your own one-time link! チャット停止にエラー発生 No comment provided by engineer. + + Error switching profile + No comment provided by engineer. + Error switching profile! プロフィール切り替えにエラー発生! @@ -3094,11 +3126,6 @@ Error: %2$@ フルネーム (任意): No comment provided by engineer. - - Full name: - フルネーム: - No comment provided by engineer. - Fully decentralized – visible only to members. No comment provided by engineer. @@ -3942,6 +3969,10 @@ This is your link for group %@! Message servers No comment provided by engineer. + + Message shape + No comment provided by engineer. + Message source remains private. No comment provided by engineer. @@ -4719,14 +4750,6 @@ Error: %@ Profile images No comment provided by engineer. - - Profile name - No comment provided by engineer. - - - Profile name: - No comment provided by engineer. - Profile password プロフィールのパスワード @@ -5027,6 +5050,10 @@ Enable in *Network & servers* settings. 削除 No comment provided by engineer. + + Remove archive? + No comment provided by engineer. + Remove image No comment provided by engineer. @@ -5205,12 +5232,13 @@ Enable in *Network & servers* settings. Save 保存 - chat item action + alert button + chat item action Save (and notify contacts) 保存(連絡先に通知) - No comment provided by engineer. + alert button Save and notify contact @@ -5236,11 +5264,6 @@ Enable in *Network & servers* settings. アーカイブを保存 No comment provided by engineer. - - Save auto-accept settings - 自動受け入れ設定を保存する - No comment provided by engineer. - Save group profile グループプロフィールの保存 @@ -5276,16 +5299,15 @@ Enable in *Network & servers* settings. サーバを保存しますか? No comment provided by engineer. - - Save settings? - 設定を保存しますか? - No comment provided by engineer. - Save welcome message? ウェルカムメッセージを保存しますか? No comment provided by engineer. + + Save your profile? + alert title + Saved No comment provided by engineer. @@ -5376,6 +5398,10 @@ Enable in *Network & servers* settings. 選択 chat item action + + Select chat profile + No comment provided by engineer. + Selected %lld No comment provided by engineer. @@ -5680,6 +5706,10 @@ Enable in *Network & servers* settings. 設定 No comment provided by engineer. + + Settings were changed. + alert message + Shape profile images No comment provided by engineer. @@ -5713,6 +5743,10 @@ Enable in *Network & servers* settings. リンクを送る No comment provided by engineer. + + Share profile + No comment provided by engineer. + Share this 1-time invite link No comment provided by engineer. @@ -5867,6 +5901,10 @@ Enable in *Network & servers* settings. Soft blur media + + Some app settings were not migrated. + No comment provided by engineer. + Some file(s) were not exported: No comment provided by engineer. @@ -6029,6 +6067,10 @@ Enable in *Network & servers* settings. TCP_KEEPINTVL No comment provided by engineer. + + Tail + No comment provided by engineer. + Take picture 写真を撮影 @@ -6212,6 +6254,10 @@ It can happen because of some bug or when the connection is compromised.The text you pasted is not a SimpleX link. 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. @@ -7207,11 +7253,19 @@ Repeat connection request? チャット データベースは暗号化されていません - 暗号化するにはパスフレーズを設定してください。 No comment provided by engineer. + + Your chat preferences + alert title + 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 (%@). 連絡先が現在サポートされている最大サイズ (%@) より大きいファイルを送信しました。 @@ -7256,13 +7310,15 @@ Repeat connection request? あなたのプロファイル **%@** が共有されます。 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 サーバーはあなたのプロファイルを参照できません。 + + Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile. + プロフィールはデバイスに保存され、連絡先とのみ共有されます。 SimpleX サーバーはあなたのプロファイルを参照できません。 No comment provided by engineer. + + Your profile was changed. If you save it, the updated profile will be sent to all your contacts. + alert message + Your profile, contacts and delivered messages are stored on your device. あなたのプロフィール、連絡先、送信したメッセージがご自分の端末に保存されます。 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 15a8c01a64..61fdcc1258 100644 --- a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff @@ -137,6 +137,10 @@ %@ wil verbinding maken! notification title + + %1$@, %2$@ + format for date separator in chat + %@, %@ and %lld members %@, %@ en %lld leden @@ -1020,6 +1024,10 @@ Afbeeldingen automatisch accepteren No comment provided by engineer. + + Auto-accept settings + alert title + Back Terug @@ -1193,7 +1201,7 @@ Cancel Annuleren - No comment provided by engineer. + alert button Cancel migration @@ -1341,6 +1349,10 @@ Gesprek voorkeuren No comment provided by engineer. + + Chat preferences were changed. + alert message + Chat theme Chat thema @@ -1740,6 +1752,10 @@ Dit is uw eigen eenmalige link! Core versie: v% @ No comment provided by engineer. + + Corner + No comment provided by engineer. + Correct name to %@? Juiste naam voor %@? @@ -2724,6 +2740,10 @@ Dit is uw eigen eenmalige link! Fout bij wijzigen van adres No comment provided by engineer. + + Error changing connection profile + No comment provided by engineer. + Error changing role Fout bij wisselen van rol @@ -2734,6 +2754,10 @@ Dit is uw eigen eenmalige link! Fout bij wijzigen van instelling No comment provided by engineer. + + Error changing to incognito! + No comment provided by engineer. + Error connecting to forwarding server %@. Please try later. Fout bij het verbinden met doorstuurserver %@. Probeer het later opnieuw. @@ -2854,6 +2878,10 @@ Dit is uw eigen eenmalige link! Fout bij het laden van %@ servers No comment provided by engineer. + + Error migrating settings + No comment provided by engineer. + Error opening chat Fout bij het openen van de chat @@ -2954,6 +2982,10 @@ Dit is uw eigen eenmalige link! Fout bij het stoppen van de chat No comment provided by engineer. + + Error switching profile + No comment provided by engineer. + Error switching profile! Fout bij wisselen van profiel! @@ -3289,11 +3321,6 @@ Fout: %2$@ Volledige naam (optioneel) No comment provided by engineer. - - Full name: - Volledige naam: - No comment provided by engineer. - Fully decentralized – visible only to members. Volledig gedecentraliseerd – alleen zichtbaar voor leden. @@ -4184,6 +4211,10 @@ Dit is jouw link voor groep %@! Berichtservers No comment provided by engineer. + + Message shape + No comment provided by engineer. + Message source remains private. Berichtbron blijft privé. @@ -5021,16 +5052,6 @@ Fout: %@ Profiel afbeeldingen No comment provided by engineer. - - Profile name - Profielnaam - No comment provided by engineer. - - - Profile name: - Profielnaam: - No comment provided by engineer. - Profile password Profiel wachtwoord @@ -5354,6 +5375,10 @@ Schakel dit in in *Netwerk en servers*-instellingen. Verwijderen No comment provided by engineer. + + Remove archive? + No comment provided by engineer. + Remove image Verwijder afbeelding @@ -5547,12 +5572,13 @@ Schakel dit in in *Netwerk en servers*-instellingen. Save Opslaan - chat item action + alert button + chat item action Save (and notify contacts) Bewaar (en informeer contacten) - No comment provided by engineer. + alert button Save and notify contact @@ -5579,11 +5605,6 @@ Schakel dit in in *Netwerk en servers*-instellingen. Bewaar archief No comment provided by engineer. - - Save auto-accept settings - Sla instellingen voor automatisch accepteren op - No comment provided by engineer. - Save group profile Groep profiel opslaan @@ -5619,16 +5640,15 @@ Schakel dit in in *Netwerk en servers*-instellingen. Servers opslaan? No comment provided by engineer. - - Save settings? - Instellingen opslaan? - No comment provided by engineer. - Save welcome message? Welkom bericht opslaan? No comment provided by engineer. + + Save your profile? + alert title + Saved Opgeslagen @@ -5729,6 +5749,10 @@ Schakel dit in in *Netwerk en servers*-instellingen. Selecteer chat item action + + Select chat profile + No comment provided by engineer. + Selected %lld %lld geselecteerd @@ -6064,6 +6088,10 @@ Schakel dit in in *Netwerk en servers*-instellingen. Instellingen No comment provided by engineer. + + Settings were changed. + alert message + Shape profile images Vorm profiel afbeeldingen @@ -6099,6 +6127,10 @@ Schakel dit in in *Netwerk en servers*-instellingen. Deel link No comment provided by engineer. + + Share profile + No comment provided by engineer. + Share this 1-time invite link Deel deze eenmalige uitnodigingslink @@ -6264,6 +6296,10 @@ Schakel dit in in *Netwerk en servers*-instellingen. Soft blur media + + Some app settings were not migrated. + No comment provided by engineer. + Some file(s) were not exported: Sommige bestanden zijn niet geëxporteerd: @@ -6439,6 +6475,10 @@ Schakel dit in in *Netwerk en servers*-instellingen. TCP_KEEPINTVL No comment provided by engineer. + + Tail + No comment provided by engineer. + Take picture Foto nemen @@ -6631,6 +6671,10 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast. De tekst die u hebt geplakt is geen SimpleX link. No comment provided by engineer. + + The uploaded database archive will be permanently removed from the servers. + No comment provided by engineer. + Themes Thema's @@ -7714,11 +7758,19 @@ Verbindingsverzoek herhalen? Uw chat database is niet versleuteld, stel een wachtwoord in om deze te versleutelen. No comment provided by engineer. + + Your chat preferences + alert title + Your chat profiles Uw chat profielen 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 (%@). Uw contact heeft een bestand verzonden dat groter is dan de momenteel ondersteunde maximale grootte (%@). @@ -7764,13 +7816,15 @@ Verbindingsverzoek herhalen? Uw profiel **%@** wordt gedeeld. No comment provided by engineer. - - Your profile is stored on your device and shared only with your contacts. -SimpleX servers cannot see your profile. - Uw profiel wordt op uw apparaat opgeslagen en alleen gedeeld met uw contacten. -SimpleX servers kunnen uw profiel niet zien. + + Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile. + Uw profiel wordt op uw apparaat opgeslagen en alleen gedeeld met uw contacten. SimpleX servers kunnen uw profiel niet zien. No comment provided by engineer. + + Your profile was changed. If you save it, the updated profile will be sent to all your contacts. + alert message + Your profile, contacts and delivered messages are stored on your device. Uw profiel, contacten en afgeleverde berichten worden op uw apparaat 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 525d30daa6..87f19d69a3 100644 --- a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff @@ -137,6 +137,10 @@ %@ chce się połączyć! notification title + + %1$@, %2$@ + format for date separator in chat + %@, %@ and %lld members %@, %@ i %lld członków @@ -1020,6 +1024,10 @@ Automatyczne akceptowanie obrazów No comment provided by engineer. + + Auto-accept settings + alert title + Back Wstecz @@ -1193,7 +1201,7 @@ Cancel Anuluj - No comment provided by engineer. + alert button Cancel migration @@ -1341,6 +1349,10 @@ Preferencje czatu No comment provided by engineer. + + Chat preferences were changed. + alert message + Chat theme Motyw czatu @@ -1740,6 +1752,10 @@ To jest twój jednorazowy link! Wersja rdzenia: v%@ No comment provided by engineer. + + Corner + No comment provided by engineer. + Correct name to %@? Poprawić imię na %@? @@ -2724,6 +2740,10 @@ To jest twój jednorazowy link! Błąd zmiany adresu No comment provided by engineer. + + Error changing connection profile + No comment provided by engineer. + Error changing role Błąd zmiany roli @@ -2734,6 +2754,10 @@ To jest twój jednorazowy link! Błąd zmiany ustawienia No comment provided by engineer. + + Error changing to incognito! + No comment provided by engineer. + Error connecting to forwarding server %@. Please try later. Błąd połączenia z serwerem przekierowania %@. Spróbuj ponownie później. @@ -2854,6 +2878,10 @@ To jest twój jednorazowy link! Błąd ładowania %@ serwerów No comment provided by engineer. + + Error migrating settings + No comment provided by engineer. + Error opening chat Błąd otwierania czatu @@ -2954,6 +2982,10 @@ To jest twój jednorazowy link! Błąd zatrzymania czatu No comment provided by engineer. + + Error switching profile + No comment provided by engineer. + Error switching profile! Błąd przełączania profilu! @@ -3289,11 +3321,6 @@ Błąd: %2$@ Pełna nazwa (opcjonalna) No comment provided by engineer. - - Full name: - Pełna nazwa: - No comment provided by engineer. - Fully decentralized – visible only to members. W pełni zdecentralizowana – widoczna tylko dla członków. @@ -4184,6 +4211,10 @@ To jest twój link do grupy %@! Serwery wiadomości No comment provided by engineer. + + Message shape + No comment provided by engineer. + Message source remains private. Źródło wiadomości pozostaje prywatne. @@ -5021,16 +5052,6 @@ Błąd: %@ Zdjęcia profilowe No comment provided by engineer. - - Profile name - Nazwa profilu - No comment provided by engineer. - - - Profile name: - Nazwa profilu: - No comment provided by engineer. - Profile password Hasło profilu @@ -5354,6 +5375,10 @@ Włącz w ustawianiach *Sieć i serwery* . Usuń No comment provided by engineer. + + Remove archive? + No comment provided by engineer. + Remove image Usuń obraz @@ -5547,12 +5572,13 @@ Włącz w ustawianiach *Sieć i serwery* . Save Zapisz - chat item action + alert button + chat item action Save (and notify contacts) Zapisz (i powiadom kontakty) - No comment provided by engineer. + alert button Save and notify contact @@ -5579,11 +5605,6 @@ Włącz w ustawianiach *Sieć i serwery* . Zapisz archiwum No comment provided by engineer. - - Save auto-accept settings - Zapisz ustawienia automatycznej akceptacji - No comment provided by engineer. - Save group profile Zapisz profil grupy @@ -5619,16 +5640,15 @@ Włącz w ustawianiach *Sieć i serwery* . Zapisać serwery? No comment provided by engineer. - - Save settings? - Zapisać ustawienia? - No comment provided by engineer. - Save welcome message? Zapisać wiadomość powitalną? No comment provided by engineer. + + Save your profile? + alert title + Saved Zapisane @@ -5729,6 +5749,10 @@ Włącz w ustawianiach *Sieć i serwery* . Wybierz chat item action + + Select chat profile + No comment provided by engineer. + Selected %lld Zaznaczono %lld @@ -6064,6 +6088,10 @@ Włącz w ustawianiach *Sieć i serwery* . Ustawienia No comment provided by engineer. + + Settings were changed. + alert message + Shape profile images Kształtuj obrazy profilowe @@ -6099,6 +6127,10 @@ Włącz w ustawianiach *Sieć i serwery* . Udostępnij link No comment provided by engineer. + + Share profile + No comment provided by engineer. + Share this 1-time invite link Udostępnij ten jednorazowy link @@ -6264,6 +6296,10 @@ Włącz w ustawianiach *Sieć i serwery* . Łagodny blur media + + Some app settings were not migrated. + No comment provided by engineer. + Some file(s) were not exported: Niektóre plik(i) nie zostały wyeksportowane: @@ -6439,6 +6475,10 @@ Włącz w ustawianiach *Sieć i serwery* . TCP_KEEPINTVL No comment provided by engineer. + + Tail + No comment provided by engineer. + Take picture Zrób zdjęcie @@ -6631,6 +6671,10 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom Tekst, który wkleiłeś nie jest linkiem SimpleX. No comment provided by engineer. + + The uploaded database archive will be permanently removed from the servers. + No comment provided by engineer. + Themes Motywy @@ -7714,11 +7758,19 @@ Powtórzyć prośbę połączenia? Baza danych czatu nie jest szyfrowana - ustaw hasło, aby ją zaszyfrować. No comment provided by engineer. + + Your chat preferences + alert title + Your chat profiles Twoje profile czatu 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 (%@). Twój kontakt wysłał plik, który jest większy niż obecnie obsługiwany maksymalny rozmiar (%@). @@ -7764,13 +7816,15 @@ Powtórzyć prośbę połączenia? Twój profil **%@** zostanie udostępniony. No comment provided by engineer. - - Your profile is stored on your device and shared only with your contacts. -SimpleX servers cannot see your profile. - Twój profil jest przechowywany na urządzeniu i udostępniany tylko Twoim kontaktom. -Serwery SimpleX nie mogą zobaczyć Twojego profilu. + + Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile. + Twój profil jest przechowywany na urządzeniu i udostępniany tylko Twoim kontaktom. Serwery SimpleX nie mogą zobaczyć Twojego profilu. No comment provided by engineer. + + Your profile was changed. If you save it, the updated profile will be sent to all your contacts. + alert message + Your profile, contacts and delivered messages are stored on your device. Twój profil, kontakty i dostarczone wiadomości są przechowywane na Twoim urządzeniu. 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 969a7d68e0..f2e278c9a4 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff @@ -137,6 +137,10 @@ %@ хочет соединиться! notification title + + %1$@, %2$@ + format for date separator in chat + %@, %@ and %lld members %@, %@ и %lld членов группы @@ -1020,6 +1024,10 @@ Автоприем изображений No comment provided by engineer. + + Auto-accept settings + alert title + Back Назад @@ -1193,7 +1201,7 @@ Cancel Отменить - No comment provided by engineer. + alert button Cancel migration @@ -1341,6 +1349,10 @@ Предпочтения No comment provided by engineer. + + Chat preferences were changed. + alert message + Chat theme Тема чата @@ -1740,6 +1752,10 @@ This is your own one-time link! Версия ядра: v%@ No comment provided by engineer. + + Corner + No comment provided by engineer. + Correct name to %@? Исправить имя на %@? @@ -2724,6 +2740,10 @@ This is your own one-time link! Ошибка при изменении адреса No comment provided by engineer. + + Error changing connection profile + No comment provided by engineer. + Error changing role Ошибка при изменении роли @@ -2734,6 +2754,10 @@ This is your own one-time link! Ошибка при изменении настройки No comment provided by engineer. + + Error changing to incognito! + No comment provided by engineer. + Error connecting to forwarding server %@. Please try later. Ошибка подключения к пересылающему серверу %@. Попробуйте позже. @@ -2854,6 +2878,10 @@ This is your own one-time link! Ошибка загрузки %@ серверов No comment provided by engineer. + + Error migrating settings + No comment provided by engineer. + Error opening chat Ошибка доступа к чату @@ -2954,6 +2982,10 @@ This is your own one-time link! Ошибка при остановке чата No comment provided by engineer. + + Error switching profile + No comment provided by engineer. + Error switching profile! Ошибка выбора профиля! @@ -3289,11 +3321,6 @@ Error: %2$@ Полное имя (не обязательно) No comment provided by engineer. - - Full name: - Полное имя: - No comment provided by engineer. - Fully decentralized – visible only to members. Группа полностью децентрализована – она видна только членам. @@ -4184,6 +4211,10 @@ This is your link for group %@! Серверы сообщений No comment provided by engineer. + + Message shape + No comment provided by engineer. + Message source remains private. Источник сообщения остаётся конфиденциальным. @@ -5021,16 +5052,6 @@ Error: %@ Картинки профилей No comment provided by engineer. - - Profile name - Имя профиля - No comment provided by engineer. - - - Profile name: - Имя профиля: - No comment provided by engineer. - Profile password Пароль профиля @@ -5354,6 +5375,10 @@ Enable in *Network & servers* settings. Удалить No comment provided by engineer. + + Remove archive? + No comment provided by engineer. + Remove image Удалить изображение @@ -5547,12 +5572,13 @@ Enable in *Network & servers* settings. Save Сохранить - chat item action + alert button + chat item action Save (and notify contacts) Сохранить (и уведомить контакты) - No comment provided by engineer. + alert button Save and notify contact @@ -5579,11 +5605,6 @@ Enable in *Network & servers* settings. Сохранить архив No comment provided by engineer. - - Save auto-accept settings - Сохранить настройки автоприема - No comment provided by engineer. - Save group profile Сохранить профиль группы @@ -5619,16 +5640,15 @@ Enable in *Network & servers* settings. Сохранить серверы? No comment provided by engineer. - - Save settings? - Сохранить настройки? - No comment provided by engineer. - Save welcome message? Сохранить приветственное сообщение? No comment provided by engineer. + + Save your profile? + alert title + Saved Сохранено @@ -5729,6 +5749,10 @@ Enable in *Network & servers* settings. Выбрать chat item action + + Select chat profile + No comment provided by engineer. + Selected %lld Выбрано %lld @@ -6064,6 +6088,10 @@ Enable in *Network & servers* settings. Настройки No comment provided by engineer. + + Settings were changed. + alert message + Shape profile images Форма картинок профилей @@ -6099,6 +6127,10 @@ Enable in *Network & servers* settings. Поделиться ссылкой No comment provided by engineer. + + Share profile + No comment provided by engineer. + Share this 1-time invite link Поделиться одноразовой ссылкой-приглашением @@ -6264,6 +6296,10 @@ Enable in *Network & servers* settings. Слабое blur media + + Some app settings were not migrated. + No comment provided by engineer. + Some file(s) were not exported: Некоторые файл(ы) не были экспортированы: @@ -6439,6 +6475,10 @@ Enable in *Network & servers* settings. TCP_KEEPINTVL No comment provided by engineer. + + Tail + No comment provided by engineer. + Take picture Сделать фото @@ -6631,6 +6671,10 @@ It can happen because of some bug or when the connection is compromised.Вставленный текст не является SimpleX-ссылкой. No comment provided by engineer. + + The uploaded database archive will be permanently removed from the servers. + No comment provided by engineer. + Themes Темы @@ -7714,11 +7758,19 @@ Repeat connection request? База данных НЕ зашифрована. Установите пароль, чтобы защитить Ваши данные. No comment provided by engineer. + + Your chat preferences + alert title + 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 (%@). Ваш контакт отправил файл, размер которого превышает максимальный размер (%@). @@ -7764,13 +7816,15 @@ Repeat connection request? Будет отправлен Ваш профиль **%@**. 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 серверы не могут получить доступ к Вашему профилю. + + Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile. + Ваш профиль хранится на Вашем устройстве и отправляется только Вашим контактам. SimpleX серверы не могут получить доступ к Вашему профилю. No comment provided by engineer. + + Your profile was changed. If you save it, the updated profile will be sent to all your contacts. + alert message + Your profile, contacts and delivered messages are stored on your device. Ваш профиль, контакты и доставленные сообщения хранятся на Вашем устройстве. 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 646a94a337..870c01af8f 100644 --- a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff +++ b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff @@ -129,6 +129,10 @@ %@ อยากเชื่อมต่อ! notification title + + %1$@, %2$@ + format for date separator in chat + %@, %@ and %lld members No comment provided by engineer. @@ -959,6 +963,10 @@ ยอมรับภาพอัตโนมัติ No comment provided by engineer. + + Auto-accept settings + alert title + Back กลับ @@ -1112,7 +1120,7 @@ Cancel ยกเลิก - No comment provided by engineer. + alert button Cancel migration @@ -1251,6 +1259,10 @@ ค่ากําหนดในการแชท No comment provided by engineer. + + Chat preferences were changed. + alert message + Chat theme No comment provided by engineer. @@ -1606,6 +1618,10 @@ This is your own one-time link! รุ่นหลัก: v%@ No comment provided by engineer. + + Corner + No comment provided by engineer. + Correct name to %@? No comment provided by engineer. @@ -2530,6 +2546,10 @@ This is your own one-time link! เกิดข้อผิดพลาดในการเปลี่ยนที่อยู่ No comment provided by engineer. + + Error changing connection profile + No comment provided by engineer. + Error changing role เกิดข้อผิดพลาดในการเปลี่ยนบทบาท @@ -2540,6 +2560,10 @@ This is your own one-time link! เกิดข้อผิดพลาดในการเปลี่ยนการตั้งค่า No comment provided by engineer. + + Error changing to incognito! + No comment provided by engineer. + Error connecting to forwarding server %@. Please try later. No comment provided by engineer. @@ -2654,6 +2678,10 @@ This is your own one-time link! โหลดเซิร์ฟเวอร์ %@ ผิดพลาด No comment provided by engineer. + + Error migrating settings + No comment provided by engineer. + Error opening chat No comment provided by engineer. @@ -2747,6 +2775,10 @@ This is your own one-time link! เกิดข้อผิดพลาดในการหยุดแชท No comment provided by engineer. + + Error switching profile + No comment provided by engineer. + Error switching profile! เกิดข้อผิดพลาดในการเปลี่ยนโปรไฟล์! @@ -3054,11 +3086,6 @@ Error: %2$@ ชื่อเต็ม (ไม่บังคับ) No comment provided by engineer. - - Full name: - ชื่อเต็ม: - No comment provided by engineer. - Fully decentralized – visible only to members. No comment provided by engineer. @@ -3901,6 +3928,10 @@ This is your link for group %@! Message servers No comment provided by engineer. + + Message shape + No comment provided by engineer. + Message source remains private. No comment provided by engineer. @@ -4672,14 +4703,6 @@ Error: %@ Profile images No comment provided by engineer. - - Profile name - No comment provided by engineer. - - - Profile name: - No comment provided by engineer. - Profile password รหัสผ่านโปรไฟล์ @@ -4979,6 +5002,10 @@ Enable in *Network & servers* settings. ลบ No comment provided by engineer. + + Remove archive? + No comment provided by engineer. + Remove image No comment provided by engineer. @@ -5157,12 +5184,13 @@ Enable in *Network & servers* settings. Save บันทึก - chat item action + alert button + chat item action Save (and notify contacts) บันทึก (และแจ้งผู้ติดต่อ) - No comment provided by engineer. + alert button Save and notify contact @@ -5188,11 +5216,6 @@ Enable in *Network & servers* settings. บันทึกไฟล์เก็บถาวร No comment provided by engineer. - - Save auto-accept settings - บันทึกการตั้งค่าการยอมรับอัตโนมัติ - No comment provided by engineer. - Save group profile บันทึกโปรไฟล์กลุ่ม @@ -5228,16 +5251,15 @@ Enable in *Network & servers* settings. บันทึกเซิร์ฟเวอร์? No comment provided by engineer. - - Save settings? - บันทึกการตั้งค่า? - No comment provided by engineer. - Save welcome message? บันทึกข้อความต้อนรับ? No comment provided by engineer. + + Save your profile? + alert title + Saved No comment provided by engineer. @@ -5328,6 +5350,10 @@ Enable in *Network & servers* settings. เลือก chat item action + + Select chat profile + No comment provided by engineer. + Selected %lld No comment provided by engineer. @@ -5637,6 +5663,10 @@ Enable in *Network & servers* settings. การตั้งค่า No comment provided by engineer. + + Settings were changed. + alert message + Shape profile images No comment provided by engineer. @@ -5670,6 +5700,10 @@ Enable in *Network & servers* settings. แชร์ลิงก์ No comment provided by engineer. + + Share profile + No comment provided by engineer. + Share this 1-time invite link No comment provided by engineer. @@ -5821,6 +5855,10 @@ Enable in *Network & servers* settings. Soft blur media + + Some app settings were not migrated. + No comment provided by engineer. + Some file(s) were not exported: No comment provided by engineer. @@ -5983,6 +6021,10 @@ Enable in *Network & servers* settings. TCP_KEEPINTVL No comment provided by engineer. + + Tail + No comment provided by engineer. + Take picture ถ่ายภาพ @@ -6167,6 +6209,10 @@ It can happen because of some bug or when the connection is compromised.The text you pasted is not a SimpleX link. 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. @@ -7158,11 +7204,19 @@ Repeat connection request? ฐานข้อมูลการแชทของคุณไม่ได้ถูก encrypt - ตั้งรหัสผ่านเพื่อ encrypt No comment provided by engineer. + + Your chat preferences + alert title + 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 (%@). ผู้ติดต่อของคุณส่งไฟล์ที่ใหญ่กว่าขนาดสูงสุดที่รองรับในปัจจุบัน (%@) @@ -7206,13 +7260,15 @@ Repeat connection request? Your profile **%@** will be shared. 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 ไม่สามารถดูโปรไฟล์ของคุณได้ + + Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile. + โปรไฟล์ของคุณจะถูกจัดเก็บไว้ในอุปกรณ์ของคุณและแชร์กับผู้ติดต่อของคุณเท่านั้น เซิร์ฟเวอร์ SimpleX ไม่สามารถดูโปรไฟล์ของคุณได้ No comment provided by engineer. + + Your profile was changed. If you save it, the updated profile will be sent to all your contacts. + alert message + Your profile, contacts and delivered messages are stored on your device. โปรไฟล์ รายชื่อผู้ติดต่อ และข้อความที่ส่งของคุณจะถูกจัดเก็บไว้ในอุปกรณ์ของคุณ 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 054f65110f..3e1199666f 100644 --- a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff @@ -137,6 +137,10 @@ %@ bağlanmak istiyor! notification title + + %1$@, %2$@ + format for date separator in chat + %@, %@ and %lld members %@, %@ ve %lld üyeleri @@ -1005,6 +1009,10 @@ Fotoğrafları otomatik kabul et No comment provided by engineer. + + Auto-accept settings + alert title + Back Geri @@ -1169,7 +1177,7 @@ Cancel İptal et - No comment provided by engineer. + alert button Cancel migration @@ -1313,6 +1321,10 @@ Sohbet tercihleri No comment provided by engineer. + + Chat preferences were changed. + alert message + Chat theme No comment provided by engineer. @@ -1689,6 +1701,10 @@ Bu senin kendi tek kullanımlık bağlantın! Çekirdek sürümü: v%@ No comment provided by engineer. + + Corner + No comment provided by engineer. + Correct name to %@? İsim %@ olarak düzeltilsin mi? @@ -2653,6 +2669,10 @@ Bu senin kendi tek kullanımlık bağlantın! Adres değiştirilirken hata oluştu No comment provided by engineer. + + Error changing connection profile + No comment provided by engineer. + Error changing role Rol değiştirilirken hata oluştu @@ -2663,6 +2683,10 @@ Bu senin kendi tek kullanımlık bağlantın! Ayar değiştirilirken hata oluştu No comment provided by engineer. + + Error changing to incognito! + No comment provided by engineer. + Error connecting to forwarding server %@. Please try later. No comment provided by engineer. @@ -2781,6 +2805,10 @@ Bu senin kendi tek kullanımlık bağlantın! %@ sunucuları yüklenirken hata oluştu No comment provided by engineer. + + Error migrating settings + No comment provided by engineer. + Error opening chat Sohbeti açarken sorun oluştu @@ -2878,6 +2906,10 @@ Bu senin kendi tek kullanımlık bağlantın! Sohbet durdurulurken hata oluştu No comment provided by engineer. + + Error switching profile + No comment provided by engineer. + Error switching profile! Profil değiştirilirken hata oluştu! @@ -3203,11 +3235,6 @@ Hata: %2$@ Bütün isim (opsiyonel) No comment provided by engineer. - - Full name: - Bütün isim: - No comment provided by engineer. - Fully decentralized – visible only to members. Tamamiyle merkezi olmayan - sadece kişilere görünür. @@ -4084,6 +4111,10 @@ Bu senin grup için bağlantın %@! Message servers No comment provided by engineer. + + Message shape + No comment provided by engineer. + Message source remains private. Mesaj kaynağı gizli kalır. @@ -4902,16 +4933,6 @@ Hata: %@ Profil resimleri No comment provided by engineer. - - Profile name - Profil ismi - No comment provided by engineer. - - - Profile name: - Profil ismi: - No comment provided by engineer. - Profile password Profil parolası @@ -5222,6 +5243,10 @@ Enable in *Network & servers* settings. Sil No comment provided by engineer. + + Remove archive? + No comment provided by engineer. + Remove image No comment provided by engineer. @@ -5408,12 +5433,13 @@ Enable in *Network & servers* settings. Save Kaydet - chat item action + alert button + chat item action Save (and notify contacts) Kaydet (ve kişilere bildir) - No comment provided by engineer. + alert button Save and notify contact @@ -5439,11 +5465,6 @@ Enable in *Network & servers* settings. Arşivi kaydet No comment provided by engineer. - - Save auto-accept settings - Otomatik kabul et ayarlarını kaydet - No comment provided by engineer. - Save group profile Grup profilini kaydet @@ -5479,16 +5500,15 @@ Enable in *Network & servers* settings. Sunucular kaydedilsin mi? No comment provided by engineer. - - Save settings? - Ayarlar kaydedilsin mi? - No comment provided by engineer. - Save welcome message? Hoşgeldin mesajı kaydedilsin mi? No comment provided by engineer. + + Save your profile? + alert title + Saved Kaydedildi @@ -5585,6 +5605,10 @@ Enable in *Network & servers* settings. Seç chat item action + + Select chat profile + No comment provided by engineer. + Selected %lld No comment provided by engineer. @@ -5904,6 +5928,10 @@ Enable in *Network & servers* settings. Ayarlar No comment provided by engineer. + + Settings were changed. + alert message + Shape profile images Profil resimlerini şekillendir @@ -5938,6 +5966,10 @@ Enable in *Network & servers* settings. Bağlantıyı paylaş No comment provided by engineer. + + Share profile + No comment provided by engineer. + Share this 1-time invite link Bu tek kullanımlık bağlantı davetini paylaş @@ -6098,6 +6130,10 @@ Enable in *Network & servers* settings. Soft blur media + + Some app settings were not migrated. + No comment provided by engineer. + Some file(s) were not exported: No comment provided by engineer. @@ -6264,6 +6300,10 @@ Enable in *Network & servers* settings. TCP_TVLDEKAL No comment provided by engineer. + + Tail + No comment provided by engineer. + Take picture Fotoğraf çek @@ -6453,6 +6493,10 @@ Bazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir. Yapıştırdığın metin bir SimpleX bağlantısı değildir. 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. @@ -7512,11 +7556,19 @@ Bağlantı isteği tekrarlansın mı? Sohbet veritabanınız şifrelenmemiş - şifrelemek için parola ayarlayın. No comment provided by engineer. + + Your chat preferences + alert title + Your chat profiles Sohbet profillerin 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 (%@). Kişiniz şu anda desteklenen maksimum boyuttan (%@) daha büyük bir dosya gönderdi. @@ -7562,13 +7614,15 @@ Bağlantı isteği tekrarlansın mı? Profiliniz **%@** paylaşılacaktır. No comment provided by engineer. - - Your profile is stored on your device and shared only with your contacts. -SimpleX servers cannot see your profile. - Profiliniz cihazınızda saklanır ve sadece kişilerinizle paylaşılır. -SimpleX sunucuları profilinizi göremez. + + Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile. + Profiliniz cihazınızda saklanır ve sadece kişilerinizle paylaşılır. SimpleX sunucuları profilinizi göremez. No comment provided by engineer. + + Your profile was changed. If you save it, the updated profile will be sent to all your contacts. + alert message + Your profile, contacts and delivered messages are stored on your device. Profiliniz, kişileriniz ve gönderilmiş mesajlar cihazınızda saklanı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 7bcb30c1db..d371b29109 100644 --- a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff @@ -137,6 +137,10 @@ %@ хоче підключитися! notification title + + %1$@, %2$@ + format for date separator in chat + %@, %@ and %lld members %@, %@ та %lld учасників @@ -1020,6 +1024,10 @@ Автоматичне прийняття зображень No comment provided by engineer. + + Auto-accept settings + alert title + Back Назад @@ -1193,7 +1201,7 @@ Cancel Скасувати - No comment provided by engineer. + alert button Cancel migration @@ -1341,6 +1349,10 @@ Налаштування чату No comment provided by engineer. + + Chat preferences were changed. + alert message + Chat theme Тема чату @@ -1740,6 +1752,10 @@ This is your own one-time link! Основна версія: v%@ No comment provided by engineer. + + Corner + No comment provided by engineer. + Correct name to %@? Виправити ім'я на %@? @@ -2724,6 +2740,10 @@ This is your own one-time link! Помилка зміни адреси No comment provided by engineer. + + Error changing connection profile + No comment provided by engineer. + Error changing role Помилка зміни ролі @@ -2734,6 +2754,10 @@ This is your own one-time link! Помилка зміни налаштування No comment provided by engineer. + + Error changing to incognito! + No comment provided by engineer. + Error connecting to forwarding server %@. Please try later. Помилка підключення до сервера переадресації %@. Спробуйте пізніше. @@ -2854,6 +2878,10 @@ This is your own one-time link! Помилка завантаження %@ серверів No comment provided by engineer. + + Error migrating settings + No comment provided by engineer. + Error opening chat Помилка відкриття чату @@ -2954,6 +2982,10 @@ This is your own one-time link! Помилка зупинки чату No comment provided by engineer. + + Error switching profile + No comment provided by engineer. + Error switching profile! Помилка перемикання профілю! @@ -3289,11 +3321,6 @@ Error: %2$@ Повне ім'я (необов'язково) No comment provided by engineer. - - Full name: - Повне ім'я: - No comment provided by engineer. - Fully decentralized – visible only to members. Повністю децентралізована - видима лише для учасників. @@ -4184,6 +4211,10 @@ This is your link for group %@! Сервери повідомлень No comment provided by engineer. + + Message shape + No comment provided by engineer. + Message source remains private. Джерело повідомлення залишається приватним. @@ -5021,16 +5052,6 @@ Error: %@ Зображення профілю No comment provided by engineer. - - Profile name - Назва профілю - No comment provided by engineer. - - - Profile name: - Ім'я профілю: - No comment provided by engineer. - Profile password Пароль до профілю @@ -5354,6 +5375,10 @@ Enable in *Network & servers* settings. Видалити No comment provided by engineer. + + Remove archive? + No comment provided by engineer. + Remove image Видалити зображення @@ -5547,12 +5572,13 @@ Enable in *Network & servers* settings. Save Зберегти - chat item action + alert button + chat item action Save (and notify contacts) Зберегти (і повідомити контактам) - No comment provided by engineer. + alert button Save and notify contact @@ -5579,11 +5605,6 @@ Enable in *Network & servers* settings. Зберегти архів No comment provided by engineer. - - Save auto-accept settings - Зберегти налаштування автоприйому - No comment provided by engineer. - Save group profile Зберегти профіль групи @@ -5619,16 +5640,15 @@ Enable in *Network & servers* settings. Зберегти сервери? No comment provided by engineer. - - Save settings? - Зберегти налаштування? - No comment provided by engineer. - Save welcome message? Зберегти вітальне повідомлення? No comment provided by engineer. + + Save your profile? + alert title + Saved Збережено @@ -5729,6 +5749,10 @@ Enable in *Network & servers* settings. Виберіть chat item action + + Select chat profile + No comment provided by engineer. + Selected %lld Вибрано %lld @@ -6064,6 +6088,10 @@ Enable in *Network & servers* settings. Налаштування No comment provided by engineer. + + Settings were changed. + alert message + Shape profile images Сформуйте зображення профілю @@ -6099,6 +6127,10 @@ Enable in *Network & servers* settings. Поділіться посиланням No comment provided by engineer. + + Share profile + No comment provided by engineer. + Share this 1-time invite link Поділіться цим одноразовим посиланням-запрошенням @@ -6264,6 +6296,10 @@ Enable in *Network & servers* settings. М'який blur media + + Some app settings were not migrated. + No comment provided by engineer. + Some file(s) were not exported: Деякі файли не було експортовано: @@ -6439,6 +6475,10 @@ Enable in *Network & servers* settings. TCP_KEEPINTVL No comment provided by engineer. + + Tail + No comment provided by engineer. + Take picture Сфотографуйте @@ -6631,6 +6671,10 @@ It can happen because of some bug or when the connection is compromised.Текст, який ви вставили, не є посиланням SimpleX. No comment provided by engineer. + + The uploaded database archive will be permanently removed from the servers. + No comment provided by engineer. + Themes Теми @@ -7714,11 +7758,19 @@ Repeat connection request? Ваша база даних чату не зашифрована - встановіть ключову фразу, щоб зашифрувати її. No comment provided by engineer. + + Your chat preferences + alert title + 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 (%@). Ваш контакт надіслав файл, розмір якого перевищує підтримуваний на цей момент максимальний розмір (%@). @@ -7764,13 +7816,15 @@ Repeat connection request? Ваш профіль **%@** буде опублікований. 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 не бачать ваш профіль. + + Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile. + Ваш профіль зберігається на вашому пристрої і доступний лише вашим контактам. Сервери SimpleX не бачать ваш профіль. No comment provided by engineer. + + Your profile was changed. If you save it, the updated profile will be sent to all your contacts. + alert message + Your profile, contacts and delivered messages are stored on your device. Ваш профіль, контакти та доставлені повідомлення зберігаються на вашому пристрої. 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 8c3641549d..b7abefd465 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 @@ -109,6 +109,7 @@ %@ downloaded + %@ 已下载 No comment provided by engineer. @@ -128,6 +129,7 @@ %@ uploaded + %@ 已上传 No comment provided by engineer. @@ -135,6 +137,10 @@ %@ 要连接! notification title + + %1$@, %2$@ + format for date separator in chat + %@, %@ and %lld members %@, %@ 和 %lld 成员 @@ -222,14 +228,17 @@ %lld messages blocked by admin + %lld 被管理员阻止的消息 No comment provided by engineer. %lld messages marked deleted + %lld 标记为已删除的消息 No comment provided by engineer. %lld messages moderated by %@ + %lld 审核的留言 by %@ No comment provided by engineer. @@ -304,10 +313,12 @@ (new) + (新) No comment provided by engineer. (this device v%@) + (此设备 v%@) No comment provided by engineer. @@ -317,6 +328,7 @@ **Add contact**: to create a new invitation link, or connect via a link you received. + **添加联系人**: 创建新的邀请链接,或通过您收到的链接进行连接. No comment provided by engineer. @@ -326,6 +338,7 @@ **Create group**: to create a new group. + **创建群组**: 创建一个新群组. No comment provided by engineer. @@ -340,6 +353,7 @@ **Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection. + **请注意**: 在两台设备上使用相同的数据库将破坏来自您的连接的消息解密,作为一种安全保护. No comment provided by engineer. @@ -359,6 +373,7 @@ **Warning**: the archive will be removed. + **警告**: 存档将被删除. No comment provided by engineer. @@ -424,6 +439,7 @@ 0 sec + 0 秒 time to disappear @@ -542,6 +558,7 @@ Accent + 强调 No comment provided by engineer. @@ -569,14 +586,17 @@ Acknowledged + 确认 No comment provided by engineer. Acknowledgement errors + 确认错误 No comment provided by engineer. Active connections + 活动连接 No comment provided by engineer. @@ -621,14 +641,17 @@ Additional accent + 附加重音 No comment provided by engineer. Additional accent 2 + 附加重音 2 No comment provided by engineer. Additional secondary + 附加二级 No comment provided by engineer. @@ -658,6 +681,7 @@ Advanced settings + 高级设置 No comment provided by engineer. @@ -677,6 +701,7 @@ All data is private to your device. + 所有数据都是您设备的私有数据. No comment provided by engineer. @@ -696,10 +721,12 @@ All new messages from %@ will be hidden! + 来自 %@ 的所有新消息都将被隐藏! No comment provided by engineer. All profiles + 所有配置文件 No comment provided by engineer. @@ -729,6 +756,7 @@ Allow calls? + 允许通话? No comment provided by engineer. @@ -738,11 +766,12 @@ Allow downgrade + 允许降级 No comment provided by engineer. Allow irreversible message deletion only if your contact allows it to you. (24 hours) - 仅有您的联系人许可后才允许不可撤回消息移除。 + 仅有您的联系人许可后才允许不可撤回消息移除 No comment provided by engineer. @@ -767,11 +796,12 @@ Allow sharing + 允许共享 No comment provided by engineer. Allow to irreversibly delete sent messages. (24 hours) - 允许不可撤回地删除已发送消息。 + 允许不可撤回地删除已发送消息 No comment provided by engineer. @@ -811,7 +841,7 @@ Allow your contacts to irreversibly delete sent messages. (24 hours) - 允许您的联系人不可撤回地删除已发送消息。 + 允许您的联系人不可撤回地删除已发送消息 No comment provided by engineer. @@ -841,6 +871,7 @@ Always use private routing. + 始终使用私有路由。 No comment provided by engineer. @@ -910,6 +941,7 @@ Apply to + 应用于 No comment provided by engineer. @@ -919,10 +951,12 @@ Archive contacts to chat later. + 存档联系人以便稍后聊天. No comment provided by engineer. Archived contacts + 已存档的联系人 No comment provided by engineer. @@ -990,6 +1024,10 @@ 自动接受图片 No comment provided by engineer. + + Auto-accept settings + alert title + Back 返回 @@ -997,6 +1035,7 @@ Background + 背景 No comment provided by engineer. @@ -1026,10 +1065,12 @@ Better networking + 更好的网络 No comment provided by engineer. Black + 黑色 No comment provided by engineer. @@ -1069,10 +1110,12 @@ Blur for better privacy. + 模糊处理,提高私密性. No comment provided by engineer. Blur media + 模糊媒体 No comment provided by engineer. @@ -1082,7 +1125,7 @@ Both you and your contact can irreversibly delete sent messages. (24 hours) - 您和您的联系人都可以不可逆转地删除已发送的消息。 + 您和您的联系人都可以不可逆转地删除已发送的消息 No comment provided by engineer. @@ -1122,6 +1165,7 @@ Calls prohibited! + 禁止来电! No comment provided by engineer. @@ -1131,10 +1175,12 @@ Can't call contact + 无法呼叫联系人 No comment provided by engineer. Can't call member + 无法呼叫成员 No comment provided by engineer. @@ -1149,12 +1195,13 @@ Can't message member + 无法向成员发送消息 No comment provided by engineer. Cancel 取消 - No comment provided by engineer. + alert button Cancel migration @@ -1168,6 +1215,7 @@ Cannot forward message + 无法转发消息 No comment provided by engineer. @@ -1177,6 +1225,7 @@ Capacity exceeded - recipient did not receive previously sent messages. + 超出容量-收件人未收到以前发送的邮件。 snd error text @@ -1242,6 +1291,7 @@ Chat colors + 聊天颜色 No comment provided by engineer. @@ -1261,6 +1311,7 @@ Chat database exported + 导出的聊天数据库 No comment provided by engineer. @@ -1285,6 +1336,7 @@ Chat list + 聊天列表 No comment provided by engineer. @@ -1297,8 +1349,13 @@ 聊天偏好设置 No comment provided by engineer. + + Chat preferences were changed. + alert message + Chat theme + 聊天主题 No comment provided by engineer. @@ -1318,6 +1375,7 @@ Choose _Migrate from another device_ on the new device and scan QR code. + 在新设备上选择“从另一个设备迁移”并扫描二维码。 No comment provided by engineer. @@ -1332,14 +1390,17 @@ Chunks deleted + 已删除的块 No comment provided by engineer. Chunks downloaded + 下载的块 No comment provided by engineer. Chunks uploaded + 已下载的区块 No comment provided by engineer. @@ -1369,10 +1430,12 @@ Color chats with the new themes. + 使用新主题为聊天着色。 No comment provided by engineer. Color mode + 颜色模式 No comment provided by engineer. @@ -1387,6 +1450,7 @@ Completed + 已完成 No comment provided by engineer. @@ -1396,6 +1460,7 @@ Configured %@ servers + 已配置 %@ 服务器 No comment provided by engineer. @@ -1410,6 +1475,7 @@ Confirm contact deletion? + 确认删除联系人? No comment provided by engineer. @@ -1419,6 +1485,7 @@ Confirm files from unknown servers. + 确认来自未知服务器的文件。 No comment provided by engineer. @@ -1468,6 +1535,7 @@ Connect to your friends faster. + 更快地与您的朋友联系。 No comment provided by engineer. @@ -1478,15 +1546,20 @@ Connect to yourself? This is your own SimpleX address! + 与自己建立联系? +这是您自己的 SimpleX 地址! No comment provided by engineer. Connect to yourself? This is your own one-time link! + 与自己建立联系? +这是您自己的一次性链接! No comment provided by engineer. Connect via contact address + 通过联系地址连接 No comment provided by engineer. @@ -1501,10 +1574,12 @@ This is your own one-time link! Connect with %@ + 与 %@连接 No comment provided by engineer. Connected + 已连接 No comment provided by engineer. @@ -1514,6 +1589,7 @@ This is your own one-time link! Connected servers + 已连接的服务器 No comment provided by engineer. @@ -1523,6 +1599,7 @@ This is your own one-time link! Connecting + 正在连接 No comment provided by engineer. @@ -1537,6 +1614,7 @@ This is your own one-time link! Connecting to contact, please wait or check later! + 正在连接到联系人,请稍候或稍后检查! No comment provided by engineer. @@ -1551,6 +1629,7 @@ This is your own one-time link! Connection and servers status. + 连接和服务器状态。 No comment provided by engineer. @@ -1565,6 +1644,7 @@ This is your own one-time link! Connection notifications + 连接通知 No comment provided by engineer. @@ -1584,10 +1664,12 @@ This is your own one-time link! Connection with desktop stopped + 与桌面的连接已停止 No comment provided by engineer. Connections + 连接 No comment provided by engineer. @@ -1602,6 +1684,7 @@ This is your own one-time link! Contact deleted! + 联系人已删除! No comment provided by engineer. @@ -1616,6 +1699,7 @@ This is your own one-time link! Contact is deleted. + 联系人被删除。 No comment provided by engineer. @@ -1630,6 +1714,7 @@ This is your own one-time link! Contact will be deleted - this cannot be undone! + 联系人将被删除-这是无法撤消的! No comment provided by engineer. @@ -1649,6 +1734,7 @@ This is your own one-time link! Conversation deleted! + 对话已删除! No comment provided by engineer. @@ -1658,6 +1744,7 @@ This is your own one-time link! Copy error + 复制错误 No comment provided by engineer. @@ -1665,8 +1752,13 @@ This is your own one-time link! 核心版本: v%@ No comment provided by engineer. + + Corner + No comment provided by engineer. + Correct name to %@? + 将名称更正为 %@? No comment provided by engineer. @@ -1681,7 +1773,7 @@ This is your own one-time link! Create a group using a random profile. - 使用随机身份创建群组 + 使用随机身份创建群组. No comment provided by engineer. @@ -1736,6 +1828,7 @@ This is your own one-time link! Created + 已创建 No comment provided by engineer. @@ -1745,6 +1838,7 @@ This is your own one-time link! Created at: %@ + 创建于:%@ copied message info @@ -1774,6 +1868,7 @@ This is your own one-time link! Current profile + 当前配置文件 No comment provided by engineer. @@ -1788,6 +1883,7 @@ This is your own one-time link! Customize theme + 自定义主题 No comment provided by engineer. @@ -1797,6 +1893,7 @@ This is your own one-time link! Dark mode colors + 深色模式颜色 No comment provided by engineer. @@ -1899,6 +1996,7 @@ This is your own one-time link! Debug delivery + 调试交付 No comment provided by engineer. @@ -1919,10 +2017,12 @@ This is your own one-time link! Delete %lld messages of members? + 删除成员的 %lld 消息? No comment provided by engineer. Delete %lld messages? + 删除 %lld 消息? No comment provided by engineer. @@ -1982,6 +2082,7 @@ This is your own one-time link! Delete contact? + 删除联系人? No comment provided by engineer. @@ -2091,6 +2192,7 @@ This is your own one-time link! Delete up to 20 messages at once. + 一次最多删除 20 条信息。 No comment provided by engineer. @@ -2100,10 +2202,12 @@ This is your own one-time link! Delete without notification + 删除而不通知 No comment provided by engineer. Deleted + 已删除 No comment provided by engineer. @@ -2118,6 +2222,7 @@ This is your own one-time link! Deletion errors + 删除错误 No comment provided by engineer. @@ -2147,6 +2252,7 @@ This is your own one-time link! Desktop app version %@ is not compatible with this app. + 桌面应用程序版本 %@ 与此应用程序不兼容。 No comment provided by engineer. @@ -2156,22 +2262,27 @@ This is your own one-time link! Destination server address of %@ is incompatible with forwarding server %@ settings. + 目标服务器地址 %@ 与转发服务器 %@ 设置不兼容。 No comment provided by engineer. Destination server error: %@ + 目标服务器错误:%@ snd error text Destination server version of %@ is incompatible with forwarding server %@. + 目标服务器版本 %@ 与转发服务器 %@ 不兼容。 No comment provided by engineer. Detailed statistics + 详细的统计数据 No comment provided by engineer. Details + 详细信息 No comment provided by engineer. @@ -2181,6 +2292,7 @@ This is your own one-time link! Developer options + 开发者选项 No comment provided by engineer. @@ -2235,6 +2347,7 @@ This is your own one-time link! Disabled + 禁用 No comment provided by engineer. @@ -2289,6 +2402,7 @@ This is your own one-time link! Do NOT send messages directly, even if your or destination server does not support private routing. + 请勿直接发送消息,即使您的服务器或目标服务器不支持私有路由。 No comment provided by engineer. @@ -2298,6 +2412,7 @@ This is your own one-time link! Do NOT use private routing. + 不要使用私有路由。 No comment provided by engineer. @@ -2337,6 +2452,7 @@ This is your own one-time link! Download errors + 下载错误 No comment provided by engineer. @@ -2351,10 +2467,12 @@ This is your own one-time link! Downloaded + 已下载 No comment provided by engineer. Downloaded files + 下载的文件 No comment provided by engineer. @@ -2459,6 +2577,7 @@ This is your own one-time link! Enabled + 已启用 No comment provided by engineer. @@ -2498,6 +2617,7 @@ This is your own one-time link! Encrypted message: app is stopped + 加密消息:应用程序已停止 notification @@ -2547,6 +2667,7 @@ This is your own one-time link! Enter group name… + 输入组名称… No comment provided by engineer. @@ -2586,6 +2707,7 @@ This is your own one-time link! Enter your name… + 请输入您的姓名… No comment provided by engineer. @@ -2618,6 +2740,10 @@ This is your own one-time link! 更改地址错误 No comment provided by engineer. + + Error changing connection profile + No comment provided by engineer. + Error changing role 更改角色错误 @@ -2628,8 +2754,13 @@ This is your own one-time link! 更改设置错误 No comment provided by engineer. + + Error changing to incognito! + No comment provided by engineer. + Error connecting to forwarding server %@. Please try later. + 连接到转发服务器 %@ 时出错。请稍后尝试。 No comment provided by engineer. @@ -2729,6 +2860,7 @@ This is your own one-time link! Error exporting theme: %@ + 导出主题时出错: %@ No comment provided by engineer. @@ -2746,8 +2878,13 @@ This is your own one-time link! 加载 %@ 服务器错误 No comment provided by engineer. + + Error migrating settings + No comment provided by engineer. + Error opening chat + 打开聊天时出错 No comment provided by engineer. @@ -2757,10 +2894,12 @@ This is your own one-time link! Error reconnecting server + 重新连接服务器时出错 No comment provided by engineer. Error reconnecting servers + 重新连接服务器时出错 No comment provided by engineer. @@ -2770,6 +2909,7 @@ This is your own one-time link! Error resetting statistics + 重置统计信息时出错 No comment provided by engineer. @@ -2809,6 +2949,7 @@ This is your own one-time link! Error scanning code: %@ + 扫描代码时出错:%@ No comment provided by engineer. @@ -2841,6 +2982,10 @@ This is your own one-time link! 停止聊天错误 No comment provided by engineer. + + Error switching profile + No comment provided by engineer. + Error switching profile! 切换资料错误! @@ -2904,6 +3049,7 @@ This is your own one-time link! Errors + 错误 No comment provided by engineer. @@ -2933,6 +3079,7 @@ This is your own one-time link! Export theme + 导出主题 No comment provided by engineer. @@ -2972,22 +3119,27 @@ This is your own one-time link! File error + 文件错误 No comment provided by engineer. File not found - most likely file was deleted or cancelled. + 找不到文件 - 很可能文件已被删除或取消。 file error text File server error: %@ + 文件服务器错误:%@ file error text File status + 文件状态 No comment provided by engineer. File status: %@ + 文件状态:%@ copied message info @@ -3012,6 +3164,7 @@ This is your own one-time link! Files + 文件 No comment provided by engineer. @@ -3051,7 +3204,7 @@ This is your own one-time link! Finalize migration on another device. - 在另一部设备上完成迁移 + 在另一部设备上完成迁移. No comment provided by engineer. @@ -3121,24 +3274,31 @@ This is your own one-time link! Forwarding server %@ failed to connect to destination server %@. Please try later. + 转发服务器 %@ 无法连接到目标服务器 %@。请稍后尝试。 No comment provided by engineer. Forwarding server address is incompatible with network settings: %@. + 转发服务器地址与网络设置不兼容:%@。 No comment provided by engineer. Forwarding server version is incompatible with network settings: %@. + 转发服务器版本与网络设置不兼容:%@。 No comment provided by engineer. Forwarding server: %1$@ Destination server error: %2$@ + 转发服务器: %1$@ +目标服务器错误: %2$@ snd error text Forwarding server: %1$@ Error: %2$@ + 转发服务器: %1$@ +错误: %2$@ snd error text @@ -3161,11 +3321,6 @@ Error: %2$@ 全名(可选) No comment provided by engineer. - - Full name: - 全名: - No comment provided by engineer. - Fully decentralized – visible only to members. 完全去中心化 - 仅对成员可见。 @@ -3188,10 +3343,12 @@ Error: %2$@ Good afternoon! + 下午好! message preview Good morning! + 早上好! message preview @@ -3201,6 +3358,7 @@ Error: %2$@ Group already exists + 群组已存在 No comment provided by engineer. @@ -3255,7 +3413,7 @@ Error: %2$@ Group members can irreversibly delete sent messages. (24 hours) - 群组成员可以不可撤回地删除已发送的消息。 + 群组成员可以不可撤回地删除已发送的消息 No comment provided by engineer. @@ -3400,6 +3558,7 @@ Error: %2$@ Hungarian interface + 匈牙利语界面 No comment provided by engineer. @@ -3474,6 +3633,7 @@ Error: %2$@ Import theme + 导入主题 No comment provided by engineer. @@ -3600,6 +3760,7 @@ Error: %2$@ Interface colors + 界面颜色 No comment provided by engineer. @@ -3634,6 +3795,7 @@ Error: %2$@ Invalid response + 无效的响应 No comment provided by engineer. @@ -3704,6 +3866,7 @@ Error: %2$@ It protects your IP address and connections. + 它可以保护您的 IP 地址和连接。 No comment provided by engineer. @@ -3748,11 +3911,14 @@ Error: %2$@ Join with current profile + 使用当前档案加入 No comment provided by engineer. Join your group? This is your link for group %@! + 加入您的群组? +这是您组 %@ 的链接! No comment provided by engineer. @@ -3767,10 +3933,12 @@ This is your link for group %@! Keep conversation + 保持对话 No comment provided by engineer. Keep the app open to use it from desktop + 保持应用程序打开状态以从桌面使用它 No comment provided by engineer. @@ -3945,10 +4113,12 @@ This is your link for group %@! Media & file servers + Media & file servers No comment provided by engineer. Medium + 中等 blur media @@ -3958,6 +4128,7 @@ This is your link for group %@! Member inactive + 成员不活跃 item status text @@ -3977,6 +4148,7 @@ This is your link for group %@! Menus + 菜单 No comment provided by engineer. @@ -3991,6 +4163,7 @@ This is your link for group %@! Message delivery warning + 消息传递警告 item status text @@ -4000,14 +4173,17 @@ This is your link for group %@! Message forwarded + 消息已转发 item status text Message may be delivered later if member becomes active. + 如果 member 变为活动状态,则稍后可能会发送消息。 item status description Message queue info + 消息队列信息 No comment provided by engineer. @@ -4027,10 +4203,16 @@ This is your link for group %@! Message reception + 消息接收 No comment provided by engineer. Message servers + 消息服务器 + No comment provided by engineer. + + + Message shape No comment provided by engineer. @@ -4040,10 +4222,12 @@ This is your link for group %@! Message status + 消息状态 No comment provided by engineer. Message status: %@ + 消息状态:%@ copied message info @@ -4068,22 +4252,27 @@ This is your link for group %@! Messages from %@ will be shown! + 将显示来自 %@ 的消息! No comment provided by engineer. Messages received + 收到的消息 No comment provided by engineer. Messages sent + 已发送的消息 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. + 消息、文件和通话受到 **端到端加密** 的保护,具有完全正向保密、否认和闯入恢复。 No comment provided by engineer. Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery. + 消息、文件和通话受到 **抗量子 e2e 加密** 的保护,具有完全正向保密、否认和闯入恢复。 No comment provided by engineer. @@ -4208,6 +4397,7 @@ This is your link for group %@! Network issues - message expired after many attempts to send it. + 网络问题 - 消息在多次尝试发送后过期。 snd error text @@ -4237,6 +4427,7 @@ This is your link for group %@! New chat experience 🎉 + 新的聊天体验 🎉 No comment provided by engineer. @@ -4271,6 +4462,7 @@ This is your link for group %@! New media options + 新媒体选项 No comment provided by engineer. @@ -4320,6 +4512,7 @@ This is your link for group %@! No direct connection yet, message is forwarded by admin. + 还没有直接连接,消息由管理员转发。 item status description @@ -4339,6 +4532,7 @@ This is your link for group %@! No info, try to reload + 无信息,尝试重新加载 No comment provided by engineer. @@ -4363,6 +4557,7 @@ This is your link for group %@! Nothing selected + 未选中任何内容 No comment provided by engineer. @@ -4417,13 +4612,15 @@ This is your link for group %@! Onion hosts will be **required** for connection. Requires compatible VPN. - Onion 主机将用于连接。需要启用 VPN。 + Onion 主机将是连接所必需的。 +需要兼容的 VPN。 No comment provided by engineer. Onion hosts will be used when available. Requires compatible VPN. - 当可用时,将使用 Onion 主机。需要启用 VPN。 + 如果可用,将使用洋葱主机。 +需要兼容的 VPN。 No comment provided by engineer. @@ -4438,6 +4635,7 @@ Requires compatible VPN. Only delete conversation + 仅删除对话 No comment provided by engineer. @@ -4462,7 +4660,7 @@ Requires compatible VPN. Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours) - 只有您可以不可撤回地删除消息(您的联系人可以将它们标记为删除)。 + 只有您可以不可撤回地删除消息(您的联系人可以将它们标记为删除) No comment provided by engineer. @@ -4487,7 +4685,7 @@ Requires compatible VPN. Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours) - 只有您的联系人才能不可撤回地删除消息(您可以将它们标记为删除)。 + 只有您的联系人才能不可撤回地删除消息(您可以将它们标记为删除) No comment provided by engineer. @@ -4532,10 +4730,12 @@ Requires compatible VPN. Open migration to another device + 打开迁移到另一台设备 authentication reason Open server settings + 打开服务器设置 No comment provided by engineer. @@ -4550,6 +4750,7 @@ Requires compatible VPN. Opening app… + 正在打开应用程序… No comment provided by engineer. @@ -4579,6 +4780,7 @@ Requires compatible VPN. Other %@ servers + 其他 %@ 服务器 No comment provided by engineer. @@ -4623,6 +4825,7 @@ Requires compatible VPN. Past member %@ + 前任成员 %@ past/unknown group member @@ -4647,6 +4850,7 @@ Requires compatible VPN. Pending + 待定 No comment provided by engineer. @@ -4671,10 +4875,12 @@ Requires compatible VPN. Play from the chat list. + 从聊天列表播放。 No comment provided by engineer. Please ask your contact to enable calls. + 请要求您的联系人开通通话功能。 No comment provided by engineer. @@ -4685,6 +4891,8 @@ Requires compatible VPN. Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection. Please share any other issues with the developers. + 请检查移动设备和桌面是否连接到同一本地网络,以及桌面防火墙是否允许连接。 +请与开发人员分享任何其他问题。 No comment provided by engineer. @@ -4710,6 +4918,8 @@ Please share any other issues with the developers. Please contact developers. Error: %@ + 请联系开发人员。 +错误:%@ No comment provided by engineer. @@ -4784,6 +4994,7 @@ Error: %@ Previously connected servers + 以前连接的服务器 No comment provided by engineer. @@ -4803,10 +5014,12 @@ Error: %@ Private message routing + 私有消息路由 No comment provided by engineer. Private message routing 🚀 + 私有消息路由 🚀 No comment provided by engineer. @@ -4816,10 +5029,12 @@ Error: %@ Private routing + 专用路由 No comment provided by engineer. Private routing error + 专用路由错误 No comment provided by engineer. @@ -4837,15 +5052,6 @@ Error: %@ 个人资料图 No comment provided by engineer. - - Profile name - No comment provided by engineer. - - - Profile name: - 显示名: - No comment provided by engineer. - Profile password 个人资料密码 @@ -4853,6 +5059,7 @@ Error: %@ Profile theme + 个人资料主题 No comment provided by engineer. @@ -4882,6 +5089,7 @@ Error: %@ Prohibit sending SimpleX links. + 禁止发送 SimpleX 链接。 No comment provided by engineer. @@ -4906,6 +5114,7 @@ Error: %@ Protect IP address + 保护 IP 地址 No comment provided by engineer. @@ -4916,6 +5125,8 @@ Error: %@ Protect your IP address from the messaging relays chosen by your contacts. Enable in *Network & servers* settings. + 保护您的 IP 地址免受联系人选择的消息中继的攻击。 +在*网络和服务器*设置中启用。 No comment provided by engineer. @@ -4935,10 +5146,12 @@ Enable in *Network & servers* settings. Proxied + 代理 No comment provided by engineer. Proxied servers + 代理服务器 No comment provided by engineer. @@ -4948,6 +5161,7 @@ Enable in *Network & servers* settings. Push server + 推送服务器 No comment provided by engineer. @@ -4962,6 +5176,7 @@ Enable in *Network & servers* settings. Reachable chat toolbar + 可访问的聊天工具栏 No comment provided by engineer. @@ -5011,6 +5226,7 @@ Enable in *Network & servers* settings. Receive errors + 接收错误 No comment provided by engineer. @@ -5035,14 +5251,17 @@ Enable in *Network & servers* settings. Received messages + 收到的消息 No comment provided by engineer. Received reply + 已收到回复 No comment provided by engineer. Received total + 接收总数 No comment provided by engineer. @@ -5062,6 +5281,7 @@ Enable in *Network & servers* settings. 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). + 最近的历史记录和改进的 [目录机器人](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). No comment provided by engineer. @@ -5076,6 +5296,7 @@ Enable in *Network & servers* settings. Reconnect + 重新连接 No comment provided by engineer. @@ -5085,18 +5306,22 @@ Enable in *Network & servers* settings. Reconnect all servers + 重新连接所有服务器 No comment provided by engineer. Reconnect all servers? + 重新连接所有服务器? No comment provided by engineer. Reconnect server to force message delivery. It uses additional traffic. + 重新连接服务器以强制发送信息。它使用额外的流量。 No comment provided by engineer. Reconnect server? + 重新连接服务器? No comment provided by engineer. @@ -5150,8 +5375,13 @@ Enable in *Network & servers* settings. 移除 No comment provided by engineer. + + Remove archive? + No comment provided by engineer. + Remove image + 移除图片 No comment provided by engineer. @@ -5226,14 +5456,17 @@ Enable in *Network & servers* settings. Reset all hints + 重置所有提示 No comment provided by engineer. Reset all statistics + 重置所有统计信息 No comment provided by engineer. Reset all statistics? + 重置所有统计信息? No comment provided by engineer. @@ -5243,6 +5476,7 @@ Enable in *Network & servers* settings. Reset to app theme + 重置为应用程序主题 No comment provided by engineer. @@ -5252,6 +5486,7 @@ Enable in *Network & servers* settings. Reset to user theme + 重置为用户主题 No comment provided by engineer. @@ -5321,10 +5556,12 @@ Enable in *Network & servers* settings. SMP server + SMP 服务器 No comment provided by engineer. Safely receive files + 安全接收文件 No comment provided by engineer. @@ -5335,12 +5572,13 @@ Enable in *Network & servers* settings. Save 保存 - chat item action + alert button + chat item action Save (and notify contacts) 保存(并通知联系人) - No comment provided by engineer. + alert button Save and notify contact @@ -5354,6 +5592,7 @@ Enable in *Network & servers* settings. Save and reconnect + 保存并重新连接 No comment provided by engineer. @@ -5366,11 +5605,6 @@ Enable in *Network & servers* settings. 保存存档 No comment provided by engineer. - - Save auto-accept settings - 保存自动接受设置 - No comment provided by engineer. - Save group profile 保存群组资料 @@ -5406,16 +5640,15 @@ Enable in *Network & servers* settings. 保存服务器? No comment provided by engineer. - - Save settings? - 保存设置? - No comment provided by engineer. - Save welcome message? 保存欢迎信息? No comment provided by engineer. + + Save your profile? + alert title + Saved 已保存 @@ -5438,10 +5671,12 @@ Enable in *Network & servers* settings. Scale + 规模 No comment provided by engineer. Scan / Paste link + 扫描 / 粘贴链接 No comment provided by engineer. @@ -5486,6 +5721,7 @@ Enable in *Network & servers* settings. Secondary + 二级 No comment provided by engineer. @@ -5495,6 +5731,7 @@ Enable in *Network & servers* settings. Secured + 担保 No comment provided by engineer. @@ -5512,12 +5749,18 @@ Enable in *Network & servers* settings. 选择 chat item action + + Select chat profile + No comment provided by engineer. + Selected %lld + 选定的 %lld No comment provided by engineer. Selected chat preferences prohibit this message. + 选定的聊天首选项禁止此消息。 No comment provided by engineer. @@ -5567,6 +5810,7 @@ Enable in *Network & servers* settings. Send errors + 发送错误 No comment provided by engineer. @@ -5581,14 +5825,17 @@ Enable in *Network & servers* settings. Send message to enable calls. + 发送消息以启用呼叫。 No comment provided by engineer. Send messages directly when IP address is protected and your or destination server does not support private routing. + 当 IP 地址受到保护并且您或目标服务器不支持私有路由时,直接发送消息。 No comment provided by engineer. Send messages directly when your or destination server does not support private routing. + 当您或目标服务器不支持私有路由时,直接发送消息。 No comment provided by engineer. @@ -5683,6 +5930,7 @@ Enable in *Network & servers* settings. Sent directly + 直接发送 No comment provided by engineer. @@ -5697,6 +5945,7 @@ Enable in *Network & servers* settings. Sent messages + 已发送的消息 No comment provided by engineer. @@ -5706,26 +5955,32 @@ Enable in *Network & servers* settings. Sent reply + 已发送回复 No comment provided by engineer. Sent total + 发送总数 No comment provided by engineer. Sent via proxy + 通过代理发送 No comment provided by engineer. Server address + 服务器地址 No comment provided by engineer. Server address is incompatible with network settings. + 服务器地址与网络设置不兼容。 srv error text. Server address is incompatible with network settings: %@. + 服务器地址与网络设置不兼容:%@。 No comment provided by engineer. @@ -5745,14 +6000,17 @@ Enable in *Network & servers* settings. Server type + 服务器类型 No comment provided by engineer. Server version is incompatible with network settings. + 服务器版本与网络设置不兼容。 srv error text Server version is incompatible with your app: %@. + 服务器版本与你的应用程序不兼容:%@。 No comment provided by engineer. @@ -5762,10 +6020,12 @@ Enable in *Network & servers* settings. Servers info + 服务器信息 No comment provided by engineer. Servers statistics will be reset - this cannot be undone! + 服务器统计信息将被重置 - 此操作无法撤消! No comment provided by engineer. @@ -5785,6 +6045,7 @@ Enable in *Network & servers* settings. Set default theme + 设置默认主题 No comment provided by engineer. @@ -5827,6 +6088,10 @@ Enable in *Network & servers* settings. 设置 No comment provided by engineer. + + Settings were changed. + alert message + Shape profile images 改变个人资料图形状 @@ -5854,6 +6119,7 @@ Enable in *Network & servers* settings. Share from other apps. + 从其他应用程序共享。 No comment provided by engineer. @@ -5861,6 +6127,10 @@ Enable in *Network & servers* settings. 分享链接 No comment provided by engineer. + + Share profile + No comment provided by engineer. + Share this 1-time invite link 分享此一次性邀请链接 @@ -5868,6 +6138,7 @@ Enable in *Network & servers* settings. Share to SimpleX + 分享到 SimpleX No comment provided by engineer. @@ -5897,10 +6168,12 @@ Enable in *Network & servers* settings. Show message status + 显示消息状态 No comment provided by engineer. Show percentage + 显示百分比 No comment provided by engineer. @@ -5910,6 +6183,7 @@ Enable in *Network & servers* settings. Show → on messages sent via private routing. + 显示 → 通过专用路由发送的信息. No comment provided by engineer. @@ -5919,6 +6193,7 @@ Enable in *Network & servers* settings. SimpleX + SimpleX No comment provided by engineer. @@ -5998,6 +6273,7 @@ Enable in *Network & servers* settings. Size + 大小 No comment provided by engineer. @@ -6017,10 +6293,16 @@ Enable in *Network & servers* settings. Soft + blur media + + Some app settings were not migrated. + No comment provided by engineer. + Some file(s) were not exported: + 某些文件未导出: No comment provided by engineer. @@ -6030,6 +6312,7 @@ Enable in *Network & servers* settings. Some non-fatal errors occurred during import: + 导入过程中出现一些非致命错误: No comment provided by engineer. @@ -6039,7 +6322,7 @@ Enable in *Network & servers* settings. Square, circle, or anything in between. - 方形、圆形、或两者之间的任意形状 + 方形、圆形、或两者之间的任意形状. No comment provided by engineer. @@ -6059,10 +6342,12 @@ Enable in *Network & servers* settings. Starting from %@. + 从 %@ 开始。 No comment provided by engineer. Statistics + 统计 No comment provided by engineer. @@ -6127,6 +6412,7 @@ Enable in *Network & servers* settings. Strong + 加粗 blur media @@ -6136,14 +6422,17 @@ Enable in *Network & servers* settings. Subscribed + 已订阅 No comment provided by engineer. Subscription errors + 订阅错误 No comment provided by engineer. Subscriptions ignored + 忽略订阅 No comment provided by engineer. @@ -6163,6 +6452,7 @@ Enable in *Network & servers* settings. TCP connection + TCP 连接 No comment provided by engineer. @@ -6185,6 +6475,10 @@ Enable in *Network & servers* settings. TCP_KEEPINTVL No comment provided by engineer. + + Tail + No comment provided by engineer. + Take picture 拍照 @@ -6227,6 +6521,7 @@ Enable in *Network & servers* settings. Temporary file error + 临时文件错误 No comment provided by engineer. @@ -6283,6 +6578,7 @@ It can happen because of some bug or when the connection is compromised. The app will ask to confirm downloads from unknown file servers (except .onion). + 该应用程序将要求确认从未知文件服务器(.onion 除外)下载。 No comment provided by engineer. @@ -6332,10 +6628,12 @@ It can happen because of some bug or when the connection is compromised. The messages will be deleted for all members. + 将删除所有成员的消息。 No comment provided by engineer. The messages will be marked as moderated for all members. + 对于所有成员,这些消息将被标记为已审核。 No comment provided by engineer. @@ -6373,8 +6671,13 @@ It can happen because of some bug or when the connection is compromised.您粘贴的文本不是 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. @@ -6444,6 +6747,7 @@ It can happen because of some bug or when the connection is compromised. This link was used with another mobile device, please create a new link on the desktop. + 此链接已在其他移动设备上使用,请在桌面上创建新链接。 No comment provided by engineer. @@ -6453,6 +6757,7 @@ It can happen because of some bug or when the connection is compromised. Title + 标题 No comment provided by engineer. @@ -6487,6 +6792,7 @@ It can happen because of some bug or when the connection is compromised. To protect your IP address, private routing uses your SMP servers to deliver messages. + 为了保护您的 IP 地址,私有路由使用您的 SMP 服务器来传递邮件。 No comment provided by engineer. @@ -6518,6 +6824,7 @@ You will be prompted to complete authentication before this feature is enabled.< Toggle chat list: + 切换聊天列表: No comment provided by engineer. @@ -6527,10 +6834,12 @@ You will be prompted to complete authentication before this feature is enabled.< Toolbar opacity + 工具栏不透明度 No comment provided by engineer. Total + 共计 No comment provided by engineer. @@ -6540,6 +6849,7 @@ You will be prompted to complete authentication before this feature is enabled.< Transport sessions + 传输会话 No comment provided by engineer. @@ -6554,6 +6864,7 @@ You will be prompted to complete authentication before this feature is enabled.< Turkish interface + 土耳其语界面 No comment provided by engineer. @@ -6643,6 +6954,7 @@ You will be prompted to complete authentication before this feature is enabled.< Unknown servers! + 未知服务器! No comment provided by engineer. @@ -6709,6 +7021,7 @@ To connect, please ask your contact to create another connection link and check Update settings? + 更新设置? No comment provided by engineer. @@ -6723,6 +7036,7 @@ To connect, please ask your contact to create another connection link and check Upload errors + 上传错误 No comment provided by engineer. @@ -6737,10 +7051,12 @@ To connect, please ask your contact to create another connection link and check Uploaded + 已上传 No comment provided by engineer. Uploaded files + 已上传的文件 No comment provided by engineer. @@ -6790,14 +7106,17 @@ To connect, please ask your contact to create another connection link and check Use only local notifications? + 仅使用本地通知? No comment provided by engineer. Use private routing with unknown servers when IP address is not protected. + 当 IP 地址不受保护时,对未知服务器使用私有路由。 No comment provided by engineer. Use private routing with unknown servers. + 对未知服务器使用私有路由。 No comment provided by engineer. @@ -6807,11 +7126,12 @@ To connect, please ask your contact to create another connection link and check Use the app while in the call. - 通话时使用本应用 + 通话时使用本应用. No comment provided by engineer. Use the app with one hand. + 用一只手使用应用程序。 No comment provided by engineer. @@ -6821,6 +7141,7 @@ To connect, please ask your contact to create another connection link and check User selection + 用户选择 No comment provided by engineer. @@ -6935,6 +7256,7 @@ To connect, please ask your contact to create another connection link and check Waiting for desktop... + 正在等待桌面... No comment provided by engineer. @@ -6954,15 +7276,17 @@ To connect, please ask your contact to create another connection link and check Wallpaper accent + 壁纸装饰 No comment provided by engineer. Wallpaper background + 壁纸背景 No comment provided by engineer. Warning: starting chat on multiple devices is not supported and will cause message delivery failures - 警告:不支持在多部设备上启动聊天,这么做会导致消息传送失败。 + 警告:不支持在多部设备上启动聊天,这么做会导致消息传送失败 No comment provided by engineer. @@ -7047,10 +7371,12 @@ To connect, please ask your contact to create another connection link and check Without Tor or VPN, your IP address will be visible to file servers. + 如果没有 Tor 或 VPN,您的 IP 地址将对文件服务器可见。 No comment provided by engineer. Without Tor or VPN, your IP address will be visible to these XFTP relays: %@. + 如果没有 Tor 或 VPN,您的 IP 地址将对以下 XFTP 中继可见:%@。 No comment provided by engineer. @@ -7060,10 +7386,12 @@ To connect, please ask your contact to create another connection link and check Wrong key or unknown connection - most likely this connection is deleted. + 密钥错误或连接未知 - 很可能此连接已被删除。 snd error text Wrong key or unknown file chunk address - most likely file is deleted. + 密钥错误或文件块地址未知 - 很可能文件已删除。 file error text @@ -7073,6 +7401,7 @@ To connect, please ask your contact to create another connection link and check XFTP server + XFTP 服务器 No comment provided by engineer. @@ -7082,6 +7411,7 @@ To connect, please ask your contact to create another connection link and check You **must not** use the same database on two devices. + 您 **不得** 在两台设备上使用相同的数据库。 No comment provided by engineer. @@ -7106,6 +7436,7 @@ To connect, please ask your contact to create another connection link and check You are already connecting to %@. + 您已连接到 %@。 No comment provided by engineer. @@ -7115,14 +7446,17 @@ To connect, please ask your contact to create another connection link and check You are already in group %@. + 您已在组 %@ 中。 No comment provided by engineer. You are already joining the group %@. + 您已加入组 %@。 No comment provided by engineer. You are already joining the group via this link! + 您已经通过此链接加入群组! No comment provided by engineer. @@ -7133,6 +7467,8 @@ To connect, please ask your contact to create another connection link and check You are already joining the group! Repeat join request? + 您已经加入了这个群组! +重复加入请求? No comment provided by engineer. @@ -7147,6 +7483,7 @@ Repeat join request? You are not connected to these servers. Private routing is used to deliver messages to them. + 您未连接到这些服务器。私有路由用于向他们发送消息。 No comment provided by engineer. @@ -7156,6 +7493,7 @@ Repeat join request? You can change it in Appearance settings. + 您可以在外观设置中更改它。 No comment provided by engineer. @@ -7195,6 +7533,7 @@ Repeat join request? You can send messages to %@ from Archived contacts. + 您可以从存档的联系人向%@发送消息。 No comment provided by engineer. @@ -7224,6 +7563,7 @@ Repeat join request? You can still view conversation with %@ in the list of chats. + 您仍然可以在聊天列表中查看与 %@的对话。 No comment provided by engineer. @@ -7264,6 +7604,8 @@ Repeat join request? You have already requested connection! Repeat connection request? + 您已经请求连接了! +重复连接请求? No comment provided by engineer. @@ -7288,10 +7630,12 @@ Repeat connection request? You may migrate the exported database. + 您可以迁移导出的数据库。 No comment provided by engineer. You may save the exported archive. + 您可以保存导出的档案。 No comment provided by engineer. @@ -7301,6 +7645,7 @@ Repeat connection request? You need to allow your contact to call to be able to call them. + 您需要允许您的联系人呼叫才能呼叫他们。 No comment provided by engineer. @@ -7325,6 +7670,7 @@ Repeat connection request? You will be connected when group link host's device is online, please wait or check later! + 当 Group Link Host 的设备在线时,您将被连接,请稍候或稍后检查! No comment provided by engineer. @@ -7412,11 +7758,19 @@ Repeat connection request? 您的聊天数据库未加密——设置密码来加密。 No comment provided by engineer. + + Your chat preferences + alert title + 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 (%@). 您的联系人发送的文件大于当前支持的最大大小 (%@)。 @@ -7454,6 +7808,7 @@ Repeat connection request? Your profile + 您的个人资料 No comment provided by engineer. @@ -7461,13 +7816,15 @@ Repeat connection request? 您的个人资料 **%@** 将被共享。 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 服务器无法看到您的资料。 + + Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile. + 您的资料存储在您的设备上并仅与您的联系人共享。 SimpleX 服务器无法看到您的资料。 No comment provided by engineer. + + Your profile was changed. If you save it, the updated profile will be sent to all your contacts. + alert message + Your profile, contacts and delivered messages are stored on your device. 您的资料、联系人和发送的消息存储在您的设备上。 @@ -7560,10 +7917,12 @@ SimpleX 服务器无法看到您的资料。 and %lld other events + 和 %lld 其他事件 No comment provided by engineer. attempts + 尝试 No comment provided by engineer. @@ -7608,6 +7967,7 @@ SimpleX 服务器无法看到您的资料。 call + 呼叫 No comment provided by engineer. @@ -7727,6 +8087,7 @@ SimpleX 服务器无法看到您的资料。 contact %1$@ changed to %2$@ + 联系人 %1$@ 已更改为 %2$@ profile update event chat item @@ -7761,6 +8122,7 @@ SimpleX 服务器无法看到您的资料。 decryption errors + 解密错误 No comment provided by engineer. @@ -7815,6 +8177,7 @@ SimpleX 服务器无法看到您的资料。 duplicates + 复本 No comment provided by engineer. @@ -7899,6 +8262,7 @@ SimpleX 服务器无法看到您的资料。 expired + 过期 No comment provided by engineer. @@ -7933,6 +8297,7 @@ SimpleX 服务器无法看到您的资料。 inactive + 无效 No comment provided by engineer. @@ -7977,6 +8342,7 @@ SimpleX 服务器无法看到您的资料。 invite + 邀请 No comment provided by engineer. @@ -8026,6 +8392,7 @@ SimpleX 服务器无法看到您的资料。 member %1$@ changed to %2$@ + 成员 %1$@ 已更改为 %2$@ profile update event chat item @@ -8035,6 +8402,7 @@ SimpleX 服务器无法看到您的资料。 message + 消息 No comment provided by engineer. @@ -8069,6 +8437,7 @@ SimpleX 服务器无法看到您的资料。 mute + 静音 No comment provided by engineer. @@ -8125,10 +8494,12 @@ SimpleX 服务器无法看到您的资料。 other + 其他 No comment provided by engineer. other errors + 其他错误 No comment provided by engineer. @@ -8198,10 +8569,12 @@ SimpleX 服务器无法看到您的资料。 saved from %@ + 保存自 %@ No comment provided by engineer. search + 搜索 No comment provided by engineer. @@ -8233,6 +8606,9 @@ SimpleX 服务器无法看到您的资料。 server queue info: %1$@ last received msg: %2$@ + 服务器队列信息: %1$@ + +上次收到的消息: %2$@ queue info @@ -8267,6 +8643,7 @@ last received msg: %2$@ unblocked %@ + 未阻止 %@ rcv group event chat item @@ -8276,6 +8653,7 @@ last received msg: %2$@ unknown servers + 未知服务器 No comment provided by engineer. @@ -8285,10 +8663,12 @@ last received msg: %2$@ unmute + 取消静音 No comment provided by engineer. unprotected + 未受保护 No comment provided by engineer. @@ -8303,6 +8683,7 @@ last received msg: %2$@ v%@ + v%@ No comment provided by engineer. @@ -8332,6 +8713,7 @@ last received msg: %2$@ video + 视频 No comment provided by engineer. @@ -8361,6 +8743,7 @@ last received msg: %2$@ when IP hidden + 当 IP 隐藏时 No comment provided by engineer. @@ -8385,6 +8768,7 @@ last received msg: %2$@ you blocked %@ + 你阻止了%@ snd group event chat item @@ -8429,6 +8813,7 @@ last received msg: %2$@ you unblocked %@ + 您解封了 %@ snd group event chat item @@ -8465,6 +8850,7 @@ last received msg: %2$@ SimpleX uses local network access to allow using user chat profile via desktop app on the same network. + SimpleX 使用本地网络访问,允许通过同一网络上的桌面应用程序使用用户聊天配置文件。 Privacy - Local Network Usage Description @@ -8508,14 +8894,17 @@ last received msg: %2$@ SimpleX SE + SimpleX SE Bundle display name SimpleX SE + SimpleX SE Bundle name Copyright © 2024 SimpleX Chat. All rights reserved. + 版权所有 © 2024 SimpleX Chat。保留所有权利。 Copyright (human-readable) @@ -8527,150 +8916,187 @@ last received msg: %2$@ %@ + %@ No comment provided by engineer. App is locked! + 应用程序已锁定! No comment provided by engineer. Cancel + 取消 No comment provided by engineer. Cannot access keychain to save database password + 无法访问钥匙串以保存数据库密码 No comment provided by engineer. Cannot forward message + 无法转发消息 No comment provided by engineer. Comment + 评论 No comment provided by engineer. Currently maximum supported file size is %@. + 当前支持的最大文件大小为 %@。 No comment provided by engineer. Database downgrade required + 需要数据库降级 No comment provided by engineer. Database encrypted! + 数据库已加密! No comment provided by engineer. Database error + 数据库错误 No comment provided by engineer. Database passphrase is different from saved in the keychain. + 数据库密码与保存在钥匙串中的密码不同。 No comment provided by engineer. Database passphrase is required to open chat. + 需要数据库密码才能打开聊天。 No comment provided by engineer. Database upgrade required + 需要升级数据库 No comment provided by engineer. Error preparing file + 准备文件时出错 No comment provided by engineer. Error preparing message + 准备消息时出错 No comment provided by engineer. Error: %@ + 错误:%@ No comment provided by engineer. File error + 文件错误 No comment provided by engineer. Incompatible database version + 不兼容的数据库版本 No comment provided by engineer. Invalid migration confirmation + 无效的迁移确认 No comment provided by engineer. Keychain error + 钥匙串错误 No comment provided by engineer. Large file! + 大文件! No comment provided by engineer. No active profile + 无活动配置文件 No comment provided by engineer. Ok + 好的 No comment provided by engineer. Open the app to downgrade the database. + 打开应用程序以降级数据库。 No comment provided by engineer. Open the app to upgrade the database. + 打开应用程序以升级数据库。 No comment provided by engineer. Passphrase + 密码 No comment provided by engineer. Please create a profile in the SimpleX app + 请在 SimpleX 应用程序中创建配置文件 No comment provided by engineer. Selected chat preferences prohibit this message. + 选定的聊天首选项禁止此消息。 No comment provided by engineer. Sending a message takes longer than expected. + 发送消息所需的时间比预期的要长。 No comment provided by engineer. Sending message… + 正在发送消息… No comment provided by engineer. Share + 共享 No comment provided by engineer. Slow network? + 网络速度慢? No comment provided by engineer. Unknown database error: %@ + 未知数据库错误: %@ No comment provided by engineer. Unsupported format + 不支持的格式 No comment provided by engineer. Wait + 等待 No comment provided by engineer. Wrong database passphrase + 数据库密码错误 No comment provided by engineer. You can allow sharing in Privacy & Security / SimpleX Lock settings. + 您可以在 "隐私与安全"/"SimpleX Lock "设置中允许共享。 No comment provided by engineer. diff --git a/apps/ios/SimpleX NSE/NotificationService.swift b/apps/ios/SimpleX NSE/NotificationService.swift index 81d0c9eac1..5411a6c14b 100644 --- a/apps/ios/SimpleX NSE/NotificationService.swift +++ b/apps/ios/SimpleX NSE/NotificationService.swift @@ -573,17 +573,22 @@ func receivedMsgNtf(_ res: ChatResponse) async -> (String, NSENotification)? { // TODO profile update case let .receivedContactRequest(user, contactRequest): return (UserContact(contactRequest: contactRequest).id, .nse(createContactRequestNtf(user, contactRequest))) - case let .newChatItem(user, aChatItem): - let cInfo = aChatItem.chatInfo - var cItem = aChatItem.chatItem - if !cInfo.ntfsEnabled { - ntfBadgeCountGroupDefault.set(max(0, ntfBadgeCountGroupDefault.get() - 1)) + 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 + } else { + return nil } - if let file = cItem.autoReceiveFile() { - cItem = autoReceiveFile(file) ?? cItem - } - let ntf: NSENotification = cInfo.ntfsEnabled ? .nse(createMessageReceivedNtf(user, cInfo, cItem)) : .empty - return cItem.showNotification ? (aChatItem.chatId, ntf) : nil case let .rcvFileSndCancelled(_, aChatItem, _): cleanupFile(aChatItem) return nil diff --git a/apps/ios/SimpleX SE/ShareAPI.swift b/apps/ios/SimpleX SE/ShareAPI.swift index 47e072ae78..fcb78c64b1 100644 --- a/apps/ios/SimpleX SE/ShareAPI.swift +++ b/apps/ios/SimpleX SE/ShareAPI.swift @@ -54,32 +54,30 @@ func apiGetChats(userId: User.ID) throws -> Array { throw r } -func apiSendMessage( +func apiSendMessages( chatInfo: ChatInfo, - cryptoFile: CryptoFile?, - msgContent: MsgContent -) throws -> AChatItem { + composedMessages: [ComposedMessage] +) throws -> [AChatItem] { let r = sendSimpleXCmd( chatInfo.chatType == .local - ? .apiCreateChatItem( + ? .apiCreateChatItems( noteFolderId: chatInfo.apiId, - file: cryptoFile, - msg: msgContent + composedMessages: composedMessages ) - : .apiSendMessage( + : .apiSendMessages( type: chatInfo.chatType, id: chatInfo.apiId, - file: cryptoFile, - quotedItemId: nil, - msg: msgContent, live: false, - ttl: nil + ttl: nil, + composedMessages: composedMessages ) ) - if case let .newChatItem(_, chatItem) = r { - return chatItem + if case let .newChatItems(_, chatItems) = r { + return chatItems } else { - if let filePath = cryptoFile?.filePath { removeFile(filePath) } + for composedMessage in composedMessages { + if let filePath = composedMessage.fileSource?.filePath { removeFile(filePath) } + } throw r } } diff --git a/apps/ios/SimpleX SE/ShareModel.swift b/apps/ios/SimpleX SE/ShareModel.swift index 5bda361126..e73aeee13c 100644 --- a/apps/ios/SimpleX SE/ShareModel.swift +++ b/apps/ios/SimpleX SE/ShareModel.swift @@ -104,7 +104,7 @@ class ShareModel: ObservableObject { // Decode base64 images on background thread let profileImages = chats.reduce(into: Dictionary()) { dict, chatData in if let profileImage = chatData.chatInfo.image, - let uiImage = UIImage(base64Encoded: profileImage) { + let uiImage = imageFromBase64(profileImage) { dict[chatData.id] = uiImage } } @@ -141,23 +141,25 @@ class ShareModel: ObservableObject { do { SEChatState.shared.set(.sendingMessage) await waitForOtherProcessesToSuspend() - let ci = try apiSendMessage( + let chatItems = try apiSendMessages( chatInfo: selected.chatInfo, - cryptoFile: sharedContent.cryptoFile, - msgContent: sharedContent.msgContent(comment: self.comment) + composedMessages: [ComposedMessage(fileSource: sharedContent.cryptoFile, msgContent: sharedContent.msgContent(comment: self.comment))] ) if selected.chatInfo.chatType == .local { completion() } else { - await MainActor.run { self.bottomBar = .loadingBar(progress: 0) } - if let e = await handleEvents( - isGroupChat: ci.chatInfo.chatType == .group, - isWithoutFile: sharedContent.cryptoFile == nil, - chatItemId: ci.chatItem.id - ) { - await MainActor.run { errorAlert = e } - } else { - completion() + // TODO batch send: share multiple items + if let ci = chatItems.first { + await MainActor.run { self.bottomBar = .loadingBar(progress: 0) } + if let e = await handleEvents( + isGroupChat: ci.chatInfo.chatType == .group, + isWithoutFile: sharedContent.cryptoFile == nil, + chatItemId: ci.chatItem.id + ) { + await MainActor.run { errorAlert = e } + } else { + completion() + } } } } catch { diff --git a/apps/ios/SimpleX SE/ShareView.swift b/apps/ios/SimpleX SE/ShareView.swift index 1f502ffcff..f2b9de9f72 100644 --- a/apps/ios/SimpleX SE/ShareView.swift +++ b/apps/ios/SimpleX SE/ShareView.swift @@ -147,8 +147,8 @@ struct ShareView: View { } } - @ViewBuilder private func imagePreview(_ img: String) -> some View { - if let img = UIImage(base64Encoded: img) { + @ViewBuilder private func imagePreview(_ imgStr: String) -> some View { + if let img = imageFromBase64(imgStr) { previewArea { Image(uiImage: img) .resizable() @@ -163,7 +163,7 @@ struct ShareView: View { @ViewBuilder private func linkPreview(_ linkPreview: LinkPreview) -> some View { previewArea { HStack(alignment: .center, spacing: 8) { - if let uiImage = UIImage(base64Encoded: linkPreview.image) { + if let uiImage = imageFromBase64(linkPreview.image) { Image(uiImage: uiImage) .resizable() .aspectRatio(contentMode: .fit) diff --git a/apps/ios/SimpleX SE/zh-Hans.lproj/InfoPlist.strings b/apps/ios/SimpleX SE/zh-Hans.lproj/InfoPlist.strings index 388ac01f7f..760be62885 100644 --- a/apps/ios/SimpleX SE/zh-Hans.lproj/InfoPlist.strings +++ b/apps/ios/SimpleX SE/zh-Hans.lproj/InfoPlist.strings @@ -1,7 +1,9 @@ -/* - InfoPlist.strings - SimpleX +/* Bundle display name */ +"CFBundleDisplayName" = "SimpleX SE"; + +/* Bundle name */ +"CFBundleName" = "SimpleX SE"; + +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "版权所有 © 2024 SimpleX Chat。保留所有权利。"; - Created by EP on 30/07/2024. - Copyright © 2024 SimpleX Chat. All rights reserved. -*/ diff --git a/apps/ios/SimpleX SE/zh-Hans.lproj/Localizable.strings b/apps/ios/SimpleX SE/zh-Hans.lproj/Localizable.strings index 5ef592ec70..362e2edb74 100644 --- a/apps/ios/SimpleX SE/zh-Hans.lproj/Localizable.strings +++ b/apps/ios/SimpleX SE/zh-Hans.lproj/Localizable.strings @@ -1,7 +1,111 @@ -/* - Localizable.strings - SimpleX +/* No comment provided by engineer. */ +"%@" = "%@"; + +/* No comment provided by engineer. */ +"App is locked!" = "应用程序已锁定!"; + +/* No comment provided by engineer. */ +"Cancel" = "取消"; + +/* No comment provided by engineer. */ +"Cannot access keychain to save database password" = "无法访问钥匙串以保存数据库密码"; + +/* No comment provided by engineer. */ +"Cannot forward message" = "无法转发消息"; + +/* No comment provided by engineer. */ +"Comment" = "评论"; + +/* No comment provided by engineer. */ +"Currently maximum supported file size is %@." = "当前支持的最大文件大小为 %@。"; + +/* No comment provided by engineer. */ +"Database downgrade required" = "需要数据库降级"; + +/* No comment provided by engineer. */ +"Database encrypted!" = "数据库已加密!"; + +/* No comment provided by engineer. */ +"Database error" = "数据库错误"; + +/* No comment provided by engineer. */ +"Database passphrase is different from saved in the keychain." = "数据库密码与保存在钥匙串中的密码不同。"; + +/* No comment provided by engineer. */ +"Database passphrase is required to open chat." = "需要数据库密码才能打开聊天。"; + +/* No comment provided by engineer. */ +"Database upgrade required" = "需要升级数据库"; + +/* No comment provided by engineer. */ +"Error preparing file" = "准备文件时出错"; + +/* No comment provided by engineer. */ +"Error preparing message" = "准备消息时出错"; + +/* No comment provided by engineer. */ +"Error: %@" = "错误:%@"; + +/* No comment provided by engineer. */ +"File error" = "文件错误"; + +/* No comment provided by engineer. */ +"Incompatible database version" = "不兼容的数据库版本"; + +/* No comment provided by engineer. */ +"Invalid migration confirmation" = "无效的迁移确认"; + +/* No comment provided by engineer. */ +"Keychain error" = "钥匙串错误"; + +/* No comment provided by engineer. */ +"Large file!" = "大文件!"; + +/* No comment provided by engineer. */ +"No active profile" = "无活动配置文件"; + +/* No comment provided by engineer. */ +"Ok" = "好的"; + +/* No comment provided by engineer. */ +"Open the app to downgrade the database." = "打开应用程序以降级数据库。"; + +/* No comment provided by engineer. */ +"Open the app to upgrade the database." = "打开应用程序以升级数据库。"; + +/* No comment provided by engineer. */ +"Passphrase" = "密码"; + +/* No comment provided by engineer. */ +"Please create a profile in the SimpleX app" = "请在 SimpleX 应用程序中创建配置文件"; + +/* No comment provided by engineer. */ +"Selected chat preferences prohibit this message." = "选定的聊天首选项禁止此消息。"; + +/* No comment provided by engineer. */ +"Sending a message takes longer than expected." = "发送消息所需的时间比预期的要长。"; + +/* No comment provided by engineer. */ +"Sending message…" = "正在发送消息…"; + +/* No comment provided by engineer. */ +"Share" = "共享"; + +/* No comment provided by engineer. */ +"Slow network?" = "网络速度慢?"; + +/* No comment provided by engineer. */ +"Unknown database error: %@" = "未知数据库错误: %@"; + +/* No comment provided by engineer. */ +"Unsupported format" = "不支持的格式"; + +/* No comment provided by engineer. */ +"Wait" = "等待"; + +/* No comment provided by engineer. */ +"Wrong database passphrase" = "数据库密码错误"; + +/* No comment provided by engineer. */ +"You can allow sharing in Privacy & Security / SimpleX Lock settings." = "您可以在 \"隐私与安全\"/\"SimpleX Lock \"设置中允许共享。"; - Created by EP on 30/07/2024. - Copyright © 2024 SimpleX Chat. All rights reserved. -*/ diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 399d88b39f..dd97170700 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -196,6 +196,7 @@ 8CC956EE2BC0041000412A11 /* NetworkObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CC956ED2BC0041000412A11 /* NetworkObserver.swift */; }; 8CE848A32C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CE848A22C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift */; }; B76E6C312C5C41D900EC11AA /* ContactListNavLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = B76E6C302C5C41D900EC11AA /* ContactListNavLink.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 */; }; CE3097FB2C4C0C9F00180898 /* ErrorAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE3097FA2C4C0C9F00180898 /* ErrorAlert.swift */; }; @@ -216,11 +217,11 @@ D77B92DC2952372200A5A1CC /* SwiftyGif in Frameworks */ = {isa = PBXBuildFile; productRef = D77B92DB2952372200A5A1CC /* SwiftyGif */; }; D7F0E33929964E7E0068AF69 /* LZString in Frameworks */ = {isa = PBXBuildFile; productRef = D7F0E33829964E7E0068AF69 /* LZString */; }; E51CC1E62C62085600DB91FE /* OneHandUICard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E51CC1E52C62085600DB91FE /* OneHandUICard.swift */; }; - E5BD844D2C8220D0008C24D1 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5BD84482C8220D0008C24D1 /* libffi.a */; }; - E5BD844E2C8220D0008C24D1 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5BD84492C8220D0008C24D1 /* libgmpxx.a */; }; - E5BD844F2C8220D0008C24D1 /* libHSsimplex-chat-6.0.4.0-2x1D8vVukGZOGJwEVzeob-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5BD844A2C8220D0008C24D1 /* libHSsimplex-chat-6.0.4.0-2x1D8vVukGZOGJwEVzeob-ghc9.6.3.a */; }; - E5BD84502C8220D0008C24D1 /* libHSsimplex-chat-6.0.4.0-2x1D8vVukGZOGJwEVzeob.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5BD844B2C8220D0008C24D1 /* libHSsimplex-chat-6.0.4.0-2x1D8vVukGZOGJwEVzeob.a */; }; - E5BD84512C8220D0008C24D1 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5BD844C2C8220D0008C24D1 /* libgmp.a */; }; + E55128D32C989E13001D165C /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E55128CE2C989E12001D165C /* libgmpxx.a */; }; + E55128D42C989E13001D165C /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E55128CF2C989E12001D165C /* libffi.a */; }; + E55128D52C989E13001D165C /* libHSsimplex-chat-6.1.0.0-6SG1oRijpxxHpZcw3v92xj.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E55128D02C989E12001D165C /* libHSsimplex-chat-6.1.0.0-6SG1oRijpxxHpZcw3v92xj.a */; }; + E55128D62C989E13001D165C /* libHSsimplex-chat-6.1.0.0-6SG1oRijpxxHpZcw3v92xj-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E55128D12C989E13001D165C /* libHSsimplex-chat-6.1.0.0-6SG1oRijpxxHpZcw3v92xj-ghc9.6.3.a */; }; + E55128D72C989E13001D165C /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E55128D22C989E13001D165C /* libgmp.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 */; }; @@ -536,6 +537,7 @@ 8CC956ED2BC0041000412A11 /* NetworkObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkObserver.swift; sourceTree = ""; }; 8CE848A22C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableChatItemToolbars.swift; sourceTree = ""; }; B76E6C302C5C41D900EC11AA /* ContactListNavLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactListNavLink.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 = ""; }; CE3097FA2C4C0C9F00180898 /* ErrorAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorAlert.swift; sourceTree = ""; }; @@ -554,11 +556,11 @@ D741547929AF90B00022400A /* PushKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PushKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/PushKit.framework; sourceTree = DEVELOPER_DIR; }; D7AA2C3429A936B400737B40 /* MediaEncryption.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; name = MediaEncryption.playground; path = Shared/MediaEncryption.playground; sourceTree = SOURCE_ROOT; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; E51CC1E52C62085600DB91FE /* OneHandUICard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneHandUICard.swift; sourceTree = ""; }; - E5BD84482C8220D0008C24D1 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - E5BD84492C8220D0008C24D1 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; - E5BD844A2C8220D0008C24D1 /* libHSsimplex-chat-6.0.4.0-2x1D8vVukGZOGJwEVzeob-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.0.4.0-2x1D8vVukGZOGJwEVzeob-ghc9.6.3.a"; sourceTree = ""; }; - E5BD844B2C8220D0008C24D1 /* libHSsimplex-chat-6.0.4.0-2x1D8vVukGZOGJwEVzeob.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.0.4.0-2x1D8vVukGZOGJwEVzeob.a"; sourceTree = ""; }; - E5BD844C2C8220D0008C24D1 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; + E55128CE2C989E12001D165C /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; + E55128CF2C989E12001D165C /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; + E55128D02C989E12001D165C /* libHSsimplex-chat-6.1.0.0-6SG1oRijpxxHpZcw3v92xj.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.1.0.0-6SG1oRijpxxHpZcw3v92xj.a"; sourceTree = ""; }; + E55128D12C989E13001D165C /* libHSsimplex-chat-6.1.0.0-6SG1oRijpxxHpZcw3v92xj-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.1.0.0-6SG1oRijpxxHpZcw3v92xj-ghc9.6.3.a"; sourceTree = ""; }; + E55128D22C989E13001D165C /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.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 = ""; }; @@ -649,14 +651,14 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + E55128D32C989E13001D165C /* libgmpxx.a in Frameworks */, 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, - E5BD844E2C8220D0008C24D1 /* libgmpxx.a in Frameworks */, - E5BD84512C8220D0008C24D1 /* libgmp.a in Frameworks */, + E55128D72C989E13001D165C /* libgmp.a in Frameworks */, + E55128D62C989E13001D165C /* libHSsimplex-chat-6.1.0.0-6SG1oRijpxxHpZcw3v92xj-ghc9.6.3.a in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, + E55128D42C989E13001D165C /* libffi.a in Frameworks */, + E55128D52C989E13001D165C /* libHSsimplex-chat-6.1.0.0-6SG1oRijpxxHpZcw3v92xj.a in Frameworks */, CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */, - E5BD84502C8220D0008C24D1 /* libHSsimplex-chat-6.0.4.0-2x1D8vVukGZOGJwEVzeob.a in Frameworks */, - E5BD844D2C8220D0008C24D1 /* libffi.a in Frameworks */, - E5BD844F2C8220D0008C24D1 /* libHSsimplex-chat-6.0.4.0-2x1D8vVukGZOGJwEVzeob-ghc9.6.3.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -733,11 +735,11 @@ 5C764E5C279C70B7000C6508 /* Libraries */ = { isa = PBXGroup; children = ( - E5BD84482C8220D0008C24D1 /* libffi.a */, - E5BD844C2C8220D0008C24D1 /* libgmp.a */, - E5BD84492C8220D0008C24D1 /* libgmpxx.a */, - E5BD844A2C8220D0008C24D1 /* libHSsimplex-chat-6.0.4.0-2x1D8vVukGZOGJwEVzeob-ghc9.6.3.a */, - E5BD844B2C8220D0008C24D1 /* libHSsimplex-chat-6.0.4.0-2x1D8vVukGZOGJwEVzeob.a */, + E55128CF2C989E12001D165C /* libffi.a */, + E55128D22C989E13001D165C /* libgmp.a */, + E55128CE2C989E12001D165C /* libgmpxx.a */, + E55128D12C989E13001D165C /* libHSsimplex-chat-6.1.0.0-6SG1oRijpxxHpZcw3v92xj-ghc9.6.3.a */, + E55128D02C989E12001D165C /* libHSsimplex-chat-6.1.0.0-6SG1oRijpxxHpZcw3v92xj.a */, ); path = Libraries; sourceTree = ""; @@ -793,6 +795,7 @@ 8C9BC2642C240D5100875A27 /* ThemeModeEditor.swift */, CE984D4A2C36C5D500E3AEFF /* ChatItemClipShape.swift */, CE7548092C622630009579B7 /* SwipeLabel.swift */, + CE176F1F2C87014C00145DBC /* InvertedForegroundStyle.swift */, ); path = Helpers; sourceTree = ""; @@ -1496,6 +1499,7 @@ 5C9CC7A928C532AB00BEF955 /* DatabaseErrorView.swift in Sources */, 5C1A4C1E27A715B700EAD5AD /* ChatItemView.swift in Sources */, 64AA1C6927EE10C800AC7277 /* ContextItemView.swift in Sources */, + CE176F202C87014C00145DBC /* InvertedForegroundStyle.swift in Sources */, 5CEBD7482A5F115D00665FE2 /* SetDeliveryReceiptsView.swift in Sources */, 5C9C2DA7289957AE00CC63B1 /* AdvancedNetworkSettings.swift in Sources */, 5CADE79A29211BB900072E13 /* PreferencesView.swift in Sources */, diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index d7fc533e91..7f030cb838 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -42,13 +42,13 @@ public enum ChatCommand { case apiGetChats(userId: Int64) case apiGetChat(type: ChatType, id: Int64, pagination: ChatPagination, search: String) case apiGetChatItemInfo(type: ChatType, id: Int64, itemId: Int64) - case apiSendMessage(type: ChatType, id: Int64, file: CryptoFile?, quotedItemId: Int64?, msg: MsgContent, live: Bool, ttl: Int?) - case apiCreateChatItem(noteFolderId: Int64, file: CryptoFile?, msg: MsgContent) + case apiSendMessages(type: ChatType, id: Int64, live: Bool, ttl: Int?, composedMessages: [ComposedMessage]) + case apiCreateChatItems(noteFolderId: Int64, composedMessages: [ComposedMessage]) case apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, msg: MsgContent, live: Bool) 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 apiForwardChatItem(toChatType: ChatType, toChatId: Int64, fromChatType: ChatType, fromChatId: Int64, itemId: Int64, ttl: Int?) + case apiForwardChatItems(toChatType: ChatType, toChatId: Int64, fromChatType: ChatType, fromChatId: Int64, itemIds: [Int64], ttl: Int?) case apiGetNtfToken case apiRegisterToken(token: DeviceToken, notificationMode: NotificationsMode) case apiVerifyToken(token: DeviceToken, nonce: String, code: String) @@ -97,6 +97,7 @@ public enum ChatCommand { case apiVerifyGroupMember(groupId: Int64, groupMemberId: Int64, connectionCode: String?) case apiAddContact(userId: Int64, incognito: Bool) case apiSetConnectionIncognito(connId: Int64, incognito: Bool) + case apiChangeConnectionUser(connId: Int64, userId: Int64) case apiConnectPlan(userId: Int64, connReq: String) case apiConnect(userId: Int64, incognito: Bool, connReq: String) case apiConnectContactViaAddress(userId: Int64, incognito: Bool, contactId: Int64) @@ -128,6 +129,7 @@ public enum ChatCommand { // WebRTC calls / case apiGetNetworkStatuses case apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, 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?) case setFileToReceive(fileId: Int64, userApprovedRelays: Bool, encrypted: Bool?) @@ -190,20 +192,20 @@ public enum ChatCommand { case let .apiGetChat(type, id, pagination, search): return "/_get chat \(ref(type, id)) \(pagination.cmdString)" + (search == "" ? "" : " search=\(search)") case let .apiGetChatItemInfo(type, id, itemId): return "/_get item info \(ref(type, id)) \(itemId)" - case let .apiSendMessage(type, id, file, quotedItemId, mc, live, ttl): - let msg = encodeJSON(ComposedMessage(fileSource: file, quotedItemId: quotedItemId, msgContent: mc)) + case let .apiSendMessages(type, id, live, ttl, composedMessages): + let msgs = encodeJSON(composedMessages) let ttlStr = ttl != nil ? "\(ttl!)" : "default" - return "/_send \(ref(type, id)) live=\(onOff(live)) ttl=\(ttlStr) json \(msg)" - case let .apiCreateChatItem(noteFolderId, file, mc): - let msg = encodeJSON(ComposedMessage(fileSource: file, msgContent: mc)) - return "/_create *\(noteFolderId) json \(msg)" + return "/_send \(ref(type, id)) live=\(onOff(live)) ttl=\(ttlStr) json \(msgs)" + case let .apiCreateChatItems(noteFolderId, composedMessages): + let msgs = encodeJSON(composedMessages) + return "/_create *\(noteFolderId) json \(msgs)" case let .apiUpdateChatItem(type, id, itemId, mc, live): return "/_update item \(ref(type, id)) \(itemId) live=\(onOff(live)) \(mc.cmdString)" 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 .apiForwardChatItem(toChatType, toChatId, fromChatType, fromChatId, itemId, ttl): + case let .apiForwardChatItems(toChatType, toChatId, fromChatType, fromChatId, itemIds, ttl): let ttlStr = ttl != nil ? "\(ttl!)" : "default" - return "/_forward \(ref(toChatType, toChatId)) \(ref(fromChatType, fromChatId)) \(itemId) ttl=\(ttlStr)" + return "/_forward \(ref(toChatType, toChatId)) \(ref(fromChatType, fromChatId)) \(itemIds.map({ "\($0)" }).joined(separator: ",")) ttl=\(ttlStr)" case .apiGetNtfToken: return "/_ntf get " 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)" @@ -262,6 +264,7 @@ public enum ChatCommand { case let .apiVerifyGroupMember(groupId, groupMemberId, .none): return "/_verify code #\(groupId) \(groupMemberId)" case let .apiAddContact(userId, incognito): return "/_connect \(userId) incognito=\(onOff(incognito))" case let .apiSetConnectionIncognito(connId, incognito): return "/_set incognito :\(connId) \(onOff(incognito))" + case let .apiChangeConnectionUser(connId, userId): return "/_set conn user :\(connId) \(userId)" case let .apiConnectPlan(userId, connReq): return "/_connect plan \(userId) \(connReq)" case let .apiConnect(userId, incognito, connReq): return "/_connect \(userId) incognito=\(onOff(incognito)) \(connReq)" case let .apiConnectContactViaAddress(userId, incognito, contactId): return "/_connect contact \(userId) incognito=\(onOff(incognito)) \(contactId)" @@ -291,6 +294,7 @@ public enum ChatCommand { 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 .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))" case let .setFileToReceive(fileId, userApprovedRelays, encrypt): return "/_set_file_to_receive \(fileId)\(onOffParam("approved_relays", userApprovedRelays))\(onOffParam("encrypt", encrypt))" @@ -347,14 +351,14 @@ public enum ChatCommand { case .apiGetChats: return "apiGetChats" case .apiGetChat: return "apiGetChat" case .apiGetChatItemInfo: return "apiGetChatItemInfo" - case .apiSendMessage: return "apiSendMessage" - case .apiCreateChatItem: return "apiCreateChatItem" + case .apiSendMessages: return "apiSendMessages" + case .apiCreateChatItems: return "apiCreateChatItems" case .apiUpdateChatItem: return "apiUpdateChatItem" case .apiDeleteChatItem: return "apiDeleteChatItem" case .apiConnectContactViaAddress: return "apiConnectContactViaAddress" case .apiDeleteMemberChatItem: return "apiDeleteMemberChatItem" case .apiChatItemReaction: return "apiChatItemReaction" - case .apiForwardChatItem: return "apiForwardChatItem" + case .apiForwardChatItems: return "apiForwardChatItems" case .apiGetNtfToken: return "apiGetNtfToken" case .apiRegisterToken: return "apiRegisterToken" case .apiVerifyToken: return "apiVerifyToken" @@ -403,6 +407,7 @@ public enum ChatCommand { case .apiVerifyGroupMember: return "apiVerifyGroupMember" case .apiAddContact: return "apiAddContact" case .apiSetConnectionIncognito: return "apiSetConnectionIncognito" + case .apiChangeConnectionUser: return "apiChangeConnectionUser" case .apiConnectPlan: return "apiConnectPlan" case .apiConnect: return "apiConnect" case .apiDeleteChat: return "apiDeleteChat" @@ -431,6 +436,7 @@ public enum ChatCommand { case .apiCallStatus: return "apiCallStatus" case .apiGetNetworkStatuses: return "apiGetNetworkStatuses" case .apiChatRead: return "apiChatRead" + case .apiChatItemsRead: return "apiChatItemsRead" case .apiChatUnread: return "apiChatUnread" case .receiveFile: return "receiveFile" case .setFileToReceive: return "setFileToReceive" @@ -459,6 +465,10 @@ public enum ChatCommand { "\(type.rawValue)\(id)" } + func joinedIds(_ ids: [Int64]) -> String { + ids.map { "\($0)" }.joined(separator: ",") + } + func protoServersStr(_ servers: [ServerCfg]) -> String { encodeJSON(ProtoServersConfig(servers: servers)) } @@ -555,6 +565,7 @@ public enum ChatResponse: Decodable, Error { case connectionVerified(user: UserRef, verified: Bool, expectedCode: String) case invitation(user: UserRef, connReqInvitation: String, connection: PendingContactConnection) case connectionIncognitoUpdated(user: UserRef, toConnection: PendingContactConnection) + case connectionUserChanged(user: UserRef, fromConnection: PendingContactConnection, toConnection: PendingContactConnection, newUser: UserRef) case connectionPlan(user: UserRef, connectionPlan: ConnectionPlan) case sentConfirmation(user: UserRef, connection: PendingContactConnection) case sentInvitation(user: UserRef, connection: PendingContactConnection) @@ -588,7 +599,7 @@ public enum ChatResponse: Decodable, Error { case memberSubErrors(user: UserRef, memberSubErrors: [MemberSubError]) case groupEmpty(user: UserRef, groupInfo: GroupInfo) case userContactLinkSubscribed - case newChatItem(user: UserRef, chatItem: AChatItem) + case newChatItems(user: UserRef, chatItems: [AChatItem]) case chatItemStatusUpdated(user: UserRef, chatItem: AChatItem) case chatItemUpdated(user: UserRef, chatItem: AChatItem) case chatItemNotChanged(user: UserRef, chatItem: AChatItem) @@ -725,6 +736,7 @@ public enum ChatResponse: Decodable, Error { case .connectionVerified: return "connectionVerified" case .invitation: return "invitation" case .connectionIncognitoUpdated: return "connectionIncognitoUpdated" + case .connectionUserChanged: return "connectionUserChanged" case .connectionPlan: return "connectionPlan" case .sentConfirmation: return "sentConfirmation" case .sentInvitation: return "sentInvitation" @@ -758,7 +770,7 @@ public enum ChatResponse: Decodable, Error { case .memberSubErrors: return "memberSubErrors" case .groupEmpty: return "groupEmpty" case .userContactLinkSubscribed: return "userContactLinkSubscribed" - case .newChatItem: return "newChatItem" + case .newChatItems: return "newChatItems" case .chatItemStatusUpdated: return "chatItemStatusUpdated" case .chatItemUpdated: return "chatItemUpdated" case .chatItemNotChanged: return "chatItemNotChanged" @@ -893,6 +905,7 @@ public enum ChatResponse: Decodable, Error { case let .connectionVerified(u, verified, expectedCode): return withUser(u, "verified: \(verified)\nconnectionCode: \(expectedCode)") case let .invitation(u, connReqInvitation, connection): return withUser(u, "connReqInvitation: \(connReqInvitation)\nconnection: \(connection)") case let .connectionIncognitoUpdated(u, toConnection): return withUser(u, String(describing: toConnection)) + case let .connectionUserChanged(u, fromConnection, toConnection, newUser): return withUser(u, "fromConnection: \(String(describing: fromConnection))\ntoConnection: \(String(describing: toConnection))\newUserId: \(String(describing: newUser.userId))") case let .connectionPlan(u, connectionPlan): return withUser(u, String(describing: connectionPlan)) case let .sentConfirmation(u, connection): return withUser(u, String(describing: connection)) case let .sentInvitation(u, connection): return withUser(u, String(describing: connection)) @@ -926,7 +939,9 @@ public enum ChatResponse: Decodable, Error { case let .memberSubErrors(u, memberSubErrors): return withUser(u, String(describing: memberSubErrors)) case let .groupEmpty(u, groupInfo): return withUser(u, String(describing: groupInfo)) case .userContactLinkSubscribed: return noDetails - case let .newChatItem(u, chatItem): return withUser(u, String(describing: chatItem)) + case let .newChatItems(u, chatItems): + let itemsString = chatItems.map { chatItem in String(describing: chatItem) }.joined(separator: "\n") + return withUser(u, itemsString) case let .chatItemStatusUpdated(u, chatItem): return withUser(u, String(describing: chatItem)) case let .chatItemUpdated(u, chatItem): return withUser(u, String(describing: chatItem)) case let .chatItemNotChanged(u, chatItem): return withUser(u, String(describing: chatItem)) @@ -1113,10 +1128,16 @@ public enum ChatPagination { } } -struct ComposedMessage: Encodable { - var fileSource: CryptoFile? +public struct ComposedMessage: Encodable { + public var fileSource: CryptoFile? var quotedItemId: Int64? var msgContent: MsgContent + + public init(fileSource: CryptoFile? = nil, quotedItemId: Int64? = nil, msgContent: MsgContent) { + self.fileSource = fileSource + self.quotedItemId = quotedItemId + self.msgContent = msgContent + } } public struct ArchiveConfig: Encodable { @@ -1842,7 +1863,6 @@ public enum ChatErrorType: Decodable, Hashable { case inlineFileProhibited(fileId: Int64) case invalidQuote case invalidForward - case forwardNoFile case invalidChatItemUpdate case invalidChatItemDelete case hasCurrentCall @@ -1857,6 +1877,7 @@ public enum ChatErrorType: Decodable, Hashable { case agentCommandError(message: String) case invalidFileDescription(message: String) case connectionIncognitoChangeProhibited + case connectionUserChangeProhibited case peerChatVRangeIncompatible case internalError(message: String) case exception(message: String) diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 1a9cf4a216..84bf445601 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -2688,6 +2688,13 @@ public enum CIDirection: Decodable, Hashable { } } } + + public func sameDirection(_ dir: CIDirection) -> Bool { + switch (self, dir) { + case let (.groupRcv(m1), .groupRcv(m2)): m1.groupMemberId == m2.groupMemberId + default: sent == dir.sent + } + } } public struct CIMeta: Decodable, Hashable { @@ -2706,7 +2713,7 @@ public struct CIMeta: Decodable, Hashable { public var deletable: Bool public var editable: Bool - public var timestampText: Text { get { formatTimestampText(itemTs) } } + public var timestampText: Text { Text(formatTimestampMeta(itemTs)) } public var recent: Bool { updatedAt + 10 > .now } public var isLive: Bool { itemLive == true } public var disappearing: Bool { !isRcvNew && itemTimed?.deleteAt != nil } @@ -2716,10 +2723,6 @@ public struct CIMeta: Decodable, Hashable { return false } - public func statusIcon(_ metaColor: Color/* = .secondary*/, _ primaryColor: Color = .accentColor) -> (String, Color)? { - itemStatus.statusIcon(metaColor, primaryColor) - } - public static func getSample(_ id: Int64, _ ts: Date, _ text: String, _ status: CIStatus = .sndNew, itemDeleted: CIDeleted? = nil, itemEdited: Bool = false, itemLive: Bool = false, deletable: Bool = true, editable: Bool = true) -> CIMeta { CIMeta( itemId: id, @@ -2762,7 +2765,11 @@ let msgTimeFormat = Date.FormatStyle.dateTime.hour().minute() let msgDateFormat = Date.FormatStyle.dateTime.day(.twoDigits).month(.twoDigits) public func formatTimestampText(_ date: Date) -> Text { - return Text(date, format: recent(date) ? msgTimeFormat : msgDateFormat) + Text(verbatim: date.formatted(recent(date) ? msgTimeFormat : msgDateFormat)) +} + +public func formatTimestampMeta(_ date: Date) -> String { + date.formatted(date: .omitted, time: .shortened) } private func recent(_ date: Date) -> Bool { @@ -2805,21 +2812,22 @@ public enum CIStatus: Decodable, Hashable { } } - public func statusIcon(_ metaColor: Color/* = .secondary*/, _ primaryColor: Color = .accentColor) -> (String, Color)? { + public func statusIcon(_ metaColor: Color, _ paleMetaColor: Color, _ primaryColor: Color = .accentColor) -> (Image, Color)? { switch self { - case .sndNew: return nil - case .sndSent: return ("checkmark", metaColor) - case let .sndRcvd(msgRcptStatus, _): + case .sndNew: nil + case let .sndSent(sndProgress): + (Image("checkmark.wide"), sndProgress == .partial ? paleMetaColor : metaColor) + case let .sndRcvd(msgRcptStatus, sndProgress): switch msgRcptStatus { - case .ok: return ("checkmark", metaColor) - case .badMsgHash: return ("checkmark", .red) + case .ok: (Image("checkmark.2"), sndProgress == .partial ? paleMetaColor : metaColor) + case .badMsgHash: (Image("checkmark.2"), .red) } - case .sndErrorAuth: return ("multiply", .red) - case .sndError: return ("multiply", .red) - case .sndWarning: return ("exclamationmark.triangle.fill", .orange) - case .rcvNew: return ("circlebadge.fill", primaryColor) - case .rcvRead: return nil - case .invalid: return ("questionmark", metaColor) + case .sndErrorAuth: (Image(systemName: "multiply"), .red) + case .sndError: (Image(systemName: "multiply"), .red) + case .sndWarning: (Image(systemName: "exclamationmark.triangle.fill"), .orange) + case .rcvNew: (Image(systemName: "circlebadge.fill"), primaryColor) + case .rcvRead: nil + case .invalid: (Image(systemName: "questionmark"), metaColor) } } @@ -2914,20 +2922,20 @@ public enum GroupSndStatus: Decodable, Hashable { case warning(agentError: SndError) case invalid(text: String) - public func statusIcon(_ metaColor: Color/* = .secondary*/, _ primaryColor: Color = .accentColor) -> (String, Color) { + public func statusIcon(_ metaColor: Color, _ primaryColor: Color = .accentColor) -> (Image, Color) { switch self { - case .new: return ("ellipsis", metaColor) - case .forwarded: return ("chevron.forward.2", metaColor) - case .inactive: return ("person.badge.minus", metaColor) - case .sent: return ("checkmark", metaColor) + case .new: (Image(systemName: "ellipsis"), metaColor) + case .forwarded: (Image(systemName: "chevron.forward.2"), metaColor) + case .inactive: (Image(systemName: "person.badge.minus"), metaColor) + case .sent: (Image("checkmark.wide"), metaColor) case let .rcvd(msgRcptStatus): switch msgRcptStatus { - case .ok: return ("checkmark", metaColor) - case .badMsgHash: return ("checkmark", .red) + case .ok: (Image("checkmark.2"), metaColor) + case .badMsgHash: (Image("checkmark.2"), .red) } - case .error: return ("multiply", .red) - case .warning: return ("exclamationmark.triangle.fill", .orange) - case .invalid: return ("questionmark", metaColor) + case .error: (Image(systemName: "multiply"), .red) + case .warning: (Image(systemName: "exclamationmark.triangle.fill"), .orange) + case .invalid: (Image(systemName: "questionmark"), metaColor) } } diff --git a/apps/ios/SimpleXChat/ImageUtils.swift b/apps/ios/SimpleXChat/ImageUtils.swift index 67218a781e..36148d4324 100644 --- a/apps/ios/SimpleXChat/ImageUtils.swift +++ b/apps/ios/SimpleXChat/ImageUtils.swift @@ -383,16 +383,34 @@ extension UIImage { } return self } +} - public convenience init?(base64Encoded: String?) { - if let base64Encoded, let data = Data(base64Encoded: dropImagePrefix(base64Encoded)) { - self.init(data: data) +public func imageFromBase64(_ base64Encoded: String?) -> UIImage? { + if let base64Encoded { + if let img = imageCache.object(forKey: base64Encoded as NSString) { + return img + } else if let data = Data(base64Encoded: dropImagePrefix(base64Encoded)), + let img = UIImage(data: data) { + imageCacheQueue.async { + imageCache.setObject(img, forKey: base64Encoded as NSString) + } + return img } else { return nil } + } else { + return nil } } +private let imageCacheQueue = DispatchQueue.global(qos: .background) + +private var imageCache: NSCache = { + var cache = NSCache() + cache.countLimit = 1000 + return cache +}() + public func getLinkPreview(url: URL, cb: @escaping (LinkPreview?) -> Void) { logger.debug("getLinkMetadata: fetching URL preview") LPMetadataProvider().startFetchingMetadata(for: url){ metadata, error in diff --git a/apps/ios/bg.lproj/Localizable.strings b/apps/ios/bg.lproj/Localizable.strings index aa43902c81..b044c8bc1e 100644 --- a/apps/ios/bg.lproj/Localizable.strings +++ b/apps/ios/bg.lproj/Localizable.strings @@ -697,7 +697,7 @@ /* No comment provided by engineer. */ "Can't invite contacts!" = "Не може да поканят контактите!"; -/* No comment provided by engineer. */ +/* alert button */ "Cancel" = "Отказ"; /* No comment provided by engineer. */ @@ -1897,9 +1897,6 @@ /* No comment provided by engineer. */ "Full name (optional)" = "Пълно име (незадължително)"; -/* No comment provided by engineer. */ -"Full name:" = "Пълно име:"; - /* No comment provided by engineer. */ "Fully decentralized – visible only to members." = "Напълно децентрализирана – видима е само за членовете."; @@ -2940,12 +2937,6 @@ /* No comment provided by engineer. */ "Profile images" = "Профилни изображения"; -/* No comment provided by engineer. */ -"Profile name" = "Име на профила"; - -/* No comment provided by engineer. */ -"Profile name:" = "Име на профила:"; - /* No comment provided by engineer. */ "Profile password" = "Профилна парола"; @@ -3211,10 +3202,11 @@ /* No comment provided by engineer. */ "Safer groups" = "По-безопасни групи"; -/* chat item action */ +/* alert button + chat item action */ "Save" = "Запази"; -/* No comment provided by engineer. */ +/* alert button */ "Save (and notify contacts)" = "Запази (и уведоми контактите)"; /* No comment provided by engineer. */ @@ -3229,9 +3221,6 @@ /* No comment provided by engineer. */ "Save archive" = "Запази архив"; -/* No comment provided by engineer. */ -"Save auto-accept settings" = "Запази настройките за автоматично приемане"; - /* No comment provided by engineer. */ "Save group profile" = "Запази профила на групата"; @@ -3253,9 +3242,6 @@ /* No comment provided by engineer. */ "Save servers?" = "Запази сървърите?"; -/* No comment provided by engineer. */ -"Save settings?" = "Запази настройките?"; - /* No comment provided by engineer. */ "Save welcome message?" = "Запази съобщението при посрещане?"; @@ -4430,7 +4416,7 @@ "Your profile **%@** will be shared." = "Вашият профил **%@** ще бъде споделен."; /* No comment provided by engineer. */ -"Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "Вашият профил се съхранява на вашето устройство и се споделя само с вашите контакти.\nSimpleX сървърите не могат да видят вашия профил."; +"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Вашият профил се съхранява на вашето устройство и се споделя само с вашите контакти. SimpleX сървърите не могат да видят вашия профил."; /* No comment provided by engineer. */ "Your profile, contacts and delivered messages are stored on your device." = "Вашият профил, контакти и доставени съобщения се съхраняват на вашето устройство."; diff --git a/apps/ios/cs.lproj/Localizable.strings b/apps/ios/cs.lproj/Localizable.strings index 220550c682..f46da2d120 100644 --- a/apps/ios/cs.lproj/Localizable.strings +++ b/apps/ios/cs.lproj/Localizable.strings @@ -562,7 +562,7 @@ /* No comment provided by engineer. */ "Can't invite contacts!" = "Nelze pozvat kontakty!"; -/* No comment provided by engineer. */ +/* alert button */ "Cancel" = "Zrušit"; /* feature offered item */ @@ -1543,9 +1543,6 @@ /* No comment provided by engineer. */ "Full name (optional)" = "Celé jméno (volitelně)"; -/* No comment provided by engineer. */ -"Full name:" = "Celé jméno:"; - /* No comment provided by engineer. */ "Fully re-implemented - work in background!" = "Plně přepracováno, prácuje na pozadí!"; @@ -2602,10 +2599,11 @@ /* No comment provided by engineer. */ "Run chat" = "Spustit chat"; -/* chat item action */ +/* alert button + chat item action */ "Save" = "Uložit"; -/* No comment provided by engineer. */ +/* alert button */ "Save (and notify contacts)" = "Uložit (a informovat kontakty)"; /* No comment provided by engineer. */ @@ -2620,9 +2618,6 @@ /* No comment provided by engineer. */ "Save archive" = "Uložit archiv"; -/* No comment provided by engineer. */ -"Save auto-accept settings" = "Uložit nastavení automatického přijímání"; - /* No comment provided by engineer. */ "Save group profile" = "Uložení profilu skupiny"; @@ -2644,9 +2639,6 @@ /* No comment provided by engineer. */ "Save servers?" = "Uložit servery?"; -/* No comment provided by engineer. */ -"Save settings?" = "Uložit nastavení?"; - /* No comment provided by engineer. */ "Save welcome message?" = "Uložit uvítací zprávu?"; @@ -3554,7 +3546,7 @@ "Your profile **%@** will be shared." = "Váš profil **%@** bude sdílen."; /* No comment provided by engineer. */ -"Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "Váš profil je uložen ve vašem zařízení a sdílen pouze s vašimi kontakty.\nServery SimpleX nevidí váš profil."; +"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Váš profil je uložen ve vašem zařízení a sdílen pouze s vašimi kontakty. Servery SimpleX nevidí váš profil."; /* No comment provided by engineer. */ "Your profile, contacts and delivered messages are stored on your device." = "Váš profil, kontakty a doručené zprávy jsou uloženy ve vašem zařízení."; diff --git a/apps/ios/de.lproj/Localizable.strings b/apps/ios/de.lproj/Localizable.strings index bab14eda6b..b69b9fa370 100644 --- a/apps/ios/de.lproj/Localizable.strings +++ b/apps/ios/de.lproj/Localizable.strings @@ -781,7 +781,7 @@ /* No comment provided by engineer. */ "Can't message member" = "Mitglied kann nicht benachrichtigt werden"; -/* No comment provided by engineer. */ +/* alert button */ "Cancel" = "Abbrechen"; /* No comment provided by engineer. */ @@ -921,16 +921,16 @@ "Chunks uploaded" = "Daten-Pakete hochgeladen"; /* swipe action */ -"Clear" = "Löschen"; +"Clear" = "Entfernen"; /* No comment provided by engineer. */ -"Clear conversation" = "Chatinhalte löschen"; +"Clear conversation" = "Chat-Inhalte entfernen"; /* No comment provided by engineer. */ -"Clear conversation?" = "Unterhaltung löschen?"; +"Clear conversation?" = "Chat-Inhalte entfernen?"; /* No comment provided by engineer. */ -"Clear private notes?" = "Private Notizen löschen?"; +"Clear private notes?" = "Private Notizen entfernen?"; /* No comment provided by engineer. */ "Clear verification" = "Überprüfung zurücknehmen"; @@ -1167,7 +1167,7 @@ "Continue" = "Weiter"; /* No comment provided by engineer. */ -"Conversation deleted!" = "Unterhaltung gelöscht!"; +"Conversation deleted!" = "Chat-Inhalte entfernt!"; /* No comment provided by engineer. */ "Copy" = "Kopieren"; @@ -2203,9 +2203,6 @@ /* No comment provided by engineer. */ "Full name (optional)" = "Vollständiger Name (optional)"; -/* No comment provided by engineer. */ -"Full name:" = "Vollständiger Name:"; - /* No comment provided by engineer. */ "Fully decentralized – visible only to members." = "Vollständig dezentralisiert – nur für Mitglieder sichtbar."; @@ -2306,7 +2303,7 @@ "Group will be deleted for all members - this cannot be undone!" = "Die Gruppe wird für alle Mitglieder gelöscht. Dies kann nicht rückgängig gemacht werden!"; /* No comment provided by engineer. */ -"Group will be deleted for you - this cannot be undone!" = "Die Gruppe wird für Sie gelöscht. Dies kann nicht rückgängig gemacht werden!"; +"Group will be deleted for you - this cannot be undone!" = "Die Gruppe wird nur bei Ihnen gelöscht. Dies kann nicht rückgängig gemacht werden!"; /* No comment provided by engineer. */ "Help" = "Hilfe"; @@ -2630,7 +2627,7 @@ "Keep" = "Behalten"; /* No comment provided by engineer. */ -"Keep conversation" = "Unterhaltung behalten"; +"Keep conversation" = "Chat-Inhalte beibehalten"; /* 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"; @@ -3115,7 +3112,7 @@ "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."; /* No comment provided by engineer. */ -"Only delete conversation" = "Nur die Unterhaltung löschen"; +"Only delete conversation" = "Nur die Chat-Inhalte löschen"; /* No comment provided by engineer. */ "Only group owners can change group preferences." = "Gruppen-Präferenzen können nur von Gruppen-Eigentümern geändert werden."; @@ -3378,12 +3375,6 @@ /* No comment provided by engineer. */ "Profile images" = "Profil-Bilder"; -/* No comment provided by engineer. */ -"Profile name" = "Profilname"; - -/* No comment provided by engineer. */ -"Profile name:" = "Profilname:"; - /* No comment provided by engineer. */ "Profile password" = "Passwort für Profil"; @@ -3460,7 +3451,7 @@ "Rate the app" = "Bewerten Sie die App"; /* No comment provided by engineer. */ -"Reachable chat toolbar" = "Erreichbare Chat-Symbolleiste"; +"Reachable chat toolbar" = "Chat-Symbolleiste unten"; /* chat item menu */ "React…" = "Reagiere…"; @@ -3715,10 +3706,11 @@ /* No comment provided by engineer. */ "Safer groups" = "Sicherere Gruppen"; -/* chat item action */ +/* alert button + chat item action */ "Save" = "Speichern"; -/* No comment provided by engineer. */ +/* alert button */ "Save (and notify contacts)" = "Speichern (und Kontakte benachrichtigen)"; /* No comment provided by engineer. */ @@ -3736,9 +3728,6 @@ /* No comment provided by engineer. */ "Save archive" = "Archiv speichern"; -/* No comment provided by engineer. */ -"Save auto-accept settings" = "Einstellungen von \"Automatisch akzeptieren\" speichern"; - /* No comment provided by engineer. */ "Save group profile" = "Gruppenprofil speichern"; @@ -3760,9 +3749,6 @@ /* No comment provided by engineer. */ "Save servers?" = "Alle Server speichern?"; -/* No comment provided by engineer. */ -"Save settings?" = "Einstellungen speichern?"; - /* No comment provided by engineer. */ "Save welcome message?" = "Begrüßungsmeldung speichern?"; @@ -3815,7 +3801,7 @@ "Search bar accepts invitation links." = "In der Suchleiste werden nun auch Einladungslinks akzeptiert."; /* No comment provided by engineer. */ -"Search or paste SimpleX link" = "Suchen oder fügen Sie den SimpleX-Link ein"; +"Search or paste SimpleX link" = "Suchen oder SimpleX-Link einfügen"; /* network option */ "sec" = "sek"; @@ -4385,7 +4371,7 @@ "The message will be marked as moderated for all members." = "Diese Nachricht wird für alle Mitglieder als moderiert gekennzeichnet."; /* No comment provided by engineer. */ -"The messages will be deleted for all members." = "Die Nachrichten werden für alle Mitglieder gelöscht werden."; +"The messages will be deleted for all members." = "Die Nachrichten werden für alle Gruppenmitglieder gelöscht."; /* 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."; @@ -4709,7 +4695,7 @@ "Use the app while in the call." = "Die App kann während eines Anrufs genutzt werden."; /* No comment provided by engineer. */ -"Use the app with one hand." = "Die App mit einer Hand nutzen."; +"Use the app with one hand." = "Die App mit einer Hand bedienen."; /* No comment provided by engineer. */ "User profile" = "Benutzerprofil"; @@ -5021,7 +5007,7 @@ "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"; /* No comment provided by engineer. */ -"You can still view conversation with %@ in the list of chats." = "Sie können in der Chatliste weiterhin die Unterhaltung mit %@ einsehen."; +"You can still view conversation with %@ in the list of chats." = "Sie können in der Chat-Liste weiterhin die Unterhaltung mit %@ einsehen."; /* No comment provided by engineer. */ "You can turn on SimpleX Lock via Settings." = "Sie können die SimpleX-Sperre über die Einstellungen aktivieren."; @@ -5189,7 +5175,7 @@ "Your profile **%@** will be shared." = "Ihr Profil **%@** wird geteilt."; /* No comment provided by engineer. */ -"Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "Ihr Profil wird auf Ihrem Gerät gespeichert und nur mit Ihren Kontakten geteilt.\nSimpleX-Server können Ihr Profil nicht einsehen."; +"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Ihr Profil wird auf Ihrem Gerät gespeichert und nur mit Ihren Kontakten geteilt. SimpleX-Server können Ihr Profil nicht einsehen."; /* No comment provided by engineer. */ "Your profile, contacts and delivered messages are stored on your device." = "Ihr Profil, Ihre Kontakte und zugestellten Nachrichten werden auf Ihrem Gerät gespeichert."; diff --git a/apps/ios/es.lproj/Localizable.strings b/apps/ios/es.lproj/Localizable.strings index 3dce4e4474..ed5efe14ab 100644 --- a/apps/ios/es.lproj/Localizable.strings +++ b/apps/ios/es.lproj/Localizable.strings @@ -781,7 +781,7 @@ /* No comment provided by engineer. */ "Can't message member" = "No se pueden enviar mensajes al miembro"; -/* No comment provided by engineer. */ +/* alert button */ "Cancel" = "Cancelar"; /* No comment provided by engineer. */ @@ -2203,9 +2203,6 @@ /* No comment provided by engineer. */ "Full name (optional)" = "Nombre completo (opcional)"; -/* No comment provided by engineer. */ -"Full name:" = "Nombre completo:"; - /* No comment provided by engineer. */ "Fully decentralized – visible only to members." = "Completamente descentralizado y sólo visible para los miembros."; @@ -3378,12 +3375,6 @@ /* No comment provided by engineer. */ "Profile images" = "Forma de los perfiles"; -/* No comment provided by engineer. */ -"Profile name" = "Nombre del perfil"; - -/* No comment provided by engineer. */ -"Profile name:" = "Nombre del perfil:"; - /* No comment provided by engineer. */ "Profile password" = "Contraseña del perfil"; @@ -3715,10 +3706,11 @@ /* No comment provided by engineer. */ "Safer groups" = "Grupos más seguros"; -/* chat item action */ +/* alert button + chat item action */ "Save" = "Guardar"; -/* No comment provided by engineer. */ +/* alert button */ "Save (and notify contacts)" = "Guardar (y notificar contactos)"; /* No comment provided by engineer. */ @@ -3736,9 +3728,6 @@ /* No comment provided by engineer. */ "Save archive" = "Guardar archivo"; -/* No comment provided by engineer. */ -"Save auto-accept settings" = "Guardar configuración de auto aceptar"; - /* No comment provided by engineer. */ "Save group profile" = "Guardar perfil de grupo"; @@ -3760,9 +3749,6 @@ /* No comment provided by engineer. */ "Save servers?" = "¿Guardar servidores?"; -/* No comment provided by engineer. */ -"Save settings?" = "¿Guardar configuración?"; - /* No comment provided by engineer. */ "Save welcome message?" = "¿Guardar mensaje de bienvenida?"; @@ -5189,7 +5175,7 @@ "Your profile **%@** will be shared." = "El perfil **%@** será compartido."; /* No comment provided by engineer. */ -"Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "Tu perfil es almacenado en tu dispositivo y solamente se comparte con tus contactos.\nLos servidores SimpleX no pueden ver tu perfil."; +"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Tu perfil es almacenado en tu dispositivo y solamente se comparte con tus contactos. Los servidores SimpleX no pueden ver tu perfil."; /* No comment provided by engineer. */ "Your profile, contacts and delivered messages are stored on your device." = "Tu perfil, contactos y mensajes se almacenan en tu dispositivo."; diff --git a/apps/ios/fi.lproj/Localizable.strings b/apps/ios/fi.lproj/Localizable.strings index a7820e69b1..ee7056709e 100644 --- a/apps/ios/fi.lproj/Localizable.strings +++ b/apps/ios/fi.lproj/Localizable.strings @@ -547,7 +547,7 @@ /* No comment provided by engineer. */ "Can't invite contacts!" = "Kontakteja ei voi kutsua!"; -/* No comment provided by engineer. */ +/* alert button */ "Cancel" = "Peruuta"; /* feature offered item */ @@ -1519,9 +1519,6 @@ /* No comment provided by engineer. */ "Full name (optional)" = "Koko nimi (valinnainen)"; -/* No comment provided by engineer. */ -"Full name:" = "Koko nimi:"; - /* No comment provided by engineer. */ "Fully re-implemented - work in background!" = "Täysin uudistettu - toimii taustalla!"; @@ -2572,10 +2569,11 @@ /* No comment provided by engineer. */ "Run chat" = "Käynnistä chat"; -/* chat item action */ +/* alert button + chat item action */ "Save" = "Tallenna"; -/* No comment provided by engineer. */ +/* alert button */ "Save (and notify contacts)" = "Tallenna (ja ilmoita kontakteille)"; /* No comment provided by engineer. */ @@ -2590,9 +2588,6 @@ /* No comment provided by engineer. */ "Save archive" = "Tallenna arkisto"; -/* No comment provided by engineer. */ -"Save auto-accept settings" = "Tallenna automaattisen hyväksynnän asetukset"; - /* No comment provided by engineer. */ "Save group profile" = "Tallenna ryhmäprofiili"; @@ -2614,9 +2609,6 @@ /* No comment provided by engineer. */ "Save servers?" = "Tallenna palvelimet?"; -/* No comment provided by engineer. */ -"Save settings?" = "Tallenna asetukset?"; - /* No comment provided by engineer. */ "Save welcome message?" = "Tallenna tervetuloviesti?"; @@ -3512,7 +3504,7 @@ "Your profile **%@** will be shared." = "Profiilisi **%@** jaetaan."; /* No comment provided by engineer. */ -"Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "Profiilisi tallennetaan laitteeseesi ja jaetaan vain yhteystietojesi kanssa.\nSimpleX-palvelimet eivät näe profiiliasi."; +"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Profiilisi tallennetaan laitteeseesi ja jaetaan vain yhteystietojesi kanssa. SimpleX-palvelimet eivät näe profiiliasi."; /* No comment provided by engineer. */ "Your profile, contacts and delivered messages are stored on your device." = "Profiilisi, kontaktisi ja toimitetut viestit tallennetaan laitteellesi."; diff --git a/apps/ios/fr.lproj/Localizable.strings b/apps/ios/fr.lproj/Localizable.strings index b4f256762c..e1c4c20461 100644 --- a/apps/ios/fr.lproj/Localizable.strings +++ b/apps/ios/fr.lproj/Localizable.strings @@ -781,7 +781,7 @@ /* No comment provided by engineer. */ "Can't message member" = "Impossible d'envoyer un message à ce membre"; -/* No comment provided by engineer. */ +/* alert button */ "Cancel" = "Annuler"; /* No comment provided by engineer. */ @@ -2203,9 +2203,6 @@ /* No comment provided by engineer. */ "Full name (optional)" = "Nom complet (optionnel)"; -/* No comment provided by engineer. */ -"Full name:" = "Nom complet :"; - /* No comment provided by engineer. */ "Fully decentralized – visible only to members." = "Entièrement décentralisé – visible que par ses membres."; @@ -3378,12 +3375,6 @@ /* No comment provided by engineer. */ "Profile images" = "Images de profil"; -/* No comment provided by engineer. */ -"Profile name" = "Nom du profil"; - -/* No comment provided by engineer. */ -"Profile name:" = "Nom du profil :"; - /* No comment provided by engineer. */ "Profile password" = "Mot de passe de profil"; @@ -3715,10 +3706,11 @@ /* No comment provided by engineer. */ "Safer groups" = "Groupes plus sûrs"; -/* chat item action */ +/* alert button + chat item action */ "Save" = "Enregistrer"; -/* No comment provided by engineer. */ +/* alert button */ "Save (and notify contacts)" = "Enregistrer (et en informer les contacts)"; /* No comment provided by engineer. */ @@ -3736,9 +3728,6 @@ /* No comment provided by engineer. */ "Save archive" = "Enregistrer l'archive"; -/* No comment provided by engineer. */ -"Save auto-accept settings" = "Enregistrer les paramètres de validation automatique"; - /* No comment provided by engineer. */ "Save group profile" = "Enregistrer le profil du groupe"; @@ -3760,9 +3749,6 @@ /* No comment provided by engineer. */ "Save servers?" = "Enregistrer les serveurs ?"; -/* No comment provided by engineer. */ -"Save settings?" = "Enregistrer les paramètres ?"; - /* No comment provided by engineer. */ "Save welcome message?" = "Enregistrer le message d'accueil ?"; @@ -5189,7 +5175,7 @@ "Your profile **%@** will be shared." = "Votre profil **%@** sera partagé."; /* No comment provided by engineer. */ -"Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "Votre profil est stocké sur votre appareil et est seulement partagé avec vos contacts.\nLes serveurs SimpleX ne peuvent pas voir votre profil."; +"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."; /* 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."; diff --git a/apps/ios/hu.lproj/Localizable.strings b/apps/ios/hu.lproj/Localizable.strings index 8c70bcd626..54c0b6bd50 100644 --- a/apps/ios/hu.lproj/Localizable.strings +++ b/apps/ios/hu.lproj/Localizable.strings @@ -29,7 +29,7 @@ "- optionally notify deleted contacts.\n- profile names with spaces.\n- and more!" = "- opcionális értesítés a törölt kapcsolatokról.\n- profilnevek szóközökkel.\n- és még sok más!"; /* No comment provided by engineer. */ -"- voice messages up to 5 minutes.\n- custom time to disappear.\n- editing history." = "- hangüzenetek legfeljebb 5 perces időtartamig.\n- egyedi eltűnési időhatár megadása.\n- előzmények szerkesztése."; +"- voice messages up to 5 minutes.\n- custom time to disappear.\n- editing history." = "- 5 perc hosszúságú hangüzenetek.\n- egyedi üzenet-eltűnési időkorlát.\n- előzmények szerkesztése."; /* No comment provided by engineer. */ ", " = ", "; @@ -62,7 +62,7 @@ "[Send us email](mailto:chat@simplex.chat)" = "[Küldjön nekünk e-mailt](mailto:chat@simplex.chat)"; /* No comment provided by engineer. */ -"[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[Csillag a GitHubon](https://github.com/simplex-chat/simplex-chat)"; +"[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."; @@ -263,7 +263,7 @@ "`a + b`" = "a + b"; /* email text */ -"

Hi!

\n

Connect to me via SimpleX Chat

" = "

Üdvözlöm!

\n

Csatlakozzon hozzám a SimpleX Chaten

"; +"

Hi!

\n

Connect to me via SimpleX Chat

" = "

Üdvözlöm!

\n

Csatlakozzon hozzám a SimpleX Chaten keresztül

"; /* No comment provided by engineer. */ "~strike~" = "\\~áthúzott~"; @@ -343,7 +343,7 @@ "Accept" = "Elfogadás"; /* No comment provided by engineer. */ -"Accept connection request?" = "Kapcsolódási kérelem elfogadása?"; +"Accept connection request?" = "Ismerőskérelem elfogadása?"; /* notification body */ "Accept contact request from %@?" = "Elfogadja %@ kapcsolat kérését?"; @@ -659,10 +659,10 @@ "Bad desktop address" = "Hibás számítógép cím"; /* integrity error chat item */ -"bad message hash" = "hibás az üzenet ellenőrzőösszege"; +"bad message hash" = "hibás az üzenet hasító értéke"; /* No comment provided by engineer. */ -"Bad message hash" = "Hibás az üzenet ellenőrzőösszege"; +"Bad message hash" = "Hibás az üzenet hasító értéke"; /* integrity error chat item */ "bad message ID" = "téves üzenet ID"; @@ -749,7 +749,7 @@ "Call already ended!" = "A hívás már befejeződött!"; /* call status */ -"call error" = "hiba a hívásban"; +"call error" = "híváshiba"; /* call status */ "call in progress" = "hívás folyamatban"; @@ -781,7 +781,7 @@ /* No comment provided by engineer. */ "Can't message member" = "Nem lehet üzenetet küldeni a tagnak"; -/* No comment provided by engineer. */ +/* alert button */ "Cancel" = "Mégse"; /* No comment provided by engineer. */ @@ -1002,7 +1002,7 @@ "Connect incognito" = "Kapcsolódás inkognitóban"; /* No comment provided by engineer. */ -"Connect to desktop" = "Kapcsolódás számítógéphez"; +"Connect to desktop" = "Társítás számítógéppel"; /* No comment provided by engineer. */ "connect to SimpleX Chat developers." = "Kapcsolódás a SimpleX Chat fejlesztőkhöz."; @@ -1014,7 +1014,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 egyszer használatos hivatkozása!"; +"Connect to yourself?\nThis is your own one-time link!" = "Kapcsolódás saját magához?\nEz az ön egyszer használatos 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!"; @@ -1038,7 +1038,7 @@ "Connected" = "Kapcsolódva"; /* No comment provided by engineer. */ -"Connected desktop" = "Csatlakoztatott számítógép"; +"Connected desktop" = "Társított számítógép"; /* rcv group event chat item */ "connected directly" = "közvetlenül kapcsolódva"; @@ -1068,7 +1068,7 @@ "connecting (introduction invitation)" = "kapcsolódás (bemutatkozó meghívó)"; /* call status */ -"connecting call" = "hívás kapcsolódik…"; +"connecting call" = "kapcsolódási hívás…"; /* No comment provided by engineer. */ "Connecting server…" = "Kapcsolódás a kiszolgálóhoz…"; @@ -1128,7 +1128,7 @@ "Contact allows" = "Ismerős engedélyezi"; /* No comment provided by engineer. */ -"Contact already exists" = "Létező ismerős"; +"Contact already exists" = "Az ismerős már létezik"; /* No comment provided by engineer. */ "Contact deleted!" = "Ismerős törölve!"; @@ -1197,7 +1197,7 @@ "Create group" = "Csoport létrehozása"; /* No comment provided by engineer. */ -"Create group link" = "Csoportos hivatkozás létrehozása"; +"Create group link" = "Csoporthivatkozás létrehozása"; /* No comment provided by engineer. */ "Create link" = "Hivatkozás létrehozása"; @@ -1233,7 +1233,7 @@ "Created on %@" = "Létrehozva %@"; /* No comment provided by engineer. */ -"Creating archive link" = "Archív hivatkozás létrehozása"; +"Creating archive link" = "Archívum hivatkozás létrehozása"; /* No comment provided by engineer. */ "Creating link…" = "Hivatkozás létrehozása…"; @@ -1450,7 +1450,7 @@ "Delete old database?" = "Régi adatbázis törlése?"; /* No comment provided by engineer. */ -"Delete pending connection?" = "Függő kapcsolatfelvételi kérések törlése?"; +"Delete pending connection?" = "Függőben lévő ismerőskérelem törlése?"; /* No comment provided by engineer. */ "Delete profile" = "Profil törlése"; @@ -1654,7 +1654,7 @@ "Downloading link details" = "Letöltési hivatkozás részletei"; /* No comment provided by engineer. */ -"Duplicate display name!" = "Duplikált megjelenítési név!"; +"Duplicate display name!" = "Duplikált megjelenített név!"; /* integrity error chat item */ "duplicate message" = "duplikált üzenet"; @@ -1852,7 +1852,7 @@ "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"; +"Error adding member(s)" = "Hiba a tag(ok) hozzáadásakor"; /* No comment provided by engineer. */ "Error changing address" = "Hiba a cím megváltoztatásakor"; @@ -1873,7 +1873,7 @@ "Error creating group" = "Hiba a csoport létrehozásakor"; /* No comment provided by engineer. */ -"Error creating group link" = "Hiba a csoport hivatkozásának létrehozásakor"; +"Error creating group link" = "Hiba a csoporthivatkozás létrehozásakor"; /* No comment provided by engineer. */ "Error creating member contact" = "Hiba az ismerőssel történő kapcsolat létrehozásában"; @@ -2002,7 +2002,7 @@ "Error synchronizing connection" = "Hiba a kapcsolat szinkronizálása közben"; /* No comment provided by engineer. */ -"Error updating group link" = "Hiba a csoport hivatkozás frissítésekor"; +"Error updating group link" = "Hiba a csoporthivatkozás frissítésekor"; /* No comment provided by engineer. */ "Error updating message" = "Hiba az üzenet frissítésekor"; @@ -2203,9 +2203,6 @@ /* No comment provided by engineer. */ "Full name (optional)" = "Teljes név (opcionális)"; -/* No comment provided by engineer. */ -"Full name:" = "Teljes név:"; - /* No comment provided by engineer. */ "Fully decentralized – visible only to members." = "Teljesen decentralizált - kizárólag tagok számára látható."; @@ -2255,10 +2252,10 @@ "Group invitation is no longer valid, it was removed by sender." = "A csoport meghívó már nem érvényes, a küldője törölte."; /* No comment provided by engineer. */ -"Group link" = "Csoport hivatkozás"; +"Group link" = "Csoporthivatkozás"; /* No comment provided by engineer. */ -"Group links" = "Csoport hivatkozások"; +"Group links" = "Csoporthivatkozások"; /* No comment provided by engineer. */ "Group members can add message reactions." = "Csoporttagok üzenetreakciókat adhatnak hozzá."; @@ -2303,7 +2300,7 @@ "Group welcome message" = "Csoport üdvözlő üzenete"; /* No comment provided by engineer. */ -"Group will be deleted for all members - this cannot be undone!" = "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 minden 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!"; @@ -2444,10 +2441,10 @@ "incognito via contact address link" = "inkognitó a kapcsolattartási hivatkozáson keresztül"; /* chat list item description */ -"incognito via group link" = "inkognitó a csoportos hivatkozáson keresztül"; +"incognito via group link" = "inkognitó a csoporthivatkozáson keresztül"; /* chat list item description */ -"incognito via one-time link" = "inkognitó az egyszer használatos hivatkozáson keresztül"; +"incognito via one-time link" = "inkognitó egy egyszer használatos hivatkozáson keresztül"; /* notification */ "Incoming audio call" = "Bejövő hanghívás"; @@ -2501,13 +2498,13 @@ "invalid chat data" = "érvénytelen csevegés adat"; /* No comment provided by engineer. */ -"Invalid connection link" = "Érvénytelen kapcsolati hivatkozás"; +"Invalid connection link" = "Érvénytelen kapcsolattartási hivatkozás"; /* invalid chat item */ "invalid data" = "érvénytelen adat"; /* No comment provided by engineer. */ -"Invalid display name!" = "Érvénytelen megjelenítendő felhaszálónév!"; +"Invalid display name!" = "Érvénytelen megjelenítendő név!"; /* No comment provided by engineer. */ "Invalid link" = "Érvénytelen hivatkozás"; @@ -2558,7 +2555,7 @@ "invited to connect" = "meghívta, hogy csatlakozzon"; /* rcv group event chat item */ -"invited via your group link" = "meghíva az ön csoport hivatkozásán keresztül"; +"invited via your group link" = "meghíva az ön csoporthivatkozásán keresztül"; /* No comment provided by engineer. */ "iOS Keychain is used to securely store passphrase - it allows receiving push notifications." = "Az iOS kulcstartó a jelmondat biztonságos tárolására szolgál - lehetővé teszi a push-értesítések fogadását."; @@ -2666,7 +2663,7 @@ "left" = "elhagyta a csoportot"; /* email subject */ -"Let's talk in SimpleX Chat" = "Beszélgessünk a SimpleX Chat-ben"; +"Let's talk in SimpleX Chat" = "Beszélgessünk a SimpleX Chatben"; /* No comment provided by engineer. */ "Light" = "Világos"; @@ -2675,13 +2672,13 @@ "Limitations" = "Korlátozások"; /* No comment provided by engineer. */ -"Link mobile and desktop apps! 🔗" = "Társítsa össze a mobil és az asztali alkalmazásokat! 🔗"; +"Link mobile and desktop apps! 🔗" = "Társítsa össze a mobil és asztali alkalmazásokat! 🔗"; /* No comment provided by engineer. */ -"Linked desktop options" = "Összekapcsolt számítógép beállítások"; +"Linked desktop options" = "Társított számítógép beállítások"; /* No comment provided by engineer. */ -"Linked desktops" = "Összekapcsolt számítógépek"; +"Linked desktops" = "Társított számítógépek"; /* No comment provided by engineer. */ "LIVE" = "ÉLŐ"; @@ -2723,7 +2720,7 @@ "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-nek nincsenek felhasználói azonosítói, akkor hogyan tud üzeneteket kézbesíteni?*"; +"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ói 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"; @@ -2738,7 +2735,7 @@ "Markdown in messages" = "Markdown az üzenetekben"; /* marked deleted chat item preview text */ -"marked deleted" = "töröltnek jelölve"; +"marked deleted" = "törlésre jelölve"; /* No comment provided by engineer. */ "Max 30 seconds, received instantly." = "Max. 30 másodperc, azonnal érkezett."; @@ -2903,10 +2900,10 @@ "moderated" = "moderált"; /* No comment provided by engineer. */ -"Moderated at" = "Moderálva lett ekkor:"; +"Moderated at" = "Moderálva ekkor:"; /* copied message info */ -"Moderated at: %@" = "Moderálva lett ekkor: %@"; +"Moderated at: %@" = "Moderálva ekkor: %@"; /* marked deleted chat item preview text */ "moderated by %@" = "moderálva lett %@ által"; @@ -2966,7 +2963,7 @@ "New chat experience 🎉" = "Új csevegési élmény 🎉"; /* notification */ -"New contact request" = "Új kapcsolattartási kérelem"; +"New contact request" = "Új ismerőskérelem"; /* notification */ "New contact:" = "Új kapcsolat:"; @@ -3011,7 +3008,7 @@ "No app password" = "Nincs alkalmazás jelszó"; /* No comment provided by engineer. */ -"No contacts selected" = "Nem kerültek ismerősök kiválasztásra"; +"No contacts selected" = "Nincs kiválasztva ismerős"; /* No comment provided by engineer. */ "No contacts to add" = "Nincs hozzáadandó ismerős"; @@ -3056,7 +3053,7 @@ "Not compatible!" = "Nem kompatibilis!"; /* No comment provided by engineer. */ -"Nothing selected" = "Semmi sincs kiválasztva"; +"Nothing selected" = "Nincs kiválasztva semmi"; /* No comment provided by engineer. */ "Notifications" = "Értesítések"; @@ -3094,7 +3091,7 @@ "Old database" = "Régi adatbázis"; /* No comment provided by engineer. */ -"Old database archive" = "Régi adatbázis archívum"; +"Old database archive" = "Régi adatbázis-archívum"; /* group pref value */ "on" = "bekapcsolva"; @@ -3112,7 +3109,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 a klienseszközök tárolják a felhasználói profilokat, névjegyeket, csoportokat és a **2 rétegű végponttól-végpontig titkosítással** küldött üzeneteket."; +"Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." = "Csak a klienseszközök tárolják a felhasználói 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"; @@ -3193,7 +3190,7 @@ "Or scan QR code" = "Vagy QR-kód beolvasása"; /* No comment provided by engineer. */ -"Or securely share this file link" = "Vagy a fájl hivítkozásának biztonságos megosztása"; +"Or securely share this file link" = "Vagy ossza meg biztonságosan ezt a fájlhivatkozást"; /* No comment provided by engineer. */ "Or show this code" = "Vagy mutassa meg ezt a kódot"; @@ -3235,7 +3232,7 @@ "Password to show" = "Jelszó megjelenítése"; /* past/unknown group member */ -"Past member %@" = "Már nem tag - %@"; +"Past member %@" = "%@ (már nem tag)"; /* No comment provided by engineer. */ "Paste desktop address" = "Számítógép címének beillesztése"; @@ -3247,13 +3244,13 @@ "Paste link to connect!" = "Hivatkozás beillesztése a kapcsolódáshoz!"; /* No comment provided by engineer. */ -"Paste the link you received" = "Fogadott hivatkozás beillesztése"; +"Paste the link you received" = "Kapott hivatkozás beillesztése"; /* No comment provided by engineer. */ "peer-to-peer" = "ponttól-pontig"; /* No comment provided by engineer. */ -"Pending" = "Függő"; +"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."; @@ -3286,7 +3283,7 @@ "Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection.\nPlease share any other issues with the developers." = "Ellenőrizze, hogy a mobil és az asztali számítógép ugyanahhoz a helyi hálózathoz csatlakozik-e, valamint az asztali 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."; /* No comment provided by engineer. */ -"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 ismerősét, hogy küldjön egy másikat."; +"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 hálózati kapcsolatát a(z) %@ segítségével, és próbálja újra."; @@ -3378,12 +3375,6 @@ /* No comment provided by engineer. */ "Profile images" = "Profilképek"; -/* No comment provided by engineer. */ -"Profile name" = "Profilnév"; - -/* No comment provided by engineer. */ -"Profile name:" = "Profil neve:"; - /* No comment provided by engineer. */ "Profile password" = "Profiljelszó"; @@ -3493,7 +3484,7 @@ "Receive errors" = "Üzenetfogadási hibák"; /* No comment provided by engineer. */ -"received answer…" = "fogadott válasz…"; +"received answer…" = "válasz fogadása…"; /* No comment provided by engineer. */ "Received at" = "Fogadva ekkor:"; @@ -3647,7 +3638,7 @@ "Required" = "Szükséges"; /* No comment provided by engineer. */ -"Reset" = "Alaphelyzetbe állítás"; +"Reset" = "Visszaállítás"; /* No comment provided by engineer. */ "Reset all hints" = "Tippek visszaállítása"; @@ -3659,13 +3650,13 @@ "Reset all statistics?" = "Minden statisztika visszaállítása?"; /* No comment provided by engineer. */ -"Reset colors" = "Színek alaphelyzetbe állítása"; +"Reset colors" = "Színek visszaállítása"; /* No comment provided by engineer. */ "Reset to app theme" = "Alkalmazás témájának visszaállítása"; /* No comment provided by engineer. */ -"Reset to defaults" = "Alaphelyzetbe állítás"; +"Reset to defaults" = "Visszaállítás alaphelyzetbe"; /* No comment provided by engineer. */ "Reset to user theme" = "Felhasználó által létrehozott téma visszaállítása"; @@ -3715,10 +3706,11 @@ /* No comment provided by engineer. */ "Safer groups" = "Biztonságosabb csoportok"; -/* chat item action */ +/* alert button + chat item action */ "Save" = "Mentés"; -/* No comment provided by engineer. */ +/* alert button */ "Save (and notify contacts)" = "Mentés (és az ismerősök értesítése)"; /* No comment provided by engineer. */ @@ -3736,9 +3728,6 @@ /* No comment provided by engineer. */ "Save archive" = "Archívum mentése"; -/* No comment provided by engineer. */ -"Save auto-accept settings" = "Automatikus elfogadási beállítások mentése"; - /* No comment provided by engineer. */ "Save group profile" = "Csoportprofil elmentése"; @@ -3760,9 +3749,6 @@ /* No comment provided by engineer. */ "Save servers?" = "Kiszolgálók mentése?"; -/* No comment provided by engineer. */ -"Save settings?" = "Beállítások mentése?"; - /* No comment provided by engineer. */ "Save welcome message?" = "Üdvözlőszöveg mentése?"; @@ -4013,10 +3999,10 @@ "Servers" = "Kiszolgálók"; /* No comment provided by engineer. */ -"Servers info" = "információk a kiszolgálókról"; +"Servers info" = "Információk a kiszolgálókról"; /* No comment provided by engineer. */ -"Servers statistics will be reset - this cannot be undone!" = "A kiszolgálók statisztikái visszaállnak - ez nem vonható vissza!"; +"Servers statistics will be reset - this cannot be undone!" = "A kiszolgálók statisztikái visszaállnak - ez a művelet nem vonható vissza!"; /* No comment provided by engineer. */ "Session code" = "Munkamenet kód"; @@ -4136,7 +4122,7 @@ "SimpleX encrypted message or connection event" = "SimpleX titkosított üzenet vagy kapcsolati esemény"; /* simplex link type */ -"SimpleX group link" = "SimpleX csoport hivatkozás"; +"SimpleX group link" = "SimpleX csoporthivatkozás"; /* chat feature */ "SimpleX links" = "SimpleX hivatkozások"; @@ -4274,7 +4260,7 @@ "Subscriptions ignored" = "Elutasított feliratkozások"; /* No comment provided by engineer. */ -"Support SimpleX Chat" = "Támogassa a SimpleX Chatet"; +"Support SimpleX Chat" = "SimpleX Chat támogatása"; /* No comment provided by engineer. */ "System" = "Rendszer"; @@ -4343,7 +4329,7 @@ "Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" = "Köszönet a felhasználóknak – [hozzájárulás a Weblate-en keresztül](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"; /* No comment provided by engineer. */ -"Thanks to the users – contribute via Weblate!" = "Köszönet a felhasználóknak - hozzájárulás a Weblaten!"; +"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."; @@ -4358,10 +4344,10 @@ "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 kód nem egy SimpleX hivatkozás QR-kód."; +"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 kapcsolat vissza lesz vonva!"; +"The connection you accepted will be cancelled!" = "Az ön által elfogadott kérelem vissza lesz vonva!"; /* No comment provided by engineer. */ "The contact you shared this link with will NOT be able to connect!" = "Ismerőse, akivel megosztotta ezt a hivatkozást, NEM fog tudni kapcsolódni!"; @@ -4373,7 +4359,7 @@ "The encryption is working and the new encryption agreement is not required. It may result in connection errors!" = "A titkosítás működik, és új titkosítási egyezményre nincs szükség. Ez kapcsolati hibákat eredményezhet!"; /* No comment provided by engineer. */ -"The hash of the previous message is different." = "Az előző üzenet ellenőrzőösszege különbözik."; +"The hash of the previous message is different." = "Az előző üzenet hasító értéke különbözik."; /* No comment provided by engineer. */ "The ID of the next message is incorrect (less or equal to the previous).\nIt can happen because of some bug or when the connection is compromised." = "A következő üzenet azonosítója hibás (kisebb vagy egyenlő az előzővel).\nEz valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő."; @@ -4442,7 +4428,7 @@ "This device name" = "Ennek az eszköznek a neve"; /* No comment provided by engineer. */ -"This display name is invalid. Please choose another name." = "Ez a megjelenített felhasználónév érvénytelen. Válasszon egy másik nevet."; +"This display name is invalid. Please choose another name." = "Ez a megjelenített név érvénytelen. Válasszon egy másik nevet."; /* No comment provided by engineer. */ "This group has over %lld members, delivery receipts are not sent." = "Ennek a csoportnak több mint %lld tagja van, a kézbesítési jelentések nem kerülnek elküldésre."; @@ -4451,13 +4437,13 @@ "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 egyszer használatos hivatkozása!"; +"This is your own one-time link!" = "Ez az ön egyszer használatos hivatkozása!"; /* No comment provided by engineer. */ "This is your own SimpleX address!" = "Ez az ön SimpleX címe!"; /* No comment provided by engineer. */ -"This link was used with another mobile device, please create a new link on the desktop." = "Ezt a hivatkozást egy másik mobilleszközön már használták, hozzon létre egy új hivatkozást az asztali számítógépén."; +"This link was used with another mobile device, please create a new link on the desktop." = "Ezt a hivatkozást egy másik mobileszközön már használták, hozzon létre egy új hivatkozást az asztali 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."; @@ -4520,10 +4506,10 @@ "Transport sessions" = "Munkamenetek átvitele"; /* No comment provided by engineer. */ -"Trying to connect to the server used to receive messages from this contact (error: %@)." = "Kapcsolódási kísérlet ahhoz a kiszolgálóhoz, amely az adott ismerőstől érkező üzenetek fogadására szolgál (hiba: %@)."; +"Trying to connect to the server used to receive messages from this contact (error: %@)." = "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: %@)."; /* No comment provided by engineer. */ -"Trying to connect to the server used to receive messages from this contact." = "Kapcsolódási kísérlet ahhoz a kiszolgálóhoz, amely az adott ismerőstől érkező üzenetek fogadására szolgál."; +"Trying to connect to the server used to receive messages from this contact." = "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."; /* No comment provided by engineer. */ "Turkish interface" = "Török kezelőfelület"; @@ -4598,7 +4584,7 @@ "Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." = "Hacsak nem az iOS hívási felületét használja, engedélyezze a Ne zavarjanak módot a megszakítások elkerülése érdekében."; /* No comment provided by engineer. */ -"Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection." = "Hacsak az ismerőse nem törölte a kapcsolatot, vagy ez a hivatkozás már használatban volt, lehet hogy ez egy hiba – jelentse a problémát.\nA kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsolati hivatkozást, és ellenőrizze, hogy a hálózati kapcsolat stabil-e."; +"Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection." = "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."; /* No comment provided by engineer. */ "Unlink" = "Szétkapcsolás"; @@ -4682,7 +4668,7 @@ "Use for new connections" = "Alkalmazás új kapcsolatokhoz"; /* No comment provided by engineer. */ -"Use from desktop" = "Használat számítógépről"; +"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"; @@ -4754,10 +4740,10 @@ "via contact address link" = "kapcsolattartási cím-hivatkozáson keresztül"; /* chat list item description */ -"via group link" = "csoport hivatkozáson keresztül"; +"via group link" = "a csoporthivatkozáson keresztül"; /* chat list item description */ -"via one-time link" = "egyszer használatos hivatkozáson keresztül"; +"via one-time link" = "egy egyszer használatos hivatkozáson keresztül"; /* No comment provided by engineer. */ "via relay" = "átjátszón keresztül"; @@ -4934,7 +4920,7 @@ "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."; /* No comment provided by engineer. */ -"You are already connected to %@." = "Már kapcsolódva van hozzá: %@."; +"You are already connected to %@." = "Ön már kapcsolódva van ehhez: %@."; /* No comment provided by engineer. */ "You are already connecting to %@." = "Már folyamatban van a kapcsolódás ehhez: %@."; @@ -4958,7 +4944,7 @@ "You are already joining the group!\nRepeat join request?" = "Csatlakozás folyamatban!\nCsatlakozási ké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őstől érkező üzenetek fogadására szolgál."; +"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."; /* No comment provided by engineer. */ "you are invited to group" = "meghívást kapott a csoportba"; @@ -4973,7 +4959,7 @@ "you are observer" = "megfigyelő szerep"; /* snd group event chat item */ -"you blocked %@" = "ön letiltotta %@-t"; +"you blocked %@" = "ön letiltotta őt: %@"; /* No comment provided by engineer. */ "You can accept calls from lock screen, without device and app authentication." = "Hívásokat fogadhat a lezárási képernyőről, eszköz- és alkalmazáshitelesítés nélkül."; @@ -4997,7 +4983,7 @@ "You can hide or mute a user profile - swipe it to the right." = "Elrejtheti vagy lenémíthatja a felhasználó profiljait - csúsztassa jobbra a profilt."; /* No comment provided by engineer. */ -"You can make it visible to your SimpleX contacts via Settings." = "Láthatóvá teheti SimpleX ismerősök számára a Beállításokban."; +"You can make it visible to your SimpleX contacts via Settings." = "Láthatóvá teheti a SimpleXbeli ismerősei számára a „Beállításokban”."; /* notification body */ "You can now chat with %@" = "Mostantól küldhet üzeneteket %@ számára"; @@ -5111,7 +5097,7 @@ "You will be connected to group when the group host's device is online, please wait or check later!" = "Akkor lesz kapcsolódva a csoporthoz, amikor a csoport tulajdonosának eszköze online lesz, várjon, vagy ellenőrizze később!"; /* No comment provided by engineer. */ -"You will be connected when group link host's device is online, please wait or check later!" = "Akkor lesz kapcsolódva, amikor a csoportos hivatkozás tulajdonosának eszköze online lesz, várjon, vagy ellenőrizze később!"; +"You will be connected when group link host's device is online, please wait or check later!" = "Akkor lesz kapcsolódva, amikor a csoporthivatkozás tulajdonosának eszköze online lesz, várjon, vagy ellenőrizze később!"; /* No comment provided by engineer. */ "You will be connected when your connection request is accepted, please wait or check later!" = "Akkor lesz kapcsolódva, ha a kapcsolódási kérelme elfogadásra kerül, várjon, vagy ellenőrizze később!"; @@ -5189,7 +5175,7 @@ "Your profile **%@** will be shared." = "A(z) **%@** nevű profilja megosztásra fog kerülni."; /* No comment provided by engineer. */ -"Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "Profilja az eszközön van tárolva, és csak az ismerősökkel kerül megosztásra.\nA SimpleX kiszolgálók nem látjhatják profilját."; +"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Profilja az eszközön van tárolva, és csak az ismerősökkel kerül megosztásra. A SimpleX kiszolgálók nem látjhatják profilját."; /* No comment provided by engineer. */ "Your profile, contacts and delivered messages are stored on your device." = "Profilja, ismerősei és az elküldött üzenetei az eszközön kerülnek tárolásra."; @@ -5207,7 +5193,7 @@ "Your settings" = "Beállítások"; /* No comment provided by engineer. */ -"Your SimpleX address" = "Az ön SimpleX címe"; +"Your SimpleX address" = "Profil SimpleX címe"; /* No comment provided by engineer. */ "Your SMP servers" = "SMP kiszolgálók"; diff --git a/apps/ios/it.lproj/Localizable.strings b/apps/ios/it.lproj/Localizable.strings index f3fa0424cc..c60c1537a6 100644 --- a/apps/ios/it.lproj/Localizable.strings +++ b/apps/ios/it.lproj/Localizable.strings @@ -781,7 +781,7 @@ /* No comment provided by engineer. */ "Can't message member" = "Impossibile inviare un messaggio al membro"; -/* No comment provided by engineer. */ +/* alert button */ "Cancel" = "Annulla"; /* No comment provided by engineer. */ @@ -2203,9 +2203,6 @@ /* No comment provided by engineer. */ "Full name (optional)" = "Nome completo (facoltativo)"; -/* No comment provided by engineer. */ -"Full name:" = "Nome completo:"; - /* No comment provided by engineer. */ "Fully decentralized – visible only to members." = "Completamente decentralizzato: visibile solo ai membri."; @@ -3378,12 +3375,6 @@ /* No comment provided by engineer. */ "Profile images" = "Immagini del profilo"; -/* No comment provided by engineer. */ -"Profile name" = "Nome del profilo"; - -/* No comment provided by engineer. */ -"Profile name:" = "Nome del profilo:"; - /* No comment provided by engineer. */ "Profile password" = "Password del profilo"; @@ -3715,10 +3706,11 @@ /* No comment provided by engineer. */ "Safer groups" = "Gruppi più sicuri"; -/* chat item action */ +/* alert button + chat item action */ "Save" = "Salva"; -/* No comment provided by engineer. */ +/* alert button */ "Save (and notify contacts)" = "Salva (e avvisa i contatti)"; /* No comment provided by engineer. */ @@ -3736,9 +3728,6 @@ /* No comment provided by engineer. */ "Save archive" = "Salva archivio"; -/* No comment provided by engineer. */ -"Save auto-accept settings" = "Salva le impostazioni di accettazione automatica"; - /* No comment provided by engineer. */ "Save group profile" = "Salva il profilo del gruppo"; @@ -3760,9 +3749,6 @@ /* No comment provided by engineer. */ "Save servers?" = "Salvare i server?"; -/* No comment provided by engineer. */ -"Save settings?" = "Salvare le impostazioni?"; - /* No comment provided by engineer. */ "Save welcome message?" = "Salvare il messaggio di benvenuto?"; @@ -5189,7 +5175,7 @@ "Your profile **%@** will be shared." = "Verrà condiviso il tuo profilo **%@**."; /* No comment provided by engineer. */ -"Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "Il tuo profilo è memorizzato sul tuo dispositivo e condiviso solo con i tuoi contatti.\nI server di SimpleX non possono vedere il tuo profilo."; +"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Il tuo profilo è memorizzato sul tuo dispositivo e condiviso solo con i tuoi contatti. I server di SimpleX non possono vedere il tuo profilo."; /* No comment provided by engineer. */ "Your profile, contacts and delivered messages are stored on your device." = "Il tuo profilo, i contatti e i messaggi recapitati sono memorizzati sul tuo dispositivo."; diff --git a/apps/ios/ja.lproj/Localizable.strings b/apps/ios/ja.lproj/Localizable.strings index 2a924539c8..8a6c48532c 100644 --- a/apps/ios/ja.lproj/Localizable.strings +++ b/apps/ios/ja.lproj/Localizable.strings @@ -619,7 +619,7 @@ /* No comment provided by engineer. */ "Can't invite contacts!" = "連絡先を招待できません!"; -/* No comment provided by engineer. */ +/* alert button */ "Cancel" = "中止"; /* feature offered item */ @@ -1594,9 +1594,6 @@ /* No comment provided by engineer. */ "Full name (optional)" = "フルネーム (任意):"; -/* No comment provided by engineer. */ -"Full name:" = "フルネーム:"; - /* No comment provided by engineer. */ "Fully re-implemented - work in background!" = "完全に再実装されました - バックグラウンドで動作します!"; @@ -2647,10 +2644,11 @@ /* No comment provided by engineer. */ "Run chat" = "チャット起動"; -/* chat item action */ +/* alert button + chat item action */ "Save" = "保存"; -/* No comment provided by engineer. */ +/* alert button */ "Save (and notify contacts)" = "保存(連絡先に通知)"; /* No comment provided by engineer. */ @@ -2665,9 +2663,6 @@ /* No comment provided by engineer. */ "Save archive" = "アーカイブを保存"; -/* No comment provided by engineer. */ -"Save auto-accept settings" = "自動受け入れ設定を保存する"; - /* No comment provided by engineer. */ "Save group profile" = "グループプロフィールの保存"; @@ -2689,9 +2684,6 @@ /* No comment provided by engineer. */ "Save servers?" = "サーバを保存しますか?"; -/* No comment provided by engineer. */ -"Save settings?" = "設定を保存しますか?"; - /* No comment provided by engineer. */ "Save welcome message?" = "ウェルカムメッセージを保存しますか?"; @@ -3566,7 +3558,7 @@ "Your profile **%@** will be shared." = "あなたのプロファイル **%@** が共有されます。"; /* No comment provided by engineer. */ -"Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "プロフィールはデバイスに保存され、連絡先とのみ共有されます。\nSimpleX サーバーはあなたのプロファイルを参照できません。"; +"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "プロフィールはデバイスに保存され、連絡先とのみ共有されます。 SimpleX サーバーはあなたのプロファイルを参照できません。"; /* No comment provided by engineer. */ "Your profile, contacts and delivered messages are stored on your device." = "あなたのプロフィール、連絡先、送信したメッセージがご自分の端末に保存されます。"; diff --git a/apps/ios/nl.lproj/Localizable.strings b/apps/ios/nl.lproj/Localizable.strings index 7d452743c6..6c566fa7da 100644 --- a/apps/ios/nl.lproj/Localizable.strings +++ b/apps/ios/nl.lproj/Localizable.strings @@ -781,7 +781,7 @@ /* No comment provided by engineer. */ "Can't message member" = "Kan geen bericht sturen naar lid"; -/* No comment provided by engineer. */ +/* alert button */ "Cancel" = "Annuleren"; /* No comment provided by engineer. */ @@ -2203,9 +2203,6 @@ /* No comment provided by engineer. */ "Full name (optional)" = "Volledige naam (optioneel)"; -/* No comment provided by engineer. */ -"Full name:" = "Volledige naam:"; - /* No comment provided by engineer. */ "Fully decentralized – visible only to members." = "Volledig gedecentraliseerd – alleen zichtbaar voor leden."; @@ -3378,12 +3375,6 @@ /* No comment provided by engineer. */ "Profile images" = "Profiel afbeeldingen"; -/* No comment provided by engineer. */ -"Profile name" = "Profielnaam"; - -/* No comment provided by engineer. */ -"Profile name:" = "Profielnaam:"; - /* No comment provided by engineer. */ "Profile password" = "Profiel wachtwoord"; @@ -3715,10 +3706,11 @@ /* No comment provided by engineer. */ "Safer groups" = "Veiligere groepen"; -/* chat item action */ +/* alert button + chat item action */ "Save" = "Opslaan"; -/* No comment provided by engineer. */ +/* alert button */ "Save (and notify contacts)" = "Bewaar (en informeer contacten)"; /* No comment provided by engineer. */ @@ -3736,9 +3728,6 @@ /* No comment provided by engineer. */ "Save archive" = "Bewaar archief"; -/* No comment provided by engineer. */ -"Save auto-accept settings" = "Sla instellingen voor automatisch accepteren op"; - /* No comment provided by engineer. */ "Save group profile" = "Groep profiel opslaan"; @@ -3760,9 +3749,6 @@ /* No comment provided by engineer. */ "Save servers?" = "Servers opslaan?"; -/* No comment provided by engineer. */ -"Save settings?" = "Instellingen opslaan?"; - /* No comment provided by engineer. */ "Save welcome message?" = "Welkom bericht opslaan?"; @@ -5189,7 +5175,7 @@ "Your profile **%@** will be shared." = "Uw profiel **%@** wordt gedeeld."; /* No comment provided by engineer. */ -"Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "Uw profiel wordt op uw apparaat opgeslagen en alleen gedeeld met uw contacten.\nSimpleX servers kunnen uw profiel niet zien."; +"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Uw profiel wordt op uw apparaat opgeslagen en alleen gedeeld met uw contacten. SimpleX servers kunnen uw profiel niet zien."; /* No comment provided by engineer. */ "Your profile, contacts and delivered messages are stored on your device." = "Uw profiel, contacten en afgeleverde berichten worden op uw apparaat opgeslagen."; diff --git a/apps/ios/pl.lproj/Localizable.strings b/apps/ios/pl.lproj/Localizable.strings index 179b2d3848..7def1c7e02 100644 --- a/apps/ios/pl.lproj/Localizable.strings +++ b/apps/ios/pl.lproj/Localizable.strings @@ -781,7 +781,7 @@ /* No comment provided by engineer. */ "Can't message member" = "Nie można wysłać wiadomości do członka"; -/* No comment provided by engineer. */ +/* alert button */ "Cancel" = "Anuluj"; /* No comment provided by engineer. */ @@ -2203,9 +2203,6 @@ /* No comment provided by engineer. */ "Full name (optional)" = "Pełna nazwa (opcjonalna)"; -/* No comment provided by engineer. */ -"Full name:" = "Pełna nazwa:"; - /* No comment provided by engineer. */ "Fully decentralized – visible only to members." = "W pełni zdecentralizowana – widoczna tylko dla członków."; @@ -3378,12 +3375,6 @@ /* No comment provided by engineer. */ "Profile images" = "Zdjęcia profilowe"; -/* No comment provided by engineer. */ -"Profile name" = "Nazwa profilu"; - -/* No comment provided by engineer. */ -"Profile name:" = "Nazwa profilu:"; - /* No comment provided by engineer. */ "Profile password" = "Hasło profilu"; @@ -3715,10 +3706,11 @@ /* No comment provided by engineer. */ "Safer groups" = "Bezpieczniejsze grupy"; -/* chat item action */ +/* alert button + chat item action */ "Save" = "Zapisz"; -/* No comment provided by engineer. */ +/* alert button */ "Save (and notify contacts)" = "Zapisz (i powiadom kontakty)"; /* No comment provided by engineer. */ @@ -3736,9 +3728,6 @@ /* No comment provided by engineer. */ "Save archive" = "Zapisz archiwum"; -/* No comment provided by engineer. */ -"Save auto-accept settings" = "Zapisz ustawienia automatycznej akceptacji"; - /* No comment provided by engineer. */ "Save group profile" = "Zapisz profil grupy"; @@ -3760,9 +3749,6 @@ /* No comment provided by engineer. */ "Save servers?" = "Zapisać serwery?"; -/* No comment provided by engineer. */ -"Save settings?" = "Zapisać ustawienia?"; - /* No comment provided by engineer. */ "Save welcome message?" = "Zapisać wiadomość powitalną?"; @@ -5189,7 +5175,7 @@ "Your profile **%@** will be shared." = "Twój profil **%@** zostanie udostępniony."; /* No comment provided by engineer. */ -"Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "Twój profil jest przechowywany na urządzeniu i udostępniany tylko Twoim kontaktom.\nSerwery SimpleX nie mogą zobaczyć Twojego profilu."; +"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Twój profil jest przechowywany na urządzeniu i udostępniany tylko Twoim kontaktom. Serwery SimpleX nie mogą zobaczyć Twojego profilu."; /* No comment provided by engineer. */ "Your profile, contacts and delivered messages are stored on your device." = "Twój profil, kontakty i dostarczone wiadomości są przechowywane na Twoim urządzeniu."; diff --git a/apps/ios/ru.lproj/Localizable.strings b/apps/ios/ru.lproj/Localizable.strings index 5d74647d07..038041d0e6 100644 --- a/apps/ios/ru.lproj/Localizable.strings +++ b/apps/ios/ru.lproj/Localizable.strings @@ -781,7 +781,7 @@ /* No comment provided by engineer. */ "Can't message member" = "Не удается написать члену группы"; -/* No comment provided by engineer. */ +/* alert button */ "Cancel" = "Отменить"; /* No comment provided by engineer. */ @@ -2203,9 +2203,6 @@ /* No comment provided by engineer. */ "Full name (optional)" = "Полное имя (не обязательно)"; -/* No comment provided by engineer. */ -"Full name:" = "Полное имя:"; - /* No comment provided by engineer. */ "Fully decentralized – visible only to members." = "Группа полностью децентрализована – она видна только членам."; @@ -3378,12 +3375,6 @@ /* No comment provided by engineer. */ "Profile images" = "Картинки профилей"; -/* No comment provided by engineer. */ -"Profile name" = "Имя профиля"; - -/* No comment provided by engineer. */ -"Profile name:" = "Имя профиля:"; - /* No comment provided by engineer. */ "Profile password" = "Пароль профиля"; @@ -3715,10 +3706,11 @@ /* No comment provided by engineer. */ "Safer groups" = "Более безопасные группы"; -/* chat item action */ +/* alert button + chat item action */ "Save" = "Сохранить"; -/* No comment provided by engineer. */ +/* alert button */ "Save (and notify contacts)" = "Сохранить (и уведомить контакты)"; /* No comment provided by engineer. */ @@ -3736,9 +3728,6 @@ /* No comment provided by engineer. */ "Save archive" = "Сохранить архив"; -/* No comment provided by engineer. */ -"Save auto-accept settings" = "Сохранить настройки автоприема"; - /* No comment provided by engineer. */ "Save group profile" = "Сохранить профиль группы"; @@ -3760,9 +3749,6 @@ /* No comment provided by engineer. */ "Save servers?" = "Сохранить серверы?"; -/* No comment provided by engineer. */ -"Save settings?" = "Сохранить настройки?"; - /* No comment provided by engineer. */ "Save welcome message?" = "Сохранить приветственное сообщение?"; @@ -5189,7 +5175,7 @@ "Your profile **%@** will be shared." = "Будет отправлен Ваш профиль **%@**."; /* No comment provided by engineer. */ -"Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "Ваш профиль хранится на Вашем устройстве и отправляется только Вашим контактам.\nSimpleX серверы не могут получить доступ к Вашему профилю."; +"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Ваш профиль хранится на Вашем устройстве и отправляется только Вашим контактам. SimpleX серверы не могут получить доступ к Вашему профилю."; /* No comment provided by engineer. */ "Your profile, contacts and delivered messages are stored on your device." = "Ваш профиль, контакты и доставленные сообщения хранятся на Вашем устройстве."; diff --git a/apps/ios/th.lproj/Localizable.strings b/apps/ios/th.lproj/Localizable.strings index 9726441a1f..994a10c9db 100644 --- a/apps/ios/th.lproj/Localizable.strings +++ b/apps/ios/th.lproj/Localizable.strings @@ -523,7 +523,7 @@ /* No comment provided by engineer. */ "Can't invite contacts!" = "ไม่สามารถเชิญผู้ติดต่อได้!"; -/* No comment provided by engineer. */ +/* alert button */ "Cancel" = "ยกเลิก"; /* feature offered item */ @@ -1468,9 +1468,6 @@ /* No comment provided by engineer. */ "Full name (optional)" = "ชื่อเต็ม (ไม่บังคับ)"; -/* No comment provided by engineer. */ -"Full name:" = "ชื่อเต็ม:"; - /* No comment provided by engineer. */ "Fully re-implemented - work in background!" = "ดำเนินการใหม่อย่างสมบูรณ์ - ทำงานในพื้นหลัง!"; @@ -2503,10 +2500,11 @@ /* No comment provided by engineer. */ "Run chat" = "เรียกใช้แชท"; -/* chat item action */ +/* alert button + chat item action */ "Save" = "บันทึก"; -/* No comment provided by engineer. */ +/* alert button */ "Save (and notify contacts)" = "บันทึก (และแจ้งผู้ติดต่อ)"; /* No comment provided by engineer. */ @@ -2521,9 +2519,6 @@ /* No comment provided by engineer. */ "Save archive" = "บันทึกไฟล์เก็บถาวร"; -/* No comment provided by engineer. */ -"Save auto-accept settings" = "บันทึกการตั้งค่าการยอมรับอัตโนมัติ"; - /* No comment provided by engineer. */ "Save group profile" = "บันทึกโปรไฟล์กลุ่ม"; @@ -2545,9 +2540,6 @@ /* No comment provided by engineer. */ "Save servers?" = "บันทึกเซิร์ฟเวอร์?"; -/* No comment provided by engineer. */ -"Save settings?" = "บันทึกการตั้งค่า?"; - /* No comment provided by engineer. */ "Save welcome message?" = "บันทึกข้อความต้อนรับ?"; @@ -3413,7 +3405,7 @@ "Your privacy" = "ความเป็นส่วนตัวของคุณ"; /* No comment provided by engineer. */ -"Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "โปรไฟล์ของคุณจะถูกจัดเก็บไว้ในอุปกรณ์ของคุณและแชร์กับผู้ติดต่อของคุณเท่านั้น\nเซิร์ฟเวอร์ SimpleX ไม่สามารถดูโปรไฟล์ของคุณได้"; +"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "โปรไฟล์ของคุณจะถูกจัดเก็บไว้ในอุปกรณ์ของคุณและแชร์กับผู้ติดต่อของคุณเท่านั้น เซิร์ฟเวอร์ SimpleX ไม่สามารถดูโปรไฟล์ของคุณได้"; /* No comment provided by engineer. */ "Your profile, contacts and delivered messages are stored on your device." = "โปรไฟล์ รายชื่อผู้ติดต่อ และข้อความที่ส่งของคุณจะถูกจัดเก็บไว้ในอุปกรณ์ของคุณ"; diff --git a/apps/ios/tr.lproj/Localizable.strings b/apps/ios/tr.lproj/Localizable.strings index 892f38fcbc..f39140340b 100644 --- a/apps/ios/tr.lproj/Localizable.strings +++ b/apps/ios/tr.lproj/Localizable.strings @@ -703,7 +703,7 @@ /* No comment provided by engineer. */ "Can't invite contacts!" = "Kişiler davet edilemiyor!"; -/* No comment provided by engineer. */ +/* alert button */ "Cancel" = "İptal et"; /* No comment provided by engineer. */ @@ -1930,9 +1930,6 @@ /* No comment provided by engineer. */ "Full name (optional)" = "Bütün isim (opsiyonel)"; -/* No comment provided by engineer. */ -"Full name:" = "Bütün isim:"; - /* No comment provided by engineer. */ "Fully decentralized – visible only to members." = "Tamamiyle merkezi olmayan - sadece kişilere görünür."; @@ -2991,12 +2988,6 @@ /* No comment provided by engineer. */ "Profile images" = "Profil resimleri"; -/* No comment provided by engineer. */ -"Profile name" = "Profil ismi"; - -/* No comment provided by engineer. */ -"Profile name:" = "Profil ismi:"; - /* No comment provided by engineer. */ "Profile password" = "Profil parolası"; @@ -3271,10 +3262,11 @@ /* No comment provided by engineer. */ "Safer groups" = "Daha güvenli gruplar"; -/* chat item action */ +/* alert button + chat item action */ "Save" = "Kaydet"; -/* No comment provided by engineer. */ +/* alert button */ "Save (and notify contacts)" = "Kaydet (ve kişilere bildir)"; /* No comment provided by engineer. */ @@ -3289,9 +3281,6 @@ /* No comment provided by engineer. */ "Save archive" = "Arşivi kaydet"; -/* No comment provided by engineer. */ -"Save auto-accept settings" = "Otomatik kabul et ayarlarını kaydet"; - /* No comment provided by engineer. */ "Save group profile" = "Grup profilini kaydet"; @@ -3313,9 +3302,6 @@ /* No comment provided by engineer. */ "Save servers?" = "Sunucular kaydedilsin mi?"; -/* No comment provided by engineer. */ -"Save settings?" = "Ayarlar kaydedilsin mi?"; - /* No comment provided by engineer. */ "Save welcome message?" = "Hoşgeldin mesajı kaydedilsin mi?"; @@ -4544,7 +4530,7 @@ "Your profile **%@** will be shared." = "Profiliniz **%@** paylaşılacaktır."; /* No comment provided by engineer. */ -"Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "Profiliniz cihazınızda saklanır ve sadece kişilerinizle paylaşılır.\nSimpleX sunucuları profilinizi göremez."; +"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Profiliniz cihazınızda saklanır ve sadece kişilerinizle paylaşılır. SimpleX sunucuları profilinizi göremez."; /* No comment provided by engineer. */ "Your profile, contacts and delivered messages are stored on your device." = "Profiliniz, kişileriniz ve gönderilmiş mesajlar cihazınızda saklanır."; diff --git a/apps/ios/uk.lproj/Localizable.strings b/apps/ios/uk.lproj/Localizable.strings index 1908d386a3..4218065a2e 100644 --- a/apps/ios/uk.lproj/Localizable.strings +++ b/apps/ios/uk.lproj/Localizable.strings @@ -781,7 +781,7 @@ /* No comment provided by engineer. */ "Can't message member" = "Не можу надіслати повідомлення користувачеві"; -/* No comment provided by engineer. */ +/* alert button */ "Cancel" = "Скасувати"; /* No comment provided by engineer. */ @@ -2203,9 +2203,6 @@ /* No comment provided by engineer. */ "Full name (optional)" = "Повне ім'я (необов'язково)"; -/* No comment provided by engineer. */ -"Full name:" = "Повне ім'я:"; - /* No comment provided by engineer. */ "Fully decentralized – visible only to members." = "Повністю децентралізована - видима лише для учасників."; @@ -3378,12 +3375,6 @@ /* No comment provided by engineer. */ "Profile images" = "Зображення профілю"; -/* No comment provided by engineer. */ -"Profile name" = "Назва профілю"; - -/* No comment provided by engineer. */ -"Profile name:" = "Ім'я профілю:"; - /* No comment provided by engineer. */ "Profile password" = "Пароль до профілю"; @@ -3715,10 +3706,11 @@ /* No comment provided by engineer. */ "Safer groups" = "Безпечніші групи"; -/* chat item action */ +/* alert button + chat item action */ "Save" = "Зберегти"; -/* No comment provided by engineer. */ +/* alert button */ "Save (and notify contacts)" = "Зберегти (і повідомити контактам)"; /* No comment provided by engineer. */ @@ -3736,9 +3728,6 @@ /* No comment provided by engineer. */ "Save archive" = "Зберегти архів"; -/* No comment provided by engineer. */ -"Save auto-accept settings" = "Зберегти налаштування автоприйому"; - /* No comment provided by engineer. */ "Save group profile" = "Зберегти профіль групи"; @@ -3760,9 +3749,6 @@ /* No comment provided by engineer. */ "Save servers?" = "Зберегти сервери?"; -/* No comment provided by engineer. */ -"Save settings?" = "Зберегти налаштування?"; - /* No comment provided by engineer. */ "Save welcome message?" = "Зберегти вітальне повідомлення?"; @@ -5189,7 +5175,7 @@ "Your profile **%@** will be shared." = "Ваш профіль **%@** буде опублікований."; /* No comment provided by engineer. */ -"Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "Ваш профіль зберігається на вашому пристрої і доступний лише вашим контактам.\nСервери SimpleX не бачать ваш профіль."; +"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Ваш профіль зберігається на вашому пристрої і доступний лише вашим контактам. Сервери SimpleX не бачать ваш профіль."; /* No comment provided by engineer. */ "Your profile, contacts and delivered messages are stored on your device." = "Ваш профіль, контакти та доставлені повідомлення зберігаються на вашому пристрої."; diff --git a/apps/ios/zh-Hans.lproj/Localizable.strings b/apps/ios/zh-Hans.lproj/Localizable.strings index 1d37572498..0f78665b83 100644 --- a/apps/ios/zh-Hans.lproj/Localizable.strings +++ b/apps/ios/zh-Hans.lproj/Localizable.strings @@ -46,6 +46,12 @@ /* No comment provided by engineer. */ "(" = "("; +/* No comment provided by engineer. */ +"(new)" = "(新)"; + +/* No comment provided by engineer. */ +"(this device v%@)" = "(此设备 v%@)"; + /* No comment provided by engineer. */ ")" = ")"; @@ -58,9 +64,15 @@ /* 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 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." = "**添加新联系人**:为您的联系人创建一次性二维码或者链接。"; +/* No comment provided by engineer. */ +"**Create group**: to create a new group." = "**创建群组**: 创建一个新群组."; + /* No comment provided by engineer. */ "**e2e encrypted** audio call" = "**端到端加密** 语音通话"; @@ -73,6 +85,9 @@ /* 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 通知服务器,在后台定期检查消息(取决于您多经常使用应用程序)。"; +/* 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." = "**请注意**: 在两台设备上使用相同的数据库将破坏来自您的连接的消息解密,作为一种安全保护."; + /* No comment provided by engineer. */ "**Please note**: you will NOT be able to recover or change passphrase if you lose it." = "**请注意**:如果您丢失密码,您将无法恢复或者更改密码。"; @@ -82,6 +97,9 @@ /* No comment provided by engineer. */ "**Warning**: Instant push notifications require passphrase saved in Keychain." = "**警告**:及时推送通知需要保存在钥匙串的密码。"; +/* No comment provided by engineer. */ +"**Warning**: the archive will be removed." = "**警告**: 存档将被删除."; + /* No comment provided by engineer. */ "*bold*" = "\\*加粗*"; @@ -124,6 +142,9 @@ /* No comment provided by engineer. */ "%@ connected" = "%@ 已连接"; +/* No comment provided by engineer. */ +"%@ downloaded" = "%@ 已下载"; + /* notification title */ "%@ is connected!" = "%@ 已连接!"; @@ -133,6 +154,9 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ 已认证"; +/* No comment provided by engineer. */ +"%@ uploaded" = "%@ 已上传"; + /* notification title */ "%@ wants to connect!" = "%@ 要连接!"; @@ -187,6 +211,15 @@ /* No comment provided by engineer. */ "%lld messages blocked" = "%lld 条消息已屏蔽"; +/* No comment provided by engineer. */ +"%lld messages blocked by admin" = "%lld 被管理员阻止的消息"; + +/* No comment provided by engineer. */ +"%lld messages marked deleted" = "%lld 标记为已删除的消息"; + +/* No comment provided by engineer. */ +"%lld messages moderated by %@" = "%lld 审核的留言 by %@"; + /* No comment provided by engineer. */ "%lld minutes" = "%lld 分钟"; @@ -235,6 +268,9 @@ /* No comment provided by engineer. */ "~strike~" = "\\~删去~"; +/* time to disappear */ +"0 sec" = "0 秒"; + /* No comment provided by engineer. */ "0s" = "0秒"; @@ -298,6 +334,9 @@ /* No comment provided by engineer. */ "above, then choose:" = "上面,然后选择:"; +/* No comment provided by engineer. */ +"Accent" = "强调"; + /* accept contact request via notification accept incoming call via notification swipe action */ @@ -316,6 +355,15 @@ /* call status */ "accepted call" = "已接受通话"; +/* No comment provided by engineer. */ +"Acknowledged" = "确认"; + +/* No comment provided by engineer. */ +"Acknowledgement errors" = "确认错误"; + +/* No comment provided by engineer. */ +"Active connections" = "活动连接"; + /* 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." = "将地址添加到您的个人资料,以便您的联系人可以与其他人共享。个人资料更新将发送给您的联系人。"; @@ -340,6 +388,15 @@ /* No comment provided by engineer. */ "Add welcome message" = "添加欢迎信息"; +/* No comment provided by engineer. */ +"Additional accent" = "附加重音"; + +/* No comment provided by engineer. */ +"Additional accent 2" = "附加重音 2"; + +/* No comment provided by engineer. */ +"Additional secondary" = "附加二级"; + /* No comment provided by engineer. */ "Address" = "地址"; @@ -361,6 +418,9 @@ /* No comment provided by engineer. */ "Advanced network settings" = "高级网络设置"; +/* No comment provided by engineer. */ +"Advanced settings" = "高级设置"; + /* chat item text */ "agreeing encryption for %@…" = "正在协商将加密应用于 %@…"; @@ -376,6 +436,9 @@ /* No comment provided by engineer. */ "All data is erased when it is entered." = "所有数据在输入后将被删除。"; +/* No comment provided by engineer. */ +"All data is private to your device." = "所有数据都是您设备的私有数据."; + /* No comment provided by engineer. */ "All group members will remain connected." = "所有群组成员将保持连接。"; @@ -388,6 +451,12 @@ /* No comment provided by engineer. */ "All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." = "所有聊天记录和消息将被删除——这一行为无法撤销!只有您的消息会被删除。"; +/* No comment provided by engineer. */ +"All new messages from %@ will be hidden!" = "来自 %@ 的所有新消息都将被隐藏!"; + +/* No comment provided by engineer. */ +"All profiles" = "所有配置文件"; + /* No comment provided by engineer. */ "All your contacts will remain connected." = "所有联系人会保持连接。"; @@ -403,11 +472,17 @@ /* No comment provided by engineer. */ "Allow calls only if your contact allows them." = "仅当您的联系人允许时才允许呼叫。"; +/* No comment provided by engineer. */ +"Allow calls?" = "允许通话?"; + /* No comment provided by engineer. */ "Allow disappearing messages only if your contact allows it to you." = "仅当您的联系人允许时才允许限时消息。"; /* No comment provided by engineer. */ -"Allow irreversible message deletion only if your contact allows it to you. (24 hours)" = "仅有您的联系人许可后才允许不可撤回消息移除。"; +"Allow downgrade" = "允许降级"; + +/* No comment provided by engineer. */ +"Allow irreversible message deletion only if your contact allows it to you. (24 hours)" = "仅有您的联系人许可后才允许不可撤回消息移除"; /* No comment provided by engineer. */ "Allow message reactions only if your contact allows them." = "只有您的联系人允许时才允许消息回应。"; @@ -422,7 +497,10 @@ "Allow sending disappearing messages." = "允许发送限时消息。"; /* No comment provided by engineer. */ -"Allow to irreversibly delete sent messages. (24 hours)" = "允许不可撤回地删除已发送消息。"; +"Allow sharing" = "允许共享"; + +/* No comment provided by engineer. */ +"Allow to irreversibly delete sent messages. (24 hours)" = "允许不可撤回地删除已发送消息"; /* No comment provided by engineer. */ "Allow to send files and media." = "允许发送文件和媒体。"; @@ -446,7 +524,7 @@ "Allow your contacts to call you." = "允许您的联系人给您打电话。"; /* No comment provided by engineer. */ -"Allow your contacts to irreversibly delete sent messages. (24 hours)" = "允许您的联系人不可撤回地删除已发送消息。"; +"Allow your contacts to irreversibly delete sent messages. (24 hours)" = "允许您的联系人不可撤回地删除已发送消息"; /* No comment provided by engineer. */ "Allow your contacts to send disappearing messages." = "允许您的联系人发送限时消息。"; @@ -466,12 +544,18 @@ /* pref value */ "always" = "始终"; +/* No comment provided by engineer. */ +"Always use private routing." = "始终使用私有路由。"; + /* No comment provided by engineer. */ "Always use relay" = "一直使用中继"; /* No comment provided by engineer. */ "An empty chat profile with the provided name is created, and the app opens as usual." = "已创建一个包含所提供名字的空白聊天资料,应用程序照常打开。"; +/* No comment provided by engineer. */ +"and %lld other events" = "和 %lld 其他事件"; + /* No comment provided by engineer. */ "Answer call" = "接听来电"; @@ -505,15 +589,27 @@ /* No comment provided by engineer. */ "Apply" = "应用"; +/* No comment provided by engineer. */ +"Apply to" = "应用于"; + /* No comment provided by engineer. */ "Archive and upload" = "存档和上传"; +/* No comment provided by engineer. */ +"Archive contacts to chat later." = "存档联系人以便稍后聊天."; + +/* No comment provided by engineer. */ +"Archived contacts" = "已存档的联系人"; + /* No comment provided by engineer. */ "Archiving database" = "正在存档数据库"; /* No comment provided by engineer. */ "Attach" = "附件"; +/* No comment provided by engineer. */ +"attempts" = "尝试"; + /* No comment provided by engineer. */ "Audio & video calls" = "语音和视频通话"; @@ -556,6 +652,9 @@ /* No comment provided by engineer. */ "Back" = "返回"; +/* No comment provided by engineer. */ +"Background" = "背景"; + /* No comment provided by engineer. */ "Bad desktop address" = "糟糕的桌面地址"; @@ -577,6 +676,12 @@ /* No comment provided by engineer. */ "Better messages" = "更好的消息"; +/* No comment provided by engineer. */ +"Better networking" = "更好的网络"; + +/* No comment provided by engineer. */ +"Black" = "黑色"; + /* No comment provided by engineer. */ "Block" = "封禁"; @@ -607,6 +712,12 @@ /* No comment provided by engineer. */ "Blocked by admin" = "由管理员封禁"; +/* No comment provided by engineer. */ +"Blur for better privacy." = "模糊处理,提高私密性."; + +/* No comment provided by engineer. */ +"Blur media" = "模糊媒体"; + /* No comment provided by engineer. */ "bold" = "加粗"; @@ -614,7 +725,7 @@ "Both you and your contact can add message reactions." = "您和您的联系人都可以添加消息回应。"; /* No comment provided by engineer. */ -"Both you and your contact can irreversibly delete sent messages. (24 hours)" = "您和您的联系人都可以不可逆转地删除已发送的消息。"; +"Both you and your contact can irreversibly delete sent messages. (24 hours)" = "您和您的联系人都可以不可逆转地删除已发送的消息"; /* No comment provided by engineer. */ "Both you and your contact can make calls." = "您和您的联系人都可以拨打电话。"; @@ -631,6 +742,9 @@ /* No comment provided by engineer. */ "By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." = "通过聊天资料(默认)或者[通过连接](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)。"; +/* No comment provided by engineer. */ +"call" = "呼叫"; + /* No comment provided by engineer. */ "Call already ended!" = "通话已结束!"; @@ -646,9 +760,18 @@ /* No comment provided by engineer. */ "Calls" = "通话"; +/* No comment provided by engineer. */ +"Calls prohibited!" = "禁止来电!"; + /* No comment provided by engineer. */ "Camera not available" = "相机不可用"; +/* No comment provided by engineer. */ +"Can't call contact" = "无法呼叫联系人"; + +/* No comment provided by engineer. */ +"Can't call member" = "无法呼叫成员"; + /* No comment provided by engineer. */ "Can't invite contact!" = "无法邀请联系人!"; @@ -656,6 +779,9 @@ "Can't invite contacts!" = "无法邀请联系人!"; /* No comment provided by engineer. */ +"Can't message member" = "无法向成员发送消息"; + +/* alert button */ "Cancel" = "取消"; /* No comment provided by engineer. */ @@ -667,9 +793,15 @@ /* No comment provided by engineer. */ "Cannot access keychain to save database password" = "无法访问钥匙串以保存数据库密码"; +/* No comment provided by engineer. */ +"Cannot forward message" = "无法转发消息"; + /* No comment provided by engineer. */ "Cannot receive file" = "无法接收文件"; +/* snd error text */ +"Capacity exceeded - recipient did not receive previously sent messages." = "超出容量-收件人未收到以前发送的邮件。"; + /* No comment provided by engineer. */ "Cellular" = "移动网络"; @@ -722,6 +854,9 @@ /* No comment provided by engineer. */ "Chat archive" = "聊天档案"; +/* No comment provided by engineer. */ +"Chat colors" = "聊天颜色"; + /* No comment provided by engineer. */ "Chat console" = "聊天控制台"; @@ -731,6 +866,9 @@ /* No comment provided by engineer. */ "Chat database deleted" = "聊天数据库已删除"; +/* No comment provided by engineer. */ +"Chat database exported" = "导出的聊天数据库"; + /* No comment provided by engineer. */ "Chat database imported" = "聊天数据库已导入"; @@ -743,12 +881,18 @@ /* No comment provided by engineer. */ "Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat." = "聊天已停止。如果你已经在另一台设备商使用过此数据库,你应该在启动聊天前将数据库传输回来。"; +/* No comment provided by engineer. */ +"Chat list" = "聊天列表"; + /* No comment provided by engineer. */ "Chat migrated!" = "已迁移聊天!"; /* No comment provided by engineer. */ "Chat preferences" = "聊天偏好设置"; +/* No comment provided by engineer. */ +"Chat theme" = "聊天主题"; + /* No comment provided by engineer. */ "Chats" = "聊天"; @@ -758,12 +902,24 @@ /* No comment provided by engineer. */ "Chinese and Spanish interface" = "中文和西班牙文界面"; +/* No comment provided by engineer. */ +"Choose _Migrate from another device_ on the new device and scan QR code." = "在新设备上选择“从另一个设备迁移”并扫描二维码。"; + /* No comment provided by engineer. */ "Choose file" = "选择文件"; /* No comment provided by engineer. */ "Choose from library" = "从库中选择"; +/* No comment provided by engineer. */ +"Chunks deleted" = "已删除的块"; + +/* No comment provided by engineer. */ +"Chunks downloaded" = "下载的块"; + +/* No comment provided by engineer. */ +"Chunks uploaded" = "已下载的区块"; + /* swipe action */ "Clear" = "清除"; @@ -779,6 +935,12 @@ /* No comment provided by engineer. */ "Clear verification" = "清除验证"; +/* No comment provided by engineer. */ +"Color chats with the new themes." = "使用新主题为聊天着色。"; + +/* No comment provided by engineer. */ +"Color mode" = "颜色模式"; + /* No comment provided by engineer. */ "colored" = "彩色"; @@ -791,15 +953,27 @@ /* No comment provided by engineer. */ "complete" = "完整的"; +/* No comment provided by engineer. */ +"Completed" = "已完成"; + /* No comment provided by engineer. */ "Configure ICE servers" = "配置 ICE 服务器"; +/* No comment provided by engineer. */ +"Configured %@ servers" = "已配置 %@ 服务器"; + /* No comment provided by engineer. */ "Confirm" = "确认"; +/* No comment provided by engineer. */ +"Confirm contact deletion?" = "确认删除联系人?"; + /* No comment provided by engineer. */ "Confirm database upgrades" = "确认数据库升级"; +/* No comment provided by engineer. */ +"Confirm files from unknown servers." = "确认来自未知服务器的文件。"; + /* No comment provided by engineer. */ "Confirm network settings" = "确认网络设置"; @@ -833,30 +1007,54 @@ /* 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 to yourself?" = "连接到你自己?"; +/* No comment provided by engineer. */ +"Connect to yourself?\nThis is your own one-time link!" = "与自己建立联系?\n这是您自己的一次性链接!"; + +/* No comment provided by engineer. */ +"Connect to yourself?\nThis is your own SimpleX address!" = "与自己建立联系?\n这是您自己的 SimpleX 地址!"; + +/* No comment provided by engineer. */ +"Connect via contact address" = "通过联系地址连接"; + /* No comment provided by engineer. */ "Connect via link" = "通过链接连接"; /* No comment provided by engineer. */ "Connect via one-time link" = "通过一次性链接连接"; +/* No comment provided by engineer. */ +"Connect with %@" = "与 %@连接"; + /* No comment provided by engineer. */ "connected" = "已连接"; +/* No comment provided by engineer. */ +"Connected" = "已连接"; + /* No comment provided by engineer. */ "Connected desktop" = "已连接的桌面"; /* rcv group event chat item */ "connected directly" = "已直连"; +/* 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)" = "连接中(已接受)"; @@ -878,6 +1076,9 @@ /* 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" = "正连接到桌面"; @@ -887,6 +1088,9 @@ /* No comment provided by engineer. */ "Connection" = "连接"; +/* No comment provided by engineer. */ +"Connection and servers status." = "连接和服务器状态。"; + /* No comment provided by engineer. */ "Connection error" = "连接错误"; @@ -896,6 +1100,9 @@ /* chat list item title (it should not be shown */ "connection established" = "连接已建立"; +/* No comment provided by engineer. */ +"Connection notifications" = "连接通知"; + /* No comment provided by engineer. */ "Connection request sent!" = "已发送连接请求!"; @@ -905,15 +1112,27 @@ /* No comment provided by engineer. */ "Connection timeout" = "连接超时"; +/* No comment provided by engineer. */ +"Connection with desktop stopped" = "与桌面的连接已停止"; + /* connection information */ "connection:%@" = "连接:%@"; +/* No comment provided by engineer. */ +"Connections" = "连接"; + +/* profile update event chat item */ +"contact %@ changed to %@" = "联系人 %1$@ 已更改为 %2$@"; + /* No comment provided by engineer. */ "Contact allows" = "联系人允许"; /* No comment provided by engineer. */ "Contact already exists" = "联系人已存在"; +/* No comment provided by engineer. */ +"Contact deleted!" = "联系人已删除!"; + /* No comment provided by engineer. */ "contact has e2e encryption" = "联系人具有端到端加密"; @@ -926,12 +1145,18 @@ /* notification */ "Contact is connected" = "联系已连接"; +/* No comment provided by engineer. */ +"Contact is deleted." = "联系人被删除。"; + /* No comment provided by engineer. */ "Contact name" = "联系人姓名"; /* No comment provided by engineer. */ "Contact preferences" = "联系人偏好设置"; +/* No comment provided by engineer. */ +"Contact will be deleted - this cannot be undone!" = "联系人将被删除-这是无法撤消的!"; + /* No comment provided by engineer. */ "Contacts" = "联系人"; @@ -941,17 +1166,26 @@ /* No comment provided by engineer. */ "Continue" = "继续"; +/* No comment provided by engineer. */ +"Conversation deleted!" = "对话已删除!"; + /* No comment provided by engineer. */ "Copy" = "复制"; +/* No comment provided by engineer. */ +"Copy error" = "复制错误"; + /* No comment provided by engineer. */ "Core version: v%@" = "核心版本: v%@"; +/* No comment provided by engineer. */ +"Correct name to %@?" = "将名称更正为 %@?"; + /* No comment provided by engineer. */ "Create" = "创建"; /* No comment provided by engineer. */ -"Create a group using a random profile." = "使用随机身份创建群组"; +"Create a group using a random profile." = "使用随机身份创建群组."; /* No comment provided by engineer. */ "Create an address to let people connect with you." = "创建一个地址,让人们与您联系。"; @@ -986,9 +1220,15 @@ /* No comment provided by engineer. */ "Create your profile" = "创建您的资料"; +/* No comment provided by engineer. */ +"Created" = "已创建"; + /* No comment provided by engineer. */ "Created at" = "创建于"; +/* copied message info */ +"Created at: %@" = "创建于:%@"; + /* No comment provided by engineer. */ "Created on %@" = "创建于 %@"; @@ -1007,6 +1247,9 @@ /* No comment provided by engineer. */ "Current passphrase…" = "现有密码……"; +/* No comment provided by engineer. */ +"Current profile" = "当前配置文件"; + /* No comment provided by engineer. */ "Currently maximum supported file size is %@." = "目前支持的最大文件大小为 %@。"; @@ -1016,9 +1259,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" = "数据库降级"; @@ -1079,12 +1328,18 @@ /* time unit */ "days" = "天"; +/* No comment provided by engineer. */ +"Debug delivery" = "调试交付"; + /* No comment provided by engineer. */ "Decentralized" = "分散式"; /* message decrypt error item */ "Decryption error" = "解密错误"; +/* No comment provided by engineer. */ +"decryption errors" = "解密错误"; + /* pref value */ "default (%@)" = "默认 (%@)"; @@ -1098,6 +1353,12 @@ swipe action */ "Delete" = "删除"; +/* No comment provided by engineer. */ +"Delete %lld messages of members?" = "删除成员的 %lld 消息?"; + +/* No comment provided by engineer. */ +"Delete %lld messages?" = "删除 %lld 消息?"; + /* No comment provided by engineer. */ "Delete address" = "删除地址"; @@ -1131,6 +1392,9 @@ /* No comment provided by engineer. */ "Delete contact" = "删除联系人"; +/* No comment provided by engineer. */ +"Delete contact?" = "删除联系人?"; + /* No comment provided by engineer. */ "Delete database" = "删除数据库"; @@ -1194,12 +1458,21 @@ /* server test step */ "Delete queue" = "删除队列"; +/* No comment provided by engineer. */ +"Delete up to 20 messages at once." = "一次最多删除 20 条信息。"; + /* No comment provided by engineer. */ "Delete user profile?" = "删除用户资料?"; +/* No comment provided by engineer. */ +"Delete without notification" = "删除而不通知"; + /* deleted chat item */ "deleted" = "已删除"; +/* No comment provided by engineer. */ +"Deleted" = "已删除"; + /* No comment provided by engineer. */ "Deleted at" = "已删除于"; @@ -1212,6 +1485,9 @@ /* rcv group event chat item */ "deleted group" = "已删除群组"; +/* No comment provided by engineer. */ +"Deletion errors" = "删除错误"; + /* No comment provided by engineer. */ "Delivery" = "传送"; @@ -1227,12 +1503,33 @@ /* No comment provided by engineer. */ "Desktop address" = "桌面地址"; +/* No comment provided by engineer. */ +"Desktop app version %@ is not compatible with this app." = "桌面应用程序版本 %@ 与此应用程序不兼容。"; + /* No comment provided by engineer. */ "Desktop devices" = "桌面设备"; +/* No comment provided by engineer. */ +"Destination server address of %@ is incompatible with forwarding server %@ settings." = "目标服务器地址 %@ 与转发服务器 %@ 设置不兼容。"; + +/* snd error text */ +"Destination server error: %@" = "目标服务器错误:%@"; + +/* No comment provided by engineer. */ +"Destination server version of %@ is incompatible with forwarding server %@." = "目标服务器版本 %@ 与转发服务器 %@ 不兼容。"; + +/* No comment provided by engineer. */ +"Detailed statistics" = "详细的统计数据"; + +/* No comment provided by engineer. */ +"Details" = "详细信息"; + /* No comment provided by engineer. */ "Develop" = "开发"; +/* No comment provided by engineer. */ +"Developer options" = "开发者选项"; + /* No comment provided by engineer. */ "Developer tools" = "开发者工具"; @@ -1272,6 +1569,9 @@ /* No comment provided by engineer. */ "disabled" = "关闭"; +/* No comment provided by engineer. */ +"Disabled" = "禁用"; + /* No comment provided by engineer. */ "Disappearing message" = "限时消息"; @@ -1308,6 +1608,12 @@ /* No comment provided by engineer. */ "Do not send history to new members." = "不给新成员发送历史消息。"; +/* 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 private routing." = "不要使用私有路由。"; + /* No comment provided by engineer. */ "Do NOT use SimpleX for emergency calls." = "请勿使用 SimpleX 进行紧急通话。"; @@ -1326,12 +1632,21 @@ /* chat item action */ "Download" = "下载"; +/* No comment provided by engineer. */ +"Download errors" = "下载错误"; + /* No comment provided by engineer. */ "Download failed" = "下载失败了"; /* server test step */ "Download file" = "下载文件"; +/* No comment provided by engineer. */ +"Downloaded" = "已下载"; + +/* No comment provided by engineer. */ +"Downloaded files" = "下载的文件"; + /* No comment provided by engineer. */ "Downloading archive" = "正在下载存档"; @@ -1344,6 +1659,9 @@ /* integrity error chat item */ "duplicate message" = "重复的消息"; +/* No comment provided by engineer. */ +"duplicates" = "复本"; + /* No comment provided by engineer. */ "Duration" = "时长"; @@ -1401,6 +1719,9 @@ /* enabled status */ "enabled" = "已启用"; +/* No comment provided by engineer. */ +"Enabled" = "已启用"; + /* No comment provided by engineer. */ "Enabled for" = "启用对象"; @@ -1428,6 +1749,9 @@ /* notification */ "Encrypted message or another event" = "加密消息或其他事件"; +/* notification */ +"Encrypted message: app is stopped" = "加密消息:应用程序已停止"; + /* notification */ "Encrypted message: database error" = "加密消息:数据库错误"; @@ -1482,6 +1806,9 @@ /* No comment provided by engineer. */ "Enter correct passphrase." = "输入正确密码。"; +/* No comment provided by engineer. */ +"Enter group name…" = "输入组名称…"; + /* No comment provided by engineer. */ "Enter Passcode" = "输入密码"; @@ -1506,6 +1833,9 @@ /* placeholder */ "Enter welcome message… (optional)" = "输入欢迎消息……(可选)"; +/* No comment provided by engineer. */ +"Enter your name…" = "请输入您的姓名…"; + /* No comment provided by engineer. */ "error" = "错误"; @@ -1533,6 +1863,9 @@ /* No comment provided by engineer. */ "Error changing setting" = "更改设置错误"; +/* No comment provided by engineer. */ +"Error connecting to forwarding server %@. Please try later." = "连接到转发服务器 %@ 时出错。请稍后尝试。"; + /* No comment provided by engineer. */ "Error creating address" = "创建地址错误"; @@ -1590,6 +1923,9 @@ /* No comment provided by engineer. */ "Error exporting chat database" = "导出聊天数据库错误"; +/* No comment provided by engineer. */ +"Error exporting theme: %@" = "导出主题时出错: %@"; + /* No comment provided by engineer. */ "Error importing chat database" = "导入聊天数据库错误"; @@ -1599,12 +1935,24 @@ /* No comment provided by engineer. */ "Error loading %@ servers" = "加载 %@ 服务器错误"; +/* No comment provided by engineer. */ +"Error opening chat" = "打开聊天时出错"; + /* No comment provided by engineer. */ "Error receiving file" = "接收文件错误"; +/* No comment provided by engineer. */ +"Error reconnecting server" = "重新连接服务器时出错"; + +/* No comment provided by engineer. */ +"Error reconnecting servers" = "重新连接服务器时出错"; + /* No comment provided by engineer. */ "Error removing member" = "删除成员错误"; +/* No comment provided by engineer. */ +"Error resetting statistics" = "重置统计信息时出错"; + /* No comment provided by engineer. */ "Error saving %@ servers" = "保存 %@ 服务器错误"; @@ -1626,6 +1974,9 @@ /* No comment provided by engineer. */ "Error saving user password" = "保存用户密码时出错"; +/* No comment provided by engineer. */ +"Error scanning code: %@" = "扫描代码时出错:%@"; + /* No comment provided by engineer. */ "Error sending email" = "发送电邮错误"; @@ -1681,6 +2032,9 @@ /* No comment provided by engineer. */ "Error: URL is invalid" = "错误:URL 无效"; +/* No comment provided by engineer. */ +"Errors" = "错误"; + /* No comment provided by engineer. */ "Even when disabled in the conversation." = "即使在对话中被禁用。"; @@ -1693,12 +2047,18 @@ /* chat item action */ "Expand" = "展开"; +/* No comment provided by engineer. */ +"expired" = "过期"; + /* No comment provided by engineer. */ "Export database" = "导出数据库"; /* No comment provided by engineer. */ "Export error:" = "导出错误:"; +/* No comment provided by engineer. */ +"Export theme" = "导出主题"; + /* No comment provided by engineer. */ "Exported database archive." = "导出数据库归档。"; @@ -1720,6 +2080,21 @@ /* swipe action */ "Favorite" = "最喜欢"; +/* No comment provided by engineer. */ +"File error" = "文件错误"; + +/* file error text */ +"File not found - most likely file was deleted or cancelled." = "找不到文件 - 很可能文件已被删除或取消。"; + +/* file error text */ +"File server error: %@" = "文件服务器错误:%@"; + +/* No comment provided by engineer. */ +"File status" = "文件状态"; + +/* copied message info */ +"File status: %@" = "文件状态:%@"; + /* No comment provided by engineer. */ "File will be deleted from servers." = "文件将从服务器中删除。"; @@ -1732,6 +2107,9 @@ /* No comment provided by engineer. */ "File: %@" = "文件:%@"; +/* No comment provided by engineer. */ +"Files" = "文件"; + /* No comment provided by engineer. */ "Files & media" = "文件和媒体"; @@ -1754,7 +2132,7 @@ "Finalize migration" = "完成迁移"; /* No comment provided by engineer. */ -"Finalize migration on another device." = "在另一部设备上完成迁移"; +"Finalize migration on another device." = "在另一部设备上完成迁移."; /* No comment provided by engineer. */ "Finally, we have them! 🚀" = "终于我们有它们了! 🚀"; @@ -1798,6 +2176,21 @@ /* No comment provided by engineer. */ "Forwarded from" = "转发自"; +/* No comment provided by engineer. */ +"Forwarding server %@ failed to connect to destination server %@. Please try later." = "转发服务器 %@ 无法连接到目标服务器 %@。请稍后尝试。"; + +/* No comment provided by engineer. */ +"Forwarding server address is incompatible with network settings: %@." = "转发服务器地址与网络设置不兼容:%@。"; + +/* No comment provided by engineer. */ +"Forwarding server version is incompatible with network settings: %@." = "转发服务器版本与网络设置不兼容:%@。"; + +/* snd error text */ +"Forwarding server: %@\nDestination server error: %@" = "转发服务器: %1$@\n目标服务器错误: %2$@"; + +/* snd error text */ +"Forwarding server: %@\nError: %@" = "转发服务器: %1$@\n错误: %2$@"; + /* No comment provided by engineer. */ "Found desktop" = "找到了桌面"; @@ -1810,9 +2203,6 @@ /* No comment provided by engineer. */ "Full name (optional)" = "全名(可选)"; -/* No comment provided by engineer. */ -"Full name:" = "全名:"; - /* No comment provided by engineer. */ "Fully decentralized – visible only to members." = "完全去中心化 - 仅对成员可见。"; @@ -1825,9 +2215,18 @@ /* No comment provided by engineer. */ "GIFs and stickers" = "GIF 和贴纸"; +/* message preview */ +"Good afternoon!" = "下午好!"; + +/* message preview */ +"Good morning!" = "早上好!"; + /* No comment provided by engineer. */ "Group" = "群组"; +/* No comment provided by engineer. */ +"Group already exists" = "群组已存在"; + /* No comment provided by engineer. */ "Group already exists!" = "群已存在!"; @@ -1862,7 +2261,7 @@ "Group 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)" = "群组成员可以不可撤回地删除已发送的消息"; /* No comment provided by engineer. */ "Group members can send direct messages." = "群组成员可以私信。"; @@ -1954,6 +2353,9 @@ /* No comment provided by engineer. */ "How to use your servers" = "如何使用您的服务器"; +/* No comment provided by engineer. */ +"Hungarian interface" = "匈牙利语界面"; + /* No comment provided by engineer. */ "ICE servers (one per line)" = "ICE 服务器(每行一个)"; @@ -1996,6 +2398,9 @@ /* No comment provided by engineer. */ "Import failed" = "导入失败了"; +/* No comment provided by engineer. */ +"Import theme" = "导入主题"; + /* No comment provided by engineer. */ "Importing archive" = "正在导入存档"; @@ -2017,6 +2422,9 @@ /* No comment provided by engineer. */ "In-call sounds" = "通话声音"; +/* No comment provided by engineer. */ +"inactive" = "无效"; + /* No comment provided by engineer. */ "Incognito" = "隐身聊天"; @@ -2080,6 +2488,9 @@ /* No comment provided by engineer. */ "Interface" = "界面"; +/* No comment provided by engineer. */ +"Interface colors" = "界面颜色"; + /* invalid chat data */ "invalid chat" = "无效聊天"; @@ -2107,6 +2518,9 @@ /* No comment provided by engineer. */ "Invalid QR code" = "无效的二维码"; +/* No comment provided by engineer. */ +"Invalid response" = "无效的响应"; + /* No comment provided by engineer. */ "Invalid server address!" = "无效的服务器地址!"; @@ -2119,6 +2533,9 @@ /* group name */ "invitation to group %@" = "邀请您加入群组 %@"; +/* No comment provided by engineer. */ +"invite" = "邀请"; + /* No comment provided by engineer. */ "Invite friends" = "邀请朋友"; @@ -2164,6 +2581,9 @@ /* No comment provided by engineer. */ "It can happen when:\n1. The messages expired in the sending client after 2 days or on the server after 30 days.\n2. Message decryption failed, because you or your contact used old database backup.\n3. The connection was compromised." = "它可能在以下情况发生:\n1. 消息在发送客户端 2 天后或在服务器上 30 天后过期。\n2. 消息解密失败,因为您或您的联系人使用了旧的数据库备份。\n3.连接被破坏。"; +/* No comment provided by engineer. */ +"It protects your IP address and connections." = "它可以保护您的 IP 地址和连接。"; + /* No comment provided by engineer. */ "It seems like you are already connected via this link. If it is not the case, there was an error (%@)." = "您似乎已经通过此链接连接。如果不是这样,则有一个错误 (%@)。"; @@ -2194,12 +2614,24 @@ /* No comment provided by engineer. */ "Join incognito" = "加入隐身聊天"; +/* No comment provided by engineer. */ +"Join with current profile" = "使用当前档案加入"; + +/* No comment provided by engineer. */ +"Join your group?\nThis is your link for group %@!" = "加入您的群组?\n这是您组 %@ 的链接!"; + /* No comment provided by engineer. */ "Joining group" = "加入群组中"; /* No comment provided by engineer. */ "Keep" = "保留"; +/* No comment provided by engineer. */ +"Keep conversation" = "保持对话"; + +/* No comment provided by engineer. */ +"Keep the app open to use it from desktop" = "保持应用程序打开状态以从桌面使用它"; + /* No comment provided by engineer. */ "Keep unused invitation?" = "保留未使用的邀请吗?"; @@ -2308,15 +2740,27 @@ /* No comment provided by engineer. */ "Max 30 seconds, received instantly." = "最长30秒,立即接收。"; +/* No comment provided by engineer. */ +"Media & file servers" = "Media & file servers"; + +/* blur media */ +"Medium" = "中等"; + /* member role */ "member" = "成员"; /* No comment provided by engineer. */ "Member" = "成员"; +/* profile update event chat item */ +"member %@ changed to %@" = "成员 %1$@ 已更改为 %2$@"; + /* rcv group event chat item */ "member connected" = "已连接"; +/* item status text */ +"Member inactive" = "成员不活跃"; + /* No comment provided by engineer. */ "Member role will be changed to \"%@\". All group members will be notified." = "成员角色将更改为 \"%@\"。所有群成员将收到通知。"; @@ -2326,15 +2770,33 @@ /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "成员将被移出群组——此操作无法撤消!"; +/* No comment provided by engineer. */ +"Menus" = "菜单"; + +/* No comment provided by engineer. */ +"message" = "消息"; + /* item status text */ "Message delivery error" = "消息传递错误"; /* No comment provided by engineer. */ "Message delivery receipts!" = "消息送达回执!"; +/* item status text */ +"Message delivery warning" = "消息传递警告"; + /* No comment provided by engineer. */ "Message draft" = "消息草稿"; +/* item status text */ +"Message forwarded" = "消息已转发"; + +/* item status description */ +"Message may be delivered later if member becomes active." = "如果 member 变为活动状态,则稍后可能会发送消息。"; + +/* No comment provided by engineer. */ +"Message queue info" = "消息队列信息"; + /* chat feature */ "Message reactions" = "消息回应"; @@ -2347,9 +2809,21 @@ /* notification */ "message received" = "消息已收到"; +/* No comment provided by engineer. */ +"Message reception" = "消息接收"; + +/* No comment provided by engineer. */ +"Message servers" = "消息服务器"; + /* No comment provided by engineer. */ "Message source remains private." = "消息来源保持私密。"; +/* No comment provided by engineer. */ +"Message status" = "消息状态"; + +/* copied message info */ +"Message status: %@" = "消息状态:%@"; + /* No comment provided by engineer. */ "Message text" = "消息正文"; @@ -2362,6 +2836,21 @@ /* No comment provided by engineer. */ "Messages & files" = "消息"; +/* No comment provided by engineer. */ +"Messages from %@ will be shown!" = "将显示来自 %@ 的消息!"; + +/* No comment provided by engineer. */ +"Messages received" = "收到的消息"; + +/* No comment provided by engineer. */ +"Messages sent" = "已发送的消息"; + +/* 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." = "消息、文件和通话受到 **端到端加密** 的保护,具有完全正向保密、否认和闯入恢复。"; + +/* No comment provided by engineer. */ +"Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery." = "消息、文件和通话受到 **抗量子 e2e 加密** 的保护,具有完全正向保密、否认和闯入恢复。"; + /* No comment provided by engineer. */ "Migrate device" = "迁移设备"; @@ -2434,6 +2923,9 @@ /* No comment provided by engineer. */ "Multiple chat profiles" = "多个聊天资料"; +/* No comment provided by engineer. */ +"mute" = "静音"; + /* swipe action */ "Mute" = "静音"; @@ -2449,6 +2941,9 @@ /* No comment provided by engineer. */ "Network connection" = "网络连接"; +/* snd error text */ +"Network issues - message expired after many attempts to send it." = "网络问题 - 消息在多次尝试发送后过期。"; + /* No comment provided by engineer. */ "Network management" = "网络管理"; @@ -2464,6 +2959,9 @@ /* No comment provided by engineer. */ "New chat" = "新聊天"; +/* No comment provided by engineer. */ +"New chat experience 🎉" = "新的聊天体验 🎉"; + /* notification */ "New contact request" = "新联系人请求"; @@ -2482,6 +2980,9 @@ /* No comment provided by engineer. */ "New in %@" = "%@ 的新内容"; +/* No comment provided by engineer. */ +"New media options" = "新媒体选项"; + /* No comment provided by engineer. */ "New member role" = "新成员角色"; @@ -2518,6 +3019,9 @@ /* No comment provided by engineer. */ "No device token!" = "无设备令牌!"; +/* item status description */ +"No direct connection yet, message is forwarded by admin." = "还没有直接连接,消息由管理员转发。"; + /* No comment provided by engineer. */ "no e2e encryption" = "无端到端加密"; @@ -2530,6 +3034,9 @@ /* No comment provided by engineer. */ "No history" = "无历史记录"; +/* No comment provided by engineer. */ +"No info, try to reload" = "无信息,尝试重新加载"; + /* No comment provided by engineer. */ "No network connection" = "无网络连接"; @@ -2545,6 +3052,9 @@ /* No comment provided by engineer. */ "Not compatible!" = "不兼容!"; +/* No comment provided by engineer. */ +"Nothing selected" = "未选中任何内容"; + /* No comment provided by engineer. */ "Notifications" = "通知"; @@ -2590,10 +3100,10 @@ "One-time invitation link" = "一次性邀请链接"; /* No comment provided by engineer. */ -"Onion hosts will be **required** for connection.\nRequires compatible VPN." = "Onion 主机将用于连接。需要启用 VPN。"; +"Onion hosts will be **required** for connection.\nRequires compatible VPN." = "Onion 主机将是连接所必需的。\n需要兼容的 VPN。"; /* No comment provided by engineer. */ -"Onion hosts will be used when available.\nRequires compatible VPN." = "当可用时,将使用 Onion 主机。需要启用 VPN。"; +"Onion hosts will be used when available.\nRequires compatible VPN." = "如果可用,将使用洋葱主机。\n需要兼容的 VPN。"; /* No comment provided by engineer. */ "Onion hosts will not be used." = "将不会使用 Onion 主机。"; @@ -2601,6 +3111,9 @@ /* No comment provided by engineer. */ "Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." = "只有客户端设备存储用户资料、联系人、群组和**双层端到端加密**发送的消息。"; +/* No comment provided by engineer. */ +"Only delete conversation" = "仅删除对话"; + /* No comment provided by engineer. */ "Only group owners can change group preferences." = "只有群主可以改变群组偏好设置。"; @@ -2614,7 +3127,7 @@ "Only you can add message reactions." = "只有您可以添加消息回应。"; /* No comment provided by engineer. */ -"Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)" = "只有您可以不可撤回地删除消息(您的联系人可以将它们标记为删除)。"; +"Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)" = "只有您可以不可撤回地删除消息(您的联系人可以将它们标记为删除)"; /* No comment provided by engineer. */ "Only you can make calls." = "只有您可以拨打电话。"; @@ -2629,7 +3142,7 @@ "Only your contact can add message reactions." = "只有您的联系人可以添加消息回应。"; /* No comment provided by engineer. */ -"Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)" = "只有您的联系人才能不可撤回地删除消息(您可以将它们标记为删除)。"; +"Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)" = "只有您的联系人才能不可撤回地删除消息(您可以将它们标记为删除)"; /* No comment provided by engineer. */ "Only your contact can make calls." = "只有您的联系人可以拨打电话。"; @@ -2652,6 +3165,12 @@ /* No comment provided by engineer. */ "Open group" = "打开群"; +/* authentication reason */ +"Open migration to another device" = "打开迁移到另一台设备"; + +/* No comment provided by engineer. */ +"Open server settings" = "打开服务器设置"; + /* No comment provided by engineer. */ "Open Settings" = "打开设置"; @@ -2661,6 +3180,9 @@ /* No comment provided by engineer. */ "Open-source protocol and code – anybody can run the servers." = "开源协议和代码——任何人都可以运行服务器。"; +/* No comment provided by engineer. */ +"Opening app…" = "正在打开应用程序…"; + /* No comment provided by engineer. */ "Or paste archive link" = "或粘贴存档链接"; @@ -2673,9 +3195,18 @@ /* No comment provided by engineer. */ "Or show this code" = "或者显示此码"; +/* No comment provided by engineer. */ +"other" = "其他"; + /* No comment provided by engineer. */ "Other" = "其他"; +/* No comment provided by engineer. */ +"Other %@ servers" = "其他 %@ 服务器"; + +/* No comment provided by engineer. */ +"other errors" = "其他错误"; + /* member role */ "owner" = "群主"; @@ -2700,6 +3231,9 @@ /* No comment provided by engineer. */ "Password to show" = "显示密码"; +/* past/unknown group member */ +"Past member %@" = "前任成员 %@"; + /* No comment provided by engineer. */ "Paste desktop address" = "粘贴桌面地址"; @@ -2715,6 +3249,9 @@ /* No comment provided by engineer. */ "peer-to-peer" = "点对点"; +/* No comment provided by engineer. */ +"Pending" = "待定"; + /* No comment provided by engineer. */ "People can connect to you only via the links you share." = "人们只能通过您共享的链接与您建立联系。"; @@ -2733,9 +3270,18 @@ /* No comment provided by engineer. */ "PING interval" = "PING 间隔"; +/* No comment provided by engineer. */ +"Play from the chat list." = "从聊天列表播放。"; + +/* No comment provided by engineer. */ +"Please ask your contact to enable calls." = "请要求您的联系人开通通话功能。"; + /* No comment provided by engineer. */ "Please ask your contact to enable sending voice messages." = "请让您的联系人启用发送语音消息。"; +/* No comment provided by engineer. */ +"Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection.\nPlease share any other issues with the developers." = "请检查移动设备和桌面是否连接到同一本地网络,以及桌面防火墙是否允许连接。\n请与开发人员分享任何其他问题。"; + /* No comment provided by engineer. */ "Please check that you used the correct link or ask your contact to send you another one." = "请检查您使用的链接是否正确,或者让您的联系人给您发送另一个链接。"; @@ -2748,6 +3294,9 @@ /* No comment provided by engineer. */ "Please confirm that network settings are correct for this device." = "请确认网络设置对此这台设备正确无误。"; +/* No comment provided by engineer. */ +"Please contact developers.\nError: %@" = "请联系开发人员。\n错误:%@"; + /* No comment provided by engineer. */ "Please contact group admin." = "请联系群组管理员。"; @@ -2790,6 +3339,9 @@ /* No comment provided by engineer. */ "Preview" = "预览"; +/* No comment provided by engineer. */ +"Previously connected servers" = "以前连接的服务器"; + /* No comment provided by engineer. */ "Privacy & security" = "隐私和安全"; @@ -2799,9 +3351,21 @@ /* No comment provided by engineer. */ "Private filenames" = "私密文件名"; +/* No comment provided by engineer. */ +"Private message routing" = "私有消息路由"; + +/* No comment provided by engineer. */ +"Private message routing 🚀" = "私有消息路由 🚀"; + /* name of notes to self */ "Private notes" = "私密笔记"; +/* No comment provided by engineer. */ +"Private routing" = "专用路由"; + +/* No comment provided by engineer. */ +"Private routing error" = "专用路由错误"; + /* No comment provided by engineer. */ "Profile and server connections" = "资料和服务器连接"; @@ -2812,10 +3376,10 @@ "Profile images" = "个人资料图"; /* No comment provided by engineer. */ -"Profile name:" = "显示名:"; +"Profile password" = "个人资料密码"; /* No comment provided by engineer. */ -"Profile password" = "个人资料密码"; +"Profile theme" = "个人资料主题"; /* No comment provided by engineer. */ "Profile update will be sent to your contacts." = "个人资料更新将被发送给您的联系人。"; @@ -2841,24 +3405,42 @@ /* No comment provided by engineer. */ "Prohibit sending files and media." = "禁止发送文件和媒体。"; +/* No comment provided by engineer. */ +"Prohibit sending SimpleX links." = "禁止发送 SimpleX 链接。"; + /* No comment provided by engineer. */ "Prohibit sending voice messages." = "禁止发送语音消息。"; /* No comment provided by engineer. */ "Protect app screen" = "保护应用程序屏幕"; +/* No comment provided by engineer. */ +"Protect IP address" = "保护 IP 地址"; + /* No comment provided by engineer. */ "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在*网络和服务器*设置中启用。"; + /* No comment provided by engineer. */ "Protocol timeout" = "协议超时"; /* No comment provided by engineer. */ "Protocol timeout per KB" = "每 KB 协议超时"; +/* No comment provided by engineer. */ +"Proxied" = "代理"; + +/* No comment provided by engineer. */ +"Proxied servers" = "代理服务器"; + /* No comment provided by engineer. */ "Push notifications" = "推送通知"; +/* No comment provided by engineer. */ +"Push server" = "推送服务器"; + /* chat item text */ "quantum resistant e2e encryption" = "抗量子端到端加密"; @@ -2868,6 +3450,9 @@ /* No comment provided by engineer. */ "Rate the app" = "评价此应用程序"; +/* No comment provided by engineer. */ +"Reachable chat toolbar" = "可访问的聊天工具栏"; + /* chat item menu */ "React…" = "回应…"; @@ -2895,6 +3480,9 @@ /* No comment provided by engineer. */ "Receipts are disabled" = "回执已禁用"; +/* No comment provided by engineer. */ +"Receive errors" = "接收错误"; + /* No comment provided by engineer. */ "received answer…" = "已收到回复……"; @@ -2913,6 +3501,15 @@ /* message info title */ "Received message" = "收到的信息"; +/* No comment provided by engineer. */ +"Received messages" = "收到的消息"; + +/* No comment provided by engineer. */ +"Received reply" = "已收到回复"; + +/* No comment provided by engineer. */ +"Received total" = "接收总数"; + /* No comment provided by engineer. */ "Receiving address will be changed to a different server. Address change will complete after sender comes online." = "接收地址将变更到不同的服务器。地址更改将在发件人上线后完成。"; @@ -2922,15 +3519,33 @@ /* No comment provided by engineer. */ "Receiving via" = "接收通过"; +/* No comment provided by engineer. */ +"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)." = "最近的历史记录和改进的 [目录机器人](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)."; + /* No comment provided by engineer. */ "Recipient(s) can't see who this message is from." = "收件人看不到这条消息来自何人。"; /* No comment provided by engineer. */ "Recipients see updates as you type them." = "对方会在您键入时看到更新。"; +/* No comment provided by engineer. */ +"Reconnect" = "重新连接"; + /* No comment provided by engineer. */ "Reconnect all connected servers to force message delivery. It uses additional traffic." = "重新连接所有已连接的服务器以强制发送信息。这会耗费更多流量。"; +/* No comment provided by engineer. */ +"Reconnect all servers" = "重新连接所有服务器"; + +/* No comment provided by engineer. */ +"Reconnect all servers?" = "重新连接所有服务器?"; + +/* No comment provided by engineer. */ +"Reconnect server to force message delivery. It uses additional traffic." = "重新连接服务器以强制发送信息。它使用额外的流量。"; + +/* No comment provided by engineer. */ +"Reconnect server?" = "重新连接服务器?"; + /* No comment provided by engineer. */ "Reconnect servers?" = "是否重新连接服务器?"; @@ -2965,6 +3580,9 @@ /* No comment provided by engineer. */ "Remove" = "移除"; +/* No comment provided by engineer. */ +"Remove image" = "移除图片"; + /* No comment provided by engineer. */ "Remove member" = "删除成员"; @@ -3022,12 +3640,27 @@ /* No comment provided by engineer. */ "Reset" = "重置"; +/* No comment provided by engineer. */ +"Reset all hints" = "重置所有提示"; + +/* No comment provided by engineer. */ +"Reset all statistics" = "重置所有统计信息"; + +/* No comment provided by engineer. */ +"Reset all statistics?" = "重置所有统计信息?"; + /* No comment provided by engineer. */ "Reset colors" = "重置颜色"; +/* No comment provided by engineer. */ +"Reset to app theme" = "重置为应用程序主题"; + /* No comment provided by engineer. */ "Reset to defaults" = "重置为默认"; +/* No comment provided by engineer. */ +"Reset to user theme" = "重置为用户主题"; + /* No comment provided by engineer. */ "Restart the app to create a new chat profile" = "重新启动应用程序以创建新的聊天资料"; @@ -3068,12 +3701,16 @@ "Run chat" = "运行聊天程序"; /* No comment provided by engineer. */ -"Safer groups" = "更安全的群组"; - -/* chat item action */ -"Save" = "保存"; +"Safely receive files" = "安全接收文件"; /* No comment provided by engineer. */ +"Safer groups" = "更安全的群组"; + +/* alert button + chat item action */ +"Save" = "保存"; + +/* alert button */ "Save (and notify contacts)" = "保存(并通知联系人)"; /* No comment provided by engineer. */ @@ -3082,15 +3719,15 @@ /* No comment provided by engineer. */ "Save and notify group members" = "保存并通知群组成员"; +/* No comment provided by engineer. */ +"Save and reconnect" = "保存并重新连接"; + /* No comment provided by engineer. */ "Save and update group profile" = "保存和更新组配置文件"; /* No comment provided by engineer. */ "Save archive" = "保存存档"; -/* No comment provided by engineer. */ -"Save auto-accept settings" = "保存自动接受设置"; - /* No comment provided by engineer. */ "Save group profile" = "保存群组资料"; @@ -3112,9 +3749,6 @@ /* No comment provided by engineer. */ "Save servers?" = "保存服务器?"; -/* No comment provided by engineer. */ -"Save settings?" = "保存设置?"; - /* No comment provided by engineer. */ "Save welcome message?" = "保存欢迎信息?"; @@ -3127,12 +3761,21 @@ /* No comment provided by engineer. */ "Saved from" = "保存自"; +/* No comment provided by engineer. */ +"saved from %@" = "保存自 %@"; + /* message info title */ "Saved message" = "已保存的消息"; /* No comment provided by engineer. */ "Saved WebRTC ICE servers will be removed" = "已保存的WebRTC ICE服务器将被删除"; +/* No comment provided by engineer. */ +"Scale" = "规模"; + +/* No comment provided by engineer. */ +"Scan / Paste link" = "扫描 / 粘贴链接"; + /* No comment provided by engineer. */ "Scan code" = "扫码"; @@ -3148,6 +3791,9 @@ /* No comment provided by engineer. */ "Scan server QR code" = "扫描服务器二维码"; +/* No comment provided by engineer. */ +"search" = "搜索"; + /* No comment provided by engineer. */ "Search" = "搜索"; @@ -3160,6 +3806,9 @@ /* network option */ "sec" = "秒"; +/* No comment provided by engineer. */ +"Secondary" = "二级"; + /* time unit */ "seconds" = "秒"; @@ -3169,6 +3818,9 @@ /* server test step */ "Secure queue" = "保护队列"; +/* No comment provided by engineer. */ +"Secured" = "担保"; + /* No comment provided by engineer. */ "Security assessment" = "安全评估"; @@ -3181,6 +3833,12 @@ /* chat item action */ "Select" = "选择"; +/* No comment provided by engineer. */ +"Selected %lld" = "选定的 %lld"; + +/* No comment provided by engineer. */ +"Selected chat preferences prohibit this message." = "选定的聊天首选项禁止此消息。"; + /* No comment provided by engineer. */ "Self-destruct" = "自毁"; @@ -3211,12 +3869,24 @@ /* No comment provided by engineer. */ "Send disappearing message" = "发送限时消息中"; +/* No comment provided by engineer. */ +"Send errors" = "发送错误"; + /* No comment provided by engineer. */ "Send link previews" = "发送链接预览"; /* No comment provided by engineer. */ "Send live message" = "发送实时消息"; +/* No comment provided by engineer. */ +"Send message to enable calls." = "发送消息以启用呼叫。"; + +/* No comment provided by engineer. */ +"Send messages directly when IP address is protected and your or destination server does not support private routing." = "当 IP 地址受到保护并且您或目标服务器不支持私有路由时,直接发送消息。"; + +/* No comment provided by engineer. */ +"Send messages directly when your or destination server does not support private routing." = "当您或目标服务器不支持私有路由时,直接发送消息。"; + /* No comment provided by engineer. */ "Send notifications" = "发送通知"; @@ -3271,15 +3941,42 @@ /* copied message info */ "Sent at: %@" = "已发送于:%@"; +/* No comment provided by engineer. */ +"Sent directly" = "直接发送"; + /* notification */ "Sent file event" = "已发送文件项目"; /* message info title */ "Sent message" = "已发信息"; +/* No comment provided by engineer. */ +"Sent messages" = "已发送的消息"; + /* No comment provided by engineer. */ "Sent messages will be deleted after set time." = "已发送的消息将在设定的时间后被删除。"; +/* No comment provided by engineer. */ +"Sent reply" = "已发送回复"; + +/* No comment provided by engineer. */ +"Sent total" = "发送总数"; + +/* No comment provided by engineer. */ +"Sent via proxy" = "通过代理发送"; + +/* No comment provided by engineer. */ +"Server address" = "服务器地址"; + +/* No comment provided by engineer. */ +"Server address is incompatible with network settings: %@." = "服务器地址与网络设置不兼容:%@。"; + +/* srv error text. */ +"Server address is incompatible with network settings." = "服务器地址与网络设置不兼容。"; + +/* queue info */ +"server queue info: %@\n\nlast received msg: %@" = "服务器队列信息: %1$@\n\n上次收到的消息: %2$@"; + /* server test error */ "Server requires authorization to create queues, check password" = "服务器需要授权才能创建队列,检查密码"; @@ -3289,9 +3986,24 @@ /* No comment provided by engineer. */ "Server test failed!" = "服务器测试失败!"; +/* No comment provided by engineer. */ +"Server type" = "服务器类型"; + +/* srv error text */ +"Server version is incompatible with network settings." = "服务器版本与网络设置不兼容。"; + +/* No comment provided by engineer. */ +"Server version is incompatible with your app: %@." = "服务器版本与你的应用程序不兼容:%@。"; + /* No comment provided by engineer. */ "Servers" = "服务器"; +/* No comment provided by engineer. */ +"Servers info" = "服务器信息"; + +/* No comment provided by engineer. */ +"Servers statistics will be reset - this cannot be undone!" = "服务器统计信息将被重置 - 此操作无法撤消!"; + /* No comment provided by engineer. */ "Session code" = "会话码"; @@ -3301,6 +4013,9 @@ /* No comment provided by engineer. */ "Set contact name…" = "设置联系人姓名……"; +/* No comment provided by engineer. */ +"Set default theme" = "设置默认主题"; + /* No comment provided by engineer. */ "Set group preferences" = "设置群组偏好设置"; @@ -3346,15 +4061,24 @@ /* No comment provided by engineer. */ "Share address with contacts?" = "与联系人分享地址?"; +/* No comment provided by engineer. */ +"Share from other apps." = "从其他应用程序共享。"; + /* No comment provided by engineer. */ "Share link" = "分享链接"; /* No comment provided by engineer. */ "Share this 1-time invite link" = "分享此一次性邀请链接"; +/* No comment provided by engineer. */ +"Share to SimpleX" = "分享到 SimpleX"; + /* No comment provided by engineer. */ "Share with contacts" = "与联系人分享"; +/* No comment provided by engineer. */ +"Show → on messages sent via private routing." = "显示 → 通过专用路由发送的信息."; + /* No comment provided by engineer. */ "Show calls in phone history" = "在电话历史记录中显示通话"; @@ -3364,6 +4088,12 @@ /* No comment provided by engineer. */ "Show last messages" = "显示最近的消息"; +/* No comment provided by engineer. */ +"Show message status" = "显示消息状态"; + +/* No comment provided by engineer. */ +"Show percentage" = "显示百分比"; + /* No comment provided by engineer. */ "Show preview" = "显示预览"; @@ -3373,6 +4103,9 @@ /* No comment provided by engineer. */ "Show:" = "显示:"; +/* No comment provided by engineer. */ +"SimpleX" = "SimpleX"; + /* No comment provided by engineer. */ "SimpleX address" = "SimpleX 地址"; @@ -3418,6 +4151,9 @@ /* No comment provided by engineer. */ "Simplified incognito mode" = "简化的隐身模式"; +/* No comment provided by engineer. */ +"Size" = "大小"; + /* No comment provided by engineer. */ "Skip" = "跳过"; @@ -3427,14 +4163,26 @@ /* No comment provided by engineer. */ "Small groups (max 20)" = "小群组(最多 20 人)"; +/* No comment provided by engineer. */ +"SMP server" = "SMP 服务器"; + +/* blur media */ +"Soft" = "软"; + +/* No comment provided by engineer. */ +"Some file(s) were not exported:" = "某些文件未导出:"; + /* No comment provided by engineer. */ "Some non-fatal errors occurred during import - you may see Chat console for more details." = "导入过程中发生了一些非致命错误——您可以查看聊天控制台了解更多详细信息。"; +/* No comment provided by engineer. */ +"Some non-fatal errors occurred during import:" = "导入过程中出现一些非致命错误:"; + /* notification title */ "Somebody" = "某人"; /* No comment provided by engineer. */ -"Square, circle, or anything in between." = "方形、圆形、或两者之间的任意形状"; +"Square, circle, or anything in between." = "方形、圆形、或两者之间的任意形状."; /* chat item text */ "standard end-to-end encryption" = "标准端到端加密"; @@ -3448,9 +4196,15 @@ /* No comment provided by engineer. */ "Start migration" = "开始迁移"; +/* No comment provided by engineer. */ +"Starting from %@." = "从 %@ 开始。"; + /* No comment provided by engineer. */ "starting…" = "启动中……"; +/* No comment provided by engineer. */ +"Statistics" = "统计"; + /* No comment provided by engineer. */ "Stop" = "停止"; @@ -3490,9 +4244,21 @@ /* No comment provided by engineer. */ "strike" = "删去"; +/* blur media */ +"Strong" = "加粗"; + /* No comment provided by engineer. */ "Submit" = "提交"; +/* No comment provided by engineer. */ +"Subscribed" = "已订阅"; + +/* No comment provided by engineer. */ +"Subscription errors" = "订阅错误"; + +/* No comment provided by engineer. */ +"Subscriptions ignored" = "忽略订阅"; + /* No comment provided by engineer. */ "Support SimpleX Chat" = "支持 SimpleX Chat"; @@ -3526,6 +4292,9 @@ /* No comment provided by engineer. */ "Tap to scan" = "轻按扫描"; +/* No comment provided by engineer. */ +"TCP connection" = "TCP 连接"; + /* No comment provided by engineer. */ "TCP connection timeout" = "TCP 连接超时"; @@ -3538,6 +4307,9 @@ /* No comment provided by engineer. */ "TCP_KEEPINTVL" = "TCP_KEEPINTVL"; +/* No comment provided by engineer. */ +"Temporary file error" = "临时文件错误"; + /* server test failure */ "Test failed at step %@." = "在步骤 %@ 上测试失败。"; @@ -3565,6 +4337,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 will ask to confirm downloads from unknown file servers (except .onion)." = "该应用程序将要求确认从未知文件服务器(.onion 除外)下载。"; + /* No comment provided by engineer. */ "The attempt to change database passphrase was not completed." = "更改数据库密码的尝试未完成。"; @@ -3595,6 +4370,12 @@ /* No comment provided by engineer. */ "The message will be marked as moderated for all members." = "该消息将对所有成员标记为已被管理员移除。"; +/* No comment provided by engineer. */ +"The messages will be deleted for all members." = "将删除所有成员的消息。"; + +/* No comment provided by engineer. */ +"The messages will be marked as moderated for all members." = "对于所有成员,这些消息将被标记为已审核。"; + /* No comment provided by engineer. */ "The next generation of private messaging" = "下一代私密通讯软件"; @@ -3616,6 +4397,9 @@ /* No comment provided by engineer. */ "The text you pasted is not a SimpleX link." = "您粘贴的文本不是 SimpleX 链接。"; +/* No comment provided by engineer. */ +"Themes" = "主题"; + /* No comment provided by engineer. */ "These settings are for your current profile **%@**." = "这些设置适用于您当前的配置文件 **%@**。"; @@ -3658,9 +4442,15 @@ /* No comment provided by engineer. */ "This is your own SimpleX address!" = "这是你自己的 SimpleX 地址!"; +/* No comment provided by engineer. */ +"This link was used with another mobile device, please create a new link on the desktop." = "此链接已在其他移动设备上使用,请在桌面上创建新链接。"; + /* No comment provided by engineer. */ "This setting applies to messages in your current chat profile **%@**." = "此设置适用于您当前聊天资料 **%@** 中的消息。"; +/* No comment provided by engineer. */ +"Title" = "标题"; + /* No comment provided by engineer. */ "To ask any questions and to receive updates:" = "要提出任何问题并接收更新,请:"; @@ -3682,6 +4472,9 @@ /* 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 IP address, private routing uses your SMP servers to deliver messages." = "为了保护您的 IP 地址,私有路由使用您的 SMP 服务器来传递邮件。"; + /* No comment provided by engineer. */ "To record voice message please grant permission to use Microphone." = "请授权使用麦克风以录制语音消息。"; @@ -3694,18 +4487,33 @@ /* No comment provided by engineer. */ "To verify end-to-end encryption with your contact compare (or scan) the code on your devices." = "要与您的联系人验证端到端加密,请比较(或扫描)您设备上的代码。"; +/* No comment provided by engineer. */ +"Toggle chat list:" = "切换聊天列表:"; + /* No comment provided by engineer. */ "Toggle incognito when connecting." = "在连接时切换隐身模式。"; +/* No comment provided by engineer. */ +"Toolbar opacity" = "工具栏不透明度"; + +/* No comment provided by engineer. */ +"Total" = "共计"; + /* No comment provided by engineer. */ "Transport isolation" = "传输隔离"; +/* No comment provided by engineer. */ +"Transport sessions" = "传输会话"; + /* No comment provided by engineer. */ "Trying to connect to the server used to receive messages from this contact (error: %@)." = "正在尝试连接到用于从该联系人接收消息的服务器(错误:%@)。"; /* No comment provided by engineer. */ "Trying to connect to the server used to receive messages from this contact." = "正在尝试连接到用于从该联系人接收消息的服务器。"; +/* No comment provided by engineer. */ +"Turkish interface" = "土耳其语界面"; + /* No comment provided by engineer. */ "Turn off" = "关闭"; @@ -3730,6 +4538,9 @@ /* No comment provided by engineer. */ "Unblock member?" = "解封成员吗?"; +/* rcv group event chat item */ +"unblocked %@" = "未阻止 %@"; + /* No comment provided by engineer. */ "Unexpected migration state" = "未预料的迁移状态"; @@ -3760,6 +4571,12 @@ /* No comment provided by engineer. */ "Unknown error" = "未知错误"; +/* No comment provided by engineer. */ +"unknown servers" = "未知服务器"; + +/* No comment provided by engineer. */ +"Unknown servers!" = "未知服务器!"; + /* No comment provided by engineer. */ "unknown status" = "未知状态"; @@ -3781,9 +4598,15 @@ /* authentication reason */ "Unlock app" = "解锁应用程序"; +/* No comment provided by engineer. */ +"unmute" = "取消静音"; + /* swipe action */ "Unmute" = "取消静音"; +/* No comment provided by engineer. */ +"unprotected" = "未受保护"; + /* swipe action */ "Unread" = "未读"; @@ -3799,6 +4622,9 @@ /* No comment provided by engineer. */ "Update network settings?" = "更新网络设置?"; +/* No comment provided by engineer. */ +"Update settings?" = "更新设置?"; + /* rcv group event chat item */ "updated group profile" = "已更新的群组资料"; @@ -3811,12 +4637,21 @@ /* No comment provided by engineer. */ "Upgrade and open chat" = "升级并打开聊天"; +/* No comment provided by engineer. */ +"Upload errors" = "上传错误"; + /* No comment provided by engineer. */ "Upload failed" = "上传失败了"; /* server test step */ "Upload file" = "上传文件"; +/* No comment provided by engineer. */ +"Uploaded" = "已上传"; + +/* No comment provided by engineer. */ +"Uploaded files" = "已上传的文件"; + /* No comment provided by engineer. */ "Uploading archive" = "正在上传存档"; @@ -3841,6 +4676,15 @@ /* No comment provided by engineer. */ "Use new incognito profile" = "使用新的隐身配置文件"; +/* No comment provided by engineer. */ +"Use only local notifications?" = "仅使用本地通知?"; + +/* No comment provided by engineer. */ +"Use private routing with unknown servers when IP address is not protected." = "当 IP 地址不受保护时,对未知服务器使用私有路由。"; + +/* No comment provided by engineer. */ +"Use private routing with unknown servers." = "对未知服务器使用私有路由。"; + /* No comment provided by engineer. */ "Use server" = "使用服务器"; @@ -3848,14 +4692,23 @@ "Use SimpleX Chat servers?" = "使用 SimpleX Chat 服务器?"; /* No comment provided by engineer. */ -"Use the app while in the call." = "通话时使用本应用"; +"Use the app while in the call." = "通话时使用本应用."; + +/* No comment provided by engineer. */ +"Use the app with one hand." = "用一只手使用应用程序。"; /* No comment provided by engineer. */ "User profile" = "用户资料"; +/* No comment provided by engineer. */ +"User selection" = "用户选择"; + /* No comment provided by engineer. */ "Using SimpleX Chat servers." = "使用 SimpleX Chat 服务器。"; +/* No comment provided by engineer. */ +"v%@" = "v%@"; + /* No comment provided by engineer. */ "v%@ (%@)" = "v%@ (%@)"; @@ -3898,6 +4751,9 @@ /* No comment provided by engineer. */ "Via secure quantum resistant protocol." = "通过安全的、抗量子计算机破解的协议。"; +/* No comment provided by engineer. */ +"video" = "视频"; + /* No comment provided by engineer. */ "Video call" = "视频通话"; @@ -3943,6 +4799,9 @@ /* No comment provided by engineer. */ "waiting for confirmation…" = "等待确认中……"; +/* No comment provided by engineer. */ +"Waiting for desktop..." = "正在等待桌面..."; + /* No comment provided by engineer. */ "Waiting for file" = "等待文件中"; @@ -3952,11 +4811,17 @@ /* No comment provided by engineer. */ "Waiting for video" = "等待视频中"; +/* No comment provided by engineer. */ +"Wallpaper accent" = "壁纸装饰"; + +/* No comment provided by engineer. */ +"Wallpaper background" = "壁纸背景"; + /* No comment provided by engineer. */ "wants to connect to you!" = "想要与您连接!"; /* No comment provided by engineer. */ -"Warning: starting chat on multiple devices is not supported and will cause message delivery failures" = "警告:不支持在多部设备上启动聊天,这么做会导致消息传送失败。"; +"Warning: starting chat on multiple devices is not supported and will cause message delivery failures" = "警告:不支持在多部设备上启动聊天,这么做会导致消息传送失败"; /* No comment provided by engineer. */ "Warning: you may lose some data!" = "警告:您可能会丢失部分数据!"; @@ -3985,6 +4850,9 @@ /* No comment provided by engineer. */ "When connecting audio and video calls." = "连接音频和视频通话时。"; +/* 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." = "当人们请求连接时,您可以接受或拒绝它。"; @@ -4009,12 +4877,27 @@ /* No comment provided by engineer. */ "With reduced battery usage." = "降低了电量使用。"; +/* No comment provided by engineer. */ +"Without Tor or VPN, your IP address will be visible to file servers." = "如果没有 Tor 或 VPN,您的 IP 地址将对文件服务器可见。"; + +/* No comment provided by engineer. */ +"Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." = "如果没有 Tor 或 VPN,您的 IP 地址将对以下 XFTP 中继可见:%@。"; + /* No comment provided by engineer. */ "Wrong database passphrase" = "数据库密码错误"; +/* snd error text */ +"Wrong key or unknown connection - most likely this connection is deleted." = "密钥错误或连接未知 - 很可能此连接已被删除。"; + +/* file error text */ +"Wrong key or unknown file chunk address - most likely file is deleted." = "密钥错误或文件块地址未知 - 很可能文件已删除。"; + /* No comment provided by engineer. */ "Wrong passphrase!" = "密码错误!"; +/* No comment provided by engineer. */ +"XFTP server" = "XFTP 服务器"; + /* pref value */ "yes" = "是"; @@ -4024,6 +4907,9 @@ /* No comment provided by engineer. */ "You" = "您"; +/* No comment provided by engineer. */ +"You **must not** use the same database on two devices." = "您 **不得** 在两台设备上使用相同的数据库。"; + /* No comment provided by engineer. */ "You accepted connection" = "您已接受连接"; @@ -4036,12 +4922,27 @@ /* No comment provided by engineer. */ "You are already connected to %@." = "您已经连接到 %@。"; +/* No comment provided by engineer. */ +"You are already connecting to %@." = "您已连接到 %@。"; + /* No comment provided by engineer. */ "You are already connecting via this one-time link!" = "你已经在通过这个一次性链接进行连接!"; +/* No comment provided by engineer. */ +"You are already in group %@." = "您已在组 %@ 中。"; + +/* No comment provided by engineer. */ +"You are already joining the group %@." = "您已加入组 %@。"; + +/* No comment provided by engineer. */ +"You are already joining the group via this link!" = "您已经通过此链接加入群组!"; + /* No comment provided by engineer. */ "You are already joining the group via this link." = "你已经在通过此链接加入该群。"; +/* No comment provided by engineer. */ +"You are already joining the group!\nRepeat join request?" = "您已经加入了这个群组!\n重复加入请求?"; + /* No comment provided by engineer. */ "You are connected to the server used to receive messages from this contact." = "您已连接到用于接收该联系人消息的服务器。"; @@ -4051,12 +4952,21 @@ /* No comment provided by engineer. */ "You are invited to group" = "您被邀请加入群组"; +/* No comment provided by engineer. */ +"You are not connected to these servers. Private routing is used to deliver messages to them." = "您未连接到这些服务器。私有路由用于向他们发送消息。"; + /* No comment provided by engineer. */ "you are observer" = "您是观察者"; +/* snd group event chat item */ +"you blocked %@" = "你阻止了%@"; + /* No comment provided by engineer. */ "You can accept calls from lock screen, without device and app authentication." = "您可以从锁屏上接听电话,无需设备和应用程序的认证。"; +/* No comment provided by engineer. */ +"You can change it in Appearance settings." = "您可以在外观设置中更改它。"; + /* No comment provided by engineer. */ "You can create it later" = "您可以以后创建它"; @@ -4078,6 +4988,9 @@ /* notification body */ "You can now chat with %@" = "您现在可以给 %@ 发送消息"; +/* No comment provided by engineer. */ +"You can send messages to %@ from Archived contacts." = "您可以从存档的联系人向%@发送消息。"; + /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "您可以通过设置来设置锁屏通知预览。"; @@ -4093,6 +5006,9 @@ /* No comment provided by engineer. */ "You can start chat via app Settings / Database or by restarting the app" = "您可以通过应用程序设置/数据库或重新启动应用程序开始聊天"; +/* No comment provided by engineer. */ +"You can still view conversation with %@ in the list of chats." = "您仍然可以在聊天列表中查看与 %@的对话。"; + /* No comment provided by engineer. */ "You can turn on SimpleX Lock via Settings." = "您可以通过设置开启 SimpleX 锁定。"; @@ -4126,6 +5042,9 @@ /* No comment provided by engineer. */ "You have already requested connection via this address!" = "你已经请求通过此地址进行连接!"; +/* No comment provided by engineer. */ +"You have already requested connection!\nRepeat connection request?" = "您已经请求连接了!\n重复连接请求?"; + /* No comment provided by engineer. */ "You have to enter passphrase every time the app starts - it is not stored on the device." = "您必须在每次应用程序启动时输入密码——它不存储在设备上。"; @@ -4141,9 +5060,18 @@ /* snd group event chat item */ "you left" = "您已离开"; +/* No comment provided by engineer. */ +"You may migrate the exported database." = "您可以迁移导出的数据库。"; + +/* No comment provided by engineer. */ +"You may save the exported archive." = "您可以保存导出的档案。"; + /* No comment provided by engineer. */ "You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts." = "您只能在一台设备上使用最新版本的聊天数据库,否则您可能会停止接收来自某些联系人的消息。"; +/* No comment provided by engineer. */ +"You need to allow your contact to call to be able to call them." = "您需要允许您的联系人呼叫才能呼叫他们。"; + /* No comment provided by engineer. */ "You need to allow your contact to send voice messages to be able to send them." = "您需要允许您的联系人发送语音消息,以便您能够发送语音消息。"; @@ -4162,9 +5090,15 @@ /* chat list item description */ "you shared one-time link incognito" = "您分享了一次性链接隐身聊天"; +/* snd group event chat item */ +"you unblocked %@" = "您解封了 %@"; + /* No comment provided by engineer. */ "You will be connected to group when the group host's device is online, please wait or check later!" = "您将在组主设备上线时连接到该群组,请稍等或稍后再检查!"; +/* No comment provided by engineer. */ +"You will be connected when group link host's device is online, please wait or check later!" = "当 Group Link Host 的设备在线时,您将被连接,请稍候或稍后检查!"; + /* No comment provided by engineer. */ "You will be connected when your connection request is accepted, please wait or check later!" = "当您的连接请求被接受后,您将可以连接,请稍等或稍后检查!"; @@ -4234,11 +5168,14 @@ /* No comment provided by engineer. */ "Your privacy" = "您的隐私设置"; +/* No comment provided by engineer. */ +"Your profile" = "您的个人资料"; + /* No comment provided by engineer. */ "Your profile **%@** will be shared." = "您的个人资料 **%@** 将被共享。"; /* No comment provided by engineer. */ -"Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "您的资料存储在您的设备上并仅与您的联系人共享。\nSimpleX 服务器无法看到您的资料。"; +"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "您的资料存储在您的设备上并仅与您的联系人共享。 SimpleX 服务器无法看到您的资料。"; /* No comment provided by engineer. */ "Your profile, contacts and delivered messages are stored on your device." = "您的资料、联系人和发送的消息存储在您的设备上。"; diff --git a/apps/ios/zh-Hans.lproj/SimpleX--iOS--InfoPlist.strings b/apps/ios/zh-Hans.lproj/SimpleX--iOS--InfoPlist.strings index b3192851c8..199d5faf7c 100644 --- a/apps/ios/zh-Hans.lproj/SimpleX--iOS--InfoPlist.strings +++ b/apps/ios/zh-Hans.lproj/SimpleX--iOS--InfoPlist.strings @@ -7,6 +7,9 @@ /* Privacy - Face ID Usage Description */ "NSFaceIDUsageDescription" = "SimpleX 使用Face ID进行本地身份验证"; +/* Privacy - Local Network Usage Description */ +"NSLocalNetworkUsageDescription" = "SimpleX 使用本地网络访问,允许通过同一网络上的桌面应用程序使用用户聊天配置文件。"; + /* Privacy - Microphone Usage Description */ "NSMicrophoneUsageDescription" = "SimpleX 需要麦克风访问权限才能进行音频和视频通话,以及录制语音消息。"; diff --git a/apps/multiplatform/android/build.gradle.kts b/apps/multiplatform/android/build.gradle.kts index 5c2c786a21..250616ea5c 100644 --- a/apps/multiplatform/android/build.gradle.kts +++ b/apps/multiplatform/android/build.gradle.kts @@ -15,7 +15,7 @@ android { namespace = "chat.simplex.app" minSdk = 26 //noinspection OldTargetApi - targetSdk = 33 + targetSdk = 34 // !!! // skip version code after release to F-Droid, as it uses two version codes versionCode = (extra["android.version_code"] as String).toInt() @@ -126,29 +126,29 @@ android { dependencies { implementation(project(":common")) - implementation("androidx.core:core-ktx:1.12.0") + implementation("androidx.core:core-ktx:1.13.1") //implementation("androidx.compose.ui:ui:${rootProject.extra["compose.version"] as String}") //implementation("androidx.compose.material:material:$compose_version") //implementation("androidx.compose.ui:ui-tooling-preview:$compose_version") - implementation("androidx.appcompat:appcompat:1.6.1") - implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0") - implementation("androidx.lifecycle:lifecycle-process:2.7.0") - implementation("androidx.activity:activity-compose:1.8.2") - val workVersion = "2.9.0" + implementation("androidx.appcompat:appcompat:1.7.0") + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.4") + implementation("androidx.lifecycle:lifecycle-process:2.8.4") + implementation("androidx.activity:activity-compose:1.9.1") + val workVersion = "2.9.1" implementation("androidx.work:work-runtime-ktx:$workVersion") implementation("androidx.work:work-multiprocess:$workVersion") - implementation("com.jakewharton:process-phoenix:2.2.0") + implementation("com.jakewharton:process-phoenix:3.0.0") //Camera Permission - implementation("com.google.accompanist:accompanist-permissions:0.23.0") + implementation("com.google.accompanist:accompanist-permissions:0.34.0") //implementation("androidx.compose.material:material-icons-extended:$compose_version") //implementation("androidx.compose.ui:ui-util:$compose_version") testImplementation("junit:junit:4.13.2") - androidTestImplementation("androidx.test.ext:junit:1.1.5") - androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") + androidTestImplementation("androidx.test.ext:junit:1.2.1") + androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1") //androidTestImplementation("androidx.compose.ui:ui-test-junit4:$compose_version") debugImplementation("androidx.compose.ui:ui-tooling:1.6.4") } diff --git a/apps/multiplatform/android/src/main/AndroidManifest.xml b/apps/multiplatform/android/src/main/AndroidManifest.xml index 073f1bf8c8..deb5d83e5f 100644 --- a/apps/multiplatform/android/src/main/AndroidManifest.xml +++ b/apps/multiplatform/android/src/main/AndroidManifest.xml @@ -21,6 +21,12 @@ + + + + + + + android:stopWithTask="false" + android:foregroundServiceType="remoteMessaging" + /> @@ -141,7 +149,9 @@ android:name=".CallService" android:enabled="true" android:exported="false" - android:stopWithTask="false"/> + android:stopWithTask="false" + android:foregroundServiceType="mediaPlayback|microphone|camera|remoteMessaging" + /> = 34) { + ServiceInfo.FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING + } else { + 0 + } + } else if (Build.VERSION.SDK_INT >= 30) { + if (call.supportsVideo()) { + ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE or ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA + } else { + ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE + } + } else if (Build.VERSION.SDK_INT >= 29) { + ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK + } else { + 0 + } } private fun createNotificationChannel(): NotificationManager? { 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 5a69d282b4..c63b6cb497 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 @@ -54,7 +54,7 @@ class MainActivity: FragmentActivity() { SimplexApp.context.schedulePeriodicWakeUp() } - override fun onNewIntent(intent: Intent?) { + override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) processIntent(intent) processExternalIntent(intent) 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 4d3b390189..6adaa1d4e0 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 androidx.compose.animation.core.* import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.ui.graphics.Color @@ -65,7 +66,7 @@ class SimplexApp: Application(), LifecycleEventObserver { } } context = this - initHaskell() + initHaskell(packageName) initMultiplatform() runMigrations() tmpDir.deleteRecursively() @@ -119,7 +120,10 @@ class SimplexApp: Application(), LifecycleEventObserver { * */ if (chatModel.chatRunning.value != false && chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete && - appPrefs.notificationsMode.get() == NotificationsMode.SERVICE + appPrefs.notificationsMode.get() == NotificationsMode.SERVICE && + // New installation passes all checks above and tries to start the service which is not needed at all + // because preferred notification type is not yet chosen. So, check that the user has initialized db already + appPrefs.newDatabaseInitialized.get() ) { SimplexService.start() } @@ -256,7 +260,6 @@ class SimplexApp: Application(), LifecycleEventObserver { override fun androidSetNightModeIfSupported() { if (Build.VERSION.SDK_INT < 31) return - val light = if (CurrentColors.value.name == DefaultTheme.SYSTEM_THEME_NAME) { null } else { @@ -271,6 +274,31 @@ 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 + window.statusBarColor = baseBackgroundColor.mixWith(drawerShadingColor.copy(1f), 1 - drawerShadingColor.alpha).toArgb() + val navBar = navBarColor.toArgb() + + 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) { val window = mainActivity.get()?.window ?: return @Suppress("DEPRECATION") 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 7fc1bd151c..ce3f0825b8 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 @@ -4,6 +4,7 @@ import android.annotation.SuppressLint import android.app.* import android.content.* import android.content.pm.PackageManager +import android.content.pm.ServiceInfo import android.net.Uri import android.os.* import android.os.SystemClock @@ -15,8 +16,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.core.app.NotificationCompat +import androidx.core.app.ServiceCompat import androidx.core.content.ContextCompat import androidx.work.* +import chat.simplex.app.model.NtfManager import chat.simplex.common.AppLock import chat.simplex.common.helpers.requiresIgnoringBattery import chat.simplex.common.model.ChatController @@ -52,18 +55,15 @@ class SimplexService: Service() { } else { Log.d(TAG, "null intent. Probably restarted by the system.") } - startForeground(SIMPLEX_SERVICE_ID, serviceNotification) + ServiceCompat.startForeground(this, SIMPLEX_SERVICE_ID, createNotificationIfNeeded(), foregroundServiceType()) return START_STICKY // to restart if killed } override fun onCreate() { super.onCreate() Log.d(TAG, "Simplex service created") - val title = generalGetString(MR.strings.simplex_service_notification_title) - val text = generalGetString(MR.strings.simplex_service_notification_text) - notificationManager = createNotificationChannel() - serviceNotification = createNotification(title, text) - startForeground(SIMPLEX_SERVICE_ID, serviceNotification) + createNotificationIfNeeded() + ServiceCompat.startForeground(this, SIMPLEX_SERVICE_ID, createNotificationIfNeeded(), foregroundServiceType()) /** * The reason [stopAfterStart] exists is because when the service is not called [startForeground] yet, and * we call [stopSelf] on the same service, [ForegroundServiceDidNotStartInTimeException] will be thrown. @@ -103,6 +103,26 @@ class SimplexService: Service() { super.onDestroy() } + private fun createNotificationIfNeeded(): Notification { + val ntf = serviceNotification + if (ntf != null) return ntf + + val title = generalGetString(MR.strings.simplex_service_notification_title) + val text = generalGetString(MR.strings.simplex_service_notification_text) + notificationManager = createNotificationChannel() + val newNtf = createNotification(title, text) + serviceNotification = newNtf + return newNtf + } + + private fun foregroundServiceType(): Int { + return if (Build.VERSION.SDK_INT >= 34) { + ServiceInfo.FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING + } else { + 0 + } + } + private fun startService() { Log.d(TAG, "SimplexService startService") if (wakeLock != null || isCheckingNewMessages) return @@ -292,6 +312,10 @@ class SimplexService: Service() { } private suspend fun serviceAction(action: Action) { + if (!NtfManager.areNotificationsEnabledInSystem()) { + Log.d(TAG, "SimplexService serviceAction: ${action.name}. Notifications are not enabled in OS yet, not starting service") + return + } Log.d(TAG, "SimplexService serviceAction: ${action.name}") withContext(Dispatchers.IO) { Intent(androidAppContext, SimplexService::class.java).also { diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/model/NtfManager.android.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/model/NtfManager.android.kt index 417a81a953..cf19589d4a 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/model/NtfManager.android.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/model/NtfManager.android.kt @@ -53,7 +53,7 @@ object NtfManager { private val msgNtfTimeoutMs = 30000L init { - if (manager.areNotificationsEnabled()) createNtfChannelsMaybeShowAlert() + if (areNotificationsEnabledInSystem()) createNtfChannelsMaybeShowAlert() } private fun callNotificationChannel(channelId: String, channelName: String): NotificationChannel { @@ -287,6 +287,8 @@ object NtfManager { } } + fun areNotificationsEnabledInSystem() = manager.areNotificationsEnabled() + /** * This function creates notifications channels. On Android 13+ calling it for the first time will trigger system alert, * The alert asks a user to allow or disallow to show notifications for the app. That's why it should be called only when the user diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/call/CallActivity.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/views/call/CallActivity.kt index 323eb4417b..a9697069c0 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/call/CallActivity.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/views/call/CallActivity.kt @@ -120,6 +120,7 @@ class CallActivity: ComponentActivity(), ServiceConnection { return grantedAudio && grantedCamera } + @Deprecated("Was deprecated in OS") override fun onBackPressed() { if (isOnLockScreenNow()) { super.onBackPressed() @@ -139,6 +140,7 @@ class CallActivity: ComponentActivity(), ServiceConnection { } override fun onUserLeaveHint() { + super.onUserLeaveHint() // On Android 12+ PiP is enabled automatically when a user hides the app if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R && callSupportsVideo() && platform.androidPictureInPictureAllowed()) { enterPictureInPictureMode() @@ -248,6 +250,9 @@ fun CallActivityView() { ) if (permissionsState.allPermissionsGranted) { ActiveCallView() + LaunchedEffect(Unit) { + activity.startServiceAndBind() + } } else { CallPermissionsView(remember { m.activeCallViewIsCollapsed }.value, callSupportsVideo()) { withBGApi { chatModel.callManager.endCall(call) } @@ -285,11 +290,6 @@ fun CallActivityView() { AlertManager.shared.showInView() } } - LaunchedEffect(call == null) { - if (call != null) { - activity.startServiceAndBind() - } - } LaunchedEffect(invitation, call, switchingCall, showCallView) { if (!switchingCall && invitation == null && (!showCallView || call == null)) { Log.d(TAG, "CallActivityView: finishing activity") diff --git a/apps/multiplatform/android/src/main/res/values/colors.xml b/apps/multiplatform/android/src/main/res/values/colors.xml index e1a994e57f..1833a6d9a3 100644 --- a/apps/multiplatform/android/src/main/res/values/colors.xml +++ b/apps/multiplatform/android/src/main/res/values/colors.xml @@ -2,6 +2,5 @@ #FF000000 #FFFFFFFF - #8b8786 #121212 \ No newline at end of file diff --git a/apps/multiplatform/common/build.gradle.kts b/apps/multiplatform/common/build.gradle.kts index dd9c7ab161..1aaa061daa 100644 --- a/apps/multiplatform/common/build.gradle.kts +++ b/apps/multiplatform/common/build.gradle.kts @@ -61,8 +61,8 @@ kotlin { val androidMain by getting { kotlin.srcDir("build/generated/moko/androidMain/src") dependencies { - implementation("androidx.activity:activity-compose:1.8.2") - val workVersion = "2.9.0" + implementation("androidx.activity:activity-compose:1.9.1") + val workVersion = "2.9.1" implementation("androidx.work:work-runtime-ktx:$workVersion") implementation("com.google.accompanist:accompanist-insets:0.30.1") @@ -78,31 +78,36 @@ kotlin { //Camera Permission implementation("com.google.accompanist:accompanist-permissions:0.34.0") - implementation("androidx.webkit:webkit:1.10.0") + implementation("androidx.webkit:webkit:1.11.0") // GIFs support implementation("io.coil-kt:coil-compose:2.6.0") implementation("io.coil-kt:coil-gif:2.6.0") - implementation("com.jakewharton:process-phoenix:2.2.0") + implementation("com.jakewharton:process-phoenix:3.0.0") - val cameraXVersion = "1.3.2" + val cameraXVersion = "1.3.4" implementation("androidx.camera:camera-core:${cameraXVersion}") implementation("androidx.camera:camera-camera2:${cameraXVersion}") implementation("androidx.camera:camera-lifecycle:${cameraXVersion}") implementation("androidx.camera:camera-view:${cameraXVersion}") // Calls lifecycle listener - implementation("androidx.lifecycle:lifecycle-process:2.4.1") + implementation("androidx.lifecycle:lifecycle-process:2.8.4") } } val desktopMain by getting { dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:1.8.0") - implementation("com.github.Dansoftowner:jSystemThemeDetector:3.8") + implementation("com.github.Dansoftowner:jSystemThemeDetector:3.8") { + exclude("net.java.dev.jna") + } + // For jSystemThemeDetector only + implementation("net.java.dev.jna:jna-platform:5.14.0") implementation("com.sshtools:two-slices:0.9.0-SNAPSHOT") implementation("org.slf4j:slf4j-simple:2.0.12") implementation("uk.co.caprica:vlcj:4.8.3") + implementation("net.java.dev.jna:jna:5.14.0") implementation("com.github.NanoHttpd.nanohttpd:nanohttpd:efb2ebf85a") implementation("com.github.NanoHttpd.nanohttpd:nanohttpd-websocket:efb2ebf85a") implementation("com.squareup.okhttp3:okhttp:4.12.0") @@ -119,8 +124,8 @@ android { defaultConfig { minSdk = 26 } - testOptions.targetSdk = 33 - lint.targetSdk = 33 + testOptions.targetSdk = 34 + lint.targetSdk = 34 val isAndroid = gradle.startParameter.taskNames.find { val lower = it.lowercase() lower.contains("release") || lower.startsWith("assemble") || lower.startsWith("install") diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/AppCommon.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/AppCommon.android.kt index 8cd51e8298..cd1672f3e9 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/AppCommon.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/AppCommon.android.kt @@ -31,22 +31,19 @@ lateinit var androidAppContext: Context var mainActivity: WeakReference = WeakReference(null) var callActivity: WeakReference = WeakReference(null) -fun initHaskell() { - val socketName = "chat.simplex.app.local.socket.address.listen.native.cmd2" + Random.nextLong(100000) +fun initHaskell(packageName: String) { val s = Semaphore(0) thread(name="stdout/stderr pipe") { Log.d(TAG, "starting server") - var server: LocalServerSocket? = null - for (i in 0..100) { - try { - server = LocalServerSocket(socketName + i) - break - } catch (e: IOException) { - Log.e(TAG, e.stackTraceToString()) - } - } - if (server == null) { - throw Error("Unable to setup local server socket. Contact developers") + val server: LocalServerSocket + try { + server = LocalServerSocket(packageName) + } catch (e: IOException) { + Log.e(TAG, e.stackTraceToString()) + Log.e(TAG, "Unable to setup local server socket. Contact developers") + s.release() + // Will not have logs from backend + return@thread } Log.d(TAG, "started server") s.release() @@ -60,7 +57,7 @@ fun initHaskell() { Log.d(TAG, "starting receiver loop") while (true) { val line = input.readLine() ?: break - Log.w("$TAG (stdout/stderr)", line) + Log.w(TAG, "(stdout/stderr) $line") logbuffer.add(line) } Log.w(TAG, "exited receiver loop") @@ -70,7 +67,7 @@ fun initHaskell() { System.loadLibrary("app-lib") s.acquire() - pipeStdOutToSocket(socketName) + pipeStdOutToSocket(packageName) initHS() } 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 new file mode 100644 index 0000000000..6c16a75874 --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.android.kt @@ -0,0 +1,222 @@ +package chat.simplex.common.views.chatlist + +import SectionItemView +import androidx.compose.foundation.* +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.material.DrawerDefaults.ScrimOpacity +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.graphics.* +import androidx.compose.ui.layout.onSizeChanged +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 chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow + +@Composable +actual fun UserPickerInactiveUsersSection( + users: List, + stopped: Boolean, + onShowAllProfilesClicked: () -> Unit, + onUserClicked: (user: User) -> Unit, +) { + val scrollState = rememberScrollState() + + if (users.isNotEmpty()) { + SectionItemView( + padding = PaddingValues( + start = 16.dp, + top = if (windowOrientation() == WindowOrientation.PORTRAIT) DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL else DEFAULT_PADDING_HALF, + bottom = DEFAULT_PADDING_HALF), + disabled = stopped + ) { + Box { + Row( + modifier = Modifier.padding(end = DEFAULT_PADDING + 30.dp).horizontalScroll(scrollState) + ) { + users.forEach { u -> + UserPickerInactiveUserBadge(u, stopped) { + onUserClicked(it) + withBGApi { + delay(500) + scrollState.scrollTo(0) + } + } + Spacer(Modifier.width(20.dp)) + } + Spacer(Modifier.width(60.dp)) + } + Row( + horizontalArrangement = Arrangement.End, + modifier = Modifier + .fillMaxWidth() + .padding(end = DEFAULT_PADDING + 30.dp) + .height(60.dp) + ) { + Canvas(modifier = Modifier.size(60.dp)) { + drawRect( + brush = Brush.horizontalGradient( + colors = listOf( + Color.Transparent, + CurrentColors.value.colors.surface, + ) + ), + ) + } + } + Row( + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .height(60.dp) + .fillMaxWidth() + .padding(end = DEFAULT_PADDING) + ) { + IconButton( + onClick = onShowAllProfilesClicked, + enabled = !stopped + ) { + Icon( + painterResource(MR.images.ic_chevron_right), + stringResource(MR.strings.your_chat_profiles), + tint = MaterialTheme.colors.secondary, + modifier = Modifier.size(34.dp) + ) + } + } + } + } + } else { + UserPickerOptionRow( + painterResource(MR.images.ic_manage_accounts), + stringResource(MR.strings.your_chat_profiles), + onShowAllProfilesClicked + ) + } +} + +private fun calculateFraction(pos: Float) = + (pos / 1f).coerceIn(0f, 1f) + +@Composable +actual fun PlatformUserPicker(modifier: Modifier, pickerState: MutableStateFlow, content: @Composable () -> Unit) { + val pickerIsVisible = pickerState.collectAsState().value.isVisible() + val dismissState = rememberDismissState(initialValue = if (pickerIsVisible) DismissValue.Default else DismissValue.DismissedToEnd) { + if (it == DismissValue.DismissedToEnd && pickerState.value.isVisible()) { + pickerState.value = AnimatedViewState.HIDING + } + true + } + val height = remember { mutableIntStateOf(0) } + val heightValue = height.intValue + val clickableModifier = if (pickerIsVisible) { + Modifier.clickable(interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = { pickerState.value = AnimatedViewState.HIDING }) + } 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.surface + ) + } 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( + 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() + } + } + } +} + +private fun Modifier.draggableBottomDrawerModifier( + state: DismissState, + swipeDistance: Float, +): Modifier = this.swipeable( + state = state, + anchors = mapOf(0f to DismissValue.Default, swipeDistance to DismissValue.DismissedToEnd), + thresholds = { _, _ -> FractionalThreshold(0.3f) }, + orientation = Orientation.Vertical, + resistance = null +) diff --git a/apps/multiplatform/common/src/androidMain/res/drawable/edit_text_cursor.xml b/apps/multiplatform/common/src/androidMain/res/drawable/edit_text_cursor.xml index 683c3a4dd4..948ae4d4bf 100644 --- a/apps/multiplatform/common/src/androidMain/res/drawable/edit_text_cursor.xml +++ b/apps/multiplatform/common/src/androidMain/res/drawable/edit_text_cursor.xml @@ -1,5 +1,5 @@ - + 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 3cba89922d..b95aed45d2 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 @@ -6,14 +6,11 @@ import androidx.compose.animation.core.Animatable 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.* -import androidx.compose.material.ripple.rememberRipple 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.clip import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer @@ -42,12 +39,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.sqrt - -data class SettingsViewState( - val userPickerState: MutableStateFlow, - val scaffoldState: ScaffoldState -) @Composable fun AppScreen() { @@ -145,13 +136,11 @@ fun MainScreen() { userPickerState.value = AnimatedViewState.VISIBLE } } - val scaffoldState = rememberScaffoldState() - val settingsState = remember { SettingsViewState(userPickerState, scaffoldState) } SetupClipboardListener() if (appPlatform.isAndroid) { - AndroidScreen(settingsState) + AndroidScreen(userPickerState) } else { - DesktopScreen(settingsState) + DesktopScreen(userPickerState) } } } @@ -249,7 +238,7 @@ fun MainScreen() { val ANDROID_CALL_TOP_PADDING = 40.dp @Composable -fun AndroidScreen(settingsState: SettingsViewState) { +fun AndroidScreen(userPickerState: MutableStateFlow) { BoxWithConstraints { val call = remember { chatModel.activeCall} .value val showCallArea = call != null && call.callState != CallState.WaitCapabilities && call.callState != CallState.InvitationAccepted @@ -262,7 +251,7 @@ fun AndroidScreen(settingsState: SettingsViewState) { } .padding(top = if (showCallArea) ANDROID_CALL_TOP_PADDING else 0.dp) ) { - StartPartOfScreen(settingsState) + StartPartOfScreen(userPickerState) } val scope = rememberCoroutineScope() val onComposed: suspend (chatId: String?) -> Unit = { chatId -> @@ -318,15 +307,15 @@ fun AndroidScreen(settingsState: SettingsViewState) { } @Composable -fun StartPartOfScreen(settingsState: SettingsViewState) { +fun StartPartOfScreen(userPickerState: MutableStateFlow) { if (chatModel.setDeliveryReceipts.value) { SetDeliveryReceiptsView(chatModel) } else { val stopped = chatModel.chatRunning.value == false if (chatModel.sharedContent.value == null) - ChatListView(chatModel, settingsState, AppLock::setPerformLA, stopped) + ChatListView(chatModel, userPickerState, AppLock::setPerformLA, stopped) else - ShareListView(chatModel, settingsState, stopped) + ShareListView(chatModel, stopped) } } @@ -367,49 +356,41 @@ fun EndPartOfScreen() { } @Composable -fun DesktopScreen(settingsState: SettingsViewState) { - Box { - // 56.dp is a size of unused space of settings drawer - Box(Modifier.width(DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier + 56.dp)) { - StartPartOfScreen(settingsState) - } - Box(Modifier.widthIn(max = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier)) { - ModalManager.start.showInView() - SwitchingUsersView() - } - Row(Modifier.padding(start = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier).clipToBounds()) { - Box(Modifier.widthIn(min = DEFAULT_MIN_CENTER_MODAL_WIDTH).weight(1f)) { - CenterPartOfScreen() - } - if (ModalManager.end.hasModalsOpen()) { - VerticalDivider() - } - Box(Modifier.widthIn(max = DEFAULT_END_MODAL_WIDTH * fontSizeSqrtMultiplier).clipToBounds()) { - EndPartOfScreen() - } - } - val (userPickerState, scaffoldState ) = settingsState - val scope = rememberCoroutineScope() - if (scaffoldState.drawerState.isOpen || (ModalManager.start.hasModalsOpen && !ModalManager.center.hasModalsOpen)) { - Box( - Modifier - .fillMaxSize() - .padding(start = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier) - .clickable(interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = { - ModalManager.start.closeModals() - scope.launch { settingsState.scaffoldState.drawerState.close() } - }) - ) - } - VerticalDivider(Modifier.padding(start = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier)) +fun DesktopScreen(userPickerState: MutableStateFlow) { + Box(Modifier.width(DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier)) { + StartPartOfScreen(userPickerState) tryOrShowError("UserPicker", error = {}) { - UserPicker(chatModel, userPickerState) { - scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() } - userPickerState.value = AnimatedViewState.GONE - } + UserPicker(chatModel, userPickerState, setPerformLA = AppLock::setPerformLA) } - ModalManager.fullscreen.showInView() } + Box(Modifier.widthIn(max = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier)) { + ModalManager.start.showInView() + SwitchingUsersView() + } + Row(Modifier.padding(start = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier).clipToBounds()) { + Box(Modifier.widthIn(min = DEFAULT_MIN_CENTER_MODAL_WIDTH).weight(1f)) { + CenterPartOfScreen() + } + if (ModalManager.end.hasModalsOpen()) { + VerticalDivider() + } + Box(Modifier.widthIn(max = DEFAULT_END_MODAL_WIDTH * fontSizeSqrtMultiplier).clipToBounds()) { + EndPartOfScreen() + } + } + if (userPickerState.collectAsState().value.isVisible() || (ModalManager.start.hasModalsOpen && !ModalManager.center.hasModalsOpen)) { + Box( + Modifier + .fillMaxSize() + .padding(start = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier) + .clickable(interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = { + ModalManager.start.closeModals() + userPickerState.value = AnimatedViewState.HIDING + }) + ) + } + VerticalDivider(Modifier.padding(start = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier)) + ModalManager.fullscreen.showInView() } @Composable 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 618ad8f4b8..5671322d17 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 @@ -785,7 +785,8 @@ object ChatModel { data class ShowingInvitation( val connId: String, val connReq: String, - val connChatUsed: Boolean + val connChatUsed: Boolean, + val conn: PendingContactConnection ) enum class ChatType(val type: String) { 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 5c41eccd58..61d158a781 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 @@ -846,15 +846,15 @@ object ChatController { return null } - suspend fun apiSendMessage(rh: Long?, type: ChatType, id: Long, file: CryptoFile? = null, quotedItemId: Long? = null, mc: MsgContent, live: Boolean = false, ttl: Int? = null): AChatItem? { - val cmd = CC.ApiSendMessage(type, id, file, quotedItemId, mc, live, ttl) + suspend fun apiSendMessages(rh: Long?, type: ChatType, id: Long, live: Boolean = false, ttl: Int? = null, composedMessages: List): List? { + val cmd = CC.ApiSendMessages(type, id, live, ttl, composedMessages) return processSendMessageCmd(rh, cmd) } - private suspend fun processSendMessageCmd(rh: Long?, cmd: CC): AChatItem? { + private suspend fun processSendMessageCmd(rh: Long?, cmd: CC): List? { val r = sendCmd(rh, cmd) return when (r) { - is CR.NewChatItem -> r.chatItem + is CR.NewChatItems -> r.chatItems else -> { if (!(networkErrorAlert(r))) { apiErrorAlert("processSendMessageCmd", generalGetString(MR.strings.error_sending_message), r) @@ -863,13 +863,13 @@ object ChatController { } } } - suspend fun apiCreateChatItem(rh: Long?, noteFolderId: Long, file: CryptoFile? = null, mc: MsgContent): AChatItem? { - val cmd = CC.ApiCreateChatItem(noteFolderId, file, mc) + suspend fun apiCreateChatItems(rh: Long?, noteFolderId: Long, composedMessages: List): List? { + val cmd = CC.ApiCreateChatItems(noteFolderId, composedMessages) val r = sendCmd(rh, cmd) return when (r) { - is CR.NewChatItem -> r.chatItem + is CR.NewChatItems -> r.chatItems else -> { - apiErrorAlert("apiCreateChatItem", generalGetString(MR.strings.error_creating_message), r) + apiErrorAlert("apiCreateChatItems", generalGetString(MR.strings.error_creating_message), r) null } } @@ -885,9 +885,9 @@ object ChatController { } } - suspend fun apiForwardChatItem(rh: Long?, toChatType: ChatType, toChatId: Long, fromChatType: ChatType, fromChatId: Long, itemId: Long, ttl: Int?): ChatItem? { - val cmd = CC.ApiForwardChatItem(toChatType, toChatId, fromChatType, fromChatId, itemId, ttl) - return processSendMessageCmd(rh, cmd)?.chatItem + suspend fun apiForwardChatItems(rh: Long?, toChatType: ChatType, toChatId: Long, fromChatType: ChatType, fromChatId: Long, itemIds: List, ttl: Int?): List? { + val cmd = CC.ApiForwardChatItems(toChatType, toChatId, fromChatType, fromChatId, itemIds, ttl) + return processSendMessageCmd(rh, cmd)?.map { it.chatItem } } @@ -1134,9 +1134,30 @@ object ChatController { suspend fun apiSetConnectionIncognito(rh: Long?, connId: Long, incognito: Boolean): PendingContactConnection? { val r = sendCmd(rh, CC.ApiSetConnectionIncognito(connId, incognito)) - if (r is CR.ConnectionIncognitoUpdated) return r.toConnection - Log.e(TAG, "apiSetConnectionIncognito bad response: ${r.responseType} ${r.details}") - return null + + return when (r) { + is CR.ConnectionIncognitoUpdated -> r.toConnection + else -> { + if (!(networkErrorAlert(r))) { + apiErrorAlert("apiSetConnectionIncognito", generalGetString(MR.strings.error_sending_message), r) + } + null + } + } + } + + suspend fun apiChangeConnectionUser(rh: Long?, connId: Long, userId: Long): PendingContactConnection? { + val r = sendCmd(rh, CC.ApiChangeConnectionUser(connId, userId)) + + return when (r) { + is CR.ConnectionUserChanged -> r.toConnection + else -> { + if (!(networkErrorAlert(r))) { + apiErrorAlert("apiChangeConnectionUser", generalGetString(MR.strings.error_sending_message), r) + } + null + } + } } suspend fun apiConnectPlan(rh: Long?, connReq: String): ConnectionPlan? { @@ -1461,6 +1482,13 @@ object ChatController { return false } + suspend fun apiChatItemsRead(rh: Long?, type: ChatType, id: Long, itemIds: List): Boolean { + val r = sendCmd(rh, CC.ApiChatItemsRead(type, id, itemIds)) + if (r is CR.CmdOk) return true + Log.e(TAG, "apiChatItemsRead bad response: ${r.responseType} ${r.details}") + return false + } + suspend fun apiChatUnread(rh: Long?, type: ChatType, id: Long, unreadChat: Boolean): Boolean { val r = sendCmd(rh, CC.ApiChatUnread(type, id, unreadChat)) if (r is CR.CmdOk) return true @@ -2134,27 +2162,30 @@ object ChatController { chatModel.networkStatuses[s.agentConnId] = s.networkStatus } } - is CR.NewChatItem -> withBGApi { - val cInfo = r.chatItem.chatInfo - val cItem = r.chatItem.chatItem - if (active(r.user)) { - withChats { - addChatItem(rhId, cInfo, cItem) + is CR.NewChatItems -> withBGApi { + r.chatItems.forEach { chatItem -> + val cInfo = chatItem.chatInfo + val cItem = chatItem.chatItem + if (active(r.user)) { + withChats { + addChatItem(rhId, cInfo, cItem) + } + } else if (cItem.isRcvNew && cInfo.ntfsEnabled) { + chatModel.increaseUnreadCounter(rhId, r.user) } - } else if (cItem.isRcvNew && cInfo.ntfsEnabled) { - chatModel.increaseUnreadCounter(rhId, r.user) - } - val file = cItem.file - val mc = cItem.content.msgContent - if (file != null && + val file = cItem.file + val mc = cItem.content.msgContent + if (file != null && appPrefs.privacyAcceptImages.get() && ((mc is MsgContent.MCImage && file.fileSize <= MAX_IMAGE_SIZE_AUTO_RCV) || (mc is MsgContent.MCVideo && file.fileSize <= MAX_VIDEO_SIZE_AUTO_RCV) - || (mc is MsgContent.MCVoice && file.fileSize <= MAX_VOICE_SIZE_AUTO_RCV && file.fileStatus !is CIFileStatus.RcvAccepted))) { - 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) + || (mc is MsgContent.MCVoice && file.fileSize <= MAX_VOICE_SIZE_AUTO_RCV && file.fileStatus !is CIFileStatus.RcvAccepted)) + ) { + 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) + } } } is CR.ChatItemStatusUpdated -> { @@ -2865,13 +2896,13 @@ sealed class CC { class ApiGetChats(val userId: Long): CC() class ApiGetChat(val type: ChatType, val id: Long, val pagination: ChatPagination, val search: String = ""): CC() class ApiGetChatItemInfo(val type: ChatType, val id: Long, val itemId: Long): CC() - class ApiSendMessage(val type: ChatType, val id: Long, val file: CryptoFile?, val quotedItemId: Long?, val mc: MsgContent, val live: Boolean, val ttl: Int?): CC() - class ApiCreateChatItem(val noteFolderId: Long, val file: CryptoFile?, val mc: MsgContent): CC() + class ApiSendMessages(val type: ChatType, val id: Long, val live: Boolean, val ttl: Int?, val composedMessages: List): CC() + class ApiCreateChatItems(val noteFolderId: Long, val composedMessages: List): CC() class ApiUpdateChatItem(val type: ChatType, val id: Long, val itemId: Long, val mc: MsgContent, val live: Boolean): 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 ApiForwardChatItem(val toChatType: ChatType, val toChatId: Long, val fromChatType: ChatType, val fromChatId: Long, val itemId: Long, val ttl: Int?): 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() class ApiAddMember(val groupId: Long, val contactId: Long, val memberRole: GroupMemberRole): CC() class ApiJoinGroup(val groupId: Long): CC() @@ -2915,6 +2946,7 @@ sealed class CC { class APIVerifyGroupMember(val groupId: Long, val groupMemberId: Long, val connectionCode: String?): CC() class APIAddContact(val userId: Long, val incognito: Boolean): CC() class ApiSetConnectionIncognito(val connId: Long, val incognito: Boolean): CC() + class ApiChangeConnectionUser(val connId: Long, val userId: Long): CC() class APIConnectPlan(val userId: Long, val connReq: String): CC() class APIConnect(val userId: Long, val incognito: Boolean, val connReq: String): CC() class ApiConnectContactViaAddress(val userId: Long, val incognito: Boolean, val contactId: Long): CC() @@ -2944,6 +2976,7 @@ sealed class 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 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() class CancelFile(val fileId: Long): CC() @@ -3010,20 +3043,22 @@ sealed class CC { is ApiGetChats -> "/_get chats $userId pcc=on" is ApiGetChat -> "/_get chat ${chatRef(type, id)} ${pagination.cmdString}" + (if (search == "") "" else " search=$search") is ApiGetChatItemInfo -> "/_get item info ${chatRef(type, id)} $itemId" - is ApiSendMessage -> { + is ApiSendMessages -> { + val msgs = json.encodeToString(composedMessages) val ttlStr = if (ttl != null) "$ttl" else "default" - "/_send ${chatRef(type, id)} live=${onOff(live)} ttl=${ttlStr} json ${json.encodeToString(ComposedMessage(file, quotedItemId, mc))}" + "/_send ${chatRef(type, id)} live=${onOff(live)} ttl=${ttlStr} json $msgs" } - is ApiCreateChatItem -> { - "/_create *$noteFolderId json ${json.encodeToString(ComposedMessage(file, null, mc))}" + is ApiCreateChatItems -> { + val msgs = json.encodeToString(composedMessages) + "/_create *$noteFolderId json $msgs" } is ApiUpdateChatItem -> "/_update item ${chatRef(type, id)} $itemId live=${onOff(live)} ${mc.cmdString}" 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 ApiForwardChatItem -> { + is ApiForwardChatItems -> { val ttlStr = if (ttl != null) "$ttl" else "default" - "/_forward ${chatRef(toChatType, toChatId)} ${chatRef(fromChatType, fromChatId)} $itemId ttl=${ttlStr}" + "/_forward ${chatRef(toChatType, toChatId)} ${chatRef(fromChatType, fromChatId)} ${itemIds.joinToString(",")} ttl=${ttlStr}" } is ApiNewGroup -> "/_group $userId incognito=${onOff(incognito)} ${json.encodeToString(groupProfile)}" is ApiAddMember -> "/_add #$groupId $contactId ${memberRole.memberRole}" @@ -3068,6 +3103,7 @@ sealed class CC { is APIVerifyGroupMember -> "/_verify code #$groupId $groupMemberId" + if (connectionCode != null) " $connectionCode" else "" is APIAddContact -> "/_connect $userId incognito=${onOff(incognito)}" is ApiSetConnectionIncognito -> "/_set incognito :$connId ${onOff(incognito)}" + is ApiChangeConnectionUser -> "/_set conn user :$connId $userId" is APIConnectPlan -> "/_connect plan $userId $connReq" is APIConnect -> "/_connect $userId incognito=${onOff(incognito)} $connReq" is ApiConnectContactViaAddress -> "/_connect contact $userId incognito=${onOff(incognito)} $contactId" @@ -3097,6 +3133,7 @@ sealed class CC { 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 ApiChatItemsRead -> "/_read chat items ${chatRef(type, id)} ${itemIds.joinToString(",")}" is ApiChatUnread -> "/_unread chat ${chatRef(type, id)} ${onOff(unreadChat)}" is ReceiveFile -> "/freceive $fileId" + @@ -3160,13 +3197,13 @@ sealed class CC { is ApiGetChats -> "apiGetChats" is ApiGetChat -> "apiGetChat" is ApiGetChatItemInfo -> "apiGetChatItemInfo" - is ApiSendMessage -> "apiSendMessage" - is ApiCreateChatItem -> "apiCreateChatItem" + is ApiSendMessages -> "apiSendMessages" + is ApiCreateChatItems -> "apiCreateChatItems" is ApiUpdateChatItem -> "apiUpdateChatItem" is ApiDeleteChatItem -> "apiDeleteChatItem" is ApiDeleteMemberChatItem -> "apiDeleteMemberChatItem" is ApiChatItemReaction -> "apiChatItemReaction" - is ApiForwardChatItem -> "apiForwardChatItem" + is ApiForwardChatItems -> "apiForwardChatItems" is ApiNewGroup -> "apiNewGroup" is ApiAddMember -> "apiAddMember" is ApiJoinGroup -> "apiJoinGroup" @@ -3210,6 +3247,7 @@ sealed class CC { is APIVerifyGroupMember -> "apiVerifyGroupMember" is APIAddContact -> "apiAddContact" is ApiSetConnectionIncognito -> "apiSetConnectionIncognito" + is ApiChangeConnectionUser -> "apiChangeConnectionUser" is APIConnectPlan -> "apiConnectPlan" is APIConnect -> "apiConnect" is ApiConnectContactViaAddress -> "apiConnectContactViaAddress" @@ -3239,6 +3277,7 @@ sealed class CC { is ApiCallStatus -> "apiCallStatus" is ApiGetNetworkStatuses -> "apiGetNetworkStatuses" is ApiChatRead -> "apiChatRead" + is ApiChatItemsRead -> "apiChatItemsRead" is ApiChatUnread -> "apiChatUnread" is ReceiveFile -> "receiveFile" is CancelFile -> "cancelFile" @@ -4754,6 +4793,7 @@ sealed class CR { @Serializable @SerialName("connectionVerified") class ConnectionVerified(val user: UserRef, val verified: Boolean, val expectedCode: String): CR() @Serializable @SerialName("invitation") class Invitation(val user: UserRef, val connReqInvitation: String, val connection: PendingContactConnection): CR() @Serializable @SerialName("connectionIncognitoUpdated") class ConnectionIncognitoUpdated(val user: UserRef, val toConnection: PendingContactConnection): CR() + @Serializable @SerialName("connectionUserChanged") class ConnectionUserChanged(val user: UserRef, val fromConnection: PendingContactConnection, val toConnection: PendingContactConnection, val newUser: UserRef): CR() @Serializable @SerialName("connectionPlan") class CRConnectionPlan(val user: UserRef, val connectionPlan: ConnectionPlan): CR() @Serializable @SerialName("sentConfirmation") class SentConfirmation(val user: UserRef, val connection: PendingContactConnection): CR() @Serializable @SerialName("sentInvitation") class SentInvitation(val user: UserRef, val connection: PendingContactConnection): CR() @@ -4792,7 +4832,7 @@ sealed class CR { @Serializable @SerialName("memberSubErrors") class MemberSubErrors(val user: UserRef, val memberSubErrors: List): CR() @Serializable @SerialName("groupEmpty") class GroupEmpty(val user: UserRef, val group: GroupInfo): CR() @Serializable @SerialName("userContactLinkSubscribed") class UserContactLinkSubscribed: CR() - @Serializable @SerialName("newChatItem") class NewChatItem(val user: UserRef, val chatItem: AChatItem): CR() + @Serializable @SerialName("newChatItems") class NewChatItems(val user: UserRef, val chatItems: List): CR() @Serializable @SerialName("chatItemStatusUpdated") class ChatItemStatusUpdated(val user: UserRef, val chatItem: AChatItem): 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() @@ -4932,6 +4972,7 @@ sealed class CR { is ConnectionVerified -> "connectionVerified" is Invitation -> "invitation" is ConnectionIncognitoUpdated -> "connectionIncognitoUpdated" + is ConnectionUserChanged -> "ConnectionUserChanged" is CRConnectionPlan -> "connectionPlan" is SentConfirmation -> "sentConfirmation" is SentInvitation -> "sentInvitation" @@ -4968,7 +5009,7 @@ sealed class CR { is MemberSubErrors -> "memberSubErrors" is GroupEmpty -> "groupEmpty" is UserContactLinkSubscribed -> "userContactLinkSubscribed" - is NewChatItem -> "newChatItem" + is NewChatItems -> "newChatItems" is ChatItemStatusUpdated -> "chatItemStatusUpdated" is ChatItemUpdated -> "chatItemUpdated" is ChatItemNotChanged -> "chatItemNotChanged" @@ -5100,6 +5141,7 @@ sealed class CR { is ConnectionVerified -> withUser(user, "verified: $verified\nconnectionCode: $expectedCode") is Invitation -> withUser(user, "connReqInvitation: $connReqInvitation\nconnection: $connection") is ConnectionIncognitoUpdated -> withUser(user, json.encodeToString(toConnection)) + is ConnectionUserChanged -> withUser(user, "fromConnection: ${json.encodeToString(fromConnection)}\ntoConnection: ${json.encodeToString(toConnection)}\nnewUser: ${json.encodeToString(newUser)}" ) is CRConnectionPlan -> withUser(user, json.encodeToString(connectionPlan)) is SentConfirmation -> withUser(user, json.encodeToString(connection)) is SentInvitation -> withUser(user, json.encodeToString(connection)) @@ -5136,7 +5178,7 @@ sealed class CR { is MemberSubErrors -> withUser(user, json.encodeToString(memberSubErrors)) is GroupEmpty -> withUser(user, json.encodeToString(group)) is UserContactLinkSubscribed -> noDetails() - is NewChatItem -> withUser(user, json.encodeToString(chatItem)) + is NewChatItems -> withUser(user, chatItems.joinToString("\n") { json.encodeToString(it) }) is ChatItemStatusUpdated -> withUser(user, json.encodeToString(chatItem)) is ChatItemUpdated -> withUser(user, json.encodeToString(chatItem)) is ChatItemNotChanged -> withUser(user, json.encodeToString(chatItem)) @@ -5551,6 +5593,7 @@ sealed class ChatErrorType { is AgentCommandError -> "agentCommandError" is InvalidFileDescription -> "invalidFileDescription" is ConnectionIncognitoChangeProhibited -> "connectionIncognitoChangeProhibited" + is ConnectionUserChangeProhibited -> "connectionUserChangeProhibited" is PeerChatVRangeIncompatible -> "peerChatVRangeIncompatible" is InternalError -> "internalError" is CEException -> "exception $message" @@ -5628,6 +5671,7 @@ sealed class ChatErrorType { @Serializable @SerialName("agentCommandError") class AgentCommandError(val message: String): ChatErrorType() @Serializable @SerialName("invalidFileDescription") class InvalidFileDescription(val message: String): ChatErrorType() @Serializable @SerialName("connectionIncognitoChangeProhibited") object ConnectionIncognitoChangeProhibited: ChatErrorType() + @Serializable @SerialName("connectionUserChangeProhibited") object ConnectionUserChangeProhibited: ChatErrorType() @Serializable @SerialName("peerChatVRangeIncompatible") object PeerChatVRangeIncompatible: ChatErrorType() @Serializable @SerialName("internalError") class InternalError(val message: String): ChatErrorType() @Serializable @SerialName("exception") class CEException(val message: String): ChatErrorType() 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 44fcddb54c..5dfa5aa200 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 @@ -1,7 +1,6 @@ package chat.simplex.common.platform -import androidx.compose.animation.core.Animatable -import androidx.compose.animation.core.AnimationVector1D +import androidx.compose.animation.core.* import androidx.compose.foundation.ScrollState import androidx.compose.foundation.lazy.LazyListState import androidx.compose.runtime.* @@ -22,6 +21,7 @@ interface PlatformInterface { 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 androidStartCallActivity(acceptCall: Boolean, remoteHostId: Long? = null, chatId: ChatId? = null) {} fun androidPictureInPictureAllowed(): Boolean = true fun androidCallEnded() {} 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 5cb97d7d80..e44a174b53 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 @@ -109,7 +109,6 @@ fun TerminalLayout( } }, contentColor = LocalContentColor.current, - drawerContentColor = LocalContentColor.current, modifier = Modifier.navigationBarsWithImePadding() ) { contentPadding -> Surface( 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 690ba89ef9..511230cc83 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 @@ -660,7 +660,6 @@ fun ChatLayout( modifier = Modifier.navigationBarsWithImePadding(), floatingActionButton = { floatingButton.value() }, contentColor = LocalContentColor.current, - drawerContentColor = LocalContentColor.current, backgroundColor = Color.Unspecified ) { contentPadding -> val wallpaperImage = MaterialTheme.wallpaper.type.image 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 372de02b41..821a449509 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 @@ -380,24 +380,28 @@ fun ComposeView( suspend fun send(chat: Chat, mc: MsgContent, quoted: Long?, file: CryptoFile? = null, live: Boolean = false, ttl: Int?): ChatItem? { val cInfo = chat.chatInfo - val aChatItem = if (chat.chatInfo.chatType == ChatType.Local) - chatModel.controller.apiCreateChatItem(rh = chat.remoteHostId, noteFolderId = chat.chatInfo.apiId, file = file, mc = mc) + val chatItems = if (chat.chatInfo.chatType == ChatType.Local) + chatModel.controller.apiCreateChatItems( + rh = chat.remoteHostId, + noteFolderId = chat.chatInfo.apiId, + composedMessages = listOf(ComposedMessage(file, null, mc)) + ) else - chatModel.controller.apiSendMessage( - rh = chat.remoteHostId, - type = cInfo.chatType, - id = cInfo.apiId, - file = file, - quotedItemId = quoted, - mc = mc, - live = live, - ttl = ttl - ) - if (aChatItem != null) { - withChats { - addChatItem(chat.remoteHostId, cInfo, aChatItem.chatItem) + chatModel.controller.apiSendMessages( + rh = chat.remoteHostId, + type = cInfo.chatType, + id = cInfo.apiId, + live = live, + ttl = ttl, + composedMessages = listOf(ComposedMessage(file, quoted, mc)) + ) + if (!chatItems.isNullOrEmpty()) { + chatItems.forEach { aChatItem -> + withChats { + addChatItem(chat.remoteHostId, cInfo, aChatItem.chatItem) + } } - return aChatItem.chatItem + return chatItems.first().chatItem } if (file != null) removeFile(file.filePath) return null @@ -414,21 +418,22 @@ fun ComposeView( } suspend fun forwardItem(rhId: Long?, forwardedItem: ChatItem, fromChatInfo: ChatInfo, ttl: Int?): ChatItem? { - val chatItem = controller.apiForwardChatItem( + val chatItems = controller.apiForwardChatItems( rh = rhId, toChatType = chat.chatInfo.chatType, toChatId = chat.chatInfo.apiId, fromChatType = fromChatInfo.chatType, fromChatId = fromChatInfo.apiId, - itemId = forwardedItem.id, + itemIds = listOf(forwardedItem.id), ttl = ttl ) - if (chatItem != null) { + chatItems?.forEach { chatItem -> withChats { addChatItem(rhId, chat.chatInfo, chatItem) } } - return chatItem + // TODO batch send: forward multiple messages + return chatItems?.firstOrNull() } fun checkLinkPreview(): MsgContent { @@ -519,6 +524,7 @@ fun ComposeView( ComposePreview.NoPreview -> msgs.add(MsgContent.MCText(msgText)) is ComposePreview.CLinkPreview -> msgs.add(checkLinkPreview()) is ComposePreview.MediaPreview -> { + // TODO batch send: batch media previews preview.content.forEachIndexed { index, it -> val file = when (it) { is UploadContent.SimpleImage -> 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 11b1006f41..54c67674ad 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 @@ -23,7 +23,7 @@ import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.* -import chat.simplex.common.SettingsViewState +import chat.simplex.common.AppLock import chat.simplex.common.model.* import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatController.stopRemoteHostAndReloadHosts @@ -135,7 +135,7 @@ fun ToggleChatListCard() { } @Composable -fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerformLA: (Boolean) -> Unit, stopped: Boolean) { +fun ChatListView(chatModel: ChatModel, userPickerState: MutableStateFlow, setPerformLA: (Boolean) -> Unit, stopped: Boolean) { val oneHandUI = remember { appPrefs.oneHandUI.state } LaunchedEffect(Unit) { if (shouldShowWhatsNew(chatModel)) { @@ -153,18 +153,15 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf VideoPlayerHolder.stopAll() } } - val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) } - val scope = rememberCoroutineScope() - val (userPickerState, scaffoldState ) = settingsState Scaffold( topBar = { if (!oneHandUI.value) { - Column(Modifier.padding(end = endPadding)) { + Column { ChatListToolbar( - scaffoldState.drawerState, userPickerState, stopped, + setPerformLA, ) Divider() } @@ -172,33 +169,17 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf }, bottomBar = { if (oneHandUI.value) { - Column(Modifier.padding(end = endPadding)) { + Column { Divider() ChatListToolbar( - scaffoldState.drawerState, userPickerState, stopped, - ) - } - } - }, - scaffoldState = scaffoldState, - drawerContent = { - tryOrShowError("Settings", error = { ErrorSettingsView() }) { - val handler = remember { AppBarHandler() } - CompositionLocalProvider( - LocalAppBarHandler provides handler - ) { - ModalView(showClose = appPlatform.isDesktop, close = { scope.launch { scaffoldState.drawerState.close() } }) { - SettingsView(chatModel, setPerformLA, scaffoldState.drawerState) - } + setPerformLA, + ) } } }, contentColor = LocalContentColor.current, - drawerContentColor = LocalContentColor.current, - drawerScrimColor = MaterialTheme.colors.onSurface.copy(alpha = if (isInDarkTheme()) 0.16f else 0.32f), - drawerGesturesEnabled = appPlatform.isAndroid, floatingActionButton = { if (!oneHandUI.value && searchText.value.text.isEmpty() && !chatModel.desktopNoUserNoRemote && chatModel.chatRunning.value == true) { FloatingActionButton( @@ -208,7 +189,7 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf } }, Modifier - .padding(end = DEFAULT_PADDING - 16.dp + endPadding, bottom = DEFAULT_PADDING - 16.dp) + .padding(end = DEFAULT_PADDING - 16.dp, bottom = DEFAULT_PADDING - 16.dp) .size(AppBarHeight * fontSizeSqrtMultiplier), elevation = FloatingActionButtonDefaults.elevation( defaultElevation = 0.dp, @@ -224,7 +205,7 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf } } ) { - Box(Modifier.padding(it).padding(end = endPadding)) { + Box(Modifier.padding(it)) { Box( modifier = Modifier .fillMaxSize() @@ -252,11 +233,8 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf UserPicker( chatModel = chatModel, userPickerState = userPickerState, - contentAlignment = if (oneHandUI.value) Alignment.BottomStart else Alignment.TopStart - ) { - scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() } - userPickerState.value = AnimatedViewState.GONE - } + setPerformLA = AppLock::setPerformLA + ) } } } @@ -278,7 +256,7 @@ private fun ConnectButton(text: String, onClick: () -> Unit) { } @Composable -private fun ChatListToolbar(drawerState: DrawerState, userPickerState: MutableStateFlow, stopped: Boolean) { +private fun ChatListToolbar(userPickerState: MutableStateFlow, stopped: Boolean, setPerformLA: (Boolean) -> Unit) { val serversSummary: MutableState = remember { mutableStateOf(null) } val barButtons = arrayListOf<@Composable RowScope.() -> Unit>() val updatingProgress = remember { chatModel.updatingProgress }.value @@ -344,23 +322,22 @@ private fun ChatListToolbar(drawerState: DrawerState, userPickerState: MutableSt } } } - val scope = rememberCoroutineScope() val clipboard = LocalClipboardManager.current DefaultTopAppBar( navigationButton = { if (chatModel.users.isEmpty() && !chatModel.desktopNoUserNoRemote) { - NavigationButtonMenu { scope.launch { if (drawerState.isOpen) drawerState.close() else drawerState.open() } } + NavigationButtonMenu { + ModalManager.start.showModalCloseable { close -> + SettingsView(chatModel, setPerformLA, close) + } + } } else { val users by remember { derivedStateOf { chatModel.users.filter { u -> u.user.activeUser || !u.user.hidden } } } val allRead = users .filter { u -> !u.user.activeUser && !u.user.hidden } .all { u -> u.unreadCount == 0 } UserProfileButton(chatModel.currentUser.value?.profile?.image, allRead) { - if (users.size == 1 && chatModel.remoteHosts.isEmpty()) { - scope.launch { drawerState.open() } - } else { userPickerState.value = AnimatedViewState.VISIBLE - } } } }, 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 886b82de7d..b4d0b05584 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,31 +11,24 @@ 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.SettingsViewState 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.views.newchat.ActiveProfilePicker import chat.simplex.res.MR -import kotlinx.coroutines.flow.MutableStateFlow @Composable -fun ShareListView(chatModel: ChatModel, settingsState: SettingsViewState, stopped: Boolean) { +fun ShareListView(chatModel: ChatModel, stopped: Boolean) { var searchInList by rememberSaveable { mutableStateOf("") } - val (userPickerState, scaffoldState) = settingsState - val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp val oneHandUI = remember { appPrefs.oneHandUI.state } Scaffold( - Modifier.padding(end = endPadding), contentColor = LocalContentColor.current, - drawerContentColor = LocalContentColor.current, - scaffoldState = scaffoldState, topBar = { if (!oneHandUI.value) { Column { - ShareListToolbar(chatModel, userPickerState, stopped) { searchInList = it.trim() } + ShareListToolbar(chatModel, stopped) { searchInList = it.trim() } Divider() } } @@ -44,7 +37,7 @@ fun ShareListView(chatModel: ChatModel, settingsState: SettingsViewState, stoppe if (oneHandUI.value) { Column { Divider() - ShareListToolbar(chatModel, userPickerState, stopped) { searchInList = it.trim() } + ShareListToolbar(chatModel, stopped) { searchInList = it.trim() } } } } @@ -92,21 +85,6 @@ fun ShareListView(chatModel: ChatModel, settingsState: SettingsViewState, stoppe } } } - if (appPlatform.isAndroid) { - tryOrShowError("UserPicker", error = {}) { - UserPicker( - chatModel, - userPickerState, - showSettings = false, - showCancel = true, - contentAlignment = if (oneHandUI.value) Alignment.BottomStart else Alignment.TopStart, - cancelClicked = { - chatModel.sharedContent.value = null - userPickerState.value = AnimatedViewState.GONE - } - ) - } - } } private fun hasSimplexLink(msg: String): Boolean { @@ -122,7 +100,7 @@ private fun EmptyList() { } @Composable -private fun ShareListToolbar(chatModel: ChatModel, userPickerState: MutableStateFlow, stopped: Boolean, onSearchValueChanged: (String) -> Unit) { +private fun ShareListToolbar(chatModel: ChatModel, stopped: Boolean, onSearchValueChanged: (String) -> Unit) { var showSearch by rememberSaveable { mutableStateOf(false) } val hideSearchOnBack = { onSearchValueChanged(""); showSearch = false } if (showSearch) { @@ -138,7 +116,24 @@ private fun ShareListToolbar(chatModel: ChatModel, userPickerState: MutableState .filter { u -> !u.user.activeUser && !u.user.hidden } .all { u -> u.unreadCount == 0 } UserProfileButton(chatModel.currentUser.value?.profile?.image, allRead) { - userPickerState.value = AnimatedViewState.VISIBLE + ModalManager.start.showCustomModal { close -> + val search = rememberSaveable { mutableStateOf("") } + ModalView( + { close() }, + endButtons = { + SearchTextField(Modifier.fillMaxWidth(), placeholder = stringResource(MR.strings.search_verb), alwaysVisible = true) { search.value = it } + }, + content = { + ActiveProfilePicker( + search = search, + rhId = chatModel.remoteHostId, + close = close, + contactConnection = null, + showIncognito = false + ) + } + ) + } } } else -> NavigationButtonBack(onButtonClicked = { 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 e15bc3863e..41fe127093 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 @@ -1,53 +1,51 @@ package chat.simplex.common.views.chatlist import SectionItemView -import androidx.compose.animation.core.* +import SectionView +import TextIconSpaced import androidx.compose.foundation.* import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsHoveredAsState import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.* import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.* import androidx.compose.ui.draw.* -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.graphics.* +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.text.TextStyle import dev.icerock.moko.resources.compose.painterResource import androidx.compose.ui.text.capitalize import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.intl.Locale +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.stopRemoteHostAndReloadHosts import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.common.platform.* import chat.simplex.common.views.CreateProfile +import chat.simplex.common.views.localauth.VerticalDivider +import chat.simplex.common.views.newchat.* import chat.simplex.common.views.remote.* -import chat.simplex.common.views.usersettings.doWithAuth +import chat.simplex.common.views.usersettings.* +import chat.simplex.common.views.usersettings.AppearanceScope.ColorModeSwitcher import chat.simplex.res.MR import dev.icerock.moko.resources.compose.stringResource import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import kotlin.math.roundToInt @Composable fun UserPicker( chatModel: ChatModel, userPickerState: MutableStateFlow, - showSettings: Boolean = true, - contentAlignment: Alignment = Alignment.TopStart, - showCancel: Boolean = false, - cancelClicked: () -> Unit = {}, - useFromDesktopClicked: () -> Unit = {}, - settingsClicked: () -> Unit = {}, + setPerformLA: (Boolean) -> Unit, ) { - val scope = rememberCoroutineScope() var newChat by remember { mutableStateOf(userPickerState.value) } if (newChat.isVisible()) { BackHandler { @@ -67,18 +65,32 @@ fun UserPicker( .sortedBy { it.hostDeviceName } } } - val animatedFloat = remember { Animatable(if (newChat.isVisible()) 0f else 1f) } + + val view = LocalMultiplatformView() LaunchedEffect(Unit) { launch { userPickerState.collect { newChat = it + if (it.isVisible()) { + hideKeyboard(view) + } launch { - animatedFloat.animateTo(if (newChat.isVisible()) 1f else 0f, newChatSheetAnimSpec()) if (newChat.isHiding()) userPickerState.value = AnimatedViewState.GONE } } } } + + LaunchedEffect(Unit) { + launch { + snapshotFlow { ModalManager.start.modalCount.value } + .filter { it > 0 } + .collect { + closePicker(userPickerState) + } + } + } + LaunchedEffect(Unit) { snapshotFlow { newChat.isVisible() } .distinctUntilChanged() @@ -124,110 +136,143 @@ fun UserPicker( } } } - val UsersView: @Composable ColumnScope.() -> Unit = { - users.forEach { u -> - UserProfilePickerItem(u.user, u.unreadCount, openSettings = settingsClicked) { - userPickerState.value = AnimatedViewState.HIDING - if (!u.user.activeUser) { - withBGApi { - controller.showProgressIfNeeded { - ModalManager.closeAllModalsEverywhere() - chatModel.controller.changeActiveUser(u.user.remoteHostId, u.user.userId, null) - } - } - } - } - Divider(Modifier.requiredHeight(1.dp)) - if (u.user.activeUser) Divider(Modifier.requiredHeight(0.5.dp)) - } - } - val xOffset = with(LocalDensity.current) { 10.dp.roundToPx() } - val maxWidth = with(LocalDensity.current) { windowWidth() * density } - Box(Modifier - .fillMaxSize() - .offset { IntOffset(if (newChat.isGone()) -maxWidth.value.roundToInt() else xOffset, 0) } - .clickable(interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = { userPickerState.value = AnimatedViewState.HIDING }) - .padding(bottom = 10.dp, top = 10.dp) - .graphicsLayer { - alpha = animatedFloat.value - translationY = (if (appPrefs.oneHandUI.state.value) -1 else 1) * (animatedFloat.value - 1) * xOffset - }, - contentAlignment = contentAlignment + + PlatformUserPicker( + modifier = Modifier + .height(IntrinsicSize.Min) + .fillMaxWidth() + .then(if (newChat.isVisible()) Modifier.shadow(8.dp, clip = true) else Modifier) + .background(MaterialTheme.colors.surface) + .padding(vertical = DEFAULT_PADDING), + pickerState = userPickerState ) { - Column( - Modifier - .widthIn(min = 260.dp) - .width(IntrinsicSize.Min) - .height(IntrinsicSize.Min) - .shadow(8.dp, RoundedCornerShape(corner = CornerSize(25.dp)), clip = true) - .background(MaterialTheme.colors.surface, RoundedCornerShape(corner = CornerSize(25.dp))) - .clip(RoundedCornerShape(corner = CornerSize(25.dp))) - ) { - val currentRemoteHost = remember { chatModel.currentRemoteHost }.value - Column(Modifier.weight(1f).verticalScroll(rememberScrollState())) { - if (remoteHosts.isNotEmpty()) { - if (currentRemoteHost == null && chatModel.localUserCreated.value == true) { - LocalDevicePickerItem(true) { - userPickerState.value = AnimatedViewState.HIDING - switchToLocalDevice() - } - Divider(Modifier.requiredHeight(1.dp)) - } else if (currentRemoteHost != null) { - val connecting = rememberSaveable { mutableStateOf(false) } - RemoteHostPickerItem(currentRemoteHost, - actionButtonClick = { - userPickerState.value = AnimatedViewState.HIDING - stopRemoteHostAndReloadHosts(currentRemoteHost, true) - }) { - userPickerState.value = AnimatedViewState.HIDING - switchToRemoteHost(currentRemoteHost, connecting) - } - Divider(Modifier.requiredHeight(1.dp)) - } - } + @Composable + fun FirstSection() { + if (remoteHosts.isNotEmpty()) { + val currentRemoteHost = remember { chatModel.currentRemoteHost }.value + val localDeviceActive = currentRemoteHost == null && chatModel.localUserCreated.value == true - UsersView() - - if (remoteHosts.isNotEmpty() && currentRemoteHost != null && chatModel.localUserCreated.value == true) { - LocalDevicePickerItem(false) { + DevicePickerRow( + localDeviceActive = localDeviceActive, + remoteHosts = remoteHosts, + onRemoteHostClick = { h, connecting -> + userPickerState.value = AnimatedViewState.HIDING + switchToRemoteHost(h, connecting) + }, + onLocalDeviceClick = { userPickerState.value = AnimatedViewState.HIDING switchToLocalDevice() + }, + onRemoteHostActionButtonClick = { h -> + userPickerState.value = AnimatedViewState.HIDING + stopRemoteHostAndReloadHosts(h, true) + } + ) + } + ActiveUserSection( + chatModel = chatModel, + userPickerState = userPickerState, + ) + } + + @Composable + fun SecondSection() { + GlobalSettingsSection( + chatModel = chatModel, + userPickerState = userPickerState, + setPerformLA = setPerformLA, + onUserClicked = { user -> + userPickerState.value = AnimatedViewState.HIDING + if (!user.activeUser) { + withBGApi { + controller.showProgressIfNeeded { + ModalManager.closeAllModalsEverywhere() + chatModel.controller.changeActiveUser(user.remoteHostId, user.userId, null) + } + } + } + }, + onShowAllProfilesClicked = { + doWithAuth( + generalGetString(MR.strings.auth_open_chat_profiles), + generalGetString(MR.strings.auth_log_in_using_credential) + ) { + ModalManager.start.showCustomModal { 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 } + }, + content = { UserProfilesView(chatModel, search, profileHidden) }) + } } - Divider(Modifier.requiredHeight(1.dp)) } - remoteHosts.filter { !it.activeHost }.forEach { h -> - val connecting = rememberSaveable { mutableStateOf(false) } - RemoteHostPickerItem(h, - actionButtonClick = { - userPickerState.value = AnimatedViewState.HIDING - stopRemoteHostAndReloadHosts(h, false) - }) { - userPickerState.value = AnimatedViewState.HIDING - switchToRemoteHost(h, connecting) - } - Divider(Modifier.requiredHeight(1.dp)) + ) + } + + if (appPlatform.isDesktop || windowOrientation() == WindowOrientation.PORTRAIT) { + Column { + FirstSection() + Divider(Modifier.padding(DEFAULT_PADDING)) + SecondSection() + } + } else { + Row { + Box(Modifier.weight(1f)) { + FirstSection() + } + VerticalDivider() + Box(Modifier.weight(1f)) { + SecondSection() } } - if (appPlatform.isAndroid) { - UseFromDesktopPickerItem { - ModalManager.start.showCustomModal { close -> - ConnectDesktopView(close) - } - userPickerState.value = AnimatedViewState.GONE - } - Divider(Modifier.requiredHeight(1.dp)) - } else { - if (remoteHosts.isEmpty()) { - LinkAMobilePickerItem { - ModalManager.start.showModal { - ConnectMobileView() - } - userPickerState.value = AnimatedViewState.GONE - } - Divider(Modifier.requiredHeight(1.dp)) - } - if (chatModel.desktopNoUserNoRemote) { - CreateInitialProfile { + } + } +} + +@Composable +private fun ActiveUserSection( + chatModel: ChatModel, + userPickerState: MutableStateFlow, +) { + val showCustomModal: (@Composable() (ModalData.(ChatModel, () -> Unit) -> Unit)) -> () -> Unit = { modalView -> + { + ModalManager.start.showCustomModal { close -> modalView(chatModel, close) } + } + } + val currentUser = remember { chatModel.currentUser }.value + val stopped = chatModel.chatRunning.value == false + + if (currentUser != null) { + SectionView { + SectionItemView(showCustomModal { chatModel, close -> UserProfileView(chatModel, close) }, 80.dp, padding = PaddingValues(start = 16.dp, end = DEFAULT_PADDING), disabled = stopped) { + ProfilePreview(currentUser.profile, stopped = stopped) + } + UserPickerOptionRow( + painterResource(MR.images.ic_qr_code), + if (chatModel.userAddress.value != null) generalGetString(MR.strings.your_simplex_contact_address) else generalGetString(MR.strings.create_simplex_address), + showCustomModal { it, close -> UserAddressView(it, shareViaProfile = it.currentUser.value!!.addressShared, close = close) }, disabled = stopped + ) + UserPickerOptionRow( + painterResource(MR.images.ic_toggle_on), + stringResource(MR.strings.chat_preferences), + click = if (stopped) null else ({ + showCustomModal { m, close -> + PreferencesView(m, m.currentUser.value ?: return@showCustomModal, close) + }() + }), + disabled = stopped + ) + } + } else { + SectionView { + if (chatModel.desktopNoUserNoRemote) { + UserPickerOptionRow( + painterResource(MR.images.ic_manage_accounts), + generalGetString(MR.strings.create_chat_profile), + { doWithAuth(generalGetString(MR.strings.auth_open_chat_profiles), generalGetString(MR.strings.auth_log_in_using_credential)) { ModalManager.center.showModalCloseable { close -> LaunchedEffect(Unit) { @@ -237,15 +282,76 @@ fun UserPicker( } } } - Divider(Modifier.requiredHeight(1.dp)) + ) + } + } + } +} + +@Composable +private fun GlobalSettingsSection( + chatModel: ChatModel, + userPickerState: MutableStateFlow, + setPerformLA: (Boolean) -> Unit, + onUserClicked: (user: User) -> Unit, + onShowAllProfilesClicked: () -> Unit +) { + val stopped = chatModel.chatRunning.value == false + val users by remember { + derivedStateOf { + chatModel.users + .filter { u -> !u.user.hidden && !u.user.activeUser } + } + } + + SectionView(headerBottomPadding = if (appPlatform.isDesktop || windowOrientation() == WindowOrientation.PORTRAIT) DEFAULT_PADDING else 0.dp) { + UserPickerInactiveUsersSection( + users = users, + onShowAllProfilesClicked = onShowAllProfilesClicked, + onUserClicked = onUserClicked, + stopped = stopped + ) + + if (appPlatform.isAndroid) { + val text = generalGetString(MR.strings.settings_section_title_use_from_desktop).lowercase().capitalize(Locale.current) + + UserPickerOptionRow( + painterResource(MR.images.ic_desktop), + text, + click = { + ModalManager.start.showCustomModal { close -> + ConnectDesktopView(close) + } } - } - if (showSettings) { - SettingsPickerItem(settingsClicked) - } - if (showCancel) { - CancelPickerItem(cancelClicked) - } + ) + } else { + UserPickerOptionRow( + icon = painterResource(MR.images.ic_smartphone_300), + text = stringResource(if (remember { chat.simplex.common.platform.chatModel.remoteHosts }.isEmpty()) MR.strings.link_a_mobile else MR.strings.linked_mobiles), + click = { + userPickerState.value = AnimatedViewState.HIDING + ModalManager.start.showModal { + ConnectMobileView() + } + }, + disabled = stopped + ) + } + + SectionItemView( + click = { + ModalManager.start.showModalCloseable { close -> + SettingsView(chatModel, setPerformLA, close) + } + }, + padding = PaddingValues(start = DEFAULT_PADDING * 1.7f, end = DEFAULT_PADDING + 2.dp) + ) { + val text = generalGetString(MR.strings.settings_section_title_settings).lowercase().capitalize(Locale.current) + Icon(painterResource(MR.images.ic_settings), text, tint = MaterialTheme.colors.secondary) + TextIconSpaced() + Text(text, color = Color.Unspecified) + Spacer(Modifier.weight(1f)) + ColorModeSwitcher() } } } @@ -296,7 +402,7 @@ fun UserProfilePickerItem( } } else if (!u.showNtfs) { Icon(painterResource(MR.images.ic_notifications_off), null, Modifier.size(20.dp), tint = MaterialTheme.colors.secondary) - } else { + } else { Box(Modifier.size(20.dp)) } } @@ -325,136 +431,157 @@ fun UserProfileRow(u: User, enabled: Boolean = chatModel.chatRunning.value == tr } @Composable -fun RemoteHostPickerItem(h: RemoteHostInfo, onLongClick: () -> Unit = {}, actionButtonClick: () -> Unit = {}, onClick: () -> Unit) { - Row( - Modifier - .fillMaxWidth() - .background(color = if (h.activeHost) MaterialTheme.colors.surface.mixWith(MaterialTheme.colors.onBackground, 0.95f) else Color.Unspecified) - .sizeIn(minHeight = DEFAULT_MIN_SECTION_ITEM_HEIGHT) - .combinedClickable( - onClick = onClick, - onLongClick = onLongClick - ) - .onRightClick { onLongClick() } - .padding(start = DEFAULT_PADDING_HALF, end = DEFAULT_PADDING), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - RemoteHostRow(h) - if (h.sessionState is RemoteHostSessionState.Connected) { - HostDisconnectButton(actionButtonClick) - } else { - Box(Modifier.size(20.dp)) +fun UserPickerOptionRow(icon: Painter, text: String, click: (() -> Unit)? = null, disabled: Boolean = false) { + SectionItemView(click, disabled = disabled, extraPadding = true) { + Icon(icon, text, tint = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.secondary) + TextIconSpaced() + Text(text = text, color = if (disabled) MaterialTheme.colors.secondary else Color.Unspecified) + } +} + +@Composable +fun UserPickerInactiveUserBadge(userInfo: UserInfo, stopped: Boolean, size: Dp = 60.dp, onClick: (user: User) -> Unit) { + Box { + IconButton( + onClick = { onClick(userInfo.user) }, + enabled = !stopped + ) { + Box { + ProfileImage(size = size, image = userInfo.user.profile.image, color = MaterialTheme.colors.secondaryVariant) + + if (userInfo.unreadCount > 0) { + unreadBadge(userInfo.unreadCount, userInfo.user.showNtfs) + } + } } } } @Composable -fun RemoteHostRow(h: RemoteHostInfo) { - Row( - Modifier - .widthIn(max = windowWidth() * 0.7f) - .padding(start = 17.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon(painterResource(MR.images.ic_smartphone_300), h.hostDeviceName, Modifier.size(20.dp * fontSizeSqrtMultiplier), tint = MaterialTheme.colors.onBackground) - Text( - h.hostDeviceName, - modifier = Modifier.padding(start = 26.dp, end = 8.dp), - color = if (h.activeHost) MaterialTheme.colors.onBackground else MenuTextColor, - fontSize = 14.sp, - ) - } -} - -@Composable -fun LocalDevicePickerItem(active: Boolean, onLongClick: () -> Unit = {}, onClick: () -> Unit) { +private fun DevicePickerRow( + localDeviceActive: Boolean, + remoteHosts: List, + onLocalDeviceClick: () -> Unit, + onRemoteHostClick: (rh: RemoteHostInfo, connecting: MutableState) -> Unit, + onRemoteHostActionButtonClick: (rh: RemoteHostInfo) -> Unit, +) { Row( Modifier .fillMaxWidth() - .background(color = if (active) MaterialTheme.colors.surface.mixWith(MaterialTheme.colors.onBackground, 0.95f) else Color.Unspecified) .sizeIn(minHeight = DEFAULT_MIN_SECTION_ITEM_HEIGHT) - .combinedClickable( - onClick = if (active) {{}} else onClick, - onLongClick = onLongClick, - interactionSource = remember { MutableInteractionSource() }, - indication = if (!active) LocalIndication.current else null - ) - .onRightClick { onLongClick() } - .padding(start = DEFAULT_PADDING_HALF, end = DEFAULT_PADDING), - horizontalArrangement = Arrangement.SpaceBetween, + .padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING, top = DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL), + horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically ) { - LocalDeviceRow(active) - Box(Modifier.size(20.dp)) + val activeHost = remoteHosts.firstOrNull { h -> h.activeHost } + + if (activeHost != null) { + val connecting = rememberSaveable { mutableStateOf(false) } + + DevicePill( + active = true, + icon = painterResource(MR.images.ic_smartphone_300), + text = activeHost.hostDeviceName, + actionButtonVisible = activeHost.sessionState is RemoteHostSessionState.Connected, + onActionButtonClick = { onRemoteHostActionButtonClick(activeHost) } + ) { + onRemoteHostClick(activeHost, connecting) + } + } + + DevicePill( + active = localDeviceActive, + icon = painterResource(MR.images.ic_desktop), + text = stringResource(MR.strings.this_device), + actionButtonVisible = false + ) { + onLocalDeviceClick() + } + + remoteHosts.filter { h -> h.sessionState is RemoteHostSessionState.Connected && !h.activeHost }.forEach { h -> + val connecting = rememberSaveable { mutableStateOf(false) } + + DevicePill( + active = h.activeHost, + icon = painterResource(MR.images.ic_smartphone_300), + text = h.hostDeviceName, + actionButtonVisible = h.sessionState is RemoteHostSessionState.Connected, + onActionButtonClick = { onRemoteHostActionButtonClick(h) } + ) { + onRemoteHostClick(h, connecting) + } + } } } @Composable -fun LocalDeviceRow(active: Boolean) { +expect fun UserPickerInactiveUsersSection( + users: List, + stopped: Boolean, + onShowAllProfilesClicked: () -> Unit, + onUserClicked: (user: User) -> Unit, +) + +@Composable +expect fun PlatformUserPicker( + modifier: Modifier, + pickerState: MutableStateFlow, + content: @Composable () -> Unit +) + +@Composable +fun DevicePill( + active: Boolean, + icon: Painter, + text: String, + actionButtonVisible: Boolean, + onActionButtonClick: (() -> Unit)? = null, + onClick: () -> Unit) { Row( Modifier - .widthIn(max = windowWidth() * 0.7f) - .padding(start = 17.dp, end = DEFAULT_PADDING), + .clip(RoundedCornerShape(8.dp)) + .border( + BorderStroke(1.dp, MaterialTheme.colors.secondaryVariant), + shape = RoundedCornerShape(8.dp) + ) + .background(if (active) MaterialTheme.colors.secondaryVariant else Color.Transparent) + .clickable( + enabled = !active, + onClick = onClick, + interactionSource = remember { MutableInteractionSource() }, + indication = LocalIndication.current + ), verticalAlignment = Alignment.CenterVertically ) { - Icon(painterResource(MR.images.ic_desktop), stringResource(MR.strings.this_device), Modifier.size(20.dp * fontSizeSqrtMultiplier), tint = MaterialTheme.colors.onBackground) - Text( - stringResource(MR.strings.this_device), - modifier = Modifier.padding(start = 26.dp, end = 8.dp), - color = if (active) MaterialTheme.colors.onBackground else MenuTextColor, - fontSize = 14.sp, - ) - } -} - -@Composable -private fun UseFromDesktopPickerItem(onClick: () -> Unit) { - SectionItemView(onClick, padding = PaddingValues(start = DEFAULT_PADDING + 7.dp, end = DEFAULT_PADDING), minHeight = 68.dp) { - val text = generalGetString(MR.strings.settings_section_title_use_from_desktop).lowercase().capitalize(Locale.current) - Icon(painterResource(MR.images.ic_desktop), text, Modifier.size(20.dp * fontSizeSqrtMultiplier), tint = MaterialTheme.colors.onBackground) - Spacer(Modifier.width(DEFAULT_PADDING + 6.dp)) - Text(text, color = MenuTextColor) - } -} - -@Composable -private fun LinkAMobilePickerItem(onClick: () -> Unit) { - SectionItemView(onClick, padding = PaddingValues(start = DEFAULT_PADDING + 7.dp, end = DEFAULT_PADDING), minHeight = 68.dp) { - val text = generalGetString(MR.strings.link_a_mobile) - Icon(painterResource(MR.images.ic_smartphone_300), text, Modifier.size(20.dp * fontSizeSqrtMultiplier), tint = MaterialTheme.colors.onBackground) - Spacer(Modifier.width(DEFAULT_PADDING + 6.dp)) - Text(text, color = MenuTextColor) - } -} - -@Composable -private fun CreateInitialProfile(onClick: () -> Unit) { - SectionItemView(onClick, padding = PaddingValues(start = DEFAULT_PADDING + 7.dp, end = DEFAULT_PADDING), minHeight = 68.dp) { - val text = generalGetString(MR.strings.create_chat_profile) - Icon(painterResource(MR.images.ic_manage_accounts), text, Modifier.size(20.dp * fontSizeSqrtMultiplier), tint = MaterialTheme.colors.onBackground) - Spacer(Modifier.width(DEFAULT_PADDING + 6.dp)) - Text(text, color = MenuTextColor) - } -} - -@Composable -private fun SettingsPickerItem(onClick: () -> Unit) { - SectionItemView(onClick, padding = PaddingValues(start = DEFAULT_PADDING + 7.dp, end = DEFAULT_PADDING), minHeight = 68.dp) { - val text = generalGetString(MR.strings.settings_section_title_settings).lowercase().capitalize(Locale.current) - Icon(painterResource(MR.images.ic_settings), text, Modifier.size(20.dp * fontSizeSqrtMultiplier), tint = MaterialTheme.colors.onBackground) - Spacer(Modifier.width(DEFAULT_PADDING + 6.dp)) - Text(text, color = MenuTextColor) - } -} - -@Composable -private fun CancelPickerItem(onClick: () -> Unit) { - SectionItemView(onClick, padding = PaddingValues(start = DEFAULT_PADDING + 7.dp, end = DEFAULT_PADDING), minHeight = 68.dp) { - val text = generalGetString(MR.strings.cancel_verb) - Icon(painterResource(MR.images.ic_close), text, Modifier.size(20.dp * fontSizeSqrtMultiplier), tint = MaterialTheme.colors.onBackground) - Spacer(Modifier.width(DEFAULT_PADDING + 6.dp)) - Text(text, color = MenuTextColor) + Row( + Modifier.padding(horizontal = 6.dp, vertical = 4.dp) + ) { + Icon( + icon, + text, + Modifier.size(16.dp * fontSizeSqrtMultiplier), + tint = MaterialTheme.colors.onSurface + ) + Spacer(Modifier.width(DEFAULT_SPACE_AFTER_ICON * fontSizeSqrtMultiplier)) + Text( + text, + color = MaterialTheme.colors.onSurface, + fontSize = 12.sp, + ) + if (onActionButtonClick != null && actionButtonVisible) { + val interactionSource = remember { MutableInteractionSource() } + val hovered = interactionSource.collectIsHoveredAsState().value + Spacer(Modifier.width(DEFAULT_SPACE_AFTER_ICON * fontSizeSqrtMultiplier)) + IconButton(onActionButtonClick, Modifier.requiredSize(16.dp * fontSizeSqrtMultiplier)) { + Icon( + painterResource(if (hovered) MR.images.ic_wifi_off else MR.images.ic_wifi), + null, + Modifier.size(16.dp * fontSizeSqrtMultiplier).hoverable(interactionSource), + tint = if (hovered) WarningOrange else MaterialTheme.colors.onBackground + ) + } + } + } } } @@ -472,6 +599,29 @@ fun HostDisconnectButton(onClick: (() -> Unit)?) { } } +@Composable +private fun BoxScope.unreadBadge(unreadCount: Int, userMuted: Boolean) { + Text( + if (unreadCount > 0) unreadCountStr(unreadCount) else "", + color = Color.White, + fontSize = 10.sp, + style = TextStyle(textAlign = TextAlign.Center), + modifier = Modifier + .offset(y = 3.sp.toDp()) + .background(if (userMuted) MaterialTheme.colors.primaryVariant else MaterialTheme.colors.secondary, shape = CircleShape) + .badgeLayout() + .padding(horizontal = 2.sp.toDp()) + .padding(vertical = 2.sp.toDp()) + .align(Alignment.TopEnd) + ) +} + + +private suspend fun closePicker(userPickerState: MutableStateFlow) { + delay(500) + userPickerState.value = AnimatedViewState.HIDING +} + private fun switchToLocalDevice() { withBGApi { chatController.switchUIRemoteHost(null) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AnimationUtils.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AnimationUtils.kt index 6a400295ed..4fdbd97d23 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AnimationUtils.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AnimationUtils.kt @@ -7,3 +7,5 @@ fun chatListAnimationSpec() = tween(durationMillis = 250, easing = FastOu fun newChatSheetAnimSpec() = tween(256, 0, LinearEasing) fun audioProgressBarAnimationSpec() = tween(durationMillis = 30, easing = LinearEasing) + +fun userPickerAnimSpec() = tween(256, 0, FastOutSlowInEasing) 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 index 90f8299404..104c05309c 100644 --- 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 @@ -16,6 +16,7 @@ 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 @@ -157,9 +158,13 @@ private fun bottomTitleAlpha(connection: CollapsingAppBarNestedScrollConnection? @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) { - Icon(painterResource(if (hostDevice.first == null) MR.images.ic_desktop else MR.images.ic_smartphone_300), null, Modifier.size(15.dp), tint = MaterialTheme.colors.secondary) - Spacer(Modifier.width(10.dp)) - Text(hostDevice.second, color = MaterialTheme.colors.secondary) + 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/Utils.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt index 7512cf872e..7b504116cc 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt @@ -588,9 +588,11 @@ fun KeyChangeEffect( var anyChange by remember { mutableStateOf(false) } LaunchedEffect(key1) { if (anyChange || key1 != prevKey) { - block(prevKey) + val prev = prevKey prevKey = key1 anyChange = true + // Call it as the last statement because the coroutine can be cancelled earlier + block(prev) } } } @@ -610,8 +612,8 @@ fun KeyChangeEffect( var anyChange by remember { mutableStateOf(false) } LaunchedEffect(key1, key2) { if (anyChange || key1 != initialKey || key2 != initialKey2) { - block() anyChange = true + block() } } } @@ -633,8 +635,8 @@ fun KeyChangeEffect( var anyChange by remember { mutableStateOf(false) } LaunchedEffect(key1, key2, key3) { if (anyChange || key1 != initialKey || key2 != initialKey2 || key3 != initialKey3) { - block() anyChange = true + block() } } } 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 88e483e92d..64ff7e4f40 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 @@ -39,7 +39,7 @@ fun ContactConnectionInfoView( ) { LaunchedEffect(connReqInvitation) { if (connReqInvitation != null) { - chatModel.showingInvitation.value = ShowingInvitation(contactConnection.id, connReqInvitation, false) + chatModel.showingInvitation.value = ShowingInvitation(contactConnection.id, connReqInvitation, false, conn = contactConnection) } } /** When [AddContactLearnMore] is open, we don't need to drop [ChatModel.showingInvitation]. 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 d2e8ac7a6c..a05de0e8b3 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 @@ -4,18 +4,22 @@ import SectionBottomSpacer import SectionItemView import SectionTextFooter import SectionView +import TextIconSpaced import androidx.compose.foundation.* -import androidx.compose.foundation.gestures.scrollBy import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.text.BasicTextField 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.draw.alpha +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.text.TextStyle @@ -32,6 +36,7 @@ import chat.simplex.common.ui.theme.* 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 java.net.URI @@ -43,7 +48,7 @@ enum class NewChatOption { fun ModalData.NewChatView(rh: RemoteHostInfo?, selection: NewChatOption, showQRCodeScanner: Boolean = false, close: () -> Unit) { val selection = remember { stateGetOrPut("selection") { selection } } val showQRCodeScanner = remember { stateGetOrPut("showQRCodeScanner") { showQRCodeScanner } } - val contactConnection: MutableState = rememberSaveable(stateSaver = serializableSaver()) { mutableStateOf(null) } + val contactConnection: MutableState = rememberSaveable(stateSaver = serializableSaver()) { mutableStateOf(chatModel.showingInvitation.value?.conn) } val connReqInvitation by remember { derivedStateOf { chatModel.showingInvitation.value?.connReq ?: "" } } val creatingConnReq = rememberSaveable { mutableStateOf(false) } val pastedLink = rememberSaveable { mutableStateOf("") } @@ -177,6 +182,15 @@ private fun CreatingLinkProgressView() { DefaultProgressView(stringResource(MR.strings.creating_link)) } +private fun updateShownConnection(conn: PendingContactConnection) { + chatModel.showingInvitation.value = chatModel.showingInvitation.value?.copy( + conn = conn, + connId = conn.id, + connReq = conn.connReqInv ?: "", + connChatUsed = true + ) +} + @Composable private fun RetryButton(onClick: () -> Unit) { Column( @@ -192,6 +206,238 @@ private fun RetryButton(onClick: () -> Unit) { } } +@Composable +private fun ProfilePickerOption( + title: String, + selected: Boolean, + disabled: Boolean, + onSelected: () -> Unit, + image: @Composable () -> Unit, + onInfo: (() -> Unit)? = null +) { + Row( + Modifier + .fillMaxWidth() + .sizeIn(minHeight = DEFAULT_MIN_SECTION_ITEM_HEIGHT + 8.dp) + .clickable(enabled = !disabled, onClick = onSelected) + .padding(horizontal = DEFAULT_PADDING, vertical = 4.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + image() + TextIconSpaced(false) + Text(title, modifier = Modifier.align(Alignment.CenterVertically)) + if (onInfo != null) { + Spacer(Modifier.padding(6.dp)) + Column(Modifier + .size(48.dp) + .clip(CircleShape) + .clickable( + enabled = !disabled, + onClick = { ModalManager.start.showModal { IncognitoView() } } + ), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Icon( + painterResource(MR.images.ic_info), + stringResource(MR.strings.incognito), + tint = MaterialTheme.colors.primary + ) + } + } + Spacer(Modifier.weight(1f)) + if (selected) { + Icon( + painterResource( + MR.images.ic_check + ), + title, + Modifier.size(20.dp), + tint = MaterialTheme.colors.primary, + ) + } + } + Divider( + Modifier.padding( + start = DEFAULT_PADDING_HALF, + end = DEFAULT_PADDING_HALF, + ) + ) +} + +@Composable +fun ActiveProfilePicker( + search: MutableState, + contactConnection: PendingContactConnection?, + close: () -> Unit, + rhId: Long?, + showIncognito: Boolean = true +) { + val switchingProfile = remember { mutableStateOf(false) } + val incognito = remember { + chatModel.showingInvitation.value?.conn?.incognito ?: controller.appPrefs.incognito.get() + } + val selectedProfile by remember { chatModel.currentUser } + val searchTextOrPassword = rememberSaveable { search } + // Intentionally don't use derivedStateOf in order to NOT change an order after user was selected + val filteredProfiles = remember(searchTextOrPassword.value) { + filteredProfiles(chatModel.users.map { it.user }.sortedBy { !it.activeUser }, searchTextOrPassword.value) + } + + var progressByTimeout by rememberSaveable { mutableStateOf(false) } + + LaunchedEffect(switchingProfile.value) { + progressByTimeout = if (switchingProfile.value) { + delay(500) + switchingProfile.value + } else { + false + } + } + + @Composable + fun ProfilePickerUserOption(user: User) { + val selected = selectedProfile?.userId == user.userId && !incognito + + ProfilePickerOption( + title = user.chatViewName, + disabled = switchingProfile.value || selected, + selected = selected, + onSelected = { + switchingProfile.value = true + withApi { + try { + var updatedConn: PendingContactConnection? = null; + + if (contactConnection != null) { + updatedConn = controller.apiChangeConnectionUser(rhId, contactConnection.pccConnId, user.userId) + if (updatedConn != null) { + withChats { + updateContactConnection(rhId, updatedConn) + updateShownConnection(updatedConn) + } + } + } + + controller.changeActiveUser_( + rhId = user.remoteHostId, + toUserId = user.userId, + viewPwd = if (user.hidden) searchTextOrPassword.value else null + ) + + if (chatModel.currentUser.value?.userId != user.userId) { + AlertManager.shared.showAlertMsg(generalGetString( + MR.strings.switching_profile_error_title), + String.format(generalGetString(MR.strings.switching_profile_error_message), user.chatViewName) + ) + } + + if (updatedConn != null) { + withChats { + updateContactConnection(user.remoteHostId, updatedConn) + } + } + + close() + } finally { + switchingProfile.value = false + } + } + }, + image = { ProfileImage(size = 42.dp, image = user.image) } + ) + } + + @Composable + fun IncognitoUserOption() { + ProfilePickerOption( + disabled = switchingProfile.value, + title = stringResource(MR.strings.incognito), + selected = incognito, + onSelected = { + if (incognito || switchingProfile.value || contactConnection == null) return@ProfilePickerOption + + switchingProfile.value = true + withApi { + try { + val conn = controller.apiSetConnectionIncognito(rhId, contactConnection.pccConnId, true) + if (conn != null) { + withChats { + updateContactConnection(rhId, conn) + updateShownConnection(conn) + } + close() + } + } finally { + switchingProfile.value = false + } + } + }, + image = { + Spacer(Modifier.width(8.dp)) + Icon( + painterResource(MR.images.ic_theater_comedy_filled), + contentDescription = stringResource(MR.strings.incognito), + Modifier.size(32.dp), + tint = Indigo, + ) + Spacer(Modifier.width(2.dp)) + }, + onInfo = { ModalManager.start.showModal { IncognitoView() } }, + ) + } + + BoxWithConstraints { + Column( + Modifier + .fillMaxSize() + .alpha(if (progressByTimeout) 0.6f else 1f) + ) { + LazyColumnWithScrollBar(userScrollEnabled = !switchingProfile.value) { + item { + AppBarTitle(stringResource(MR.strings.select_chat_profile), hostDevice(rhId), bottomPadding = DEFAULT_PADDING) + } + val activeProfile = filteredProfiles.firstOrNull { it.activeUser } + + if (activeProfile != null) { + val otherProfiles = filteredProfiles.filter { it.userId != activeProfile.userId } + item { + when { + !showIncognito -> + ProfilePickerUserOption(activeProfile) + incognito -> { + IncognitoUserOption() + ProfilePickerUserOption(activeProfile) + } + else -> { + ProfilePickerUserOption(activeProfile) + IncognitoUserOption() + } + } + } + + itemsIndexed(otherProfiles) { _, p -> + ProfilePickerUserOption(p) + } + } else { + if (showIncognito) { + item { + IncognitoUserOption() + } + } + itemsIndexed(filteredProfiles) { _, p -> + ProfilePickerUserOption(p) + } + } + } + } + if (progressByTimeout) { + DefaultProgressView("") + } + } +} + @Composable private fun InviteView(rhId: Long?, connReqInvitation: String, contactConnection: MutableState) { SectionView(stringResource(MR.strings.share_this_1_time_link).uppercase(), headerBottomPadding = 5.dp) { @@ -204,23 +450,72 @@ private fun InviteView(rhId: Long?, connReqInvitation: String, contactConnection SimpleXLinkQRCode(connReqInvitation, onShare = { chatModel.markShowingInvitationUsed() }) } - Spacer(Modifier.height(10.dp)) - val incognito = remember { mutableStateOf(controller.appPrefs.incognito.get()) } - IncognitoToggle(controller.appPrefs.incognito, incognito) { - ModalManager.start.showModal { IncognitoView() } + Spacer(Modifier.height(DEFAULT_PADDING)) + val incognito by remember(chatModel.showingInvitation.value?.conn?.incognito, controller.appPrefs.incognito.get()) { + derivedStateOf { + chatModel.showingInvitation.value?.conn?.incognito ?: controller.appPrefs.incognito.get() + } } - KeyChangeEffect(incognito.value) { - withBGApi { - val contactConn = contactConnection.value ?: return@withBGApi - val conn = controller.apiSetConnectionIncognito(rhId, contactConn.pccConnId, incognito.value) ?: return@withBGApi - withChats { - contactConnection.value = conn - updateContactConnection(rhId, conn) + val currentUser = remember { chatModel.currentUser }.value + + if (currentUser != null) { + SectionView(stringResource(MR.strings.new_chat_share_profile).uppercase(), headerBottomPadding = 5.dp) { + SectionItemView( + padding = PaddingValues( + top = 0.dp, + bottom = 0.dp, + start = 16.dp, + end = 16.dp + ), + click = { + ModalManager.start.showCustomModal { close -> + val search = rememberSaveable { mutableStateOf("") } + ModalView( + { close() }, + endButtons = { + SearchTextField(Modifier.fillMaxWidth(), placeholder = stringResource(MR.strings.search_verb), alwaysVisible = true) { search.value = it } + }, + content = { + ActiveProfilePicker( + search = search, + close = close, + rhId = rhId, + contactConnection = contactConnection.value + ) + }) + } + } + ) { + if (incognito) { + Spacer(Modifier.width(8.dp)) + Icon( + painterResource(MR.images.ic_theater_comedy_filled), + contentDescription = stringResource(MR.strings.incognito), + tint = Indigo, + modifier = Modifier.size(32.dp) + ) + Spacer(Modifier.width(2.dp)) + } else { + ProfileImage(size = 42.dp, image = currentUser.image) + } + TextIconSpaced(false) + Text( + text = if (incognito) stringResource(MR.strings.incognito) else currentUser.chatViewName, + color = MaterialTheme.colors.onBackground + ) + Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.End) { + Icon( + painter = painterResource(MR.images.ic_arrow_forward_ios), + contentDescription = stringResource(MR.strings.new_chat_share_profile), + tint = MaterialTheme.colors.secondary, + ) + } } } - chatModel.markShowingInvitationUsed() + if (incognito) { + SectionTextFooter(generalGetString(MR.strings.connect__a_new_random_profile_will_be_shared)) + } } - SectionTextFooter(sharedProfileInfo(chatModel, incognito.value)) } @Composable @@ -335,6 +630,18 @@ fun LinkTextView(link: String, share: Boolean) { } } +private fun filteredProfiles(users: List, searchTextOrPassword: String): List { + val s = searchTextOrPassword.trim() + val lower = s.lowercase() + return users.filter { u -> + if ((u.activeUser || !u.hidden) && (s == "" || u.anyNameContains(lower))) { + true + } else { + correctPassword(u, s) + } + } +} + private suspend fun verify(rhId: Long?, text: String?, close: () -> Unit): Boolean { if (text != null && strIsSimplexLink(text)) { connect(rhId, text, close) @@ -366,7 +673,7 @@ private fun createInvitation( if (r != null) { withChats { updateContactConnection(rhId, r.second) - chatModel.showingInvitation.value = ShowingInvitation(connId = r.second.id, connReq = simplexChatLink(r.first), connChatUsed = false) + chatModel.showingInvitation.value = ShowingInvitation(connId = r.second.id, connReq = simplexChatLink(r.first), connChatUsed = false, conn = r.second) contactConnection.value = r.second } } else { 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 3747ae047a..bef837ba94 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 @@ -9,6 +9,7 @@ import SectionView import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.grid.* +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.material.MaterialTheme.colors @@ -606,6 +607,39 @@ object AppearanceScope { } } + @Composable + fun ColorModeSwitcher() { + val currentTheme by CurrentColors.collectAsState() + val themeMode = if (remember { appPrefs.currentTheme.state }.value == DefaultTheme.SYSTEM_THEME_NAME) { + if (systemInDarkThemeCurrently) DefaultThemeMode.DARK else DefaultThemeMode.LIGHT + } else { + currentTheme.base.mode + } + + val onLongClick = { + ThemeManager.applyTheme(DefaultTheme.SYSTEM_THEME_NAME) + showToast(generalGetString(MR.strings.system_mode_toast)) + + saveThemeToDatabase(null) + } + Box( + modifier = Modifier + .clip(CircleShape) + .combinedClickable( + onClick = { + ThemeManager.applyTheme(if (themeMode == DefaultThemeMode.LIGHT) appPrefs.systemDarkTheme.get()!! else DefaultTheme.LIGHT.themeName) + saveThemeToDatabase(null) + }, + onLongClick = onLongClick + ) + .onRightClick(onLongClick) + .size(44.dp), + contentAlignment = Alignment.Center + ) { + Icon(painterResource(if (themeMode == DefaultThemeMode.LIGHT) MR.images.ic_light_mode else MR.images.ic_bedtime_moon), stringResource(MR.strings.color_mode_light), tint = MaterialTheme.colors.secondary) + } + } + private var updateBackendJob: Job = Job() private fun saveThemeToDatabase(themeUserDestination: Pair?) { val remoteHostId = chatModel.remoteHostId() 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 2f6c0395ec..0229e7da2a 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 @@ -73,10 +73,7 @@ private fun SetDeliveryReceiptsLayout( skip: () -> Unit, userCount: Int, ) { - // This view located in the left panel which means it has to have a padding from right side in order - // to see scroll bar. And this padding should be applied to upper element, not scrollable column modifier - val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp - Box(Modifier.padding(top = DEFAULT_PADDING, end = endPadding)) { + Box(Modifier.padding(top = DEFAULT_PADDING)) { ColumnWithScrollBar( Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, 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 3e1522b288..bb4a0b61b0 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 @@ -31,13 +31,11 @@ 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.remote.ConnectDesktopView -import chat.simplex.common.views.remote.ConnectMobileView import chat.simplex.res.MR import kotlinx.coroutines.* @Composable -fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, drawerState: DrawerState) { +fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, close: () -> Unit) { val user = chatModel.currentUser.value val stopped = chatModel.chatRunning.value == false SettingsLayout( @@ -71,10 +69,9 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, drawerSt } }, withAuth = ::doWithAuth, - drawerState = drawerState, ) KeyChangeEffect(chatModel.updatingProgress.value != null) { - drawerState.close() + close() } } @@ -96,18 +93,11 @@ fun SettingsLayout( showCustomModal: (@Composable ModalData.(ChatModel, () -> Unit) -> Unit) -> (() -> Unit), showVersion: () -> Unit, withAuth: (title: String, desc: String, block: () -> Unit) -> Unit, - drawerState: DrawerState, ) { val scope = rememberCoroutineScope() - val closeSettings: () -> Unit = { scope.launch { drawerState.close() } } val view = LocalMultiplatformView() - if (drawerState.isOpen) { - BackHandler { - closeSettings() - } - LaunchedEffect(Unit) { - hideKeyboard(view) - } + LaunchedEffect(Unit) { + hideKeyboard(view) } val theme = CurrentColors.collectAsState() val uriHandler = LocalUriHandler.current @@ -118,46 +108,22 @@ fun SettingsLayout( ) { AppBarTitle(stringResource(MR.strings.your_settings)) - SectionView(stringResource(MR.strings.settings_section_title_you)) { - val profileHidden = rememberSaveable { mutableStateOf(false) } - if (profile != null) { - SectionItemView(showCustomModal { chatModel, close -> UserProfileView(chatModel, close) }, 80.dp, padding = PaddingValues(start = 16.dp, end = DEFAULT_PADDING), disabled = stopped) { - ProfilePreview(profile, stopped = stopped) - } - SettingsActionItem(painterResource(MR.images.ic_manage_accounts), stringResource(MR.strings.your_chat_profiles), { withAuth(generalGetString(MR.strings.auth_open_chat_profiles), generalGetString(MR.strings.auth_log_in_using_credential)) { showSettingsModalWithSearch { it, search -> UserProfilesView(it, search, profileHidden, drawerState) } } }, disabled = stopped) - SettingsActionItem(painterResource(MR.images.ic_qr_code), stringResource(MR.strings.your_simplex_contact_address), showCustomModal { it, close -> UserAddressView(it, shareViaProfile = it.currentUser.value!!.addressShared, close = close) }, disabled = stopped) - ChatPreferencesItem(showCustomModal, stopped = stopped) - } else if (chatModel.localUserCreated.value == false) { - SettingsActionItem(painterResource(MR.images.ic_manage_accounts), stringResource(MR.strings.create_chat_profile), { - withAuth(generalGetString(MR.strings.auth_open_chat_profiles), generalGetString(MR.strings.auth_log_in_using_credential)) { - ModalManager.center.showModalCloseable { close -> - LaunchedEffect(Unit) { - closeSettings() - } - CreateProfile(chatModel, close) - } - } - }, disabled = stopped) - } - if (appPlatform.isDesktop) { - SettingsActionItem(painterResource(MR.images.ic_smartphone), stringResource(if (remember { chatModel.remoteHosts }.isEmpty()) MR.strings.link_a_mobile else MR.strings.linked_mobiles), showModal { ConnectMobileView() }, disabled = stopped) - } else { - SettingsActionItem(painterResource(MR.images.ic_desktop), stringResource(MR.strings.settings_section_title_use_from_desktop), showCustomModal { it, close -> ConnectDesktopView(close) }, disabled = 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) - } - SectionDividerSpaced() - 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_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) }) - DatabaseItem(encrypted, passphraseSaved, showSettingsModal { DatabaseView(it, showSettingsModal) }, stopped) } SectionDividerSpaced() + SectionView(stringResource(MR.strings.settings_section_title_chat_database)) { + DatabaseItem(encrypted, passphraseSaved, showSettingsModal { DatabaseView(it, showSettingsModal) }, 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) + } + + SectionDividerSpaced() + 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) @@ -535,7 +501,6 @@ fun PreviewSettingsLayout() { showCustomModal = { {} }, showVersion = {}, withAuth = { _, _, _ -> }, - drawerState = DrawerState(DrawerValue.Closed), ) } } 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 e3636ec9c5..10acaffe1a 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 @@ -34,6 +34,7 @@ fun UserProfileView(chatModel: ChatModel, close: () -> Unit) { KeyChangeEffect(u.value?.remoteHostId, u.value?.userId) { close() } + if (user != null) { var profile by remember { mutableStateOf(user.profile.toProfile()) } UserProfileLayout( 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 a7bf5920e4..dcf8351166 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,11 +36,10 @@ import dev.icerock.moko.resources.StringResource import kotlinx.coroutines.* @Composable -fun UserProfilesView(m: ChatModel, search: MutableState, profileHidden: MutableState, drawerState: DrawerState) { +fun UserProfilesView(m: ChatModel, search: MutableState, profileHidden: MutableState) { val searchTextOrPassword = rememberSaveable { search } val users by remember { derivedStateOf { m.users.map { it.user } } } val filteredUsers by remember { derivedStateOf { filteredUsers(m, searchTextOrPassword.value) } } - val scope = rememberCoroutineScope() UserProfilesLayout( users = users, filteredUsers = filteredUsers, @@ -51,12 +50,6 @@ fun UserProfilesView(m: ChatModel, search: MutableState, profileHidden: addUser = { ModalManager.center.showModalCloseable { close -> CreateProfile(m, close) - if (appPlatform.isDesktop) { - // Hide settings to allow clicks to pass through to CreateProfile view - DisposableEffectOnGone(always = { scope.launch { drawerState.close() } }) { - // Show settings again to allow intercept clicks to close modals after profile creation finishes - scope.launch(NonCancellable) { drawerState.open() } } - } } }, activateUser = { user -> @@ -303,7 +296,7 @@ private fun ProfileActionView(action: UserProfileAction, user: User, doAction: ( } } -private 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 -> @@ -317,7 +310,7 @@ private fun filteredUsers(m: ChatModel, searchTextOrPassword: String): List !u.user.hidden }.size -private fun correctPassword(user: User, pwd: String): Boolean { +fun correctPassword(user: User, pwd: String): Boolean { val ph = user.viewPwdHash return ph != null && pwd != "" && chatPasswordHash(pwd, ph.salt) == ph.hash } 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 036640a636..4ab0992a30 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -666,6 +666,10 @@ 1-time link SimpleX address Or show this code + Share profile + Select chat profile + Error switching profile + Your connection was moved to %s but an unexpected error occurred while redirecting you to the profile. Or scan QR code Keep unused invitation? You can view invitation link again in connection details. @@ -1156,6 +1160,7 @@ YOU SETTINGS + CHAT DATABASE HELP SUPPORT SIMPLEX CHAT APP @@ -1720,6 +1725,7 @@ Remove image Font size Zoom + System mode Good afternoon! 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 0c4e78a269..595545213a 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml @@ -224,7 +224,7 @@ Jak používat servery Vaše servery ICE Konfigurace serverů ICE - Nastavení sítě + Pokročilé nastavení Použít proxy server SOCKS\? Použít přímé připojení k internetu\? Ne @@ -434,7 +434,7 @@ Upravit obrázek Smazat obrázek chyba volání - Protokol a kód s otevřeným zdrojovým kódem - servery může provozovat kdokoli. + Servery může provozovat kdokoli. Vytvořte si svůj profil Vytvořte si soukromé připojení Videohovor šifrovaný e2e @@ -799,7 +799,7 @@ pozval %1$s připojen změnil roli %s na %s - změnil svou roli na %s + změnil vaši roli na %s odstraněn %1$s odstranil vás skupina odstraněna @@ -1861,4 +1861,5 @@ Volání zakázáno! Nelze zavolat člena skupiny Archivované kontakty + Archivujte kontakty pro pozdější chatování. \ 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 897ea64be8..e7b327c4ff 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml @@ -277,11 +277,11 @@ Inkognito akzeptieren Ablehnen - Chatinhalte löschen? + Chat-Inhalte entfernen? Es werden alle Nachrichten gelöscht. Dies kann nicht rückgängig gemacht werden! Die Nachrichten werden NUR bei Ihnen gelöscht. - Löschen - Chatinhalte löschen - Chatinhalte löschen + Entfernen + Chat-Inhalte entfernen + Chat-Inhalte entfernen Löschen Löschen Als gelesen markieren @@ -753,7 +753,7 @@ Mitgliedereinladungen überspringen Kontakte auswählen Kontakt geprüft - Löschen + Entfernen %d Kontakt(e) ausgewählt Keine Kontakte ausgewählt Kontakt kann nicht eingeladen werden! @@ -765,7 +765,7 @@ Gruppe löschen Gruppe löschen? Die Gruppe wird für alle Mitglieder gelöscht. Dies kann nicht rückgängig gemacht werden! - Die Gruppe wird für Sie gelöscht. Dies kann nicht rückgängig gemacht werden! + Die Gruppe wird nur bei Ihnen gelöscht. Dies kann nicht rückgängig gemacht werden! Gruppe verlassen Gruppenprofil bearbeiten Gruppen-Link @@ -1397,7 +1397,7 @@ Wir haben das zweite Häkchen vermisst! ✅ Reparatur der Verschlüsselung nach Wiedereinspielen von Backups. Ein paar weitere Dinge - Auch wenn sie im Chat deaktiviert sind. + Auch wenn sie in den Unterhaltungen deaktiviert sind. - stabilere Zustellung von Nachrichten. \n- ein bisschen verbesserte Gruppen. \n- und mehr! @@ -1630,7 +1630,7 @@ Zum Scannen tippen Behalten Zum Link einfügen tippen - Suchen oder fügen Sie den SimpleX-Link ein + Suchen oder SimpleX-Link einfügen Der Chat wurde gestoppt. Wenn diese Datenbank bereits auf einem anderen Gerät von Ihnen verwendet wurde, sollten Sie diese dorthin zurück übertragen, bevor Sie den Chat starten. Chat starten? Interne Fehler anzeigen @@ -1673,7 +1673,7 @@ Mit verschlüsselten Dateien und Medien. Private Notizen Es werden alle Nachrichten gelöscht. Dies kann nicht rückgängig gemacht werden! - Private Notizen löschen? + Private Notizen entfernen? %s wurde blockiert %s wurde freigegeben Sie haben %s blockiert @@ -2081,16 +2081,16 @@ Verbinden Nachricht Öffnen - Unterhaltung gelöscht! - Nur die Unterhaltung löschen + Chat-Inhalte gelöscht! + Nur die Chat-Inhalte löschen Suchen Video - Sie können in der Chatliste weiterhin die Unterhaltung mit %1$s einsehen. + Sie können in der Chat-Liste weiterhin die Unterhaltung mit %1$s einsehen. Link einfügen Archivierte Kontakte Keine gefilterten Kontakte Ihre Kontakte - Erreichbare Chat-Symbolleiste + Chat-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? @@ -2106,11 +2106,11 @@ Kontakt wird gelöscht. Dies kann nicht rückgängig gemacht werden! Ohne Benachrichtigung löschen Einladen - Unterhaltung behalten + Chat-Inhalte beibehalten Nachricht senden, um Anrufe zu aktivieren. Sie können aus den archivierten Kontakten heraus Nachrichten an %1$s versenden. Einstellungen - Die Nachrichten werden für alle Mitglieder gelöscht. + Die Nachrichten werden für alle Gruppenmitglieder gelöscht. Die Nachrichten werden für alle Mitglieder als moderiert markiert. %d Nachrichten der Mitglieder löschen? Nachricht @@ -2120,7 +2120,7 @@ Auswählen Einladen TCP-Verbindung - Schriftgröße erhöhen. + Schriftgröße anpassen. Neue Chat-Erfahrung 🎉 Die App automatisch aktualisieren Verbindungs- und Server-Status. @@ -2136,8 +2136,8 @@ Kontakte für spätere Chats archivieren. Ihre IP-Adresse und Verbindungen werden geschützt. Löschen Sie bis zu 20 Nachrichten auf einmal. - Erreichbare Chat-Symbolleiste - Die App mit einer Hand nutzen. + Chat-Symbolleiste unten + Die App mit einer Hand bedienen. Schneller mit Ihren Freunden verbinden. Chat-Datenbank wurde exportiert Weiter 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 db06d74378..3ec6bc021b 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml @@ -15,10 +15,10 @@ 30 másodperc Egyszer használatos hivatkozás %1$s szeretne kapcsolatba lépni önnel ezen keresztül: - A SimpleX Chat-ről + A SimpleX Chatről 1 nap Címváltoztatás megszakítása - A SimpleX-ről + 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. @@ -26,25 +26,25 @@ Elfogadás gombra fent, majd: Elfogadás inkognítóban - Kapcsolódási kérelem elfogadása? + Ismerőskérelem elfogadása? Elfogadás Elfogadás 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ősök számára. További kiemelés - hiba a hívásban + híváshiba Csoporttagok letiltása Hitelesítés Egy üres csevegési profil jön létre a megadott névvel, és az alkalmazás a szokásos módon megnyílik. %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. - Külön TCP kapcsolat (és SOCKS bejelentkezési adatok) lesz használva minden ismerős és csoporttag számára. -\nFigyelem: ha sok ismerőse van, az akkumulátor- és adathasználat jelentősen megnövekedhet és néhány kapcsolódási kísérlet sikertelen lehet. - hivatkozás előnézet visszavonása + Külön TCP kapcsolat (és SOCKS bejelentkezési adatok) lesz használva minden ismerős és csoporttag számára. \u0020 +\nFigyelem: ha sok ismerőse van, az akkumulátor- és az adathasználat jelentősen megnövekedhet, és néhány kapcsolódási kísérlet sikertelen lehet. + hivatkozás előnézetének visszavonása az alkalmazásban minden csevegési profiljához .]]> 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 ellenőrzőösszege + Hibás az üzenet hasító értéke Háttér Tudnivaló: az üzenet- és fájl átjátszók SOCKS proxy által vannak kapcsolatban. A hívások és URL hivatkozás előnézetek közvetlen kapcsolatot használnak.]]> Alkalmazásadatok biztonsági mentése @@ -114,7 +114,7 @@ hanghívás félkövér Az alkalmazás jelkód helyettesítésre kerül egy önmegsemmisítő jelkóddal. - Arab, bulgár, finn, héber, thai és ukrán - köszönet a felhasználóknak és a Weblate-nek! + Arab, bulgár, finn, héber, thai és ukrán - köszönet a felhasználóknak és a Weblate-nek. Hangüzenetek engedélyezése? Mindig használjon átjátszó kiszolgálót mindig @@ -124,9 +124,9 @@ Élő csevegési üzenet visszavonása Az üzenetek végleges törlése kizárólag 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 ellenőrzőösszege + 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é téve az értesítések fogadását. + 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. Legjobb akkumulátoridő. Csak akkor kap értesítéseket, amikor az alkalmazás meg van nyitva. (NINCS háttérszolgáltatás.)]]> Megjelenés @@ -137,7 +137,7 @@ szerző Az elküldött üzenetek végleges törlése engedélyezve van az ismerősei számára. (24 óra) Mégse - Az alkalmazás csak akkor tud értesítéseket fogadni, amikor meg van nyitva. A háttérszolgáltatás nem indul el. + Az alkalmazás csak akkor tud értesítéseket fogadni, amikor meg van nyitva. A háttérszolgáltatás nem indul el Jobb üzenetek A cím módosítása megszakad. A régi fogadási cím kerül felhasználásra. Engedélyezés @@ -147,7 +147,7 @@ Alkalmazás jelkód Felkérték a kép fogadására Kamera - A Keystore-hoz nem sikerül hozzáférni az adatbázis jelszó mentése végett + Nem érhető el a Keystore az adatbázis jelszavának mentéséhez 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. @@ -183,7 +183,7 @@ kapcsolódott Kapcsolódás kapcsolódott - Csatlakoztatott telefon + Társított mobil eszköz kapcsolódva Szerepkör megváltoztatása Kapcsolódva @@ -202,7 +202,7 @@ 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álatos hivatkozással? - Kapcsolódás egy hivatkozás / QR-kód által + Kapcsolódás egy hivatkozáson vagy QR-kódon keresztül Kapcsolódási hiba (AUTH) Ismerős neve Kapcsolódik a kapcsolattartási címen keresztül? @@ -210,7 +210,7 @@ Másolás Folytatás Kapcsolódás egy hivatkozáson keresztül? - Létező ismerős + Az ismerős már létezik Fő verzió: v%s Ismerős ellenőrizve Kapcsolódás saját magához? @@ -229,7 +229,7 @@ az ismerősnek nincs e2e titkosítása Ismerős engedélyezi Ismerős elrejtve: - Kapcsolódás számítógéphez + Társítás számítógéppel Környezeti ikon Kapcsolódás egy hivatkozáson keresztül Ismerősök @@ -271,7 +271,7 @@ kapcsolódás… Csevegési profil törlése egyedi - hívás kapcsolódik… + kapcsolódási hívás… Téma személyre szabása Jelenleg támogatott legnagyobb fájl méret: %1$s. Fájl törlése @@ -325,14 +325,14 @@ kapcsolódás… Csevegési adatbázis törölve kapcsolódás (bejelentve) - Csoportos hivatkozás létrehozása + Csoporthivatkozás létrehozása Csevegési konzol Fájlok törlése minden csevegési profilból Várólista törlése Ismerős törlése Létrehozva ekkor: %1$s cím megváltoztatása… - Csatlakoztatva a mobilhoz + Társítva a mobil eszközzel Jelenlegi jelmondat… Fájl kiválasztás Kép törlése @@ -346,7 +346,7 @@ Fájl összehasonlítás Csevegések Üzenet törlése? - Függő kapcsolatfelvételi kérések törlése? + Függőben lévő ismerőskérelem törlése? Adatbázis titkosítva! Üzenetek kiürítése? Adatbázis visszafejlesztése @@ -358,7 +358,7 @@ Adatbázis ID Adatbázis ID: %d Adatbázis azonosítók és átviteli izolációs beállítások. - Az adatbázis titkosítás jelmondata megváltoztatásra és mentésre kerül a Keystore-ban. + Az adatbázis-titkosítási jelmondat megváltoztatásra és mentésre kerül a Keystore-ban. Az adatbázis titkosításra kerül és a jelmondat eltárolásra a beállításokban. Kiszolgáló törlése A készüléken nincs beállítva a képernyőzár. A SimpleX zár ki van kapcsolva. @@ -381,7 +381,7 @@ %d hónap Cím törlése? Üzenet kézbesítési jelentések letiltása? - Az adatbázis jelmondata eltér a Keystore-ban lévőtől. + Az adatbázis jelmondat eltér a Keystore-ban lévőtől. Közvetlen üzenetek E-mail Letiltás mindenki számára @@ -415,7 +415,7 @@ nap %d nap Csevegési archívum törlése? - Duplikált megjelenítési név! + Duplikált megjelenített név! Letiltás (felülírások megtartásával) Adatbázis fejlesztése %d üzenet letiltva @@ -430,7 +430,7 @@ Minden 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 eltárolásra a Keystore-ban. + Az adatbázis titkosításra kerül és a jelmondat a Keystore-ban lesz tárolva. Automatikus üzenet törlés engedélyezése? Törlés az adatbázis verziója újabb, mint az alkalmazásé, visszafelé átköltöztetés nem lehetséges a következőhöz: %s @@ -450,7 +450,7 @@ Kép szerkesztése Értesítések letiltása Eszközök - Látható helyi hálózaton + 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. @@ -483,8 +483,8 @@ Hiba a hálózat konfigurációjának frissítésekor TCP életben tartása Kamera váltás - Üdv! -\nCsatlakozzon hozzám SimpleX Chat-en 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… (opcionális) @@ -499,7 +499,7 @@ A csevegések betöltése sikertelen A csoport már létezik! Francia kezelőfelület - Csoport hivatkozások + Csoporthivatkozások Végre, megvannak! 🚀 Hiba a csevegés elindításakor A csoport profilja a tagok eszközein tárolódik, nem a kiszolgálókon. @@ -515,7 +515,7 @@ Kedvenc Csoport moderáció Fájl - Csoport hivatkozás + Csoporthivatkozás titkosítás újraegyeztetés szükséges %s számára Hiba a profil váltásakor! Kísérleti funkciók @@ -536,7 +536,7 @@ Teljesen decentralizált - kizárólag tagok számára látható. Fájl: %s Hívás befejezése - Hiba a csoport hivatkozásának törlésekor + Hiba a csoporthivatkozás törlésekor Fájl elmentve Kapcsolat javítása? Fájlok és médiatartalom @@ -552,8 +552,8 @@ A csevegés betöltése sikertelen Kiszolgáló megadása kézzel A fájl akkor érkezik meg, amikor a küldője elérhető lesz, várjon, vagy ellenőrizze később! - Hiba a csoport hivatkozásának létrehozásakor - A Galériából + Hiba a csoporthivatkozás létrehozásakor + 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) @@ -562,12 +562,12 @@ A csoport tagjai küldhetnek eltűnő üzeneteket. Kapcsolat javítása Hiba a profil létrehozásakor! - Hiba a tag(-ok) hozzáadásakor + Hiba a tag(ok) hozzáadásakor Fájl A csoport tagjai 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 csoport hivatkozás frissítésekor + Hiba a csoporthivatkozás frissítésekor a csoport törölve csoportprofil frissítve Hiba a függőben lévő ismerős kapcsolatának törlésekor @@ -593,7 +593,7 @@ Hiba a cím megváltoztatásának megszakításakor Hiba a fájl fogadásakor titkosítás rendben - Hiba az ismerős kérelem törlésekor + Hiba az ismerőskérelem törlésekor Üzenet kézbesítési jelentéseket engedélyezése csoportok számára? Ismerős általi javítás nem támogatott Fájl nem található @@ -604,7 +604,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 - 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 minden 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 @@ -623,7 +623,7 @@ titkosítás újraegyeztetés szükséges Rejtett csevegési profilok Fájlok és média - A kép mentve a Galériába + A kép mentve a „Galériába” Elrejt Azonnal A fájlok- és a médiatartalom küldése le van tiltva! @@ -648,7 +648,7 @@ Figyelmen kívül hagyás Kép elküldve Rejtett - Házigazda + Kiszolgáló Kezdeti szerepkör érvénytelen csevegés óra @@ -678,12 +678,12 @@ Megerősítés esetén az üzenetküldő kiszolgálók látni fogják az IP-címét és a szolgáltatóját – azt, hogy mely kiszolgálókhoz kapcsolódik. A kép akkor érkezik meg, amikor a küldője befejezte annak feltöltését. QR kód beolvasásával.]]> - A kapott SimpleX Chat meghívó hivatkozását megnyithatja böngészőjében: + A kapott SimpleX Chat meghívó hivatkozását megnyithatja a böngészőjében: Ha az alkalmazás megnyitásakor megadja az önmegsemmisítő jelkódot: Megtalált számítógép Számítógépek A markdown használata - Csevegő profil létrehozása + Csevegési profil létrehozása Levélszemét elleni védelem Mobilok leválasztása Különböző nevek, avatarok és átviteli izoláció. @@ -691,7 +691,7 @@ Szerepkör kiválasztásának bővítése A kép akkor érkezik meg, amikor a küldője elérhető lesz, várjon, vagy ellenőrizze később! meghíva - Érvénytelen kapcsolati hivatkozás + Érvénytelen kapcsolattartási hivatkozás Némítás nincsenek részletek Nem fogadott hívás @@ -699,13 +699,13 @@ Az üzenet törlésre kerül - ez a művelet nem vonható vissza! Markdown segítség új üzenet - Régi adatbázis archívum + Régi adatbázis-archívum Haladó beállítások Nincs kézbesítési információ moderált A tag eltávolítása a csoportból - ez a művelet nem vonható vissza! Győződjön meg róla, hogy az XFTP kiszolgáló címei megfelelő formátumúak, sorszeparáltak és nincsenek duplikálva. - Nem kerültek ismerősök kiválasztásra + Nincs kiválasztva ismerős Nincsenek fogadott, vagy küldött fájlok Megnyitás mobil alkalmazásban, majd koppintson a Kapcsolódás gombra az alkalmazásban.]]> Markdown az üzenetekben @@ -721,7 +721,7 @@ Helyi név Hálózat és kiszolgálók Értesítés előnézet - Társítsa össze a mobil és az asztali alkalmazásokat! 🔗 + Társítsa össze a mobil és asztali alkalmazásokat! 🔗 közvetett (%1$s) Hamarosan további fejlesztések érkeznek! Az üzenetreakciók küldése le van tiltva ebben a csevegésben. @@ -749,12 +749,12 @@ Onion kiszolgálók nem lesznek használva. perc Tudjon meg többet - Új kapcsolattartási kérelem + Új ismerőskérelem Csatlakozás a csoporthoz - Összekapcsolt számítógép beállítások - meghíva az ön csoport hivatkozásán keresztül + Társított számítógép beállítások + meghíva az ön csoporthivatkozásán keresztül elhagyta a csoportot - Összekapcsolt számítógépek + Társított számítógépek Nincs alkalmazás jelkód Némítás, ha inaktív! A meghívó lejárt! @@ -779,7 +779,7 @@ Olasz kezelőfelület Nincsenek háttérhívások Üzenetek - Összekapcsolt mobil eszközök + Társított mobil eszközök 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. Az üzenet törlésre lesz jelölve. A címzett(ek) képes(ek) lesz(nek) felfedni ezt az üzenetet. Elhagyás @@ -800,12 +800,12 @@ Tegye priváttá a profilját! Üzenetkézbesítési hiba Több csevegőprofil - töröltnek jelölve + törlésre jelölve Némítás - Egy mobil összekapcsolása + Egy mobil 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égponttól-végpontig titkosítással küldött üzeneteket.]]> + 2 rétegű végpontok közötti titkosítással küldött üzeneteket.]]> Érvénytelen átköltöztetési visszaigazolás Csak a csoporttulajdonosok módosíthatják a csoportbeállításokat. Nincsenek előzmények @@ -823,26 +823,26 @@ ajánlott %s Csoport elhagyása Minden %s által írt üzenet megjelenik! - Ha a SimpleX Chat-nek nincs felhasználói azonosítója, hogyan lehet mégis üzeneteket küldeni?]]> + Ha a SimpleX Chatnek nincs felhasználói 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 csoportos hivatkozáson keresztül + inkognitó a csoporthivatkozáson keresztül Onion kiszolgálók használata, ha azok rendelkezésre állnak. Ismerősök meghívása Menük és figyelmeztetések Tagok meghívása csatlakozás mint %s - Nincs kiválasztott csevegés + Nincs kiválasztva csevegés Csak helyi profiladatok - inkognitó az egyszer használatos hivatkozáson keresztül + inkognitó egy egyszer használatos hivatkozáson keresztül Moderálva lett ekkor: %s Egyszer használatos meghívó hivatkozás Érvénytelen név! - Beszélgessünk a SimpleX Chat-ben - Moderálva lett ekkor: + Beszélgessünk a SimpleX Chatben + Moderálva ekkor: Élő üzenetek Hitelesítés Üzenetkézbesítési bizonylatok! @@ -864,7 +864,7 @@ tag Privát kapcsolat létrehozása moderálva lett %s által - Győződjön meg arról, hogy a fájl helyes YAML-szintaxist tartalmaz. Exportálja a témát, hogy legyen egy példa a téma fájl szerkezetére. + Győződjön meg arról, hogy a fájl helyes YAML-szintaxist tartalmaz. Exportálja a témát, hogy legyen egy példa a témafájl szerkezetére. dőlt Érvénytelen fájl elérési útvonal Csatlakozik a csoporthoz? @@ -904,13 +904,13 @@ Előnézet megjelenítése várakozás a visszaigazolásra… Fájl megállítása - csoport hivatkozáson keresztül + a csoporthivatkozáson keresztül PING időköze Eltűnő üzenet küldése Önmegsemmisítési jelkód Mentés és csoportprofil frissítése Adatvédelem - Az ön SimpleX címe + Profil SimpleX címe Jelentse a fejlesztőknek. Ön dönti el, hogy kivel beszélget. Az eltűnő üzenetek küldése le van tiltva. @@ -946,7 +946,7 @@ (beolvasás, vagy beillesztés a vágólapról) Videóra várakozás Válasz - Ez az egyszer használatos hivatkozása! + Ez az ön egyszer használatos hivatkozása! SimpleX Chat hívások Új inkognító profil használata Frissítse az alkalmazást, és lépjen kapcsolatba a fejlesztőkkel. @@ -964,10 +964,10 @@ Biztonsági kód beolvasása az ismerősének alkalmazásából. Lépjen kapcsolatba a csoport adminnal. Videó bekapcsolva - Profil neve: + Profilnév: Beillesztés Köszönjük, hogy telepítette a SimpleX Chatet! - Csillagozás a GitHub-on + Csillagozás a GitHubon Eltávolítás Keresés Titkosítás újraegyeztetése? @@ -1001,7 +1001,7 @@ Véletlen Megosztás az ismerősökkel ön - Nincsenek csevegési üzenetek + Nincsenek csevegései Küldés %s másodperc %s: %s @@ -1020,7 +1020,7 @@ Profiljelszó Téma Jelmondat eltávolítása a beállításokból? - SimpleX csoport hivatkozás + SimpleX csoporthivatkozás Képre várakozás Önmegsemmisítés várakozás a válaszra… @@ -1041,11 +1041,11 @@ Véletlenszerű jelmondat használata egyenrangú CSEVEGÉSI SZOLGÁLTATÁS INDÍTÁSA - Fogadott hivatkozás beillesztése + Kapott hivatkozás beillesztése Kiszolgálók mentése? A SimpleX Chat biztonsága a Trail of Bits által lett auditálva. frissítette a csoport profilját - TÁMOGASSA A SIMPLEX CHATET + SIMPLEX CHAT TÁMOGATÁSA SimpleX Chat szolgáltatás Nem lehet üzeneteket küldeni! %s ellenőrzött @@ -1122,13 +1122,13 @@ SMP kiszolgálók Az üzenet kézbesítési jelentések le vannak tiltva Adatbázis mappa megnyitása - egyszer használatos hivatkozáson keresztül + egy egyszer használatos hivatkozáson keresztül Csoportbeállítások megadása ezen keresztül: %1$s igen Hangüzenet - Használat számítógépről - ÖN + Társítás számítógéppel + PROFIL port %d Kapcsolódás hivatkozáson keresztül Cím megosztása @@ -1144,7 +1144,7 @@ eltávolítottak SimpleX cím Megjelenítés: - fogadott válasz… + válasz fogadása… Adatbázismentés visszaállítása? Üzenetek fogadása… %s és %s kapcsolódott @@ -1169,7 +1169,7 @@ Az üzenetreakciók küldése le van tiltva. Rendszer olvasatlan - Függő + Függőben Üdvözöljük %1$s! Jelmondat eltávolítása a Keystrore-ból? Feloldás @@ -1185,10 +1185,10 @@ Kép/videó megoszása… ön: %1$s Beállítások - Színek alaphelyzetbe állítása + Színek visszaállítása Mentés Váltás - A kapott hivatkozás beillesztése az ismerősökhöz történő kapcsolódáshoz… + A kapott hivatkozás beillesztése az ismerőshöz való kapcsolódáshoz… Beolvasás Port megnyitása a tűzfalon indítás… @@ -1254,7 +1254,7 @@ Fogadva ekkor: %s SimpleX zár Mentés és csoporttagok értesítése - Alaphelyzetbe állítás + Visszaállítás Csak az ismerőse tud üzenetreakciókat küldeni. Hangüzenetek elhagyta a csoportot @@ -1287,9 +1287,9 @@ Videók és fájlok 1Gb méretig TCP kapcsolat időtúllépés A(z) %1$s nevű profiljának SimpleX címe megosztásra fog kerülni. - Ön már kapcsolódott ehhez: %1$s. + Ön már kapcsolódva van ehhez: %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! +\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 az új csevegés indításához @@ -1303,23 +1303,23 @@ cím megváltoztatva nála: %s fájlok fogadása egyelőre még nem támogatott Csoportprofil mentése - Alaphelyzetbe állítás - Hacsak az ismerőse nem törölte a kapcsolatot, vagy ez a hivatkozás már használatban volt, lehet hogy ez egy hiba – jelentse a problémát. -\nA kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsolati hivatkozást, és ellenőrizze, hogy a hálózati kapcsolat stabil-e. + Visszaállítás alaphelyzetbe + 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átor néhány százalékát használja naponta.]]> + 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 a kapcsolatfelvételt és törölheti 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ági mentési eszközzel. Ha nem így történt, akkor lépjen kapcsolatba a fejlesztőkkel. +\nVisszavonhatja ezt az ismerőskérelmet és törölheti 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 Az adatbázis nem működik megfelelően. Koppintson további információért A fájl küldése leállt. - Kapcsolódási kísérlet ahhoz a kiszolgálóhoz, amely az adott ismerőstől érkező üzenetek fogadására szolgál. + 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 lehetett ellenőrizni; próbálja meg újra. Az üzenet minden tag számára moderáltként lesz megjelölve. Értesítések fogadásához adja meg az adatbázis jelmondatát @@ -1327,8 +1327,8 @@ Az alkalmazás indításakor, vagy 30 másodpercnyi háttérben töltött idő után az alkalmazáshoz visszatérve hitelesítés szükséges. Az üzenet minden 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ások között érhető el - Profilja elküldésre kerül ismerőse számára, akitől ezt a hivatkozást kapta. + Ez a szöveg a „Beállításokban” érhető el + 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.]]> @@ -1341,22 +1341,22 @@ Hálózati kapcsolat ellenőrzése 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 ismerősét, hogy küldjön egy másikat. + 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. A kép nem dekódolható. Próbálja meg egy másik képpel, vagy lépjen kapcsolatba a fejlesztőkkel. É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őstől érkező üzenetek fogadására szolgál (hiba: %1$s). + 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. 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álatos hivatkozást osztott meg inkognitóban - Már kapcsolódott ahhoz a kiszolgálóhoz, amely az adott ismerőstől érkező üzenetek fogadására szolgál. + 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.]]> Profil felfedése - Ez a hivatkozás nem érvényes kapcsolati hivatkozás! + Ez nem egy érvényes kapcsolattartási hivatkozás! A végpontok közötti titkosítás ellenőrzé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 @@ -1367,7 +1367,7 @@ Ismerőse a jelenleg megengedett maximális méretű (%1$s) fájlnál nagyobbat küldött. 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 alkalmazásban gombra.]]> + Megnyitás az alkalmazásban gombra.]]> 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 @@ -1379,10 +1379,10 @@ 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. - - hangüzenetek 5 percig. -\n- egyedi eltűnési időhatár. -\n- előzmény szerkesztése. - Használat számítógépről menüt a mobil alkalmazásban és olvassa be a QR-kódot.]]> + - 5 perc hosszúságú hangüzenetek. +\n- egyedi ü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 mobil alkalmazásban és olvassa be a QR-kódot.]]> %s ekkor: %s Akkor lesz kapcsolódva, amikor az ismerősének az eszköze online lesz, várjon, vagy ellenőrizze később! Kéretlen üzenetek elrejtése. @@ -1411,7 +1411,7 @@ cím megváltoztatva 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. - Ha engedélyezni szeretné, hogy egy mobilalkalmazás csatlakozzon a számítógéphez, akkor nyissa meg ezt a portot a tűzfalában, ha engedélyezte azt + Ha engedélyezni szeretné a mobilalkalmazás társítását a számítógéphez, akkor nyissa meg ezt a portot a tűzfalában, ha engedélyezte azt Profilja, ismerősei és az elküldött üzenetei az eszközön kerülnek tárolásra. 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! @@ -1419,7 +1419,7 @@ A kapcsolódás már folyamatban van ezen az egyszer használatos 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 veled! + kapcsolatba akar lépni önnel! saját szerepköre erre változott: %s 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 ellenőrzése a mobilon @@ -1431,18 +1431,18 @@ Inkognító mód 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 csoporthoz!]]> A hangüzenetek küldése le van tiltva ebben a csevegésben. Ön irányítja csevegését! Kód ellenőrzése a számítógépen Az időzóna védelme érdekében a kép-/hangfájlok UTC-t használnak. - A kapcsolódási kérelem elküldésre kerül ezen csoporttag számára + A kapcsolódási kérelem 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ért egy kapcsolódási kérelmet ezen a címen keresztül! Megoszthatja ezt a SimpleX címet az ismerőseivel, hogy kapcsolatba léphessenek vele: %s. Amikor az emberek kapcsolódást kérelmeznek, ö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 Weblaten! + 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ős számára engedélyezésre kerül. Protokoll időkorlát KB-onként Az adatbázis jelmondatának megváltoztatására tett kísérlet nem fejeződött be. @@ -1453,7 +1453,7 @@ Ez a művelet nem vonható vissza - az összes fogadott és küldött fájl a médiatartalommal együtt törlésre kerülnek. Az alacsony felbontású képek viszont megmaradnak. 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 Weblaten! + 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. 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. @@ -1467,13 +1467,13 @@ Jelmondat beállítása az exportáláshoz Kézbesítési jelentések le vannak tiltva a(z) %d csoportban Néhány nem végzetes hiba történt az importálás közben: - Köszönet a felhasználóknak - hozzájárulás a Weblaten! + Köszönet a felhasználóknak - hozzájárulás a Weblate-en! Az átjátszó 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ő 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. Jelmondat mentése a Keystore-ba - Köszönet a felhasználóknak - hozzájárulás a Weblaten! + Köszönet a felhasználóknak - hozzájárulás a Weblate-en! Jelmondat mentése a beállításokban 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! ✅ @@ -1484,18 +1484,18 @@ Kézbesítési jelentések engedélyezve vannak a(z) %d csoportban A szerepkör meg fog változni erre: „%s”. A csoportban mindenki értesítve lesz. Profil és kiszolgálókapcsolatok - Egy üzenetküldő- és alkalmazásplatform, amely védi az ön adatait és biztonságát. + Egy üzenetküldő- és alkalmazásplatform, amely védi az adatait és biztonságát. A profil aktiválásához koppintson az ikonra. Kézbesítési jelentések le vannak tiltva %d ismerősnél Munkamenet kód - Köszönet a felhasználóknak - hozzájárulás a Weblaten! + Köszönet a felhasználóknak - hozzájárulás a Weblate-en! Kis csoportok (max. 20 tag) - Az ön által elfogadott kapcsolat vissza lesz vonva! + 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ő. - Az eszköz neve megosztásra kerül a csatlakoztatott mobil klienssel. + Az eszköz neve megosztásra kerül a társított mobil klienssel. A címzettek a beírás közben látják a frissítéseket. Tárolja el biztonságosan jelmondatát, mert ha elveszíti azt, NEM tudja megváltoztatni. A jelmondat a beállítások között egyszerű szövegként kerül tárolásra, miután megváltoztatta vagy újraindította az alkalmazást. @@ -1510,15 +1510,15 @@ Használati útmutatóban olvasható.]]> A jelmondat a beállításokban egyszerű szövegként van tárolva. Konzol megjelenítése új ablakban - Az előző üzenet ellenőrzőösszege különbözik. + Az előző üzenet hasító értéke különbözik. Ezek a beállítások a jelenlegi profiljára vonatkoznak - Várjon, amíg a fájl betöltődik a csatolt mobilról + Várjon, amíg a fájl betöltődik a társított mobilról GitHub tárolónkban.]]> - hiba a tartalom megjelenítése közben + hiba a tartalom megjelenítésekor hiba az üzenet megjelenítésekor - Láthatóvá teheti SimpleX-beli ismerősei számára a Beállításokban. + 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 kód nem egy SimpleX hivatkozás QR-kód. + 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? @@ -1567,9 +1567,9 @@ \n%s %s mobil eszköz verziója nem támogatott. Győződjön meg arról, hogy mindkét eszközön ugyanazt a verziót használja]]> %s mobil eszközzel]]> - Érvénytelen megjelenítendő felhaszálónév! - Ez a megjelenített felhasználónév érvénytelen. Válasszon egy másik nevet. - %s mobil eszközzel, a(z) %s probléma miatt]]> + Érvénytelen megjelenítendő név! + Ez a megjelenített név érvénytelen. Válasszon egy másik nevet. + %s mobil eszközzel, a(z) %s probléma miatt]]> %s probléma miatt megszakadt a kapcsolat %s mobil eszköz nem található]]> %s mobil eszközzel rossz állapotban van]]> @@ -1581,7 +1581,7 @@ Fejlesztői beállítások A funkció végrehajtása túl sokáig tart: %1$d másodperc: %2$s %s mobil eszköz elfoglalt]]> - Már nem tag - %1$s + %1$s (már nem tag) ismeretlen státusz %1$s megváltoztatta a nevét erre: %2$s törölt kapcsolattartási cím @@ -1612,13 +1612,13 @@ letiltva letiltva az admin által Letiltva az admin által - letiltotta %s-t + letiltotta őt: %s Letiltás mindenki számára Mindenki számára letiltja ezt a tagot? %d üzenet letiltva az admin által Letiltás feloldása mindenki számára Mindenki számára feloldja a tag letiltását? - ön letiltotta %s-t + ön letiltotta őt: %s Hiba a tag mindenki számára való letiltása közben Az üzenet túl nagy Az üdvözlő üzenet túl hosszú @@ -1642,7 +1642,7 @@ Átköltöztetés visszavonása A csevegés átköltöztetve! Ellenőrizze az internetkapcsolatot, és próbálja újra - Archív hivatkozás létrehozása + Archívum hivatkozás létrehozása Adatbázis törlése erről az eszközről Sikertelen letöltés Archívum letöltése @@ -1671,7 +1671,7 @@ Ellenőrizze, hogy a hálózati beállítások megfelelőek-e ehhez az eszközhöz. A folytatáshoz a csevegést meg kell szakítani. Csevegés megállítása folyamatban - Vagy a fájl hivítkozásának biztonságos megosztása + Vagy ossza meg biztonságosan ezt a fájlhivatkozást Csevegés indítása Nem szabad ugyanazt az adatbázist használni egyszerre két eszközön.]]> Erősítse meg, hogy emlékszik az adatbázis jelmondatára az átköltöztetéshez. @@ -1726,7 +1726,7 @@ tulajdonosok adminok minden tag - SimpleX hivatkozás + 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 @@ -1840,14 +1840,14 @@ Privát üzenet útválasztás 🚀 Fájlok biztonságos fogadása Csökkentett akkumulátor-használattal. - Hiba a WebView inicializálásában. Frissítse rendszerét az új verzióra. Kérjük, lépjen kapcsolatba a fejlesztőkkel. + Hiba a WebView inicializálásában. 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 várakoztatási információ nincs Kézbesítési hibák felderítése - Kiszolgáló várakoztatási infó: %1$s -\nUtoljára kézbesített üzenet: %2$s + kiszolgáló várakoztatási infó: %1$s +\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. Ideiglenes fájlhiba Üzenetállapot @@ -1858,7 +1858,7 @@ Fájlállapot Fájlállapot: %s Másolási hiba - Ezt a hivatkozást egy másik mobilleszközön már használták, hozzon létre egy új hivatkozást az asztali számítógépén. + Ezt a hivatkozást egy másik mobileszközön már használták, hozzon létre egy új hivatkozást az asztali számítógépén. Ellenőrizze, hogy a mobil és az asztali számítógép ugyanahhoz a helyi hálózathoz csatlakozik-e, valamint az asztali 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 @@ -1870,13 +1870,13 @@ 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. Hivatkozás beolvasása / beillesztése - Beállított SMP-kiszolgálók + Konfigurált SMP-kiszolgálók Egyéb SMP-kiszolgálók Egyéb XFTP-kiszolgálók letiltva inaktív Nagyítás - információk a kiszolgálókról + Információk a kiszolgálókról Kapcsolódás Hibák Függőben @@ -1892,7 +1892,7 @@ Visszaállítás Minden statisztika visszaállítása Minden statisztika visszaállítása? - A kiszolgálók statisztikái visszaállnak - ez nem vonható vissza! + A kiszolgálók statisztikái visszaállnak - ez a művelet nem vonható vissza! Részletes statisztikák Letöltve lejárt @@ -1930,7 +1930,7 @@ Feltöltött fájltöredékek Elkészült Kapcsolódott kiszolgálók - Beállított XFTP-kiszolgálók + Konfigurált XFTP-kiszolgálók Kapcsolódva Jelenlegi profil Részletek @@ -2023,7 +2023,7 @@ Engedélyeznie kell a hívásokat az ismerőse számára, hogy fel tudják hívni egymást. 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 + Kiválasztás Az üzenetek minden 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. @@ -2032,7 +2032,7 @@ Az üzenetek minden 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 + Kapcsolódjon gyorsabban az ismerőseihez. Folytatás Ellenőrízze a hálózatát Média- és fájlkiszolgálók @@ -2052,11 +2052,11 @@ Csevegőlista átváltása: Ezt a „Megjelenés” menüben módosíthatja. Új médiabeállítások - Lejátszás a csevegési listából - Elhomályosítás a jobb adatvédelemért + Lejátszás a csevegési listából. + Elhomályosítás a jobb adatvédelemért. Automatikus frissítés Létrehozás - Új verziók letöltése a GitHub-ról + Új verziók letöltése a GitHubról Betűméret növelése. Meghívás Új csevegési élmény 🎉 diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_add_group.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_add_group.svg new file mode 100644 index 0000000000..158f4cfab0 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_add_group.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bedtime_moon.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bedtime_moon.svg new file mode 100644 index 0000000000..ed5bc12d4a --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bedtime_moon.svg @@ -0,0 +1 @@ + \ 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 b6d6770250..9f6fa799b7 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml @@ -2,16 +2,16 @@ %1$s ANGGOTA Alamat - %1$d pesan dihapus admin %2$s + %1$d pesan dimoderasi oleh %2$s 1 menit 30 detik 5 menit Semua pesan akan dihapus - ini tidak bisa dikembalikan! Pesan akan HANYA dihapus untukmu. - Terima permintaan relasi? + Terima permintaan koneksi? Tentang alamat SimpleX Tambah kontak Tentang SimpleX - %1$d gagal mendekripsi pesan. + %1$d pesan gagal terdekripsi. Tolong laporkan hal ini ke pengembang. 1 bulan 1 minggu @@ -19,7 +19,7 @@ %1$s.]]> Panggilan suara Batal - izinkan pesan suara? + Izinkan pesan suara? Terima Versi aplikasi Versi aplikasi: v%s @@ -30,13 +30,13 @@ Tambah profil Corak Tambahan sekunder - Motif aplikasi + Tema aplikasi selalu - diizinkan mengirim pesan suara. + Izinkan mengirim pesan suara. semua anggota Panggilan suara dan video - Hal-hal lain - Sudah menghubungi! + Beberapa hal lainnya + Sudah terhubungkan! Sudah bergabung dengan grup! %1$s ingin menghubungimu lewat Batalkan penggantian alamat? @@ -54,7 +54,7 @@ tebal menelepon… Bluetooth - Panggilan ditutup + Panggilan diakhiri Ubah Peringatan: arsip akan dihapus.]]> Buat grup: untuk membuat grup baru.]]> @@ -112,4 +112,154 @@ Tampilan macet Anda membagikan lokasi file yang tidak valid. Laporkan masalah ini ke pengembang aplikasi. error + Tuatan 1 kali pakai + %1$d pesan yang terlewati + %1$d pesan yang dilewati + %s tidak didukung. Harap pastikan kamu menggunakan versi yang sama pada kedua perangkat.]]> + %s sedang sibuk]]> + %s tidak terhubung]]> + %s tidak aktif]]> + %s tidak di temukan]]> + Terima penyamaran + panggilan diterima + 6 bahasa antarmuka baru + %s tidak terhubung]]> + Tidak ada kode sandi aplikasi + Belum ada koneksi langsung, pesan diteruskan oleh admin. + Tidak ada obrolan yang difilter + Tidak ada yang dipilih + Hanya 10 gambar dapat dikirim pada saat bersamaan + Notifikasi + Hanya menghapus percakapan + Koneksi jaringan yang lebih handal. + Pengalaman obrolan yang baru 🎉 + Pilihan media baru + Izinkan panggilan? + Pesan baru + Hanya kamu yang dapat mengirim pesan menghilang. + Tidak ada pengidentifikasi pengguna. + Hanya pemilik grup yang dapat mengubah preferensi grup. + dan %d peristiwa lainnya + Peran baru anggota + Tidak ada kontak di pilih + Tidak ada kontak untuk ditambahkan + Tidak ada teks + Semua pesan baru dari %s akan disembunyikan! + tidak ada + Status jaringan + Seluruh obrolan dan pesan akan dihapus - ini tidak bisa dibatalkan! + Tidak ada perangkat terkoneksi + Tidak ada panggilan latar + Pratinjau notifikasi + Layanan notifikasi + Selalu aktif + Permintaan kontak baru + Pesan baru + Kemungkinan besar kontak ini telah menghapus koneksi dengan kamu. + Masalah jaringan - pesan kadaluwarsa setelah beberapa kali mencoba mengirim. + Tidak ada obrolan dipilih + Hanya pemilik grup yang dapat mengaktifkan file dan media. + (hanya disimpan oleh anggota grup) + Lagi + Tautan undangan satu kali + Obrolan baru + Jaringan & server + Pengaturan tingkat lanjut + Host Onion akan diperlukan untuk koneksi. +\nHarap diperhatikan: Anda tidak akan dapat terhubung ke server tanpa alamat .onion. + 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. + Tidak ada enkripsi ujung-ujung + Kode sandi baru + Mati + Seluruh data aplikasi dihapus. + Arsip database baru + Arsip database lama + tidak pernah + Tidak ada file yang diterima atau dikirim + Menyetujui enkripsi… + Bisukan ketika tidak aktif! + Seluruh mode warna + mati` + tidak + on + mati + Izinkan kontak kamu menghapus pesan terkirim secara permanen. (24 jam) + Izinkan penghapusan pesan yang tidak dapat diubah hanya jika kontak kamu mengizinkannya. (24 jam) + Izinkan kontak kamu mengirim pesan suara. + Hanya kamu yang dapat menghapus pesan secara permanen (kontak kamu dapat menandainya untuk dihapus). (24 jam) + Hanya kontak kamu yang dapat menghapus pesan secara permanen (kamu dapat menandainya untuk dihapus). (24 jam) + Hanya kamu yang dapat menambahkan reaksi pesan. + Hanya kamu yang dapat melakukan panggilan. + Hanya kontak kamu yang dapat menambahkan reaksi pesan. + Izinkan pengiriman pesan langsung ke anggota. + Izinkan untuk mengirim pesan menghilang. + ditawarkan %s: %2s + Beberapa profil obrolan + Lebih banyak peningkatan akan segera hadir! + Tidak ada admin yang dapat: +\n- menghapus pesan anggota. +\n- menonaktifkan anggota (peran “pengamat”) + Lebih banyak peningkatan akan segera hadir! + - pengiriman pesan yang lebih stabil. +\n- group yang sedikit lebih baik. +\n- dan lainnya! + Aplikasi desktop baru! + Manajemen jaringan + bulan + Semua kontak, percakapan, dan file kamu akan dienkripsi dengan aman dan diunggah dalam beberapa bagian ke relay XFTP yang telah dikonfigurasi. + Tidak ada koneksi jaringan + Bisu + Koneksi jaringan + TIdak pernah + Mati + Hanya 10 video dapat dikirim pada saat bersamaan + enkripsi ujung-ke-ujung 2 lapis.]]> + Hanya pemilik grup yang dapat mengaktifkan pesan suara. + Seluruh pesan akan dihapus - ini tidak bisa dibatalkan! + Izinkan turun versi + Tidak ada informasi pengiriman + Boleh + OK + Tidak ada rincian + Tautan undangan satu kali + Host Onion akan di pakai jika tersedia. + Selalu gunakan relay + Nama tampilan baru: + Frasa sandi baru + Pemberitahuan akan dikirim hanya sampai saat aplikasi berhenti! + Pengaturan tingkat lanjut + Izinkan reaksi pesan hanya jika kontak Anda mengizinkannya. + Izinkan pesan suara hanya jika kontak kamu mengizinkannya. + Izinkan kontak kamu menambahkan reaksi pesan. + Izinkan kontak kamu untuk menghubungimu. + Izinkan kontak kamu untuk mengirim pesan menghilang. + Izinkan panggilan hanya jika kontak kamu mengizinkannya. + Izinkan pesan menghilang hanya jika kontak kamu mengizinkannya. + Izinkan reaksi pesan. + Izinkan untuk menghapus pesan terkirim secara permanen. (24 jam) + Bisu + Tidak ada riwayat + Tema obrolan yang baru + Semua profil + Izinkan untuk mengirim tautan SimpleX. + Semua data akan terhapus jika ini dimasukkan. + Tidak kompatibel! + Perangkat seluler baru + Hanya satu perangkat yang dapat bekerja pada saat bersamaan + Tidak + Tidak ada kontak yang di filter + pengamat + menyetujui enkripsi untuk %s… + Semua anggota grup akan tetap terhubung. + Tidak ada info, coba muat ulang + Tidak + Baru di %s + ditawarkan %s + Hanya kamu yang dapat mengirim pesan suara. + Hanya kontak kamu yang dapat melakukan panggilan. + Izinkan untuk mengirim file dan media. + Semua kontak kamu akan tetap terhubung. \ 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 3c639cf777..92055493fb 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 @@ -74,7 +74,7 @@ Verifique o endereço do servidor e tente novamente. para cada perfil de bate-papo que você tiver no aplicativo.]]> Melhor para bateria. Você receberá notificações apenas quando o aplicativo estiver em execução (SEM o serviço em segundo plano).]]> - Consome mais bateria! O serviço em segundo plano está sempre em execução - as notificações são exibidas assim que as mensagens estiverem disponíveis.]]> + Consome mais bateria! O aplicativo em segundo plano está sempre em execução - as notificações são exibidas instantaneamente.]]> BATE-PAPOS ÍCONE DO APLICATIVO BANCO DE DADOS DE BATE-PAPO @@ -91,7 +91,7 @@ O serviço em segundo plano está sempre em execução - as notificações serão exibidas assim que as mensagens estiverem disponíveis. Uma conexão TCP separada (e credencial SOCKS) será usada para cada contato e membro do grupo. \nAtenção: se você tiver muitas conexões, o consumo de bateria e tráfego pode ser substancialmente maior e algumas conexões podem falhar. - Bom para bateria. O serviço em segundo plano procura por mensagens a cada 10 minutos. Você pode perder chamadas ou mensagens urgentes.]]> + Bom para bateria. O aplicativo procura por mensagens a cada 10 minutos. Você pode perder chamadas ou mensagens urgentes.]]> chamda encerrada %1$s Converse com os desenvolvedores Criar link de grupo @@ -256,12 +256,12 @@ DISPOSITIVO Ferramentas de desenvolvedor conectando (introduzido) - Realçe + Tonalidade Erro ao remover membro Erro ao alterar cargo direto erro - Falha ao carregar o bate-papo + Falha ao carregar a conversa Erro ao atualizar a configuração de conexão Erro ao enviar mensagem Erro ao adicionar membro(s) @@ -317,8 +317,8 @@ Expandir seleção de cargo Erro ao salvar o perfil do grupo Mensagens diretas - habilitado - habilitado para contato + ativado + ativado para contato ativado para você %dm %d min @@ -379,7 +379,7 @@ Nome completo do grupo: Links de grupo Privacidade e segurança aprimoradas - Falha ao carregar o bate-papo + Falha ao carregar as conversas Arquivo: %s Arquivo salvo Os membros do grupo podem enviar mensagens de voz. @@ -403,7 +403,7 @@ Código de segurança incorreto! Instale o SimpleX para terminal Como funciona - Imune a spam e abuso + Imune a spam Vire a câmera Desligar Modo anônimo @@ -551,7 +551,7 @@ formato de mensagem inválido AO VIVO moderado por %s - chat inválido + conversa inválida Abrir o link no navegador pode reduzir a privacidade e a segurança da conexão. Links SimpleX não confiáveis ficarão vermelhos. Link de conexão inválido Certifique-se de que os endereços do servidor SMP estejam no formato correto, separados por linhas e não estejam duplicados. @@ -562,7 +562,7 @@ Executa quando o aplicativo está aberto enviado o envio falhou - Bate-papos + Conversas Colar Link de convite de uso único Enviar perguntas e idéias @@ -764,7 +764,7 @@ Privacidade redefinida Notificações privadas Fazer uma conexão privada - Pessoas podem se conectar com você somente via links compartilhados. + Você decide quem pode se conectar. Pode acontecer quando: \n1. As mensagens expiraram no remetente após 2 dias ou no servidor após 30 dias. \n2. A descriptografia da mensagem falhou porque você ou seu contato usou o backup do banco de dados antigo. @@ -785,7 +785,7 @@ Gravar mensagem de voz Certifique-se de que os endereços do servidor WebRTC ICE estão em formato correto, separados por linha e não estejam duplicados. Conexão e servidores - Configurações de conexão + Configurações avançadas sem detalhes Link de convite de uso único Seu endereço de servidor @@ -803,7 +803,7 @@ Não Usar conexão direta com a internet\? Ocultar: - Seu perfil é guardado no seu diapositivo e é compartilhado somente com seus contatos. Servidores SimpleX não podem ver seu perfil. + Seu perfil é guardado no seu dispositivo e é compartilhado somente com seus contatos. Servidores SimpleX não podem ver seu perfil. Salvar preferências\? Salvar e notificar membros do grupo Erro ao salvar a senha do usuário @@ -857,7 +857,8 @@ Para começar um novo bate-papo Ligar Bem-vindo(a)! - A próxima geração de 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. @@ -883,7 +884,7 @@ Inicia periodicamente envio não autorizado Toque para iniciar um novo bate-papo - Você não tem bate-papos + Você não tem conversas aguardando resposta… Seu banco de dados de bate-papo atual será EXCLUÍDO e SUBSTITUÍDO pelo importado. \nEsta ação não pode ser desfeita - seu perfil, contatos, mensagens e arquivos serão perdidos de forma irreversível. @@ -902,12 +903,12 @@ Atualizar O app busca novas mensagens periodicamente – ele usa alguns por cento da bateria por dia. O aplicativo não usa notificações por push – os dados do seu dispositivo não são enviados para os servidores. Para receber notificações, por favor, digite a senha do banco de dados - Serviço SimpleX + Serviço de Chat SimpleX Mostrar prévia Mostrar contato e mensagem Mostrar somente contato Compartilhar - Parar bate-papo + Parar conversa Desbloquear A mensagem será excluída para todos os membros. A mensagem será marcada como moderada para todos os membros. @@ -925,7 +926,7 @@ simplexmq: v%s (%2s) Isolamento de transporte Você controla sua conversa! - A 1ª plataforma sem nenhum identificador de usuário – privada por design. + Sem identificadores de usuário. Alto-falante ligado Vídeo desativado Alto-falante desligado @@ -976,7 +977,7 @@ iniciando… aguardando confirmação… Não armazenamos nenhum dos seus contatos ou mensagens (uma vez entregues) nos servidores. - Mensagens omitidas + Mensagens ignoradas Toque para ativar o perfil. Mostrar perfil de chat Mostrar perfil @@ -994,7 +995,7 @@ imagem de pré-visualização do link Para proteger suas informações, ative o bloqueio SimpleX. \nVocê será solicitado a completar a autenticação antes que este recurso seja ativado. - Protocolo de código aberto – qualquer um pode hospedar os servidores. + Qualquer um pode hospedar os servidores. Claro O contato permite ativado @@ -1109,7 +1110,7 @@ Revogar Sobre o endereço SimpleX Secundária adicional - Realçe adicional + Tonalidade adicional Link de uso único Adicione o endereço ao seu perfil, para que seus contatos possam compartilhá-lo com outras pessoas. A atualização do perfil será enviada aos seus contatos. Crie um endereço para permitir que as pessoas se conectem com você. @@ -1139,7 +1140,7 @@ Você não perderá seus contatos se, posteriormente, excluir seu endereço. Endereço SimpleX Quando as pessoas solicitam uma conexão, você pode aceitá-la ou rejeitá-la. - CORES DO TEMA + CORES DA INTERFACE compartilhar com os contatos A atualização do perfil será enviada aos seus contatos. Salvar configurações\? @@ -1238,7 +1239,7 @@ Alterar senha de auto-destruição Se você digitar sua senha de auto-destruição ao abrir o aplicativo: sem texto - Alguns erros não-fatais ocurreram durante importação - pode ver o console de Chat para mais detalhes. + Alguns erros não fatais ocorreram durante importação: Pesquisar Desativado Arquivos e mídia @@ -1281,7 +1282,7 @@ Desligar Corrigir conexão Corrigir conexão\? - Sem bate-papo filtrados + Sem conversas filtradas Renegociar Desfavoritar Renegociar a criptografia\? @@ -1335,7 +1336,7 @@ Enviar recibos de entrega serão habilitados para todos os contatos em todos os perfis visíveis. %s e %s conectados Conectar diretamente\? - Nenhum bate-papo selecionado + Nenhuma conversa selecionada Rascunho de mensagem desativado SimpleX não pode ser executado em segundo plano. Você receberá as notificações somente quando o aplicativo estiver em execução. @@ -1562,7 +1563,7 @@ Erro de renegociação de criptografia Erro ao abrir o navegador Erro ao enviar o convite - Carregando bate-papos… + Carregando conversas… %s foi desconectado]]> %s foi desconectado]]> Apenas um dispositivo pode funcionar ao mesmo tempo @@ -1659,4 +1660,412 @@ Servidores XFTP configurados Verifique sua conexão de internet e tente novamente Verificar atualizações + Modo de cor + Apagar banco de dados desse dispositivo + O endereço do servidor de destino de %1$s é incompatível com o as configurações %2$s do servidor de encaminhamento. + Capacidade excedida - o destinatário não recebeu as mensagens enviadas anteriormente. + Erro do servidor de destino: %1$s + Inativo + Migre de outro dispositivono novo dispositivo e escaneie o QR code.]]> + Confirme se você se lembra da senha do banco de dados para migrá-lo. + Borrar conteúdo + Borrar para melhor privacidade. + Confirmar exclusão do contato? + conectar + Apagar sem notificar + Tonalidade adicional 2 + Confirmar configurações de rede + Todos os modos de cor + Modo escuro + Não é possível enviar mensagem + Cores do chat + Criar + Confirmar upload + administradores + O contato foi apagado. + Permitir chamadas? + Não é possível chamar o contato + Conectando ao contato, por favor aguarde ou volte depois! + Chamadas proibidas! + Não é possível chamar membro do grupo + Não é possível mandar mensagem para o membro do grupo + Controle sua rede + Apague até 20 mensagens por vez. + Arquivar contatos para conversar depois. + Conecte aos seus amigos mais rapidamente. + Escuro + Confirmar arquivos de servidores desconhecidos. + Copiar erro + Conversa migrada! + chamar + O contato será apagado - essa ação não pode ser desfeita! + Beta + A atualização do aplicativo foi baixada + Tema da conversa + Entrega de depuração + Cores do modo escuro + Criando link de arquivo + Permitir downgrade + Todos os usuários + Conectado + Conectando + Perfil atual + Servidores conectados + Conexões ativas + tentativas + Reconhecido + Erros conhecidos + Conexões + Criado + erros de decriptação + Apagado + Erros de exclusão + Pedaços excluídos + Pedaços baixados + Pedaços carregados + Apagar %d mensagens dos membros? + Contato apagado! + Conversa apagada! + Contatos arquivados + Banco de dados da conversa exportado + Continuar + Conexão e status dos servidores. + Repetir download + Recebendo simultaneidade + Redefinir para o tema do usuário + IU Persa + Aviso de entrega de mensagem + Erro: %1$s + Chave incorreta ou conexão desconhecida - provavelmente esta conexão foi excluída. + Essa conversa é protegida por criptografia ponta a ponta + Repetir upload + Erro de conexão ao servidor de encaminhamento %1$s. Por favor tente mais tarde. + A versão do servidor de encaminhamento é incompatível com as configurações de rede: %1$s. + O servidor de encaminhamento %1$s falhou ao se conectar ao servidor de destino %2$s. Por favor tente mais tarde. + O endereço do servidor de encaminhamento é incompatível com as configurações de rede: %1$s. + A versão do servidor de destino de %1$s é incompatível com o servidor de encaminhamento %2$s. + Problemas de rede - a mensagem expirou após muitas tentativas de envio. + Servidor de encaminhamento: %1$s +\nErro: %2$s + Destinatário(s) não podem ver de onde essa mensagem veio. + A mensagem poderá ser entregue mais tarde se o membro se tornar ativo. + Outros servidores SMP + Para proteger seu endereço IP, roteamento privado usa seus servidores SMP para entregar mensagens. + Pular essa versão + Baixar %s (%s) + Download da atualização cancelado + Mostrar lista de conversas em nova janela + Tamanho da fonte + Fundo do papel de parede + Tonalidade do papel de parede + Remover imagem + Zoom + Definir tema padrão + Proibido enviar links SimpleX + Erro ao carregar o arquivo + Falha ao carregar + Carregando arquivo + Estatísticas detalhadas + Erro no servidor de arquivo: %1$s + Salvo de + Baixar + Encaminhar + Status de arquivo: %s + Convidar + Aumentar tamanho da fonte. + Aprimorar aplicativo automaticamente + Redefinir todas as estatísticas? + As estatísticas dos servidores serão redefinidas - isso não poderá ser desfeito! + Link inválido + Por favor cheque se o link SimpleX está correto + criptografia quantum resistant e2e com perfeito sigilo direto, repúdio e recuperação de vazamento.]]> + Essa conversa é protegida por criptografia quantum resistant ponta a ponta + Por favor tente mais tarde. + Selecionado %d + SimpleX links não permitidos + Arquivos e mídia não permitidos + Mensagem + Erro de arquivo temporário + mensagem + pesquisar + vídeo + Roteamento privado + NÃO use roteamento privado. + Abrir configurações + Fone de ouvido + Erro ao iniciar o WebView. Atualize seu sistema para a nova versão. Por favor contate os desenvolvedores. +\nErro: %s + Desativado + Forte + Ativar em conversas diretas (BETA)! + Finalizar migração em outro dispositivo. + Claro + A origem da mensagem permanece privada. + Migrar aqui + Migrando + Nova experiência de conversa 🎉 + Novas opções de mídia + Ou cole o link do arquivo + Reproduzir da lista de conversa. + Redefinir todas as estatísticas + Aviso: iniciar conversa em múltiplos dispositivos não é suportado e pode causar falhas na entrega de mensagens + Internet cabeada + não deve usar a mesma base de dados em dois dispositivos.]]> + Membros do grupo podem enviar link SimpleX + Importando arquivo + Modo claro + Ativado para + Ao conectar em chamadas de áudio de vídeo. + Migrar dispositivo + Erro ao salvar configurações + Arquivo exportado não existe + %s carregados + Para continuar, a conversa precisa ser interrompida. + Outro + Sem conexão de rede + As preferências de conversa selecionadas proíbem essa mensagem. + Erro de arquivo + Redefinir cor + Sons de chamada + Formato das imagens de perfil + IU Lituana + Roteamento de mensagem privada 🚀 + Novos temas de conversa + Com uso de bateria reduzida. + Falha no download + Falha na importação + Baixando arquivo + Repetir importação + Você pode tentar novamente. + Erro ao exportar banco de dados de conversa + Erro ao verificar a palavra-chave: + salvo + salvo de %s + Encaminhado de + Mensagens de voz não permitidas + Nova mensagem + Colar link + Links SimpleX + Encaminhar e salvar mensagens + Suave + Médio + Você precisa permitir seu contato ligue para poder ligar para ele. + Redefinir para o tema do aplicativo + Resposta recebida + Boa tarde! + Bom dia! + Baixe novas versões no GitHub. + encaminhado + O arquivo foi deletado ou o link está inválido + Abrir tela de migração + Grupos seguros + Preparando upload + Conexão de rede + Servidores desconhecidos! + Sem Tor ou VPN, seu endereço de IP ficará visível para esses relays XFTP +\n%1$s. + Erro ao exibir notificação, contate os desenvolvedores. + Salvo + Encaminhado + Servidores desconhecidos + Desprotegido + Nunca + Modo de roteamento de mensagens + Conceder permissões + Alto falante + Headphones + Sem Tor ou VPN, seu endereço de IP ficará visível para servidores de arquivo. + ARQUIVOS + Fotos de perfil + ROTEAMENTO DE MENSAGEM PRIVADA + criptografia padrão ponta a ponta + proprietários + Migrar para outro dispositivo + Verificar palavra-passe + WiFi + Alternar lista de conversa: + Você pode mudar isso em configurações de Aparência. + desativado + nenhum + informações da fila do servidor: %1$s +\n +\núltima mensagem recebida: %2$s + Por favor peça para seu contato ativar as chamadas. + Enviar mensagem para ativar chamadas. + Salvar e reconectar + Conexão TCP + Barra de ferramentas de conversa acessível + Isso protege seu endereço de IP e conexões. + Use o aplicativo com uma mão. + Arquivos carregados + Baixado + Erro ao redefinir estatísticas + Redefinir + Status de arquivo + Informações da fila de mensagens + Sistema + Repetir + Escala + Preencher + Ajustar + Links SimpleX são proibidos neste grupo. + Migrar para outro dispositivo via QR code. + Chamadas picture-in-picture + Use o aplicativo enquanto está em chamada. + Será ativado em conversas diretas! + Receber arquivos de forma segura + Gerenciamento de rede + Faça suas conversas terem uma aparência diferente! + Conexão de rede mais confiável. + Preparando download + %s baixados + Colar link de arquivo + Insira a palavra-chave + Erro ao baixar o arquivo + Ou de forma segura compartilhe esse link de arquivo + Verifique a palavra-passe do banco de dados + criptografia ponta-a-ponta com perfeito sigilo direto, repúdio e recuperação de vazamento.]]> + Arquivo não encontrado - provavelmente o arquivo foi excluído ou cancelado. + Chave incorreta ou arquivo de pedaço de endereço - provavelmente o arquivo foi excluído. + Mande mensagens diretamente quando o seu endereço de IP está protegido e o servidor de destino não suporta roteamento privado. + Use roteamento privado em servidores desconhecidos. + Escanear / Colar link + Baixando detalhes de link + Finalizar migração + Criptografia Quantum resistant + Migração concluída + Proteja seu endereço de IP dos retransmissores de mensagem escolhidos por seus contatos. +\nAtive nas configurações *Redes e servidores* . + Iniciar conversa + Configurações + abrir + Manter conversa + Apenas excluir conversa + Outros servidores XFTP + Use roteamento privado em servidores desconhecidos quando o endereço de IP não está protegido. + Sim + Instalar atualização + Atualização disponível: %s + Abrir local do arquivo + Desativar + Microfone + Conceder nas configurações + Encontre essa permissão nas configurações do Android e conceda-a manualmente. + O aplicativo irá perguntar para confirmar os downloads de servidores de arquivo desconhecidos (exceto .onion ou quando o proxy SOCKS estiver habilitado). + Tema de perfil + Defina uma palavra-chave + criptografia quantum resistant e2e + Convidar + Status da mensagem + Entrega de mensagens aprimorada + Quadrado, circulo, ou qualquer coisa entre eles. + Por favor verifique se o celular e o computador estão conectados na mesma rede local e o firewall do computador permite a conexão. +\nPor favor compartilhe qualquer outro problema com os desenvolvedores. + Esse link foi usado em outros dispositivo móvel, por favor crie um novo link no computador. + Erro ao excluir banco de dados + Por favor confirme que as configurações de rede estão corretas para este dispositivo. + Parando conversa + Você pode tentar novamente. + A versão do servidor é incompatível com seu aplicativo: %1$s. + Erro de roteamento privado + O endereço do servidor é incompatível com as configurações de rede: %1$s. + Servidor de encaminhamento: %1$s +\nErro no servidor de destino: %2$s + Endereço do servidor é incompatível com as configurações de rede. + A versão do servidor é incompatível com as configurações de rede. + Encaminhar mensagem… + Quando IP oculto + Proteger endereço IP + Enviar resposta + Arquivos + Não + Baixando atualização do aplicativo, não feche o aplicativo + Conceder permissão para fazer chamadas + Link inválido + Detalhes + Erros + Mensagens recebidas + Mensagens enviadas + Sem informação, tente recarregar + Informação dos servidores + Mostrando informação para + Estatísticas + Sessões de transporte + Recepção de mensagem + Pendente + Começando de %s. +\nTodos os dados são privados do seu dispositivo. + Total + Servidores proxiados + Servidores conectados anteriormente + Reconecte todos os servidores conectados para forçar entrega de mensagem. Isso usa tráfego adicional. + Reconectar servidor? + Reconectar servidores? + Reconectar servidor para forçar entrega de mensagem. Isso usa tráfego adicional. + Você não está conectado nesses servidores. Roteamento privado é usado para entregar mensagens para eles. + Erro + Erro ao reconectar servidor + Erro ao reconectar servidores + Reconectar todos os servidores + Reconectar + Enviar diretamente + Enviar mensagens + Enviar total + Enviar via proxy + Servidor SMP + Mensagens recebidas + Total recebido + Receber erros + Começando de %s. + Servidor XFTP + Seguro + Enviar erros + Inscrito + duplicatas + expirada + outro + Erros de inscrição + outros erros + Proxied + Inscrições ignoradas + Erros de download + Arquivos baixados + Endereço do servidor + Tamanho + Erros de upload + Abrir configurações de servidor + Selecione + As mensagens serão excluídas para todos os membros. + As mensagens serão marcadas como moderadas para todos os membros. + Mensagem encaminhada + Ainda não há conexão direta, a mensagem é encaminhada pelo administrador. + Membro inativo + Nada selecionado + Mensagens serão marcadas para exclusão. O(s) destinatário(s) poderá(ão) revelar essas mensagens. + Você ainda pode ver a conversa com %1$s na lista de conversas. + Nenhum contato filtrado + Seus contatos + NÃO envie mensagens diretamente, mesmo que o seu servidor ou o servidor de destino não suporte roteamento privado. + Mande mensagens diretamente quando o seu servidor ou o servidor de destino não suporta roteamento privado. + Retorno de roteamento de mensagens + Mostrar status da mensagem + Migrar de outro dispositivo + Status da mensagem: %s + Carregado + Servidores de mensagem + Servidores de mídia e arquivo + Mostrar porcentagem + Proxy SOCKS + Você pode salvar o arquivo exportado. + Você pode migrar o banco de dados exportado. + Alguns arquivos não foram exportados + Você pode enviar mensagens para %1$s de Contatos arquivados. + Redefinir todas as dicas + Desativado + Estável + Instalado com sucesso + Por favor reinicie o aplicativo. + Me lembre mais tarde + Para ser notificado sobre os novos lançamentos, habilite a checagem periódica de versões Estáveis e Beta. + Barra de ferramentas de conversa acessível \ 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 cd34f2efff..b16c96b636 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml @@ -724,4 +724,61 @@ Lỗi kết nối lại máy chủ Lỗi Lỗi khôi phục thống kê + Lỗi cập nhật liên kết nhóm + lỗi hiển thị tin nhắn + lỗi hiển thị nội dung + Lỗi lưu máy chủ ICE + Lỗi lưu máy chủ XFTP + Lỗi lưu máy chủ SMP + Lỗi gửi tin nhắn + Lỗi khởi động ứng dụng + Lỗi dừng ứng dụng + Lỗi hiển thị thông báo, liên hệ với nhà phát triển. + Lỗi lưu mật khẩu người dùng + Lỗi lưu cài đặt + Lỗi gửi lời mời + Lỗi chuyển đổi hồ sơ! + Lỗi đồng bộ kết nối + Lỗi cài đặt địa chỉ + Dù đã tắt trong cuộc trò chuyện. + Lỗi cập nhật cấu hình mạng + Lỗi cập nhật quyền riêng tư người dùng + Mở rộng chọn quyền hạn + THỬ NGHIỆM + Mở rộng + Thoát mà không lưu + đã hết hạn + Lỗi xác thực mật khẩu: + Quá trình thực hiện chức năng mất quá nhiều thời gian: %1$dgiây: %2$s + Tính năng thử nghiệm + Xuất cơ sở dữ liệu + Lỗi tải lên kho lưu trữ + Tập tin đã xuất không tồn tại + TẬP TIN + Không thể tải tin nhắn + Không tìm thấy tập tin - có thể tập tin đã bị xóa và hủy bỏ. + Lỗi tập tin + Xuất chủ đề + Không thể tải tin nhắn + Tập tin + Nhanh chóng và không cần phải đợi người gửi hoạt động! + Không tìm thấy tập tin + Tập tin: %s + Tham gia nhanh chóng hơn và xử lý tin nhắn ổn định hơn. + Tập tin + Yêu thích + Tập tin + Lỗi máy chủ tệp: %1$s + Trạng thái tệp + Tệp và phương tiện truyền thông không được cho phép + Tệp và phương tiện truyền thông bị cấm trong nhóm này. + Tệp sẽ bị xóa khỏi máy chủ. + Tệp & phương tiện truyền thông + Tệp và phương tiện truyền thông bị cấm! + Tệp sẽ được nhận khi liên hệ của bạn hoàn tất quá trình tải lên. + Tệp và phương tiện truyền thông + Tệp đã bị xóa hoặc liên kết không hợp lệ + Tệp đã được lưu + Tệp sẽ được nhận khi liên hệ của bạn hoạt động, vui lòng chờ hoặc kiểm tra lại sau! + Trạng thái tệp: %s \ No newline at end of file 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 new file mode 100644 index 0000000000..583d5437c3 --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.desktop.kt @@ -0,0 +1,86 @@ +package chat.simplex.common.views.chatlist + +import androidx.compose.animation.* +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +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.unit.dp +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.res.MR +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource +import kotlinx.coroutines.flow.MutableStateFlow + +@Composable +actual fun UserPickerInactiveUsersSection( + users: List, + stopped: Boolean, + onShowAllProfilesClicked: () -> Unit, + onUserClicked: (user: User) -> Unit, +) { + if (users.isNotEmpty()) { + val userRows = users.chunked(5) + val rowsToDisplay = if (userRows.size > 2) 2 else userRows.size + val horizontalPadding = DEFAULT_PADDING_HALF + 8.dp + + Column(Modifier + .padding(horizontal = horizontalPadding, vertical = DEFAULT_PADDING_HALF) + .height(55.dp * rowsToDisplay + (if (rowsToDisplay > 1) DEFAULT_PADDING else 0.dp)) + ) { + ColumnWithScrollBar( + verticalArrangement = Arrangement.spacedBy(DEFAULT_PADDING) + ) { + val spaceBetween = (((DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier) - (horizontalPadding)) - (55.dp * 5)) / 5 + + userRows.forEach { row -> + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(spaceBetween), + ) { + row.forEach { u -> + UserPickerInactiveUserBadge(u, stopped, size = 55.dp) { + onUserClicked(u.user) + } + } + } + } + } + } + } + + UserPickerOptionRow( + painterResource(MR.images.ic_manage_accounts), + stringResource(MR.strings.your_chat_profiles), + onShowAllProfilesClicked + ) +} + +@Composable +actual fun PlatformUserPicker(modifier: Modifier, pickerState: MutableStateFlow, content: @Composable () -> Unit) { + AnimatedVisibility( + visible = pickerState.value.isVisible(), + enter = fadeIn(), + exit = fadeOut() + ) { + Box( + Modifier + .fillMaxSize() + .clickable(interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = { pickerState.value = AnimatedViewState.HIDING }), + contentAlignment = Alignment.TopStart + ) { + ColumnWithScrollBar(modifier) { + content() + } + } + } +} \ No newline at end of file diff --git a/apps/simplex-bot-advanced/Main.hs b/apps/simplex-bot-advanced/Main.hs index 50d7005e34..4733dafb79 100644 --- a/apps/simplex-bot-advanced/Main.hs +++ b/apps/simplex-bot-advanced/Main.hs @@ -46,7 +46,7 @@ mySquaringBot _user cc = do CRContactConnected _ contact _ -> do contactConnected contact sendMessage cc contact welcomeMessage - CRNewChatItem _ (AChatItem _ SMDRcv (DirectChat contact) ChatItem {content = mc@CIRcvMsgContent {}}) -> do + CRNewChatItems {chatItems = (AChatItem _ SMDRcv (DirectChat contact) ChatItem {content = mc@CIRcvMsgContent {}}) : _} -> do let msg = T.unpack $ ciContentToText mc number_ = readMaybe msg :: Maybe Integer sendMessage cc contact $ case number_ of diff --git a/apps/simplex-broadcast-bot/src/Broadcast/Bot.hs b/apps/simplex-broadcast-bot/src/Broadcast/Bot.hs index 5fa3fff0a7..da021ee0b5 100644 --- a/apps/simplex-broadcast-bot/src/Broadcast/Bot.hs +++ b/apps/simplex-broadcast-bot/src/Broadcast/Bot.hs @@ -40,7 +40,7 @@ broadcastBot BroadcastBotOpts {publishers, welcomeMessage, prohibitedMessage} _u CRContactConnected _ ct _ -> do contactConnected ct sendMessage cc ct welcomeMessage - CRNewChatItem _ (AChatItem _ SMDRcv (DirectChat ct) ci@ChatItem {content = CIRcvMsgContent mc}) + CRNewChatItems {chatItems = (AChatItem _ SMDRcv (DirectChat ct) ci@ChatItem {content = CIRcvMsgContent mc}) : _} | publisher `elem` publishers -> if allowContent mc then do diff --git a/apps/simplex-directory-service/src/Directory/Events.hs b/apps/simplex-directory-service/src/Directory/Events.hs index 33b43a239b..64e6acf1d8 100644 --- a/apps/simplex-directory-service/src/Directory/Events.hs +++ b/apps/simplex-directory-service/src/Directory/Events.hs @@ -73,7 +73,7 @@ crDirectoryEvent = \case CRGroupDeleted {groupInfo} -> Just $ DEGroupDeleted groupInfo CRChatItemUpdated {chatItem = AChatItem _ SMDRcv (DirectChat ct) _} -> Just $ DEItemEditIgnored ct CRChatItemsDeleted {chatItemDeletions = ((ChatItemDeletion (AChatItem _ SMDRcv (DirectChat ct) _) _) : _), byUser = False} -> Just $ DEItemDeleteIgnored ct - CRNewChatItem {chatItem = AChatItem _ SMDRcv (DirectChat ct) ci@ChatItem {content = CIRcvMsgContent mc, meta = CIMeta {itemLive}}} -> + CRNewChatItems {chatItems = (AChatItem _ SMDRcv (DirectChat ct) ci@ChatItem {content = CIRcvMsgContent mc, meta = CIMeta {itemLive}}) : _} -> Just $ case (mc, itemLive) of (MCText t, Nothing) -> DEContactCommand ct ciId $ fromRight err $ A.parseOnly (directoryCmdP <* A.endOfInput) $ T.dropWhileEnd isSpace t _ -> DEUnsupportedMessage ct ciId diff --git a/cabal.project b/cabal.project index 92f5d475e9..7fb9f6d353 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: fa772af6c63fab8f04d9d32d8e8397d75d7d0391 + tag: 309ef3766cc6b69e0c3aa0c140faab25383b732a source-repository-package type: git diff --git a/docs/DOWNLOADS.md b/docs/DOWNLOADS.md index 06850e76d2..df22ff64a2 100644 --- a/docs/DOWNLOADS.md +++ b/docs/DOWNLOADS.md @@ -1,14 +1,12 @@ --- title: Download SimpleX apps permalink: /downloads/index.html -revision: 03.07.2024 +revision: 09.09.2024 --- -| Updated 03.07.2024 | Languages: EN | +| Updated 09.09.2024 | Languages: EN | # Download SimpleX apps -The latest stable version is v5.8. - You can get the latest beta releases from [GitHub](https://github.com/simplex-chat/simplex-chat/releases). - [desktop](#desktop-app) diff --git a/docs/TRANSPARENCY.md b/docs/TRANSPARENCY.md index 43fdd12ac5..bae7a4f781 100644 --- a/docs/TRANSPARENCY.md +++ b/docs/TRANSPARENCY.md @@ -17,8 +17,8 @@ This page will include any and all reports on requests for user data. Our objective is to consistently ensure that no user data and absolute minimum of the metadata required for the network to function is available for disclosure by any infrastructure operators, under any circumstances. **Helpful resources**: -- [Privacy policy](../PRIVACY.md) -- [Privacy and security: technical details and limitations](../README.md#privacy-and-security-technical-details-and-limitations) +- [Privacy policy](/PRIVACY.md) +- [Privacy and security: technical details and limitations](https://github.com/simplex-chat/simplex-chat/blob/stable/README.md#privacy-and-security-technical-details-and-limitations) - Whitepaper: - [Trust in servers](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md#trust-in-servers) - [Encryption Primitives Used](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md#encryption-primitives-used) diff --git a/package.yaml b/package.yaml index 947589acd0..3d5422612a 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: simplex-chat -version: 6.0.4.0 +version: 6.1.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 c3b85f9b53..52dd21e8cd 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."fa772af6c63fab8f04d9d32d8e8397d75d7d0391" = "07d0f89msb6p05y67q90ky9jr1rygg7v3xlkga7y255mmpjsqbip"; + "https://github.com/simplex-chat/simplexmq.git"."309ef3766cc6b69e0c3aa0c140faab25383b732a" = "1ch03kizvsq3m5jwravyil529mc0lcfwj43czb1nhykbg8yb3cjv"; "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 b3cde5ae9f..b58285a349 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.0.4.0 +version: 6.1.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.hs b/src/Simplex/Chat.hs index d20499455a..9157ac7509 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -38,10 +38,11 @@ import Data.Char import Data.Constraint (Dict (..)) import Data.Either (fromRight, lefts, partitionEithers, rights) import Data.Fixed (div') +import Data.Foldable (foldr') import Data.Functor (($>)) import Data.Functor.Identity import Data.Int (Int64) -import Data.List (find, foldl', isSuffixOf, mapAccumL, partition, sortOn) +import Data.List (find, foldl', isSuffixOf, mapAccumL, partition, sortOn, zipWith4) import Data.List.NonEmpty (NonEmpty (..), nonEmpty, toList, (<|)) import qualified Data.List.NonEmpty as L import Data.Map.Strict (Map) @@ -54,9 +55,9 @@ import Data.Text.Encoding (decodeLatin1, encodeUtf8) import Data.Time (NominalDiffTime, addUTCTime, defaultTimeLocale, formatTime) import Data.Time.Clock (UTCTime, diffUTCTime, getCurrentTime, nominalDay, nominalDiffTimeToSeconds) import Data.Time.Clock.System (systemToUTCTime) -import Data.Word (Word32) import qualified Data.UUID as UUID import qualified Data.UUID.V4 as V4 +import Data.Word (Word32) import qualified Database.SQLite.Simple as SQL import Simplex.Chat.Archive import Simplex.Chat.Call @@ -765,18 +766,18 @@ processChatCommand' vr = \case Just (CIFFGroup _ _ (Just gId) (Just fwdItemId)) -> Just <$> withFastStore (\db -> getAChatItem db vr user (ChatRef CTGroup gId) fwdItemId) _ -> pure Nothing - APISendMessage (ChatRef cType chatId) live itemTTL cm -> withUser $ \user -> case cType of + APISendMessages (ChatRef cType chatId) live itemTTL cms -> withUser $ \user -> case cType of CTDirect -> withContactLock "sendMessage" chatId $ - sendContactContentMessage user chatId live itemTTL cm Nothing + sendContactContentMessages user chatId live itemTTL (L.map (,Nothing) cms) CTGroup -> withGroupLock "sendMessage" chatId $ - sendGroupContentMessage user chatId live itemTTL cm Nothing + sendGroupContentMessages user chatId live itemTTL (L.map (,Nothing) cms) CTLocal -> pure $ chatCmdError (Just user) "not supported" CTContactRequest -> pure $ chatCmdError (Just user) "not supported" CTContactConnection -> pure $ chatCmdError (Just user) "not supported" - APICreateChatItem folderId cm -> withUser $ \user -> - createNoteFolderContentItem user folderId cm Nothing + APICreateChatItems folderId cms -> withUser $ \user -> + createNoteFolderContentItems user folderId (L.map (,Nothing) cms) APIUpdateChatItem (ChatRef cType chatId) itemId live mc -> withUser $ \user -> case cType of CTDirect -> withContactLock "updateChatItem" chatId $ do ct@Contact {contactId} <- withFastStore $ \db -> getContact db vr user chatId @@ -815,7 +816,7 @@ processChatCommand' vr = \case let changed = mc /= oldMC if changed || fromMaybe False itemLive then do - (SndMessage {msgId}, _) <- sendGroupMessage user gInfo ms (XMsgUpdate itemSharedMId mc (ttl' <$> itemTimed) (justTrue . (live &&) =<< itemLive)) + SndMessage {msgId} <- sendGroupMessage user gInfo ms (XMsgUpdate itemSharedMId mc (ttl' <$> itemTimed) (justTrue . (live &&) =<< itemLive)) ci' <- withFastStore' $ \db -> do currentTs <- liftIO getCurrentTime when changed $ @@ -842,9 +843,7 @@ processChatCommand' vr = \case CTContactConnection -> pure $ chatCmdError (Just user) "not supported" APIDeleteChatItem (ChatRef cType chatId) itemIds mode -> withUser $ \user -> case cType of CTDirect -> withContactLock "deleteChatItem" chatId $ do - ct <- withStore $ \db -> getContact db vr user chatId - (errs, items) <- lift $ partitionEithers <$> withStoreBatch (\db -> map (getDirectCI db) (L.toList itemIds)) - unless (null errs) $ toView $ CRChatErrors (Just user) errs + (ct, items) <- getCommandDirectChatItems user chatId itemIds case mode of CIDMInternal -> deleteDirectCIs user ct items True False CIDMBroadcast -> do @@ -857,13 +856,9 @@ processChatCommand' vr = \case if featureAllowed SCFFullDelete forUser ct then deleteDirectCIs user ct items True False else markDirectCIsDeleted user ct items True =<< liftIO getCurrentTime - where - getDirectCI :: DB.Connection -> ChatItemId -> IO (Either ChatError (CChatItem 'CTDirect)) - getDirectCI db itemId = runExceptT . withExceptT ChatErrorStore $ getDirectChatItem db user chatId itemId CTGroup -> withGroupLock "deleteChatItem" chatId $ do - Group gInfo ms <- withStore $ \db -> getGroup db vr user chatId - (errs, items) <- lift $ partitionEithers <$> withStoreBatch (\db -> map (getGroupCI db) (L.toList itemIds)) - unless (null errs) $ toView $ CRChatErrors (Just user) errs + (gInfo, items) <- getCommandGroupChatItems user chatId itemIds + ms <- withFastStore' $ \db -> getGroupMembers db vr user gInfo case mode of CIDMInternal -> deleteGroupCIs user gInfo items True False Nothing =<< liftIO getCurrentTime CIDMBroadcast -> do @@ -873,17 +868,9 @@ processChatCommand' vr = \case events = L.nonEmpty $ map (`XMsgDel` Nothing) msgIds mapM_ (sendGroupMessages user gInfo ms) events delGroupChatItems user gInfo items Nothing - where - getGroupCI :: DB.Connection -> ChatItemId -> IO (Either ChatError (CChatItem 'CTGroup)) - getGroupCI db itemId = runExceptT . withExceptT ChatErrorStore $ getGroupChatItem db user chatId itemId CTLocal -> do - nf <- withStore $ \db -> getNoteFolder db user chatId - (errs, items) <- lift $ partitionEithers <$> withStoreBatch (\db -> map (getLocalCI db) (L.toList itemIds)) - unless (null errs) $ toView $ CRChatErrors (Just user) errs + (nf, items) <- getCommandLocalChatItems user chatId itemIds deleteLocalCIs user nf items True False - where - getLocalCI :: DB.Connection -> ChatItemId -> IO (Either ChatError (CChatItem 'CTLocal)) - getLocalCI db itemId = runExceptT . withExceptT ChatErrorStore $ getLocalChatItem db user chatId itemId CTContactRequest -> pure $ chatCmdError (Just user) "not supported" CTContactConnection -> pure $ chatCmdError (Just user) "not supported" where @@ -901,9 +888,8 @@ processChatCommand' vr = \case itemsMsgIds :: [CChatItem c] -> [SharedMsgId] itemsMsgIds = mapMaybe (\(CChatItem _ ChatItem {meta = CIMeta {itemSharedMsgId}}) -> itemSharedMsgId) APIDeleteMemberChatItem gId itemIds -> withUser $ \user -> withGroupLock "deleteChatItem" gId $ do - Group gInfo@GroupInfo {membership} ms <- withStore $ \db -> getGroup db vr user gId - (errs, items) <- lift $ partitionEithers <$> withStoreBatch (\db -> map (getGroupCI db user) (L.toList itemIds)) - unless (null errs) $ toView $ CRChatErrors (Just user) errs + (gInfo@GroupInfo {membership}, items) <- getCommandGroupChatItems user gId itemIds + ms <- withFastStore' $ \db -> getGroupMembers db vr user gInfo assertDeletable gInfo items assertUserGroupRole gInfo GRAdmin let msgMemIds = itemsMsgMemIds gInfo items @@ -911,8 +897,6 @@ processChatCommand' vr = \case mapM_ (sendGroupMessages user gInfo ms) events delGroupChatItems user gInfo items (Just membership) where - getGroupCI :: DB.Connection -> User -> ChatItemId -> IO (Either ChatError (CChatItem 'CTGroup)) - getGroupCI db user itemId = runExceptT . withExceptT ChatErrorStore $ getGroupChatItem db user gId itemId assertDeletable :: GroupInfo -> [CChatItem 'CTGroup] -> CM () assertDeletable GroupInfo {membership = GroupMember {memberRole = membershipMemRole}} items = unless (all itemDeletable items) $ throwChatError CEInvalidChatItemDelete @@ -961,7 +945,7 @@ processChatCommand' vr = \case let GroupMember {memberId = itemMemberId} = chatItemMember g ci rs <- withFastStore' $ \db -> getGroupReactions db g membership itemMemberId itemSharedMId True checkReactionAllowed rs - (SndMessage {msgId}, _) <- sendGroupMessage user g ms (XMsgReact itemSharedMId (Just itemMemberId) reaction add) + SndMessage {msgId} <- sendGroupMessage user g ms (XMsgReact itemSharedMId (Just itemMemberId) reaction add) createdAt <- liftIO getCurrentTime reactions <- withFastStore' $ \db -> do setGroupReaction db g membership itemMemberId itemSharedMId True reaction add msgId createdAt @@ -979,76 +963,129 @@ processChatCommand' vr = \case throwChatError (CECommandError $ "reaction already " <> if add then "added" else "removed") when (add && length rs >= maxMsgReactions) $ throwChatError (CECommandError "too many reactions") - APIForwardChatItem (ChatRef toCType toChatId) (ChatRef fromCType fromChatId) itemId itemTTL -> withUser $ \user -> case toCType of - CTDirect -> do - (cm, ciff) <- prepareForward user - withContactLock "forwardChatItem, to contact" toChatId $ - sendContactContentMessage user toChatId False itemTTL cm ciff - CTGroup -> do - (cm, ciff) <- prepareForward user - withGroupLock "forwardChatItem, to group" toChatId $ - sendGroupContentMessage user toChatId False itemTTL cm ciff - CTLocal -> do - (cm, ciff) <- prepareForward user - createNoteFolderContentItem user toChatId cm ciff + 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 + CTLocal -> planForward user . snd =<< getCommandLocalChatItems user fromChatId itemIds CTContactRequest -> pure $ chatCmdError (Just user) "not supported" CTContactConnection -> pure $ chatCmdError (Just user) "not supported" where - prepareForward :: User -> CM (ComposedMessage, Maybe CIForwardedFrom) + planForward :: User -> [CChatItem c] -> CM ChatResponse + planForward user items = do + (itemIds', forwardErrors) <- unzip <$> mapM planItemForward items + let forwardConfirmation = case catMaybes forwardErrors of + [] -> Nothing + errs -> Just $ case mainErr of + FFENotAccepted _ -> FCFilesNotAccepted fileIds + FFEInProgress -> FCFilesInProgress filesCount + FFEMissing -> FCFilesMissing filesCount + FFEFailed -> FCFilesFailed filesCount + where + mainErr = minimum errs + fileIds = catMaybes $ map (\case FFENotAccepted ftId -> Just ftId; _ -> Nothing) errs + filesCount = length $ filter (mainErr ==) errs + pure CRForwardPlan {user, itemsCount = length itemIds, chatItemIds = catMaybes itemIds', forwardConfirmation} + where + planItemForward :: CChatItem c -> CM (Maybe ChatItemId, Maybe ForwardFileError) + planItemForward (CChatItem _ ci) = forwardMsgContent ci >>= maybe (pure (Nothing, Nothing)) (forwardContentPlan ci) + forwardContentPlan :: ChatItem c d -> MsgContent -> CM (Maybe ChatItemId, Maybe ForwardFileError) + forwardContentPlan ChatItem {file, meta = CIMeta {itemId}} mc = case file of + Nothing -> pure (Just itemId, Nothing) + Just CIFile {fileId, fileStatus, fileSource} -> case ciFileForwardError fileId fileStatus of + Just err -> pure $ itemIdWithoutFile err + Nothing -> case fileSource of + Just CryptoFile {filePath} -> do + exists <- doesFileExist =<< lift (toFSFilePath filePath) + pure $ if exists then (Just itemId, Nothing) else itemIdWithoutFile FFEMissing + Nothing -> pure $ itemIdWithoutFile FFEMissing + where + itemIdWithoutFile err = (if hasContent then Just itemId else Nothing, Just err) + hasContent = case mc of + MCText _ -> True + MCLink {} -> True + MCImage {} -> True + MCVideo {text} -> text /= "" + MCVoice {text} -> text /= "" + MCFile t -> t /= "" + MCUnknown {} -> True + APIForwardChatItems (ChatRef toCType toChatId) (ChatRef fromCType fromChatId) itemIds itemTTL -> withUser $ \user -> case toCType of + CTDirect -> do + cmrs <- prepareForward user + case L.nonEmpty cmrs of + Just cmrs' -> + withContactLock "forwardChatItem, to contact" toChatId $ + sendContactContentMessages user toChatId False itemTTL cmrs' + Nothing -> pure $ CRNewChatItems user [] + CTGroup -> do + cmrs <- prepareForward user + case L.nonEmpty cmrs of + Just cmrs' -> + withGroupLock "forwardChatItem, to group" toChatId $ + sendGroupContentMessages user toChatId False itemTTL cmrs' + Nothing -> pure $ CRNewChatItems user [] + CTLocal -> do + cmrs <- prepareForward user + case L.nonEmpty cmrs of + Just cmrs' -> + createNoteFolderContentItems user toChatId cmrs' + Nothing -> pure $ CRNewChatItems user [] + CTContactRequest -> pure $ chatCmdError (Just user) "not supported" + CTContactConnection -> pure $ chatCmdError (Just user) "not supported" + where + prepareForward :: User -> CM [ComposeMessageReq] prepareForward user = case fromCType of CTDirect -> withContactLock "forwardChatItem, from contact" fromChatId $ do - (ct, CChatItem _ ci) <- withFastStore $ \db -> do - ct <- getContact db vr user fromChatId - cci <- getDirectChatItem db user fromChatId itemId - pure (ct, cci) - (mc, mDir) <- forwardMC ci - file <- forwardCryptoFile ci - let ciff = forwardCIFF ci $ Just (CIFFContact (forwardName ct) mDir (Just fromChatId) (Just itemId)) - pure (ComposedMessage file Nothing mc, ciff) + (ct, items) <- getCommandDirectChatItems user fromChatId itemIds + catMaybes <$> mapM (\ci -> ciComposeMsgReq ct ci <$$> prepareMsgReq ci) items where - forwardName :: Contact -> ContactName - forwardName Contact {profile = LocalProfile {displayName, localAlias}} - | localAlias /= "" = localAlias - | otherwise = displayName + ciComposeMsgReq :: Contact -> CChatItem 'CTDirect -> (MsgContent, Maybe CryptoFile) -> ComposeMessageReq + ciComposeMsgReq ct (CChatItem md ci) (mc', file) = + let itemId = chatItemId' ci + ciff = forwardCIFF ci $ Just (CIFFContact (forwardName ct) (toMsgDirection md) (Just fromChatId) (Just itemId)) + in (ComposedMessage file Nothing mc', ciff) + where + forwardName :: Contact -> ContactName + forwardName Contact {profile = LocalProfile {displayName, localAlias}} + | localAlias /= "" = localAlias + | otherwise = displayName CTGroup -> withGroupLock "forwardChatItem, from group" fromChatId $ do - (gInfo, CChatItem _ ci) <- withFastStore $ \db -> do - gInfo <- getGroupInfo db vr user fromChatId - cci <- getGroupChatItem db user fromChatId itemId - pure (gInfo, cci) - (mc, mDir) <- forwardMC ci - file <- forwardCryptoFile ci - let ciff = forwardCIFF ci $ Just (CIFFGroup (forwardName gInfo) mDir (Just fromChatId) (Just itemId)) - pure (ComposedMessage file Nothing mc, ciff) + (gInfo, items) <- getCommandGroupChatItems user fromChatId itemIds + catMaybes <$> mapM (\ci -> ciComposeMsgReq gInfo ci <$$> prepareMsgReq ci) items where - forwardName :: GroupInfo -> ContactName - forwardName GroupInfo {groupProfile = GroupProfile {displayName}} = displayName + ciComposeMsgReq :: GroupInfo -> CChatItem 'CTGroup -> (MsgContent, Maybe CryptoFile) -> ComposeMessageReq + ciComposeMsgReq gInfo (CChatItem md ci) (mc', file) = do + let itemId = chatItemId' ci + ciff = forwardCIFF ci $ Just (CIFFGroup (forwardName gInfo) (toMsgDirection md) (Just fromChatId) (Just itemId)) + in (ComposedMessage file Nothing mc', ciff) + where + forwardName :: GroupInfo -> ContactName + forwardName GroupInfo {groupProfile = GroupProfile {displayName}} = displayName CTLocal -> do - (CChatItem _ ci) <- withFastStore $ \db -> getLocalChatItem db user fromChatId itemId - (mc, _) <- forwardMC ci - file <- forwardCryptoFile ci - let ciff = forwardCIFF ci Nothing - pure (ComposedMessage file Nothing mc, ciff) + (_, items) <- getCommandLocalChatItems user fromChatId itemIds + catMaybes <$> mapM (\ci -> ciComposeMsgReq ci <$$> prepareMsgReq ci) items + where + ciComposeMsgReq :: CChatItem 'CTLocal -> (MsgContent, Maybe CryptoFile) -> ComposeMessageReq + ciComposeMsgReq (CChatItem _ ci) (mc', file) = + let ciff = forwardCIFF ci Nothing + in (ComposedMessage file Nothing mc', ciff) CTContactRequest -> throwChatError $ CECommandError "not supported" CTContactConnection -> throwChatError $ CECommandError "not supported" where - forwardMC :: ChatItem c d -> CM (MsgContent, MsgDirection) - forwardMC ChatItem {meta = CIMeta {itemDeleted = Just _}} = throwChatError CEInvalidForward - forwardMC ChatItem {content = CISndMsgContent fmc} = pure (fmc, MDSnd) - forwardMC ChatItem {content = CIRcvMsgContent fmc} = pure (fmc, MDRcv) - forwardMC _ = throwChatError CEInvalidForward + prepareMsgReq :: CChatItem c -> CM (Maybe (MsgContent, Maybe CryptoFile)) + prepareMsgReq (CChatItem _ ci) = forwardMsgContent ci $>>= forwardContent ci forwardCIFF :: ChatItem c d -> Maybe CIForwardedFrom -> Maybe CIForwardedFrom forwardCIFF ChatItem {meta = CIMeta {itemForwarded}} ciff = case itemForwarded of Nothing -> ciff Just CIFFUnknown -> ciff Just prevCIFF -> Just prevCIFF - forwardCryptoFile :: ChatItem c d -> CM (Maybe CryptoFile) - forwardCryptoFile ChatItem {file = Nothing} = pure Nothing - forwardCryptoFile ChatItem {file = Just ciFile} = case ciFile of - CIFile {fileName, fileStatus, fileSource = Just fromCF@CryptoFile {filePath}} + forwardContent :: ChatItem c d -> MsgContent -> CM (Maybe (MsgContent, Maybe CryptoFile)) + forwardContent ChatItem {file} mc = case file of + Nothing -> pure $ Just (mc, Nothing) + Just CIFile {fileName, fileStatus, fileSource = Just fromCF@CryptoFile {filePath}} | ciFileLoaded fileStatus -> chatReadVar filesFolder >>= \case Nothing -> - ifM (doesFileExist filePath) (pure $ Just fromCF) (throwChatError CEForwardNoFile) + ifM (doesFileExist filePath) (pure $ Just (mc, Just fromCF)) (pure contentWithoutFile) Just filesFolder -> do let fsFromPath = filesFolder filePath ifM @@ -1061,10 +1098,17 @@ processChatCommand' vr = \case let toCF = CryptoFile fsNewPath cfArgs -- to keep forwarded file in case original is deleted liftIOEither $ runExceptT $ withExceptT (ChatError . CEInternalError . show) $ copyCryptoFile (fromCF {filePath = fsFromPath} :: CryptoFile) toCF - pure $ Just (toCF {filePath = takeFileName fsNewPath} :: CryptoFile) + pure $ Just (mc, Just (toCF {filePath = takeFileName fsNewPath} :: CryptoFile)) ) - (throwChatError CEForwardNoFile) - _ -> throwChatError CEForwardNoFile + (pure contentWithoutFile) + _ -> pure contentWithoutFile + where + contentWithoutFile = case mc of + MCImage {} -> Just (mc, Nothing) + MCLink {} -> Just (mc, Nothing) + _ | contentText /= "" -> Just (MCText contentText, Nothing) + _ -> Nothing + contentText = msgContentText mc copyCryptoFile :: CryptoFile -> CryptoFile -> ExceptT CF.FTCryptoError IO () copyCryptoFile fromCF@CryptoFile {filePath = fsFromPath, cryptoArgs = fromArgs} toCF@CryptoFile {cryptoArgs = toArgs} = do fromSizeFull <- getFileSize fsFromPath @@ -1086,26 +1130,24 @@ 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 cType chatId) fromToIds -> withUser $ \_ -> case cType of + APIChatRead chatRef@(ChatRef cType chatId) fromToIds -> withUser $ \_ -> case cType of CTDirect -> do user <- withFastStore $ \db -> getUserByContactId db chatId - timedItems <- withFastStore' $ \db -> getDirectUnreadTimedItems db user chatId fromToIds ts <- liftIO getCurrentTime - forM_ timedItems $ \(itemId, ttl) -> do - let deleteAt = addUTCTime (realToFrac ttl) ts - withFastStore' $ \db -> setDirectChatItemDeleteAt db user chatId itemId deleteAt - startProximateTimedItemThread user (ChatRef CTDirect chatId, itemId) deleteAt - withFastStore' $ \db -> updateDirectChatItemsRead db user chatId fromToIds + timedItems <- withFastStore' $ \db -> do + timedItems <- getDirectUnreadTimedItems db user chatId fromToIds + updateDirectChatItemsRead db user chatId fromToIds + setDirectChatItemsDeleteAt db user chatId timedItems ts + forM_ timedItems $ \(itemId, deleteAt) -> startProximateTimedItemThread user (chatRef, itemId) deleteAt ok user CTGroup -> do - user@User {userId} <- withFastStore $ \db -> getUserByGroupId db chatId - timedItems <- withFastStore' $ \db -> getGroupUnreadTimedItems db user chatId fromToIds + user <- withFastStore $ \db -> getUserByGroupId db chatId ts <- liftIO getCurrentTime - forM_ timedItems $ \(itemId, ttl) -> do - let deleteAt = addUTCTime (realToFrac ttl) ts - withFastStore' $ \db -> setGroupChatItemDeleteAt db user chatId itemId deleteAt - startProximateTimedItemThread user (ChatRef CTGroup chatId, itemId) deleteAt - withFastStore' $ \db -> updateGroupChatItemsRead db userId chatId fromToIds + timedItems <- withFastStore' $ \db -> do + timedItems <- getGroupUnreadTimedItems db user chatId fromToIds + updateGroupChatItemsRead db user chatId fromToIds + 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 @@ -1113,6 +1155,24 @@ processChatCommand' vr = \case ok user CTContactRequest -> pure $ chatCmdError Nothing "not supported" CTContactConnection -> pure $ chatCmdError Nothing "not supported" + APIChatItemsRead chatRef@(ChatRef cType chatId) itemIds -> withUser $ \_ -> case cType of + CTDirect -> do + user <- withFastStore $ \db -> getUserByContactId db chatId + timedItems <- withFastStore' $ \db -> do + timedItems <- updateDirectChatItemsReadList db user chatId itemIds + setDirectChatItemsDeleteAt db user chatId timedItems =<< getCurrentTime + forM_ timedItems $ \(itemId, deleteAt) -> startProximateTimedItemThread user (chatRef, itemId) deleteAt + ok user + CTGroup -> do + user <- withFastStore $ \db -> getUserByGroupId db chatId + timedItems <- withFastStore' $ \db -> do + timedItems <- updateGroupChatItemsReadList db user chatId itemIds + setGroupChatItemsDeleteAt db user chatId timedItems =<< getCurrentTime + forM_ timedItems $ \(itemId, deleteAt) -> startProximateTimedItemThread user (chatRef, itemId) deleteAt + ok user + CTLocal -> pure $ chatCmdError Nothing "not supported" + CTContactRequest -> pure $ chatCmdError Nothing "not supported" + CTContactConnection -> pure $ chatCmdError Nothing "not supported" APIChatUnread (ChatRef cType chatId) unreadChat -> withUser $ \user -> case cType of CTDirect -> do withFastStore $ \db -> do @@ -1274,7 +1334,7 @@ processChatCommand' vr = \case let call' = Call {contactId, callId, callUUID, chatItemId = chatItemId' ci, callState, callTs = chatItemTs' ci} call_ <- atomically $ TM.lookupInsert contactId call' calls forM_ call_ $ \call -> updateCallItemStatus user ct call WCSDisconnected Nothing - toView $ CRNewChatItem user (AChatItem SCTDirect SMDSnd (DirectChat ct) ci) + toView $ CRNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct) ci] ok user else pure $ chatCmdError (Just user) ("feature not allowed " <> T.unpack (chatFeatureNameText CFCalls)) SendCallInvitation cName callType -> withUser $ \user -> do @@ -1684,7 +1744,7 @@ processChatCommand' vr = \case pure conn' recreateConn user conn@PendingContactConnection {customUserProfileId} newUser = do subMode <- chatReadVar subscriptionMode - (agConnId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation Nothing IKPQOn subMode + (agConnId, cReq) <- withAgent $ \a -> createConnection a (aUserId newUser) True SCMInvitation Nothing IKPQOn subMode conn' <- withFastStore' $ \db -> do deleteConnectionRecord db user connId forM_ customUserProfileId $ \profileId -> @@ -1787,17 +1847,17 @@ processChatCommand' vr = \case contactId <- withFastStore $ \db -> getContactIdByName db user fromContactName forwardedItemId <- withFastStore $ \db -> getDirectChatItemIdByText' db user contactId forwardedMsg toChatRef <- getChatRef user toChatName - processChatCommand $ APIForwardChatItem toChatRef (ChatRef CTDirect contactId) forwardedItemId Nothing + processChatCommand $ APIForwardChatItems toChatRef (ChatRef CTDirect contactId) (forwardedItemId :| []) Nothing ForwardGroupMessage toChatName fromGroupName fromMemberName_ forwardedMsg -> withUser $ \user -> do groupId <- withFastStore $ \db -> getGroupIdByName db user fromGroupName forwardedItemId <- withFastStore $ \db -> getGroupChatItemIdByText db user groupId fromMemberName_ forwardedMsg toChatRef <- getChatRef user toChatName - processChatCommand $ APIForwardChatItem toChatRef (ChatRef CTGroup groupId) forwardedItemId Nothing + processChatCommand $ APIForwardChatItems toChatRef (ChatRef CTGroup groupId) (forwardedItemId :| []) Nothing ForwardLocalMessage toChatName forwardedMsg -> withUser $ \user -> do folderId <- withFastStore (`getUserNoteFolderId` user) forwardedItemId <- withFastStore $ \db -> getLocalChatItemIdByText' db user folderId forwardedMsg toChatRef <- getChatRef user toChatName - processChatCommand $ APIForwardChatItem toChatRef (ChatRef CTLocal folderId) forwardedItemId Nothing + processChatCommand $ APIForwardChatItems toChatRef (ChatRef CTLocal folderId) (forwardedItemId :| []) Nothing SendMessage (ChatName cType name) msg -> withUser $ \user -> do let mc = MCText msg case cType of @@ -1805,7 +1865,7 @@ processChatCommand' vr = \case withFastStore' (\db -> runExceptT $ getContactIdByName db user name) >>= \case Right ctId -> do let chatRef = ChatRef CTDirect ctId - processChatCommand . APISendMessage chatRef False Nothing $ ComposedMessage Nothing Nothing mc + processChatCommand $ APISendMessages chatRef False Nothing (ComposedMessage Nothing Nothing mc :| []) Left _ -> withFastStore' (\db -> runExceptT $ getActiveMembersByName db vr user name) >>= \case Right [(gInfo, member)] -> do @@ -1819,11 +1879,11 @@ processChatCommand' vr = \case CTGroup -> do gId <- withFastStore $ \db -> getGroupIdByName db user name let chatRef = ChatRef CTGroup gId - processChatCommand . APISendMessage chatRef False Nothing $ ComposedMessage Nothing Nothing mc + processChatCommand $ APISendMessages chatRef False Nothing (ComposedMessage Nothing Nothing mc :| []) CTLocal | name == "" -> do folderId <- withFastStore (`getUserNoteFolderId` user) - processChatCommand . APICreateChatItem folderId $ ComposedMessage Nothing Nothing mc + processChatCommand $ APICreateChatItems folderId (ComposedMessage Nothing Nothing mc :| []) | otherwise -> throwChatError $ CECommandError "not supported" _ -> throwChatError $ CECommandError "not supported" SendMemberContactMessage gName mName msg -> withUser $ \user -> do @@ -1842,11 +1902,11 @@ processChatCommand' vr = \case cr -> pure cr Just ctId -> do let chatRef = ChatRef CTDirect ctId - processChatCommand . APISendMessage chatRef False Nothing $ ComposedMessage Nothing Nothing mc + processChatCommand $ APISendMessages chatRef False Nothing (ComposedMessage Nothing Nothing mc :| []) SendLiveMessage chatName msg -> withUser $ \user -> do chatRef <- getChatRef user chatName let mc = MCText msg - processChatCommand . APISendMessage chatRef True Nothing $ ComposedMessage Nothing Nothing mc + processChatCommand $ APISendMessages chatRef True Nothing (ComposedMessage Nothing Nothing mc :| []) SendMessageBroadcast msg -> withUser $ \user -> do contacts <- withFastStore' $ \db -> getUserContacts db vr user withChatLock "sendMessageBroadcast" . procCmd $ do @@ -1887,7 +1947,7 @@ processChatCommand' vr = \case contactId <- withFastStore $ \db -> getContactIdByName db user cName quotedItemId <- withFastStore $ \db -> getDirectChatItemIdByText db userId contactId msgDir quotedMsg let mc = MCText msg - processChatCommand . APISendMessage (ChatRef CTDirect contactId) False Nothing $ ComposedMessage Nothing (Just quotedItemId) mc + processChatCommand $ APISendMessages (ChatRef CTDirect contactId) False Nothing (ComposedMessage Nothing (Just quotedItemId) mc :| []) DeleteMessage chatName deletedMsg -> withUser $ \user -> do chatRef <- getChatRef user chatName deletedItemId <- getSentChatItemIdByText user chatRef deletedMsg @@ -1998,9 +2058,9 @@ processChatCommand' vr = \case (Just ct, Just cReq) -> sendGrpInvitation user ct gInfo (m :: GroupMember) {memberRole = memRole} cReq _ -> throwChatError $ CEGroupCantResendInvitation gInfo cName _ -> do - (msg, _) <- sendGroupMessage user gInfo members $ XGrpMemRole mId memRole + msg <- sendGroupMessage user gInfo members $ XGrpMemRole mId memRole ci <- saveSndChatItem user (CDGroupSnd gInfo) msg (CISndGroupEvent gEvent) - toView $ CRNewChatItem user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci) + toView $ CRNewChatItems user [AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci] pure CRMemberRoleUser {user, groupInfo = gInfo, member = m {memberRole = memRole}, fromRole = mRole, toRole = memRole} APIBlockMemberForAll groupId memberId blocked -> withUser $ \user -> do Group gInfo@GroupInfo {membership} members <- withFastStore $ \db -> getGroup db vr user groupId @@ -2017,7 +2077,7 @@ processChatCommand' vr = \case msg <- sendGroupMessage' user gInfo remainingMembers event let ciContent = CISndGroupEvent $ SGEMemberBlocked memberId (fromLocalProfile bmp) blocked ci <- saveSndChatItem user (CDGroupSnd gInfo) msg ciContent - toView $ CRNewChatItem user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci) + toView $ CRNewChatItems user [AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci] bm' <- withFastStore $ \db -> do liftIO $ updateGroupMemberBlocked db user groupId memberId mrs getGroupMember db vr user groupId memberId @@ -2039,9 +2099,9 @@ processChatCommand' vr = \case deleteMemberConnection user m withFastStore' $ \db -> deleteGroupMember db user m _ -> do - (msg, _) <- sendGroupMessage user gInfo members $ XGrpMemDel mId + msg <- sendGroupMessage user gInfo members $ XGrpMemDel mId ci <- saveSndChatItem user (CDGroupSnd gInfo) msg (CISndGroupEvent $ SGEMemberDeleted memberId (fromLocalProfile memberProfile)) - toView $ CRNewChatItem user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci) + toView $ CRNewChatItems user [AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci] deleteMemberConnection' user m True -- undeleted "member connected" chat item will prevent deletion of member record deleteOrUpdateMemberRecord user m @@ -2053,7 +2113,7 @@ processChatCommand' vr = \case cancelFilesInProgress user filesInfo msg <- sendGroupMessage' user gInfo members XGrpLeave ci <- saveSndChatItem user (CDGroupSnd gInfo) msg (CISndGroupEvent SGEUserLeft) - toView $ CRNewChatItem user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci) + toView $ CRNewChatItems user [AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci] -- TODO delete direct connections that were unused deleteGroupLinkIfExists user gInfo -- member records are not deleted to keep history @@ -2152,7 +2212,7 @@ processChatCommand' vr = \case let ct' = ct {contactGrpInvSent = True} forM_ msgContent_ $ \mc -> do ci <- saveSndChatItem user (CDDirectSnd ct') sndMsg (CISndMsgContent mc) - toView $ CRNewChatItem user (AChatItem SCTDirect SMDSnd (DirectChat ct') ci) + toView $ CRNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct') ci] pure $ CRNewMemberContactSentInv user ct' g m _ -> throwChatError CEGroupMemberNotActive CreateGroupLink gName mRole -> withUser $ \user -> do @@ -2171,7 +2231,7 @@ processChatCommand' vr = \case groupId <- withFastStore $ \db -> getGroupIdByName db user gName quotedItemId <- withFastStore $ \db -> getGroupChatItemIdByText db user groupId cName quotedMsg let mc = MCText msg - processChatCommand . APISendMessage (ChatRef CTGroup groupId) False Nothing $ ComposedMessage Nothing (Just quotedItemId) mc + processChatCommand $ APISendMessages (ChatRef CTGroup groupId) False Nothing (ComposedMessage Nothing (Just quotedItemId) mc :| []) ClearNoteFolder -> withUser $ \user -> do folderId <- withFastStore (`getUserNoteFolderId` user) processChatCommand $ APIClearChat (ChatRef CTLocal folderId) @@ -2211,8 +2271,8 @@ processChatCommand' vr = \case SendFile chatName f -> withUser $ \user -> do chatRef <- getChatRef user chatName case chatRef of - ChatRef CTLocal folderId -> processChatCommand . APICreateChatItem folderId $ ComposedMessage (Just f) Nothing (MCFile "") - _ -> processChatCommand . APISendMessage chatRef False Nothing $ ComposedMessage (Just f) Nothing (MCFile "") + ChatRef CTLocal folderId -> processChatCommand $ APICreateChatItems folderId (ComposedMessage (Just f) Nothing (MCFile "") :| []) + _ -> processChatCommand $ APISendMessages chatRef False Nothing (ComposedMessage (Just f) Nothing (MCFile "") :| []) SendImage chatName f@(CryptoFile fPath _) -> withUser $ \user -> do chatRef <- getChatRef user chatName filePath <- lift $ toFSFilePath fPath @@ -2220,7 +2280,7 @@ processChatCommand' vr = \case fileSize <- getFileSize filePath unless (fileSize <= maxImageSize) $ throwChatError CEFileImageSize {filePath} -- TODO include file description for preview - processChatCommand . APISendMessage chatRef False Nothing $ ComposedMessage (Just f) Nothing (MCImage "" fixedImagePreview) + processChatCommand $ APISendMessages chatRef False Nothing (ComposedMessage (Just f) Nothing (MCImage "" fixedImagePreview) :| []) ForwardFile chatName fileId -> forwardFile chatName fileId SendFile ForwardImage chatName fileId -> forwardFile chatName fileId SendImage SendFileDescription _chatName _f -> pure $ chatCmdError Nothing "TODO" @@ -2629,11 +2689,11 @@ processChatCommand' vr = \case assertUserGroupRole g GROwner when (n /= n') $ checkValidName n' g' <- withStore $ \db -> updateGroupProfile db user g p' - (msg, _) <- sendGroupMessage user g' ms (XGrpInfo p') + msg <- sendGroupMessage user g' ms (XGrpInfo p') let cd = CDGroupSnd g' unless (sameGroupProfileInfo p p') $ do ci <- saveSndChatItem user cd msg (CISndGroupEvent $ SGEGroupUpdated p') - toView $ CRNewChatItem user (AChatItem SCTGroup SMDSnd (GroupChat g') ci) + toView $ CRNewChatItems user [AChatItem SCTGroup SMDSnd (GroupChat g') ci] createGroupFeatureChangedItems user cd CISndGroupFeature g g' pure $ CRGroupUpdated user g g' Nothing checkValidName :: GroupName -> CM () @@ -2715,7 +2775,7 @@ processChatCommand' vr = \case let content = CISndGroupInvitation (CIGroupInvitation {groupId, groupMemberId, localDisplayName, groupProfile, status = CIGISPending}) memRole timed_ <- contactCITimed ct ci <- saveSndChatItem' user (CDDirectSnd ct) msg content Nothing Nothing Nothing timed_ False - toView $ CRNewChatItem user (AChatItem SCTDirect SMDSnd (DirectChat ct) ci) + toView $ CRNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct) ci] forM_ (timed_ >>= timedDeleteAt') $ startProximateTimedItemThread user (ChatRef CTDirect contactId, chatItemId' ci) drgRandomBytes :: Int -> CM ByteString @@ -2864,77 +2924,156 @@ processChatCommand' vr = \case forM_ (timed_ >>= timedDeleteAt') $ startProximateTimedItemThread user (ChatRef CTDirect contactId, itemId) _ -> pure () -- prohibited - sendContactContentMessage :: User -> ContactId -> Bool -> Maybe Int -> ComposedMessage -> Maybe CIForwardedFrom -> CM ChatResponse - sendContactContentMessage user contactId live itemTTL (ComposedMessage file_ quotedItemId_ mc) itemForwarded = do + sendContactContentMessages :: User -> ContactId -> Bool -> Maybe Int -> NonEmpty ComposeMessageReq -> CM ChatResponse + sendContactContentMessages user contactId live itemTTL cmrs = do + assertMultiSendable live cmrs ct@Contact {contactUsed} <- withFastStore $ \db -> getContact db vr user contactId assertDirectAllowed user MDSnd ct XMsgNew_ + assertVoiceAllowed ct unless contactUsed $ withFastStore' $ \db -> updateContactUsed db user ct - if isVoice mc && not (featureAllowed SCFVoice forUser ct) - then pure $ chatCmdError (Just user) ("feature not allowed " <> T.unpack (chatFeatureNameText CFVoice)) - else do - (fInv_, ciFile_) <- L.unzip <$> setupSndFileTransfer ct - timed_ <- sndContactCITimed live ct itemTTL - (msgContainer, quotedItem_) <- prepareMsg fInv_ timed_ - (msg, _) <- sendDirectContactMessage user ct (XMsgNew msgContainer) - ci <- saveSndChatItem' user (CDDirectSnd ct) msg (CISndMsgContent mc) ciFile_ quotedItem_ itemForwarded timed_ live - forM_ (timed_ >>= timedDeleteAt') $ - startProximateTimedItemThread user (ChatRef CTDirect contactId, chatItemId' ci) - pure $ CRNewChatItem user (AChatItem SCTDirect SMDSnd (DirectChat ct) ci) + processComposedMessages ct where - setupSndFileTransfer :: Contact -> CM (Maybe (FileInvitation, CIFile 'MDSnd)) - setupSndFileTransfer ct = forM file_ $ \file -> do - fileSize <- checkSndFile file - xftpSndFileTransfer user file fileSize 1 $ CGContact ct - prepareMsg :: Maybe FileInvitation -> Maybe CITimed -> CM (MsgContainer, Maybe (CIQuote 'CTDirect)) - prepareMsg fInv_ timed_ = case (quotedItemId_, itemForwarded) of - (Nothing, Nothing) -> pure (MCSimple (ExtMsgContent mc fInv_ (ttl' <$> timed_) (justTrue live)), Nothing) - (Nothing, Just _) -> pure (MCForward (ExtMsgContent mc fInv_ (ttl' <$> timed_) (justTrue live)), Nothing) - (Just quotedItemId, Nothing) -> do - CChatItem _ qci@ChatItem {meta = CIMeta {itemTs, itemSharedMsgId}, formattedText, file} <- - withFastStore $ \db -> getDirectChatItem db user contactId quotedItemId - (origQmc, qd, sent) <- quoteData qci - let msgRef = MsgRef {msgId = itemSharedMsgId, sentAt = itemTs, sent, memberId = Nothing} - qmc = quoteContent mc origQmc file - quotedItem = CIQuote {chatDir = qd, itemId = Just quotedItemId, sharedMsgId = itemSharedMsgId, sentAt = itemTs, content = qmc, formattedText} - pure (MCQuote QuotedMsg {msgRef, content = qmc} (ExtMsgContent mc fInv_ (ttl' <$> timed_) (justTrue live)), Just quotedItem) - (Just _, Just _) -> throwChatError CEInvalidQuote + assertVoiceAllowed :: Contact -> CM () + assertVoiceAllowed ct = + when (not (featureAllowed SCFVoice forUser ct) && any (\(ComposedMessage {msgContent}, _) -> isVoice msgContent) cmrs) $ + throwChatError (CECommandError $ "feature not allowed " <> T.unpack (chatFeatureNameText CFVoice)) + processComposedMessages :: Contact -> CM ChatResponse + processComposedMessages ct = do + (fInvs_, ciFiles_) <- L.unzip <$> setupSndFileTransfers + timed_ <- sndContactCITimed live ct itemTTL + (msgContainers, quotedItems_) <- L.unzip <$> prepareMsgs (L.zip cmrs fInvs_) timed_ + msgs_ <- sendDirectContactMessages user ct $ L.map XMsgNew msgContainers + let itemsData = prepareSndItemsData msgs_ cmrs ciFiles_ quotedItems_ + when (length itemsData /= length cmrs) $ logError "sendContactContentMessages: cmrs and itemsData length mismatch" + (errs, cis) <- partitionEithers <$> saveSndChatItems user (CDDirectSnd ct) itemsData timed_ live + unless (null errs) $ toView $ CRChatErrors (Just user) errs + forM_ (timed_ >>= timedDeleteAt') $ \deleteAt -> + forM_ cis $ \ci -> + startProximateTimedItemThread user (ChatRef CTDirect contactId, chatItemId' ci) deleteAt + pure $ CRNewChatItems user (map (AChatItem SCTDirect SMDSnd (DirectChat ct)) cis) where - quoteData :: ChatItem c d -> CM (MsgContent, CIQDirection 'CTDirect, Bool) - quoteData ChatItem {meta = CIMeta {itemDeleted = Just _}} = throwChatError CEInvalidQuote - quoteData ChatItem {content = CISndMsgContent qmc} = pure (qmc, CIQDirectSnd, True) - quoteData ChatItem {content = CIRcvMsgContent qmc} = pure (qmc, CIQDirectRcv, False) - quoteData _ = throwChatError CEInvalidQuote - sendGroupContentMessage :: User -> GroupId -> Bool -> Maybe Int -> ComposedMessage -> Maybe CIForwardedFrom -> CM ChatResponse - sendGroupContentMessage user groupId live itemTTL (ComposedMessage file_ quotedItemId_ mc) itemForwarded = do + setupSndFileTransfers :: CM (NonEmpty (Maybe FileInvitation, Maybe (CIFile 'MDSnd))) + setupSndFileTransfers = + forM cmrs $ \(ComposedMessage {fileSource = file_}, _) -> case file_ of + Just file -> do + fileSize <- checkSndFile file + (fInv, ciFile) <- xftpSndFileTransfer user file fileSize 1 $ CGContact ct + pure (Just fInv, Just ciFile) + Nothing -> pure (Nothing, Nothing) + prepareMsgs :: NonEmpty (ComposeMessageReq, Maybe FileInvitation) -> Maybe CITimed -> CM (NonEmpty (MsgContainer, Maybe (CIQuote 'CTDirect))) + prepareMsgs cmsFileInvs timed_ = + forM cmsFileInvs $ \((ComposedMessage {quotedItemId, msgContent = mc}, itemForwarded), fInv_) -> + case (quotedItemId, itemForwarded) of + (Nothing, Nothing) -> pure (MCSimple (ExtMsgContent mc fInv_ (ttl' <$> timed_) (justTrue live)), Nothing) + (Nothing, Just _) -> pure (MCForward (ExtMsgContent mc fInv_ (ttl' <$> timed_) (justTrue live)), Nothing) + (Just qiId, Nothing) -> do + CChatItem _ qci@ChatItem {meta = CIMeta {itemTs, itemSharedMsgId}, formattedText, file} <- + withFastStore $ \db -> getDirectChatItem db user contactId qiId + (origQmc, qd, sent) <- quoteData qci + let msgRef = MsgRef {msgId = itemSharedMsgId, sentAt = itemTs, sent, memberId = Nothing} + qmc = quoteContent mc origQmc file + quotedItem = CIQuote {chatDir = qd, itemId = Just qiId, sharedMsgId = itemSharedMsgId, sentAt = itemTs, content = qmc, formattedText} + pure (MCQuote QuotedMsg {msgRef, content = qmc} (ExtMsgContent mc fInv_ (ttl' <$> timed_) (justTrue live)), Just quotedItem) + (Just _, Just _) -> throwChatError CEInvalidQuote + where + quoteData :: ChatItem c d -> CM (MsgContent, CIQDirection 'CTDirect, Bool) + quoteData ChatItem {meta = CIMeta {itemDeleted = Just _}} = throwChatError CEInvalidQuote + quoteData ChatItem {content = CISndMsgContent qmc} = pure (qmc, CIQDirectSnd, True) + quoteData ChatItem {content = CIRcvMsgContent qmc} = pure (qmc, CIQDirectRcv, False) + quoteData _ = throwChatError CEInvalidQuote + sendGroupContentMessages :: User -> GroupId -> Bool -> Maybe Int -> NonEmpty ComposeMessageReq -> CM ChatResponse + sendGroupContentMessages user groupId live itemTTL cmrs = do + assertMultiSendable live cmrs g@(Group gInfo _) <- withFastStore $ \db -> getGroup db vr user groupId assertUserGroupRole gInfo GRAuthor - send g + assertGroupContentAllowed gInfo + processComposedMessages g where - send g@(Group gInfo@GroupInfo {membership} ms) = - case prohibitedGroupContent gInfo membership mc file_ of - Just f -> notAllowedError f - Nothing -> do - (fInv_, ciFile_) <- L.unzip <$> setupSndFileTransfer g (length $ filter memberCurrent ms) - timed_ <- sndGroupCITimed live gInfo itemTTL - (msgContainer, quotedItem_) <- prepareGroupMsg user gInfo mc quotedItemId_ itemForwarded fInv_ timed_ live - (msg, r) <- sendGroupMessage user gInfo ms (XMsgNew msgContainer) - ci <- saveSndChatItem' user (CDGroupSnd gInfo) msg (CISndMsgContent mc) ciFile_ quotedItem_ itemForwarded timed_ live + assertGroupContentAllowed :: GroupInfo -> CM () + assertGroupContentAllowed gInfo@GroupInfo {membership} = + case findProhibited (L.toList cmrs) of + Just f -> throwChatError (CECommandError $ "feature not allowed " <> T.unpack (groupFeatureNameText f)) + Nothing -> pure () + where + findProhibited :: [ComposeMessageReq] -> Maybe GroupFeature + findProhibited = + foldr' + (\(ComposedMessage {fileSource, msgContent = mc}, _) acc -> prohibitedGroupContent gInfo membership mc fileSource <|> acc) + Nothing + processComposedMessages :: Group -> CM ChatResponse + processComposedMessages g@(Group gInfo ms) = do + (fInvs_, ciFiles_) <- L.unzip <$> setupSndFileTransfers (length $ filter memberCurrent ms) + timed_ <- sndGroupCITimed live gInfo itemTTL + (msgContainers, quotedItems_) <- L.unzip <$> prepareMsgs (L.zip cmrs fInvs_) timed_ + (msgs_, gsr) <- sendGroupMessages user gInfo ms $ L.map XMsgNew msgContainers + let itemsData = prepareSndItemsData (L.toList msgs_) cmrs ciFiles_ quotedItems_ + cis_ <- saveSndChatItems user (CDGroupSnd gInfo) itemsData timed_ live + when (length itemsData /= length cmrs) $ logError "sendGroupContentMessages: cmrs and cis_ length mismatch" + createMemberSndStatuses cis_ msgs_ gsr + let (errs, cis) = partitionEithers cis_ + unless (null errs) $ toView $ CRChatErrors (Just user) errs + forM_ (timed_ >>= timedDeleteAt') $ \deleteAt -> + forM_ cis $ \ci -> + startProximateTimedItemThread user (ChatRef CTGroup groupId, chatItemId' ci) deleteAt + pure $ CRNewChatItems user (map (AChatItem SCTGroup SMDSnd (GroupChat gInfo)) cis) + where + setupSndFileTransfers :: Int -> CM (NonEmpty (Maybe FileInvitation, Maybe (CIFile 'MDSnd))) + setupSndFileTransfers n = + forM cmrs $ \(ComposedMessage {fileSource = file_}, _) -> case file_ of + Just file -> do + fileSize <- checkSndFile file + (fInv, ciFile) <- xftpSndFileTransfer user file fileSize n $ CGGroup g + pure (Just fInv, Just ciFile) + Nothing -> pure (Nothing, Nothing) + prepareMsgs :: NonEmpty (ComposeMessageReq, Maybe FileInvitation) -> Maybe CITimed -> CM (NonEmpty (MsgContainer, Maybe (CIQuote 'CTGroup))) + prepareMsgs cmsFileInvs timed_ = + forM cmsFileInvs $ \((ComposedMessage {quotedItemId, msgContent = mc}, itemForwarded), fInv_) -> + prepareGroupMsg user gInfo mc quotedItemId itemForwarded fInv_ timed_ live + createMemberSndStatuses :: + [Either ChatError (ChatItem 'CTGroup 'MDSnd)] -> + NonEmpty (Either ChatError SndMessage) -> + GroupSndResult -> + CM () + createMemberSndStatuses cis_ msgs_ GroupSndResult {sentTo, pending, forwarded} = do + let msgToItem = mapMsgToItem withFastStore' $ \db -> do - let GroupSndResult {sentTo, pending, forwarded} = mkGroupSndResult r - createMemberSndStatuses db ci sentTo GSSNew - createMemberSndStatuses db ci forwarded GSSForwarded - createMemberSndStatuses db ci pending GSSInactive - forM_ (timed_ >>= timedDeleteAt') $ - startProximateTimedItemThread user (ChatRef CTGroup groupId, chatItemId' ci) - pure $ CRNewChatItem user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci) + forM_ sentTo (processSentTo db msgToItem) + forM_ forwarded (processForwarded db) + forM_ pending (processPending db msgToItem) where - createMemberSndStatuses db ci ms' gss = - forM_ ms' $ \GroupMember {groupMemberId} -> createGroupSndStatus db (chatItemId' ci) groupMemberId gss - notAllowedError f = pure $ chatCmdError (Just user) ("feature not allowed " <> T.unpack (groupFeatureNameText f)) - setupSndFileTransfer :: Group -> Int -> CM (Maybe (FileInvitation, CIFile 'MDSnd)) - setupSndFileTransfer g n = forM file_ $ \file -> do - fileSize <- checkSndFile file - xftpSndFileTransfer user file fileSize n $ CGGroup g + mapMsgToItem :: Map MessageId ChatItemId + mapMsgToItem = foldr' addItem M.empty (zip (L.toList msgs_) cis_) + where + addItem (Right SndMessage {msgId}, Right ci) m = M.insert msgId (chatItemId' ci) m + addItem _ m = m + processSentTo :: DB.Connection -> Map MessageId ChatItemId -> (GroupMemberId, Either ChatError [MessageId], Either ChatError ([Int64], PQEncryption)) -> IO () + processSentTo db msgToItem (mId, msgIds_, deliveryResult) = forM_ msgIds_ $ \msgIds -> do + let ciIds = mapMaybe (`M.lookup` msgToItem) msgIds + status = case deliveryResult of + Right _ -> GSSNew + Left e -> GSSError $ SndErrOther $ tshow e + forM_ ciIds $ \ciId -> createGroupSndStatus db ciId mId status + processForwarded :: DB.Connection -> GroupMember -> IO () + processForwarded db GroupMember {groupMemberId} = + forM_ cis_ $ \ci_ -> + forM_ ci_ $ \ci -> createGroupSndStatus db (chatItemId' ci) groupMemberId GSSForwarded + processPending :: DB.Connection -> Map MessageId ChatItemId -> (GroupMemberId, Either ChatError MessageId, Either ChatError ()) -> IO () + processPending db msgToItem (mId, msgId_, pendingResult) = forM_ msgId_ $ \msgId -> do + let ciId_ = M.lookup msgId msgToItem + status = case pendingResult of + Right _ -> GSSInactive + Left e -> GSSError $ SndErrOther $ tshow e + forM_ ciId_ $ \ciId -> createGroupSndStatus db ciId mId status + assertMultiSendable :: Bool -> NonEmpty ComposeMessageReq -> CM () + assertMultiSendable live cmrs + | length cmrs == 1 = pure () + | otherwise = + -- When sending multiple messages only single quote is allowed. + -- This is to support case of sending multiple attachments while also quoting another message. + -- UI doesn't allow composing with multiple quotes, so api prohibits it as well, and doesn't bother + -- batching retrieval of quoted messages (prepareMsgs). + when (live || length (L.filter (\(ComposedMessage {quotedItemId}, _) -> isJust quotedItemId) cmrs) > 1) $ + throwChatError (CECommandError "invalid multi send: live and more than one quote not supported") xftpSndFileTransfer :: User -> CryptoFile -> Integer -> Int -> ContactOrGroup -> CM (FileInvitation, CIFile 'MDSnd) xftpSndFileTransfer user file fileSize n contactOrGroup = do (fInv, ciFile, ft) <- xftpSndFileTransfer_ user file fileSize n $ Just contactOrGroup @@ -2950,27 +3089,90 @@ processChatCommand' vr = \case \db -> createSndFTDescrXFTP db user (Just m) conn ft dummyFileDescr saveMemberFD _ = pure () pure (fInv, ciFile) - createNoteFolderContentItem :: User -> NoteFolderId -> ComposedMessage -> Maybe CIForwardedFrom -> CM ChatResponse - createNoteFolderContentItem user folderId (ComposedMessage file_ quotedItemId_ mc) itemForwarded = do - forM_ quotedItemId_ $ \_ -> throwError $ ChatError $ CECommandError "not supported" + prepareSndItemsData :: + [Either ChatError SndMessage] -> + NonEmpty ComposeMessageReq -> + NonEmpty (Maybe (CIFile 'MDSnd)) -> + NonEmpty (Maybe (CIQuote c)) -> + [Either ChatError (NewSndChatItemData c)] + prepareSndItemsData msgs_ cmrs' ciFiles_ quotedItems_ = + [ ( case msg_ of + Right msg -> Right $ NewSndChatItemData msg (CISndMsgContent msgContent) f q itemForwarded + Left e -> Left e -- step over original error + ) + | (msg_, (ComposedMessage {msgContent}, itemForwarded), f, q) <- + zipWith4 (,,,) msgs_ (L.toList cmrs') (L.toList ciFiles_) (L.toList quotedItems_) + ] + getCommandDirectChatItems :: User -> Int64 -> NonEmpty ChatItemId -> CM (Contact, [CChatItem 'CTDirect]) + getCommandDirectChatItems user ctId itemIds = do + ct <- withFastStore $ \db -> getContact db vr user ctId + (errs, items) <- lift $ partitionEithers <$> withStoreBatch (\db -> map (getDirectCI db) (L.toList itemIds)) + unless (null errs) $ toView $ CRChatErrors (Just user) errs + pure (ct, items) + where + getDirectCI :: DB.Connection -> ChatItemId -> IO (Either ChatError (CChatItem 'CTDirect)) + getDirectCI db itemId = runExceptT . withExceptT ChatErrorStore $ getDirectChatItem db user ctId itemId + getCommandGroupChatItems :: User -> Int64 -> NonEmpty ChatItemId -> CM (GroupInfo, [CChatItem 'CTGroup]) + getCommandGroupChatItems user gId itemIds = do + gInfo <- withFastStore $ \db -> getGroupInfo db vr user gId + (errs, items) <- lift $ partitionEithers <$> withStoreBatch (\db -> map (getGroupCI db) (L.toList itemIds)) + unless (null errs) $ toView $ CRChatErrors (Just user) errs + pure (gInfo, items) + where + getGroupCI :: DB.Connection -> ChatItemId -> IO (Either ChatError (CChatItem 'CTGroup)) + getGroupCI db itemId = runExceptT . withExceptT ChatErrorStore $ getGroupChatItem db user gId itemId + getCommandLocalChatItems :: User -> Int64 -> NonEmpty ChatItemId -> CM (NoteFolder, [CChatItem 'CTLocal]) + getCommandLocalChatItems user nfId itemIds = do + nf <- withStore $ \db -> getNoteFolder db user nfId + (errs, items) <- lift $ partitionEithers <$> withStoreBatch (\db -> map (getLocalCI db) (L.toList itemIds)) + unless (null errs) $ toView $ CRChatErrors (Just user) errs + pure (nf, items) + where + getLocalCI :: DB.Connection -> ChatItemId -> IO (Either ChatError (CChatItem 'CTLocal)) + getLocalCI db itemId = runExceptT . withExceptT ChatErrorStore $ getLocalChatItem db user nfId itemId + forwardMsgContent :: ChatItem c d -> CM (Maybe MsgContent) + forwardMsgContent ChatItem {meta = CIMeta {itemDeleted = Just _}} = pure Nothing -- this can be deleted after selection + forwardMsgContent ChatItem {content = CISndMsgContent fmc} = pure $ Just fmc + forwardMsgContent ChatItem {content = CIRcvMsgContent fmc} = pure $ Just fmc + forwardMsgContent _ = throwChatError CEInvalidForward + createNoteFolderContentItems :: User -> NoteFolderId -> NonEmpty ComposeMessageReq -> CM ChatResponse + createNoteFolderContentItems user folderId cmrs = do + assertNoQuotes nf <- withFastStore $ \db -> getNoteFolder db user folderId createdAt <- liftIO getCurrentTime - let content = CISndMsgContent mc - let cd = CDLocalSnd nf - ciId <- createLocalChatItem user cd content itemForwarded createdAt - ciFile_ <- forM file_ $ \cf@CryptoFile {filePath, cryptoArgs} -> do - fsFilePath <- lift $ toFSFilePath filePath - fileSize <- liftIO $ CF.getFileContentsSize $ CryptoFile fsFilePath cryptoArgs - chunkSize <- asks $ fileChunkSize . config - withFastStore' $ \db -> do - fileId <- createLocalFile CIFSSndStored db user nf ciId createdAt cf fileSize chunkSize - pure CIFile {fileId, fileName = takeFileName filePath, fileSize, fileSource = Just cf, fileStatus = CIFSSndStored, fileProtocol = FPLocal} - let ci = mkChatItem cd ciId content ciFile_ Nothing Nothing itemForwarded Nothing False createdAt Nothing createdAt - pure . CRNewChatItem user $ AChatItem SCTLocal SMDSnd (LocalChat nf) ci + ciFiles_ <- createLocalFiles nf createdAt + let itemsData = prepareLocalItemsData cmrs ciFiles_ + cis <- createLocalChatItems user (CDLocalSnd nf) itemsData createdAt + pure $ CRNewChatItems user (map (AChatItem SCTLocal SMDSnd (LocalChat nf)) cis) + where + assertNoQuotes :: CM () + assertNoQuotes = + when (any (\(ComposedMessage {quotedItemId}, _) -> isJust quotedItemId) cmrs) $ + throwChatError (CECommandError "createNoteFolderContentItems: quotes not supported") + createLocalFiles :: NoteFolder -> UTCTime -> CM (NonEmpty (Maybe (CIFile 'MDSnd))) + createLocalFiles nf createdAt = + forM cmrs $ \(ComposedMessage {fileSource = file_}, _) -> + forM file_ $ \cf@CryptoFile {filePath, cryptoArgs} -> do + fsFilePath <- lift $ toFSFilePath filePath + fileSize <- liftIO $ CF.getFileContentsSize $ CryptoFile fsFilePath cryptoArgs + chunkSize <- asks $ fileChunkSize . config + withFastStore' $ \db -> do + fileId <- createLocalFile CIFSSndStored db user nf createdAt cf fileSize chunkSize + pure CIFile {fileId, fileName = takeFileName filePath, fileSize, fileSource = Just cf, fileStatus = CIFSSndStored, fileProtocol = FPLocal} + prepareLocalItemsData :: + NonEmpty ComposeMessageReq -> + NonEmpty (Maybe (CIFile 'MDSnd)) -> + [(CIContent 'MDSnd, Maybe (CIFile 'MDSnd), Maybe CIForwardedFrom)] + prepareLocalItemsData cmrs' ciFiles_ = + [ (CISndMsgContent mc, f, itemForwarded) + | ((ComposedMessage {msgContent = mc}, itemForwarded), f) <- zip (L.toList cmrs') (L.toList ciFiles_) + ] getConnQueueInfo user Connection {connId, agentConnId = AgentConnId acId} = do msgInfo <- withFastStore' (`getLastRcvMsgInfo` connId) CRQueueInfo user msgInfo <$> withAgent (`getConnectionQueueInfo` acId) +type ComposeMessageReq = (ComposedMessage, Maybe CIForwardedFrom) + contactCITimed :: Contact -> CM (Maybe CITimed) contactCITimed ct = sndContactCITimed False ct Nothing @@ -3243,7 +3445,7 @@ callStatusItemContent user Contact {contactId} chatItemId receivedStatus = do -- used during file transfer for actual operations with file system toFSFilePath :: FilePath -> CM' FilePath toFSFilePath f = - maybe f ( f) <$> (readTVarIO =<< asks filesFolder) + maybe f ( f) <$> (chatReadVar' filesFolder) setFileToEncrypt :: RcvFileTransfer -> CM RcvFileTransfer setFileToEncrypt ft@RcvFileTransfer {fileId} = do @@ -3365,7 +3567,9 @@ receiveViaCompleteFD user fileId RcvFileDescr {fileDescrText, fileDescrComplete} relaysNotApproved :: [XFTPServer] -> CM () relaysNotApproved unknownSrvs = do aci_ <- resetRcvCIFileStatus user fileId CIFSRcvInvitation - forM_ aci_ $ \aci -> toView $ CRChatItemUpdated user aci + forM_ aci_ $ \aci -> do + cleanupACIFile aci + toView $ CRChatItemUpdated user aci throwChatError $ CEFileNotApproved fileId unknownSrvs getNetworkConfig :: CM' NetworkConfig @@ -4089,14 +4293,22 @@ processAgentMsgRcvFile _corrId aFileId msg = do RFERR e | e == FILE NOT_APPROVED -> do aci_ <- resetRcvCIFileStatus user fileId CIFSRcvAborted + forM_ aci_ cleanupACIFile agentXFTPDeleteRcvFile aFileId fileId forM_ aci_ $ \aci -> toView $ CRChatItemUpdated user aci | otherwise -> do - ci <- withStore $ \db -> do + aci_ <- withStore $ \db -> do liftIO $ updateFileCancelled db user fileId (CIFSRcvError $ agentFileError e) lookupChatItemByFileId db vr user fileId + forM_ aci_ cleanupACIFile agentXFTPDeleteRcvFile aFileId fileId - toView $ CRRcvFileError user ci e ft + toView $ CRRcvFileError user aci_ e ft + +cleanupACIFile :: AChatItem -> CM () +cleanupACIFile (AChatItem _ _ _ ChatItem {file = Just CIFile {fileSource = Just CryptoFile {filePath}}}) = do + fsFilePath <- lift $ toFSFilePath filePath + removeFile fsFilePath `catchChatError` \_ -> pure () +cleanupACIFile _ = pure () processAgentMessageConn :: VersionRangeChat -> User -> ACorrId -> ConnId -> AEvent 'AEConn -> CM () processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = do @@ -4404,7 +4616,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = Just AutoAccept {autoReply = Just mc} -> do (msg, _) <- sendDirectContactMessage user ct (XMsgNew $ MCSimple (extMsgContent mc Nothing)) ci <- saveSndChatItem user (CDDirectSnd ct) msg (CISndMsgContent mc) - toView $ CRNewChatItem user (AChatItem SCTDirect SMDSnd (DirectChat ct) ci) + toView $ CRNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct) ci] _ -> pure () processGroupMessage :: AEvent e -> ConnectionEntity -> Connection -> GroupInfo -> GroupMember -> CM () @@ -4738,7 +4950,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = let GroupMember {memberId} = m ms = forwardedToGroupMembers (introducedMembers <> invitedMembers) forwardedMsgs' events = L.map (\cm -> XGrpMsgForward memberId cm brokerTs) forwardedMsgs' - unless (null ms) $ sendGroupMessages user gInfo ms events + unless (null ms) $ void $ sendGroupMessages user gInfo ms events RCVD msgMeta msgRcpt -> withAckMessage' "group rcvd" agentConnId msgMeta $ groupMsgReceived gInfo m conn msgMeta msgRcpt @@ -5246,7 +5458,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = newChatItem ciContent ciFile_ timed_ live = do ci <- saveRcvChatItem' user (CDDirectRcv ct) msg sharedMsgId_ brokerTs ciContent ciFile_ timed_ live reactions <- maybe (pure []) (\sharedMsgId -> withStore' $ \db -> getDirectCIReactions db ct sharedMsgId) sharedMsgId_ - toView $ CRNewChatItem user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci {reactions}) + toView $ CRNewChatItems user [AChatItem SCTDirect SMDRcv (DirectChat ct) ci {reactions}] autoAcceptFile :: Maybe (RcvFileTransfer, CIFile 'MDRcv) -> CM () autoAcceptFile = mapM_ $ \(ft, CIFile {fileSize}) -> do @@ -5550,7 +5762,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = let fileProtocol = if isJust xftpRcvFile then FPXFTP else FPSMP ciFile = Just $ CIFile {fileId, fileName, fileSize, fileSource = Nothing, fileStatus = CIFSRcvInvitation, fileProtocol} ci <- saveRcvChatItem' user (CDDirectRcv ct) msg sharedMsgId_ brokerTs (CIRcvMsgContent $ MCFile "") ciFile Nothing False - toView $ CRNewChatItem user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci) + toView $ CRNewChatItems user [AChatItem SCTDirect SMDRcv (DirectChat ct) ci] where brokerTs = metaBrokerTs msgMeta @@ -5717,7 +5929,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = groupMsgToView :: forall d. MsgDirectionI d => GroupInfo -> ChatItem 'CTGroup d -> CM () groupMsgToView gInfo ci = - toView $ CRNewChatItem user (AChatItem SCTGroup (msgDirection @d) (GroupChat gInfo) ci) + toView $ CRNewChatItems user [AChatItem SCTGroup (msgDirection @d) (GroupChat gInfo) ci] processGroupInvitation :: Contact -> GroupInvitation -> RcvMessage -> MsgMeta -> CM () processGroupInvitation ct inv msg msgMeta = do @@ -5744,7 +5956,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = let content = CIRcvGroupInvitation (CIGroupInvitation {groupId, groupMemberId, localDisplayName, groupProfile, status = CIGISPending}) memRole ci <- saveRcvChatItem user (CDDirectRcv ct) msg brokerTs content withStore' $ \db -> setGroupInvitationChatItemId db user groupId (chatItemId' ci) - toView $ CRNewChatItem user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci) + toView $ CRNewChatItems user [AChatItem SCTDirect SMDRcv (DirectChat ct) ci] toView $ CRReceivedGroupInvitation {user, groupInfo = gInfo, contact = ct, fromMemberRole = fromRole, memberRole = memRole} where brokerTs = metaBrokerTs msgMeta @@ -5771,7 +5983,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = activeConn' <- forM (contactConn ct') $ \conn -> pure conn {connStatus = ConnDeleted} let ct'' = ct' {activeConn = activeConn'} :: Contact ci <- saveRcvChatItem user (CDDirectRcv ct'') msg brokerTs (CIRcvDirectEvent RDEContactDeleted) - toView $ CRNewChatItem user (AChatItem SCTDirect SMDRcv (DirectChat ct'') ci) + toView $ CRNewChatItems user [AChatItem SCTDirect SMDRcv (DirectChat ct'') ci] toView $ CRContactDeletedByContact user ct'' else do contactConns <- withStore' $ \db -> getContactConnections db vr userId c @@ -5973,14 +6185,14 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = call_ <- atomically (TM.lookupInsert contactId call' calls) forM_ call_ $ \call -> updateCallItemStatus user ct call WCSDisconnected Nothing toView $ CRCallInvitation RcvCallInvitation {user, contact = ct, callType, sharedKey, callUUID, callTs = chatItemTs' ci} - toView $ CRNewChatItem user $ AChatItem SCTDirect SMDRcv (DirectChat ct) ci + toView $ CRNewChatItems user [AChatItem SCTDirect SMDRcv (DirectChat ct) ci] else featureRejected CFCalls where brokerTs = metaBrokerTs msgMeta saveCallItem status = saveRcvChatItem user (CDDirectRcv ct) msg brokerTs (CIRcvCall status 0) featureRejected f = do ci <- saveRcvChatItem' user (CDDirectRcv ct) msg sharedMsgId_ brokerTs (CIRcvChatFeatureRejected f) Nothing Nothing False - toView $ CRNewChatItem user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci) + toView $ CRNewChatItems user [AChatItem SCTDirect SMDRcv (DirectChat ct) ci] -- to party initiating call xCallOffer :: Contact -> CallId -> CallOffer -> RcvMessage -> CM () @@ -6433,7 +6645,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = toView $ CRNewMemberContactReceivedInv user mCt' g m' forM_ mContent_ $ \mc -> do ci <- saveRcvChatItem user (CDDirectRcv mCt') msg brokerTs (CIRcvMsgContent mc) - toView $ CRNewChatItem user (AChatItem SCTDirect SMDRcv (DirectChat mCt') ci) + toView $ CRNewChatItems user [AChatItem SCTDirect SMDRcv (DirectChat mCt') ci] securityCodeChanged :: Contact -> CM () securityCodeChanged ct = do @@ -6820,21 +7032,23 @@ deleteOrUpdateMemberRecord user@User {userId} member = Just _ -> updateGroupMemberStatus db userId member GSMemRemoved Nothing -> deleteGroupMember db user member -sendDirectContactMessages :: MsgEncodingI e => User -> Contact -> NonEmpty (ChatMsgEvent e) -> CM () +sendDirectContactMessages :: MsgEncodingI e => User -> Contact -> NonEmpty (ChatMsgEvent e) -> CM [Either ChatError SndMessage] sendDirectContactMessages user ct events = do Connection {connChatVersion = v} <- liftEither $ contactSendConn_ ct if v >= batchSend2Version then sendDirectContactMessages' user ct events - else mapM_ (void . sendDirectContactMessage user ct) events + else forM (L.toList events) $ \evt -> + (Right . fst <$> sendDirectContactMessage user ct evt) `catchChatError` \e -> pure (Left e) -sendDirectContactMessages' :: MsgEncodingI e => User -> Contact -> NonEmpty (ChatMsgEvent e) -> CM () +sendDirectContactMessages' :: MsgEncodingI e => User -> Contact -> NonEmpty (ChatMsgEvent e) -> CM [Either ChatError SndMessage] sendDirectContactMessages' user ct events = do conn@Connection {connId} <- liftEither $ contactSendConn_ ct let idsEvts = L.map (ConnectionId connId,) events msgFlags = MsgFlags {notification = any (hasNotification . toCMEventTag) events} - (errs, msgs) <- lift $ partitionEithers . L.toList <$> createSndMessages idsEvts - unless (null errs) $ toView $ CRChatErrors (Just user) errs - mapM_ (batchSendConnMessages user conn msgFlags) (L.nonEmpty msgs) + sndMsgs_ <- lift $ createSndMessages idsEvts + (sndMsgs', pqEnc_) <- batchSendConnMessagesB user conn msgFlags sndMsgs_ + forM_ pqEnc_ $ \pqEnc' -> void $ createContactPQSndItem user ct conn pqEnc' + pure sndMsgs' sendDirectContactMessage :: MsgEncodingI e => User -> Contact -> ChatMsgEvent e -> CM (SndMessage, Int64) sendDirectContactMessage user ct chatMsgEvent = do @@ -6894,17 +7108,31 @@ sendGroupMemberMessages user conn events groupId = do forM_ (L.nonEmpty msgs) $ \msgs' -> batchSendConnMessages user conn MsgFlags {notification = True} msgs' -batchSendConnMessages :: User -> Connection -> MsgFlags -> NonEmpty SndMessage -> CM () -batchSendConnMessages user conn msgFlags msgs = do - let batched = batchSndMessagesJSON msgs - let (errs', msgBatches) = partitionEithers batched - -- shouldn't happen, as large messages would have caused createNewSndMessage to throw SELargeMsg - unless (null errs') $ toView $ CRChatErrors (Just user) errs' - forM_ (L.nonEmpty msgBatches) $ \msgBatches' -> do - let msgReq = L.map (msgBatchReq conn msgFlags) msgBatches' - void $ deliverMessages msgReq +batchSendConnMessages :: User -> Connection -> MsgFlags -> NonEmpty SndMessage -> CM ([Either ChatError SndMessage], Maybe PQEncryption) +batchSendConnMessages user conn msgFlags msgs = + batchSendConnMessagesB user conn msgFlags $ L.map Right msgs -batchSndMessagesJSON :: NonEmpty SndMessage -> [Either ChatError MsgBatch] +batchSendConnMessagesB :: User -> Connection -> MsgFlags -> NonEmpty (Either ChatError SndMessage) -> CM ([Either ChatError SndMessage], Maybe PQEncryption) +batchSendConnMessagesB _user conn msgFlags msgs_ = do + let batched_ = batchSndMessagesJSON msgs_ + case L.nonEmpty batched_ of + Just batched' -> do + let msgReqs = L.map (fmap (msgBatchReq conn msgFlags)) batched' + delivered <- deliverMessagesB msgReqs + let msgs' = concat $ L.zipWith flattenMsgs batched' delivered + pqEnc = findLastPQEnc delivered + when (length msgs' /= length msgs_) $ logError "batchSendConnMessagesB: msgs_ and msgs' length mismatch" + pure (msgs', pqEnc) + Nothing -> pure ([], Nothing) + where + flattenMsgs :: Either ChatError MsgBatch -> Either ChatError ([Int64], PQEncryption) -> [Either ChatError SndMessage] + flattenMsgs (Right (MsgBatch _ sndMsgs)) (Right _) = map Right sndMsgs + flattenMsgs (Right (MsgBatch _ sndMsgs)) (Left ce) = replicate (length sndMsgs) (Left ce) + flattenMsgs (Left ce) _ = [Left ce] -- restore original ChatError + findLastPQEnc :: NonEmpty (Either ChatError ([Int64], PQEncryption)) -> Maybe PQEncryption + findLastPQEnc = foldr' (\x acc -> case x of Right (_, pqEnc) -> Just pqEnc; Left _ -> acc) Nothing + +batchSndMessagesJSON :: NonEmpty (Either ChatError SndMessage) -> [Either ChatError MsgBatch] batchSndMessagesJSON = batchMessages maxEncodedMsgLength . L.toList msgBatchReq :: Connection -> MsgFlags -> MsgBatch -> ChatMsgReq @@ -6956,7 +7184,7 @@ deliverMessagesB msgReqs = do lift . withStoreBatch $ \db -> L.map (bindRight $ createDelivery db) sent where compressBodies = - forME msgReqs $ \mr@(conn@Connection {pqSupport, connChatVersion = v}, msgFlags, msgBody, msgId) -> + forME msgReqs $ \mr@(conn@Connection {pqSupport, connChatVersion = v}, msgFlags, msgBody, msgIds) -> runExceptT $ case pqSupport of -- we only compress messages when: -- 1) PQ support is enabled @@ -6965,7 +7193,7 @@ deliverMessagesB msgReqs = do PQSupportOn | v >= pqEncryptionCompressionVersion && B.length msgBody > maxCompressedMsgLength -> do let msgBody' = compressedBatchMsgBody_ msgBody when (B.length msgBody' > maxCompressedMsgLength) $ throwError $ ChatError $ CEException "large compressed message" - pure (conn, msgFlags, msgBody', msgId) + pure (conn, msgFlags, msgBody', msgIds) _ -> pure mr toAgent prev = \case Right (conn@Connection {connId, pqEncryption}, msgFlags, msgBody, _msgIds) -> @@ -6989,13 +7217,23 @@ deliverMessagesB msgReqs = do where updatePQ = updateConnPQSndEnabled db connId pqSndEnabled' --- TODO combine profile update and message into one batch --- Take into account that it may not fit, and that we currently don't support sending multiple messages to the same connection in one call. -sendGroupMessage :: MsgEncodingI e => User -> GroupInfo -> [GroupMember] -> ChatMsgEvent e -> CM (SndMessage, GroupSndResultData) +sendGroupMessage :: MsgEncodingI e => User -> GroupInfo -> [GroupMember] -> ChatMsgEvent e -> CM SndMessage sendGroupMessage user gInfo members chatMsgEvent = do + sendGroupMessages user gInfo members (chatMsgEvent :| []) >>= \case + ((Right msg) :| [], _) -> pure msg + _ -> throwChatError $ CEInternalError "sendGroupMessage: expected 1 message" + +sendGroupMessage' :: MsgEncodingI e => User -> GroupInfo -> [GroupMember] -> ChatMsgEvent e -> CM SndMessage +sendGroupMessage' user gInfo members chatMsgEvent = + sendGroupMessages_ user gInfo members (chatMsgEvent :| []) >>= \case + ((Right msg) :| [], _) -> pure msg + _ -> throwChatError $ CEInternalError "sendGroupMessage': expected 1 message" + +sendGroupMessages :: MsgEncodingI e => User -> GroupInfo -> [GroupMember] -> NonEmpty (ChatMsgEvent e) -> CM (NonEmpty (Either ChatError SndMessage), GroupSndResult) +sendGroupMessages user gInfo members events = do when shouldSendProfileUpdate $ sendProfileUpdate `catchChatError` (toView . CRChatError (Just user)) - sendGroupMessage_ user gInfo members chatMsgEvent + sendGroupMessages_ user gInfo members events where User {profile = p, userMemberProfileUpdatedAt} = user GroupInfo {userMemberProfileSentAt} = gInfo @@ -7013,59 +7251,34 @@ sendGroupMessage user gInfo members chatMsgEvent = do currentTs <- liftIO getCurrentTime withStore' $ \db -> updateUserMemberProfileSentAt db user gInfo currentTs -type GroupSndResultData = (([Either ChatError ([Int64], PQEncryption)], [(GroupMember, Connection)]), ([Either ChatError ()], [GroupMember]), [GroupMember]) - data GroupSndResult = GroupSndResult - { sentTo :: [GroupMember], - pending :: [GroupMember], + { sentTo :: [(GroupMemberId, Either ChatError [MessageId], Either ChatError ([Int64], PQEncryption))], + pending :: [(GroupMemberId, Either ChatError MessageId, Either ChatError ())], forwarded :: [GroupMember] } -sendGroupMessage' :: MsgEncodingI e => User -> GroupInfo -> [GroupMember] -> ChatMsgEvent e -> CM SndMessage -sendGroupMessage' user gInfo members chatMsgEvent = fst <$> sendGroupMessage_ user gInfo members chatMsgEvent - -sendGroupMessage_ :: MsgEncodingI e => User -> GroupInfo -> [GroupMember] -> ChatMsgEvent e -> CM (SndMessage, GroupSndResultData) -sendGroupMessage_ user gInfo members chatMsgEvent = - sendGroupMessages_ user gInfo members (chatMsgEvent :| []) >>= \case - (msg :| [], r) -> pure (msg, r) - _ -> throwChatError $ CEInternalError "sendGroupMessage': expected 1 message" - -sendGroupMessages :: MsgEncodingI e => User -> GroupInfo -> [GroupMember] -> NonEmpty (ChatMsgEvent e) -> CM () -sendGroupMessages user gInfo members events = void $ sendGroupMessages_ user gInfo members events - -mkGroupSndResult :: GroupSndResultData -> GroupSndResult -mkGroupSndResult ((delivered, sentTo), (stored, pending), forwarded) = - GroupSndResult - { sentTo = filterSent' delivered sentTo fst, - pending = filterSent' stored pending id, - forwarded - } - where - -- TODO in theory this could deduplicate members and keep results only when ... some sent? or all sent? - -- This is not important, as it is not used in batch calls - filterSent' :: [Either ChatError a] -> [mem] -> (mem -> GroupMember) -> [GroupMember] - filterSent' rs ms mem = [mem m | (Right _, m) <- zip rs ms] - -sendGroupMessages_ :: MsgEncodingI e => User -> GroupInfo -> [GroupMember] -> NonEmpty (ChatMsgEvent e) -> CM (NonEmpty SndMessage, GroupSndResultData) -sendGroupMessages_ user gInfo@GroupInfo {groupId} members events = do +sendGroupMessages_ :: MsgEncodingI e => User -> GroupInfo -> [GroupMember] -> NonEmpty (ChatMsgEvent e) -> CM (NonEmpty (Either ChatError SndMessage), GroupSndResult) +sendGroupMessages_ _user gInfo@GroupInfo {groupId} members events = do let idsEvts = L.map (GroupId groupId,) events - (errs, msgs) <- lift $ partitionEithers . L.toList <$> createSndMessages idsEvts - unless (null errs) $ toView $ CRChatErrors (Just user) errs - case L.nonEmpty msgs of - Nothing -> throwChatError $ CEInternalError "sendGroupMessages: no messages created" - Just msgs' -> do - recipientMembers <- liftIO $ shuffleMembers (filter memberCurrent members) - let msgFlags = MsgFlags {notification = any (hasNotification . toCMEventTag) events} - (toSendSeparate, toSendBatched, pending, forwarded, _, dups) = - foldr addMember ([], [], [], [], S.empty, 0 :: Int) recipientMembers - when (dups /= 0) $ logError $ "sendGroupMessage: " <> tshow dups <> " duplicate members" - -- TODO PQ either somehow ensure that group members connections cannot have pqSupport/pqEncryption or pass Off's here - let msgReqs = prepareMsgReqs msgFlags msgs' toSendSeparate toSendBatched - delivered <- maybe (pure []) (fmap L.toList . deliverMessages) $ L.nonEmpty msgReqs - let errors = lefts delivered - unless (null errors) $ toView $ CRChatErrors (Just user) errors - stored <- lift . withStoreBatch' $ \db -> map (\m -> createPendingMsgs db m msgs') pending - pure (msgs', ((delivered, toSendSeparate <> toSendBatched), (stored, pending), forwarded)) + sndMsgs_ <- lift $ createSndMessages idsEvts + recipientMembers <- liftIO $ shuffleMembers (filter memberCurrent members) + let msgFlags = MsgFlags {notification = any (hasNotification . toCMEventTag) events} + (toSendSeparate, toSendBatched, toPending, forwarded, _, dups) = + foldr' addMember ([], [], [], [], S.empty, 0 :: Int) recipientMembers + when (dups /= 0) $ logError $ "sendGroupMessages_: " <> tshow dups <> " duplicate members" + -- TODO PQ either somehow ensure that group members connections cannot have pqSupport/pqEncryption or pass Off's here + -- Deliver to toSend members + let (sendToMemIds, msgReqs) = prepareMsgReqs msgFlags sndMsgs_ toSendSeparate toSendBatched + delivered <- maybe (pure []) (fmap L.toList . deliverMessagesB) $ L.nonEmpty msgReqs + when (length delivered /= length sendToMemIds) $ logError "sendGroupMessages_: sendToMemIds and delivered length mismatch" + -- Save as pending for toPending members + let (pendingMemIds, pendingReqs) = preparePending sndMsgs_ toPending + stored <- lift $ withStoreBatch (\db -> map (bindRight $ createPendingMsg db) pendingReqs) + when (length stored /= length pendingMemIds) $ logError "sendGroupMessages_: pendingMemIds and stored length mismatch" + -- Zip for easier access to results + let sentTo = zipWith3 (\mId mReq r -> (mId, fmap (\(_, _, _, msgIds) -> msgIds) mReq, r)) sendToMemIds msgReqs delivered + pending = zipWith3 (\mId pReq r -> (mId, fmap snd pReq, r)) pendingMemIds pendingReqs stored + pure (sndMsgs_, GroupSndResult {sentTo, pending, forwarded}) where shuffleMembers :: [GroupMember] -> IO [GroupMember] shuffleMembers ms = do @@ -7086,22 +7299,38 @@ sendGroupMessages_ user gInfo@GroupInfo {groupId} members events = do where mId = groupMemberId' m mIds' = S.insert mId mIds - prepareMsgReqs :: MsgFlags -> NonEmpty SndMessage -> [(GroupMember, Connection)] -> [(GroupMember, Connection)] -> [ChatMsgReq] - prepareMsgReqs msgFlags msgs toSendSeparate toSendBatched = do - let msgReqsSeparate = foldr (\(_, conn) reqs -> foldr (\msg -> (sndMessageReq conn msg :)) reqs msgs) [] toSendSeparate - batched = batchSndMessagesJSON msgs - -- _errs shouldn't happen, as large messages would have caused createNewSndMessage to throw SELargeMsg - (_errs, msgBatches) = partitionEithers batched - case L.nonEmpty msgBatches of - Just msgBatches' -> do - let msgReqsBatched = foldr (\(_, conn) reqs -> foldr (\batch -> (msgBatchReq conn msgFlags batch :)) reqs msgBatches') [] toSendBatched - msgReqsSeparate <> msgReqsBatched - Nothing -> msgReqsSeparate + prepareMsgReqs :: MsgFlags -> NonEmpty (Either ChatError SndMessage) -> [(GroupMember, Connection)] -> [(GroupMember, Connection)] -> ([GroupMemberId], [Either ChatError ChatMsgReq]) + prepareMsgReqs msgFlags msgs_ toSendSeparate toSendBatched = do + let batched_ = batchSndMessagesJSON msgs_ + case L.nonEmpty batched_ of + Just batched' -> do + let (memsSep, mreqsSep) = foldr' foldMsgs ([], []) toSendSeparate + (memsBtch, mreqsBtch) = foldr' (foldBatches batched') ([], []) toSendBatched + (memsSep <> memsBtch, mreqsSep <> mreqsBtch) + Nothing -> ([], []) where - sndMessageReq :: Connection -> SndMessage -> ChatMsgReq - sndMessageReq conn SndMessage {msgId, msgBody} = (conn, msgFlags, msgBody, [msgId]) - createPendingMsgs :: DB.Connection -> GroupMember -> NonEmpty SndMessage -> IO () - createPendingMsgs db m = mapM_ (\SndMessage {msgId} -> createPendingGroupMessage db (groupMemberId' m) msgId Nothing) + foldMsgs :: (GroupMember, Connection) -> ([GroupMemberId], [Either ChatError ChatMsgReq]) -> ([GroupMemberId], [Either ChatError ChatMsgReq]) + foldMsgs (GroupMember {groupMemberId}, conn) memIdsReqs = + foldr' (\msg_ (memIds, reqs) -> (groupMemberId : memIds, fmap sndMessageReq msg_ : reqs)) memIdsReqs msgs_ + where + sndMessageReq :: SndMessage -> ChatMsgReq + sndMessageReq SndMessage {msgId, msgBody} = (conn, msgFlags, msgBody, [msgId]) + foldBatches :: NonEmpty (Either ChatError MsgBatch) -> (GroupMember, Connection) -> ([GroupMemberId], [Either ChatError ChatMsgReq]) -> ([GroupMemberId], [Either ChatError ChatMsgReq]) + foldBatches batched' (GroupMember {groupMemberId}, conn) memIdsReqs = + foldr' (\batch_ (memIds, reqs) -> (groupMemberId : memIds, fmap (msgBatchReq conn msgFlags) batch_ : reqs)) memIdsReqs batched' + preparePending :: NonEmpty (Either ChatError SndMessage) -> [GroupMember] -> ([GroupMemberId], [Either ChatError (GroupMemberId, MessageId)]) + preparePending msgs_ = + foldr' foldMsgs ([], []) + where + foldMsgs :: GroupMember -> ([GroupMemberId], [Either ChatError (GroupMemberId, MessageId)]) -> ([GroupMemberId], [Either ChatError (GroupMemberId, MessageId)]) + foldMsgs GroupMember {groupMemberId} memIdsReqs = + foldr' (\msg_ (memIds, reqs) -> (groupMemberId : memIds, fmap pendingReq msg_ : reqs)) memIdsReqs msgs_ + where + pendingReq :: SndMessage -> (GroupMemberId, MessageId) + pendingReq SndMessage {msgId} = (groupMemberId, msgId) + createPendingMsg :: DB.Connection -> (GroupMemberId, MessageId) -> IO (Either ChatError ()) + createPendingMsg db (groupMemberId, msgId) = + createPendingGroupMessage db groupMemberId msgId Nothing $> Right () data MemberSendAction = MSASend Connection | MSASendBatched Connection | MSAPending | MSAForwarded @@ -7162,7 +7391,7 @@ sendPendingGroupMessages user GroupMember {groupMemberId} conn = do pgms <- withStore' $ \db -> getPendingGroupMessages db groupMemberId forM_ (L.nonEmpty pgms) $ \pgms' -> do let msgs = L.map (\(sndMsg, _, _) -> sndMsg) pgms' - batchSendConnMessages user conn MsgFlags {notification = True} msgs + void $ batchSendConnMessages user conn MsgFlags {notification = True} msgs lift . void . withStoreBatch' $ \db -> L.map (\SndMessage {msgId} -> deletePendingGroupMessage db groupMemberId msgId) msgs lift . void . withStoreBatch' $ \db -> L.map (\(_, tag, introId_) -> updateIntro_ db tag introId_) pgms' where @@ -7219,14 +7448,39 @@ saveSndChatItem :: ChatTypeI c => User -> ChatDirection c 'MDSnd -> SndMessage - saveSndChatItem user cd msg content = saveSndChatItem' user cd msg content Nothing Nothing Nothing Nothing False saveSndChatItem' :: ChatTypeI c => User -> ChatDirection c 'MDSnd -> SndMessage -> CIContent 'MDSnd -> Maybe (CIFile 'MDSnd) -> Maybe (CIQuote c) -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> CM (ChatItem c 'MDSnd) -saveSndChatItem' user cd msg@SndMessage {sharedMsgId} content ciFile quotedItem itemForwarded itemTimed live = do +saveSndChatItem' user cd msg content ciFile quotedItem itemForwarded itemTimed live = + saveSndChatItems user cd [Right NewSndChatItemData {msg, content, ciFile, quotedItem, itemForwarded}] itemTimed live >>= \case + [Right ci] -> pure ci + _ -> throwChatError $ CEInternalError "saveSndChatItem': expected 1 item" + +data NewSndChatItemData c = NewSndChatItemData + { msg :: SndMessage, + content :: CIContent 'MDSnd, + ciFile :: Maybe (CIFile 'MDSnd), + quotedItem :: Maybe (CIQuote c), + itemForwarded :: Maybe CIForwardedFrom + } + +saveSndChatItems :: + forall c. + ChatTypeI c => + User -> + ChatDirection c 'MDSnd -> + [Either ChatError (NewSndChatItemData c)] -> + Maybe CITimed -> + Bool -> + CM [Either ChatError (ChatItem c 'MDSnd)] +saveSndChatItems user cd itemsData itemTimed live = do createdAt <- liftIO getCurrentTime - ciId <- withStore' $ \db -> do - when (ciRequiresAttention content || contactChatDeleted cd) $ updateChatTs db user cd createdAt - ciId <- createNewSndChatItem db user cd msg content quotedItem itemForwarded itemTimed live createdAt - forM_ ciFile $ \CIFile {fileId} -> updateFileTransferChatItemId db fileId ciId createdAt - pure ciId - pure $ mkChatItem cd ciId content ciFile quotedItem (Just sharedMsgId) itemForwarded itemTimed live createdAt Nothing createdAt + when (contactChatDeleted cd || any (\NewSndChatItemData {content} -> ciRequiresAttention content) (rights itemsData)) $ + withStore' (\db -> updateChatTs db user cd createdAt) + lift $ withStoreBatch (\db -> map (bindRight $ createItem db createdAt) itemsData) + where + createItem :: DB.Connection -> UTCTime -> NewSndChatItemData c -> IO (Either ChatError (ChatItem c 'MDSnd)) + createItem db createdAt NewSndChatItemData {msg = msg@SndMessage {sharedMsgId}, content, ciFile, quotedItem, itemForwarded} = do + ciId <- createNewSndChatItem db user cd msg content quotedItem itemForwarded itemTimed live createdAt + forM_ ciFile $ \CIFile {fileId} -> updateFileTransferChatItemId db fileId ciId createdAt + pure $ Right $ mkChatItem cd ciId content ciFile quotedItem (Just sharedMsgId) itemForwarded itemTimed live createdAt Nothing createdAt saveRcvChatItem :: (ChatTypeI c, ChatTypeQuotable c) => User -> ChatDirection c 'MDRcv -> RcvMessage -> UTCTime -> CIContent 'MDRcv -> CM (ChatItem c 'MDRcv) saveRcvChatItem user cd msg@RcvMessage {sharedMsgId_} brokerTs content = @@ -7479,7 +7733,7 @@ createContactsFeatureItems user cts chatDir ciFeature ciOffer getPref = do let dirsCIContents = map contactChangedFeatures cts (errs, acis) <- partitionEithers <$> createInternalItemsForChats user Nothing dirsCIContents unless (null errs) $ toView' $ CRChatErrors (Just user) errs - forM_ acis $ \aci -> toView' $ CRNewChatItem user aci + toView' $ CRNewChatItems user acis where contactChangedFeatures :: (Contact, Contact) -> (ChatDirection 'CTDirect d, [CIContent d]) contactChangedFeatures (Contact {mergedPreferences = cups}, ct'@Contact {mergedPreferences = cups'}) = do @@ -7517,7 +7771,7 @@ sameGroupProfileInfo p p' = p {groupPreferences = Nothing} == p' {groupPreferenc 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 - [Right aci] -> toView $ CRNewChatItem user aci + [Right aci] -> toView $ CRNewChatItems user [aci] [Left e] -> throwError e rs -> throwChatError $ CEInternalError $ "createInternalChatItem: expected 1 result, got " <> show (length rs) @@ -7544,14 +7798,23 @@ createInternalItemsForChats user itemTs_ dirsCIContents = do let ci = mkChatItem cd ciId content Nothing Nothing Nothing Nothing Nothing False itemTs Nothing createdAt pure $ AChatItem (chatTypeI @c) (msgDirection @d) (toChatInfo cd) ci -createLocalChatItem :: MsgDirectionI d => User -> ChatDirection 'CTLocal d -> CIContent d -> Maybe CIForwardedFrom -> UTCTime -> CM ChatItemId -createLocalChatItem user cd content itemForwarded createdAt = do - gVar <- asks random - withStore $ \db -> do - liftIO $ updateChatTs db user cd createdAt - createWithRandomId gVar $ \sharedMsgId -> - let smi_ = Just (SharedMsgId sharedMsgId) - in createNewChatItem_ db user cd Nothing smi_ content (Nothing, Nothing, Nothing, Nothing, Nothing) itemForwarded Nothing False createdAt Nothing createdAt +createLocalChatItems :: + User -> + ChatDirection 'CTLocal 'MDSnd -> + [(CIContent 'MDSnd, Maybe (CIFile 'MDSnd), Maybe CIForwardedFrom)] -> + UTCTime -> + CM [ChatItem 'CTLocal 'MDSnd] +createLocalChatItems user cd itemsData createdAt = do + withStore' $ \db -> updateChatTs db user cd createdAt + (errs, items) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (createItem db) itemsData) + unless (null errs) $ toView $ CRChatErrors (Just user) errs + pure items + where + createItem :: DB.Connection -> (CIContent 'MDSnd, Maybe (CIFile 'MDSnd), Maybe CIForwardedFrom) -> IO (ChatItem 'CTLocal 'MDSnd) + createItem db (content, ciFile, itemForwarded) = do + ciId <- createNewChatItem_ db user cd Nothing Nothing content (Nothing, Nothing, Nothing, Nothing, Nothing) itemForwarded Nothing False createdAt Nothing createdAt + forM_ ciFile $ \CIFile {fileId} -> updateFileTransferChatItemId db fileId ciId createdAt + pure $ mkChatItem cd ciId content ciFile Nothing Nothing itemForwarded Nothing False createdAt Nothing createdAt withUser' :: (User -> CM ChatResponse) -> CM ChatResponse withUser' action = @@ -7677,16 +7940,18 @@ chatCommandP = "/_get chat " *> (APIGetChat <$> chatRefP <* A.space <*> chatPaginationP <*> optional (" search=" *> stringP)), "/_get items " *> (APIGetChatItems <$> chatPaginationP <*> optional (" search=" *> stringP)), "/_get item info " *> (APIGetChatItemInfo <$> chatRefP <* A.space <*> A.decimal), - "/_send " *> (APISendMessage <$> chatRefP <*> liveMessageP <*> sendMessageTTLP <*> (" json " *> jsonP <|> " text " *> (ComposedMessage Nothing Nothing <$> mcTextP))), - "/_create *" *> (APICreateChatItem <$> A.decimal <*> (" json " *> jsonP <|> " text " *> (ComposedMessage Nothing Nothing <$> mcTextP))), + "/_send " *> (APISendMessages <$> chatRefP <*> liveMessageP <*> sendMessageTTLP <*> (" json " *> jsonP <|> " text " *> composedMessagesTextP)), + "/_create *" *> (APICreateChatItems <$> A.decimal <*> (" json " *> jsonP <|> " text " *> composedMessagesTextP)), "/_update item " *> (APIUpdateChatItem <$> chatRefP <* A.space <*> A.decimal <*> liveMessageP <* A.space <*> msgContentP), "/_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), - "/_forward " *> (APIForwardChatItem <$> chatRefP <* A.space <*> chatRefP <* A.space <*> A.decimal <*> sendMessageTTLP), + "/_forward plan " *> (APIPlanForwardChatItems <$> chatRefP <*> _strP), + "/_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 items " *> (APIChatItemsRead <$> chatRefP <*> _strP), "/_unread chat " *> (APIChatUnread <$> chatRefP <* A.space <*> onOffP), "/_delete " *> (APIDeleteChat <$> chatRefP <*> chatDeleteMode), "/_clear chat " *> (APIClearChat <$> chatRefP), @@ -7981,6 +8246,9 @@ chatCommandP = '*' -> head "❤️" '^' -> '🚀' c -> c + composedMessagesTextP = do + text <- mcTextP + pure $ (ComposedMessage Nothing Nothing text) :| [] liveMessageP = " live=" *> onOffP <|> pure False sendMessageTTLP = " ttl=" *> ((Just <$> A.decimal) <|> ("default" $> Nothing)) <|> pure Nothing receiptSettings = do diff --git a/src/Simplex/Chat/Bot.hs b/src/Simplex/Chat/Bot.hs index f3de92e1f2..66479c0ee6 100644 --- a/src/Simplex/Chat/Bot.hs +++ b/src/Simplex/Chat/Bot.hs @@ -11,6 +11,7 @@ import Control.Concurrent.Async import Control.Concurrent.STM import Control.Monad import qualified Data.ByteString.Char8 as B +import Data.List.NonEmpty (NonEmpty (..)) import qualified Data.Text as T import Simplex.Chat.Controller import Simplex.Chat.Core @@ -31,7 +32,7 @@ chatBotRepl welcome answer _user cc = do CRContactConnected _ contact _ -> do contactConnected contact void $ sendMessage cc contact welcome - CRNewChatItem _ (AChatItem _ SMDRcv (DirectChat contact) ChatItem {content = mc@CIRcvMsgContent {}}) -> do + CRNewChatItems {chatItems = (AChatItem _ SMDRcv (DirectChat contact) ChatItem {content = mc@CIRcvMsgContent {}}) : _} -> do let msg = T.unpack $ ciContentToText mc void $ sendMessage cc contact =<< answer contact msg _ -> pure () @@ -68,8 +69,8 @@ sendComposedMessage cc = sendComposedMessage' cc . contactId' sendComposedMessage' :: ChatController -> ContactId -> Maybe ChatItemId -> MsgContent -> IO () sendComposedMessage' cc ctId quotedItemId msgContent = do let cm = ComposedMessage {fileSource = Nothing, quotedItemId, msgContent} - sendChatCmd cc (APISendMessage (ChatRef CTDirect ctId) False Nothing cm) >>= \case - CRNewChatItem {} -> printLog cc CLLInfo $ "sent message to contact ID " <> show ctId + sendChatCmd cc (APISendMessages (ChatRef CTDirect ctId) False Nothing (cm :| [])) >>= \case + CRNewChatItems {} -> printLog cc CLLInfo $ "sent message to contact ID " <> show ctId r -> putStrLn $ "unexpected send message response: " <> show r deleteMessage :: ChatController -> Contact -> ChatItemId -> IO () diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index b3fccf95ad..1e00172cea 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -292,16 +292,18 @@ data ChatCommand | APIGetChat ChatRef ChatPagination (Maybe String) | APIGetChatItems ChatPagination (Maybe String) | APIGetChatItemInfo ChatRef ChatItemId - | APISendMessage {chatRef :: ChatRef, liveMessage :: Bool, ttl :: Maybe Int, composedMessage :: ComposedMessage} - | APICreateChatItem {noteFolderId :: NoteFolderId, composedMessage :: ComposedMessage} + | APISendMessages {chatRef :: ChatRef, liveMessage :: Bool, ttl :: Maybe Int, composedMessages :: NonEmpty ComposedMessage} + | APICreateChatItems {noteFolderId :: NoteFolderId, composedMessages :: NonEmpty ComposedMessage} | APIUpdateChatItem {chatRef :: ChatRef, chatItemId :: ChatItemId, liveMessage :: Bool, msgContent :: MsgContent} | APIDeleteChatItem ChatRef (NonEmpty ChatItemId) CIDeleteMode | APIDeleteMemberChatItem GroupId (NonEmpty ChatItemId) | APIChatItemReaction {chatRef :: ChatRef, chatItemId :: ChatItemId, add :: Bool, reaction :: MsgReaction} - | APIForwardChatItem {toChatRef :: ChatRef, fromChatRef :: ChatRef, chatItemId :: ChatItemId, ttl :: Maybe Int} + | APIPlanForwardChatItems {fromChatRef :: ChatRef, chatItemIds :: NonEmpty ChatItemId} + | APIForwardChatItems {toChatRef :: ChatRef, fromChatRef :: ChatRef, chatItemIds :: NonEmpty ChatItemId, ttl :: Maybe Int} | APIUserRead UserId | UserRead | APIChatRead ChatRef (Maybe (ChatItemId, ChatItemId)) + | APIChatItemsRead ChatRef (NonEmpty ChatItemId) | APIChatUnread ChatRef Bool | APIDeleteChat ChatRef ChatDeleteMode -- currently delete mode settings are only applied to direct chats | APIClearChat ChatRef @@ -597,7 +599,7 @@ data ChatResponse | CRContactCode {user :: User, contact :: Contact, connectionCode :: Text} | CRGroupMemberCode {user :: User, groupInfo :: GroupInfo, member :: GroupMember, connectionCode :: Text} | CRConnectionVerified {user :: User, verified :: Bool, expectedCode :: Text} - | CRNewChatItem {user :: User, chatItem :: AChatItem} + | CRNewChatItems {user :: User, chatItems :: [AChatItem]} | CRChatItemStatusUpdated {user :: User, chatItem :: AChatItem} | CRChatItemUpdated {user :: User, chatItem :: AChatItem} | CRChatItemNotChanged {user :: User, chatItem :: AChatItem} @@ -648,6 +650,7 @@ data ChatResponse | CRContactRequestAlreadyAccepted {user :: User, contact :: Contact} | CRLeftMemberUser {user :: User, groupInfo :: GroupInfo} | CRGroupDeletedUser {user :: User, groupInfo :: GroupInfo} + | CRForwardPlan {user :: User, itemsCount :: Int, chatItemIds :: [ChatItemId], forwardConfirmation :: Maybe ForwardConfirmation} | CRRcvFileDescrReady {user :: User, chatItem :: AChatItem, rcvFileTransfer :: RcvFileTransfer, rcvFileDescr :: RcvFileDescr} | CRRcvFileAccepted {user :: User, chatItem :: AChatItem} | CRRcvFileAcceptedSndCancelled {user :: User, rcvFileTransfer :: RcvFileTransfer} @@ -904,6 +907,13 @@ connectionPlanProceed = \case GLPConnectingConfirmReconnect -> True _ -> False +data ForwardConfirmation + = FCFilesNotAccepted {fileIds :: [FileTransferId]} + | FCFilesInProgress {filesCount :: Int} + | FCFilesMissing {filesCount :: Int} + | FCFilesFailed {filesCount :: Int} + deriving (Show) + newtype UserPwd = UserPwd {unUserPwd :: Text} deriving (Eq, Show) @@ -1180,7 +1190,6 @@ data ChatErrorType | CEInlineFileProhibited {fileId :: FileTransferId} | CEInvalidQuote | CEInvalidForward - | CEForwardNoFile | CEInvalidChatItemUpdate | CEInvalidChatItemDelete | CEHasCurrentCall @@ -1465,6 +1474,8 @@ $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "GLP") ''GroupLinkPlan) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CP") ''ConnectionPlan) +$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "FC") ''ForwardConfirmation) + $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CE") ''ChatErrorType) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "RHE") ''RemoteHostError) diff --git a/src/Simplex/Chat/Messages.hs b/src/Simplex/Chat/Messages.hs index 78e3b4c640..50e68e5bf4 100644 --- a/src/Simplex/Chat/Messages.hs +++ b/src/Simplex/Chat/Messages.hs @@ -35,7 +35,7 @@ import Data.Maybe (fromMaybe, isJust, isNothing) import Data.Text (Text) import qualified Data.Text as T import Data.Text.Encoding (decodeLatin1, encodeUtf8) -import Data.Time.Clock (UTCTime, diffUTCTime, nominalDay, NominalDiffTime) +import Data.Time.Clock (NominalDiffTime, UTCTime, diffUTCTime, nominalDay) import Data.Type.Equality import Data.Typeable (Typeable) import Database.SQLite.Simple.FromField (FromField (..)) @@ -336,6 +336,9 @@ aChatItemId (AChatItem _ _ _ ci) = chatItemId' ci aChatItemTs :: AChatItem -> UTCTime aChatItemTs (AChatItem _ _ _ ci) = chatItemTs' ci +aChatItemDir :: AChatItem -> MsgDirection +aChatItemDir (AChatItem _ sMsgDir _ _) = toMsgDirection sMsgDir + updateFileStatus :: forall c d. ChatItem c d -> CIFileStatus d -> ChatItem c d updateFileStatus ci@ChatItem {file} status = case file of Just f -> ci {file = Just (f :: CIFile d) {fileStatus = status}} @@ -592,6 +595,27 @@ ciFileLoaded = \case CIFSRcvWarning {} -> False CIFSInvalid {} -> False +data ForwardFileError = FFENotAccepted FileTransferId | FFEInProgress | FFEFailed | FFEMissing + deriving (Eq, Ord) + +ciFileForwardError :: FileTransferId -> CIFileStatus d -> Maybe ForwardFileError +ciFileForwardError fId = \case + CIFSSndStored -> Nothing + CIFSSndTransfer {} -> Nothing + CIFSSndComplete -> Nothing + CIFSSndCancelled -> Nothing + CIFSSndError {} -> Nothing + CIFSSndWarning {} -> Nothing + CIFSRcvInvitation -> Just $ FFENotAccepted fId + CIFSRcvAccepted -> Just FFEInProgress + CIFSRcvTransfer {} -> Just FFEInProgress + CIFSRcvAborted -> Just $ FFENotAccepted fId + CIFSRcvCancelled -> Just FFEFailed + CIFSRcvComplete -> Nothing + CIFSRcvError {} -> Just FFEFailed + CIFSRcvWarning {} -> Just FFEFailed + CIFSInvalid {} -> Just FFEFailed + data ACIFileStatus = forall d. MsgDirectionI d => AFS (SMsgDirection d) (CIFileStatus d) deriving instance Show ACIFileStatus diff --git a/src/Simplex/Chat/Messages/Batch.hs b/src/Simplex/Chat/Messages/Batch.hs index 690ae5828f..c1c45d7b0a 100644 --- a/src/Simplex/Chat/Messages/Batch.hs +++ b/src/Simplex/Chat/Messages/Batch.hs @@ -17,16 +17,18 @@ import Simplex.Chat.Messages data MsgBatch = MsgBatch ByteString [SndMessage] --- | Batches [SndMessage] into batches of ByteStrings in form of JSON arrays. +-- | Batches SndMessages in [Either ChatError SndMessage] into batches of ByteStrings in form of JSON arrays. +-- Preserves original errors in the list. -- Does not check if the resulting batch is a valid JSON. -- If a single element is passed, it is returned as is (a JSON string). -- If an element exceeds maxLen, it is returned as ChatError. -batchMessages :: Int -> [SndMessage] -> [Either ChatError MsgBatch] +batchMessages :: Int -> [Either ChatError SndMessage] -> [Either ChatError MsgBatch] batchMessages maxLen = addBatch . foldr addToBatch ([], [], 0, 0) where msgBatch batch = Right (MsgBatch (encodeMessages batch) batch) - addToBatch :: SndMessage -> ([Either ChatError MsgBatch], [SndMessage], Int, Int) -> ([Either ChatError MsgBatch], [SndMessage], Int, Int) - addToBatch msg@SndMessage {msgBody} acc@(batches, batch, len, n) + addToBatch :: Either ChatError SndMessage -> ([Either ChatError MsgBatch], [SndMessage], Int, Int) -> ([Either ChatError MsgBatch], [SndMessage], Int, Int) + addToBatch (Left err) acc = (Left err : addBatch acc, [], 0, 0) -- step over original error + addToBatch (Right msg@SndMessage {msgBody}) acc@(batches, batch, len, n) | batchLen <= maxLen = (batches, msg : batch, len', n + 1) | msgLen <= maxLen = (addBatch acc, [msg], msgLen, 1) | otherwise = (errLarge msg : addBatch acc, [], 0, 0) diff --git a/src/Simplex/Chat/Remote.hs b/src/Simplex/Chat/Remote.hs index 276baf56e8..527b87c010 100644 --- a/src/Simplex/Chat/Remote.hs +++ b/src/Simplex/Chat/Remote.hs @@ -72,11 +72,11 @@ import UnliftIO.Directory (copyFile, createDirectoryIfMissing, doesDirectoryExis -- when acting as host minRemoteCtrlVersion :: AppVersion -minRemoteCtrlVersion = AppVersion [6, 0, 0, 4] +minRemoteCtrlVersion = AppVersion [6, 1, 0, 0] -- when acting as controller minRemoteHostVersion :: AppVersion -minRemoteHostVersion = AppVersion [6, 0, 0, 4] +minRemoteHostVersion = AppVersion [6, 1, 0, 0] currentAppVersion :: AppVersion currentAppVersion = AppVersion SC.version diff --git a/src/Simplex/Chat/Store/Files.hs b/src/Simplex/Chat/Store/Files.hs index d1da081cee..2c02d872b1 100644 --- a/src/Simplex/Chat/Store/Files.hs +++ b/src/Simplex/Chat/Store/Files.hs @@ -966,20 +966,20 @@ lookupFileTransferRedirectMeta db User {userId} fileId = do redirects <- DB.query db "SELECT file_id FROM files WHERE user_id = ? AND redirect_file_id = ?" (userId, fileId) rights <$> mapM (runExceptT . getFileTransferMeta_ db userId . fromOnly) redirects -createLocalFile :: ToField (CIFileStatus d) => CIFileStatus d -> DB.Connection -> User -> NoteFolder -> ChatItemId -> UTCTime -> CryptoFile -> Integer -> Integer -> IO Int64 -createLocalFile fileStatus db User {userId} NoteFolder {noteFolderId} chatItemId itemTs CryptoFile {filePath, cryptoArgs} fileSize fileChunkSize = do +createLocalFile :: ToField (CIFileStatus d) => CIFileStatus d -> DB.Connection -> User -> NoteFolder -> UTCTime -> CryptoFile -> Integer -> Integer -> IO Int64 +createLocalFile fileStatus db User {userId} NoteFolder {noteFolderId} itemTs CryptoFile {filePath, cryptoArgs} fileSize fileChunkSize = do DB.execute db [sql| INSERT INTO files - ( user_id, note_folder_id, chat_item_id, + ( user_id, note_folder_id, file_name, file_path, file_size, file_crypto_key, file_crypto_nonce, chunk_size, file_inline, ci_file_status, protocol, created_at, updated_at ) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) |] - ( (userId, noteFolderId, chatItemId) + ( (userId, noteFolderId) :. (takeFileName filePath, filePath, fileSize) :. maybe (Nothing, Nothing) (\(CFArgs key nonce) -> (Just key, Just nonce)) cryptoArgs :. (fileChunkSize, Nothing :: Maybe InlineFileMode, fileStatus, FPLocal, itemTs, itemTs) diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs index 6dbd9124c5..f6f9588f66 100644 --- a/src/Simplex/Chat/Store/Messages.hs +++ b/src/Simplex/Chat/Store/Messages.hs @@ -60,10 +60,12 @@ module Simplex.Chat.Store.Messages deleteLocalChatItem, updateDirectChatItemsRead, getDirectUnreadTimedItems, - setDirectChatItemDeleteAt, + updateDirectChatItemsReadList, + setDirectChatItemsDeleteAt, updateGroupChatItemsRead, getGroupUnreadTimedItems, - setGroupChatItemDeleteAt, + updateGroupChatItemsReadList, + setGroupChatItemsDeleteAt, updateLocalChatItemsRead, getChatRefViaItemId, getChatItemVersions, @@ -126,7 +128,9 @@ import Data.ByteString.Char8 (ByteString) import Data.Either (fromRight, rights) import Data.Int (Int64) import Data.List (sortBy) -import Data.Maybe (fromMaybe, isJust, mapMaybe) +import Data.List.NonEmpty (NonEmpty) +import qualified Data.List.NonEmpty as L +import Data.Maybe (catMaybes, fromMaybe, isJust, mapMaybe) import Data.Ord (Down (..), comparing) import Data.Text (Text) import qualified Data.Text as T @@ -1339,15 +1343,27 @@ getDirectUnreadTimedItems db User {userId} contactId itemsRange_ = case itemsRan |] (userId, contactId, CISRcvNew) -setDirectChatItemDeleteAt :: DB.Connection -> User -> ContactId -> ChatItemId -> UTCTime -> IO () -setDirectChatItemDeleteAt db User {userId} contactId chatItemId deleteAt = +updateDirectChatItemsReadList :: DB.Connection -> User -> ContactId -> NonEmpty ChatItemId -> IO [(ChatItemId, Int)] +updateDirectChatItemsReadList db user contactId itemIds = do + catMaybes . L.toList <$> mapM getUpdateDirectItem 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 + +setDirectChatItemsDeleteAt :: DB.Connection -> User -> ContactId -> [(ChatItemId, Int)] -> UTCTime -> IO [(ChatItemId, UTCTime)] +setDirectChatItemsDeleteAt db User {userId} contactId itemIds currentTs = forM itemIds $ \(chatItemId, ttl) -> do + let deleteAt = addUTCTime (realToFrac ttl) currentTs DB.execute db "UPDATE chat_items SET timed_delete_at = ? WHERE user_id = ? AND contact_id = ? AND chat_item_id = ?" (deleteAt, userId, contactId, chatItemId) + pure (chatItemId, deleteAt) -updateGroupChatItemsRead :: DB.Connection -> UserId -> GroupId -> Maybe (ChatItemId, ChatItemId) -> IO () -updateGroupChatItemsRead db userId groupId itemsRange_ = do +updateGroupChatItemsRead :: DB.Connection -> User -> GroupId -> Maybe (ChatItemId, ChatItemId) -> IO () +updateGroupChatItemsRead db User {userId} groupId itemsRange_ = do currentTs <- getCurrentTime case itemsRange_ of Just (fromItemId, toItemId) -> @@ -1392,12 +1408,24 @@ getGroupUnreadTimedItems db User {userId} groupId itemsRange_ = case itemsRange_ |] (userId, groupId, CISRcvNew) -setGroupChatItemDeleteAt :: DB.Connection -> User -> GroupId -> ChatItemId -> UTCTime -> IO () -setGroupChatItemDeleteAt db User {userId} groupId chatItemId deleteAt = +updateGroupChatItemsReadList :: DB.Connection -> User -> GroupId -> NonEmpty ChatItemId -> IO [(ChatItemId, Int)] +updateGroupChatItemsReadList db user groupId itemIds = do + catMaybes . L.toList <$> mapM getUpdateGroupItem 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 + +setGroupChatItemsDeleteAt :: DB.Connection -> User -> GroupId -> [(ChatItemId, Int)] -> UTCTime -> IO [(ChatItemId, UTCTime)] +setGroupChatItemsDeleteAt db User {userId} groupId itemIds currentTs = forM itemIds $ \(chatItemId, ttl) -> do + let deleteAt = addUTCTime (realToFrac ttl) currentTs DB.execute db "UPDATE chat_items SET timed_delete_at = ? WHERE user_id = ? AND group_id = ? AND chat_item_id = ?" (deleteAt, userId, groupId, chatItemId) + pure (chatItemId, deleteAt) updateLocalChatItemsRead :: DB.Connection -> User -> NoteFolderId -> Maybe (ChatItemId, ChatItemId) -> IO () updateLocalChatItemsRead db User {userId} noteFolderId itemsRange_ = do diff --git a/src/Simplex/Chat/Terminal/Input.hs b/src/Simplex/Chat/Terminal/Input.hs index 2d1039e585..4f6d66d2c1 100644 --- a/src/Simplex/Chat/Terminal/Input.hs +++ b/src/Simplex/Chat/Terminal/Input.hs @@ -69,7 +69,7 @@ runInputLoop ct@ChatTerminal {termState, liveMessageState} cc = forever $ do Nothing -> setActive ct "" Just rhId -> updateRemoteUser ct u rhId CRChatItems u chatName_ _ -> whenCurrUser cc u $ mapM_ (setActive ct . chatActiveTo) chatName_ - CRNewChatItem u (AChatItem _ SMDSnd cInfo _) -> whenCurrUser cc u $ setActiveChat ct cInfo + CRNewChatItems u ((AChatItem _ SMDSnd cInfo _) : _) -> whenCurrUser cc u $ setActiveChat ct cInfo CRChatItemUpdated u (AChatItem _ SMDSnd cInfo _) -> whenCurrUser cc u $ setActiveChat ct cInfo CRChatItemsDeleted u ((ChatItemDeletion (AChatItem _ _ cInfo _) _) : _) _ _ -> whenCurrUser cc u $ setActiveChat ct cInfo CRContactDeleted u c -> whenCurrUser cc u $ unsetActiveContact ct c @@ -93,7 +93,7 @@ runInputLoop ct@ChatTerminal {termState, liveMessageState} cc = forever $ do Right SendMessageBroadcast {} -> True _ -> False startLiveMessage :: Either a ChatCommand -> ChatResponse -> IO () - startLiveMessage (Right (SendLiveMessage chatName msg)) (CRNewChatItem _ (AChatItem cType SMDSnd _ ChatItem {meta = CIMeta {itemId}})) = do + startLiveMessage (Right (SendLiveMessage chatName msg)) (CRNewChatItems {chatItems = [AChatItem cType SMDSnd _ ChatItem {meta = CIMeta {itemId}}]}) = do whenM (isNothing <$> readTVarIO liveMessageState) $ do let s = T.unpack msg int = case cType of SCTGroup -> 5000000; _ -> 3000000 :: Int diff --git a/src/Simplex/Chat/Terminal/Main.hs b/src/Simplex/Chat/Terminal/Main.hs index a946ba3483..64703a3a92 100644 --- a/src/Simplex/Chat/Terminal/Main.hs +++ b/src/Simplex/Chat/Terminal/Main.hs @@ -44,7 +44,7 @@ simplexChatCLI' cfg opts@ChatOpts {chatCmd, chatCmdLog, chatCmdDelay, chatServer when (chatCmdLog /= CCLNone) . void . forkIO . forever $ do (_, _, r') <- atomically . readTBQueue $ outputQ cc case r' of - CRNewChatItem {} -> printResponse r' + CRNewChatItems {} -> printResponse r' _ -> when (chatCmdLog == CCLAll) $ printResponse r' sendChatCmdStr cc chatCmd >>= printResponse threadDelay $ chatCmdDelay * 1000000 diff --git a/src/Simplex/Chat/Terminal/Output.hs b/src/Simplex/Chat/Terminal/Output.hs index 40f14a10de..0ead850b86 100644 --- a/src/Simplex/Chat/Terminal/Output.hs +++ b/src/Simplex/Chat/Terminal/Output.hs @@ -147,7 +147,7 @@ runTerminalOutput ct cc@ChatController {outputQ, showLiveItems, logFilePath} Cha forever $ do (_, outputRH, r) <- atomically $ readTBQueue outputQ case r of - CRNewChatItem u ci -> when markRead $ markChatItemRead u ci + CRNewChatItems u (ci : _) -> when markRead $ markChatItemRead u ci -- At the moment of writing received items are created one at a time CRChatItemUpdated u ci -> when markRead $ markChatItemRead u ci CRRemoteHostConnected {remoteHost = RemoteHostInfo {remoteHostId}} -> getRemoteUser remoteHostId CRRemoteHostStopped {remoteHostId_} -> mapM_ removeRemoteUser remoteHostId_ @@ -175,7 +175,8 @@ runTerminalOutput ct cc@ChatController {outputQ, showLiveItems, logFilePath} Cha responseNotification :: ChatTerminal -> ChatController -> ChatResponse -> IO () responseNotification t@ChatTerminal {sendNotification} cc = \case - CRNewChatItem u (AChatItem _ SMDRcv cInfo ci@ChatItem {chatDir, content = CIRcvMsgContent mc, formattedText}) -> + -- At the moment of writing received items are created one at a time + CRNewChatItems u ((AChatItem _ SMDRcv cInfo ci@ChatItem {chatDir, content = CIRcvMsgContent mc, formattedText}) : _) -> when (chatDirNtf u cInfo chatDir $ isMention ci) $ do whenCurrUser cc u $ setActiveChat t cInfo case (cInfo, chatDir) of diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index e154b5b902..cb686ef2b0 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -2,6 +2,7 @@ {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE LambdaCase #-} +{-# LANGUAGE MultiWayIf #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PatternSynonyms #-} @@ -120,7 +121,16 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe CRConnectionVerified u verified code -> ttyUser u [plain $ if verified then "connection verified" else "connection not verified, current code is " <> code] CRContactCode u ct code -> ttyUser u $ viewContactCode ct code testView CRGroupMemberCode u g m code -> ttyUser u $ viewGroupMemberCode g m code testView - CRNewChatItem u (AChatItem _ _ chat item) -> ttyUser u $ unmuted u chat item $ viewChatItem chat item False ts tz <> viewItemReactions item + CRNewChatItems u chatItems + | length chatItems > 20 -> + if + | all (\aci -> aChatItemDir aci == MDRcv) chatItems -> ttyUser u [sShow (length chatItems) <> " new messages"] + | all (\aci -> aChatItemDir aci == MDSnd) chatItems -> ttyUser u [sShow (length chatItems) <> " messages sent"] + | otherwise -> ttyUser u [sShow (length chatItems) <> " new messages created"] + | otherwise -> + concatMap + (\(AChatItem _ _ chat item) -> ttyUser u $ unmuted u chat item $ viewChatItem chat item False ts tz <> viewItemReactions item) + chatItems CRChatItems u _ chatItems -> ttyUser u $ concatMap (\(AChatItem _ _ chat item) -> viewChatItem chat item True ts tz <> viewItemReactions item) chatItems CRChatItemInfo u ci ciInfo -> ttyUser u $ viewChatItemInfo ci ciInfo tz CRChatItemId u itemId -> ttyUser u [plain $ maybe "no item" show itemId] @@ -193,6 +203,7 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe CRUnknownMemberBlocked u g byM um -> ttyUser u [ttyGroup' g <> ": " <> ttyMember byM <> " blocked an unknown member, creating unknown member record " <> ttyMember um] CRUnknownMemberAnnounced u g _ um m -> ttyUser u [ttyGroup' g <> ": unknown member " <> ttyMember um <> " updated to " <> ttyMember m] CRGroupDeletedUser u g -> ttyUser u [ttyGroup' g <> ": you deleted the group"] + CRForwardPlan u count itemIds fc -> ttyUser u $ viewForwardPlan count itemIds fc CRRcvFileDescrReady _ _ _ _ -> [] CRRcvFileProgressXFTP {} -> [] CRRcvFileAccepted u ci -> ttyUser u $ savingFile' ci @@ -920,6 +931,20 @@ viewUserContactLinkDeleted = "To create a new chat address use " <> highlight' "/ad" ] +viewForwardPlan :: Int -> [ChatItemId] -> Maybe ForwardConfirmation -> [StyledString] +viewForwardPlan count itemIds = maybe [forwardCount] $ \fc -> [confirmation fc, forwardCount] + where + confirmation = \case + FCFilesNotAccepted fileIds -> plain $ "Files can be received: " <> intercalate ", " (map show fileIds) + FCFilesInProgress cnt -> plain $ "Still receiving " <> show cnt <> " file(s)" + FCFilesMissing cnt -> plain $ show cnt <> " file(s) are missing" + FCFilesFailed cnt -> plain $ "Receiving " <> show cnt <> " file(s) failed" + forwardCount + | count == len = "all messages can be forwarded" + | len == 0 = "nothing to forward" + | otherwise = plain $ show len <> " message(s) out of " <> show count <> " can be forwarded" + len = length itemIds + connReqContact_ :: StyledString -> ConnReqContact -> [StyledString] connReqContact_ intro cReq = [ intro, @@ -2024,8 +2049,7 @@ viewChatError isCmd logLevel testView = \case CEFallbackToSMPProhibited fileId -> ["recipient tried to accept file " <> sShow fileId <> " via old protocol, prohibited"] CEInlineFileProhibited _ -> ["A small file sent without acceptance - you can enable receiving such files with -f option."] CEInvalidQuote -> ["cannot reply to this message"] - CEInvalidForward -> ["cannot forward this message"] - CEForwardNoFile -> ["cannot forward this message, file not found"] + CEInvalidForward -> ["cannot forward message(s)"] CEInvalidChatItemUpdate -> ["cannot update this item"] CEInvalidChatItemDelete -> ["cannot delete this item"] CEHasCurrentCall -> ["call already in progress"] diff --git a/tests/ChatTests/Direct.hs b/tests/ChatTests/Direct.hs index 09b2d7d51c..97a9d89200 100644 --- a/tests/ChatTests/Direct.hs +++ b/tests/ChatTests/Direct.hs @@ -3,6 +3,7 @@ {-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE PostfixOperators #-} {-# LANGUAGE RankNTypes #-} +{-# LANGUAGE ScopedTypeVariables #-} module ChatTests.Direct where @@ -17,14 +18,17 @@ import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Lazy.Char8 as LB import Data.List (intercalate) import qualified Data.Text as T +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 (..)) +import Simplex.Chat.Messages (ChatItemId) import Simplex.Chat.Options (ChatOpts (..)) import Simplex.Chat.Protocol (supportedChatVRange) import Simplex.Chat.Store (agentStoreFile, chatStoreFile) import Simplex.Chat.Types (VersionRangeChat, authErrDisableCount, sameVerificationCode, verificationCode, pattern VersionChat) +import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Util (safeDecodeUtf8) import Simplex.Messaging.Version @@ -36,6 +40,7 @@ chatDirectTests :: SpecWith FilePath chatDirectTests = do describe "direct messages" $ do describe "add contact and send/receive messages" testAddContact + it "mark multiple messages as read" testMarkReadDirect it "clear chat with contact" testContactClear it "deleting contact deletes profile" testDeleteContactDeletesProfile it "delete contact keeping conversation" testDeleteContactKeepConversation @@ -52,6 +57,11 @@ chatDirectTests = do it "repeat AUTH errors disable contact" testRepeatAuthErrorsDisableContact it "should send multiline message" testMultilineMessage it "send large message" testLargeMessage + describe "batch send messages" $ do + it "send multiple messages api" testSendMulti + it "send multiple timed messages" testSendMultiTimed + it "send multiple messages, including quote" testSendMultiWithQuote + it "send multiple messages (many chat batches)" testSendMultiManyBatches describe "duplicate contacts" $ do it "duplicate contacts are separate (contacts don't merge)" testDuplicateContactsSeparate it "new contact is separate with multiple duplicate contacts (contacts don't merge)" testDuplicateContactsMultipleSeparate @@ -205,6 +215,22 @@ testAddContact = versionTestMatrix2 runTestAddContact then chatFeatures else (0, e2eeInfoNoPQStr) : tail chatFeatures +testMarkReadDirect :: HasCallStack => FilePath -> IO () +testMarkReadDirect = testChat2 aliceProfile bobProfile $ \alice bob -> do + connectUsers alice bob + alice #> "@bob 1" + alice #> "@bob 2" + alice #> "@bob 3" + alice #> "@bob 4" + bob <# "alice> 1" + bob <# "alice> 2" + bob <# "alice> 3" + bob <# "alice> 4" + bob ##> "/last_item_id" + i :: ChatItemId <- read <$> getTermLine bob + let itemIds = intercalate "," $ map show [i - 3 .. i] + bob #$> ("/_read chat items @2 " <> itemIds, id, "ok") + testDuplicateContactsSeparate :: HasCallStack => FilePath -> IO () testDuplicateContactsSeparate = testChat2 aliceProfile bobProfile $ @@ -715,22 +741,27 @@ testDirectMessageDeleteMultipleManyBatches = \alice bob -> do connectUsers alice bob - alice #> "@bob message 0" - bob <# "alice> message 0" - msgIdFirst <- lastItemId alice + msgIdZero <- lastItemId alice - forM_ [(1 :: Int) .. 300] $ \i -> do - alice #> ("@bob message " <> show i) - bob <# ("alice> message " <> show i) + let cm i = "{\"msgContent\": {\"type\": \"text\", \"text\": \"message " <> show i <> "\"}}" + cms = intercalate ", " (map cm [1 .. 300 :: Int]) + + alice `send` ("/_send @2 json [" <> cms <> "]") + _ <- getTermLine alice + + alice <## "300 messages sent" msgIdLast <- lastItemId alice - let mIdFirst = read msgIdFirst :: Int + forM_ [(1 :: Int) .. 300] $ \i -> do + bob <# ("alice> message " <> show i) + + let mIdFirst = (read msgIdZero :: Int) + 1 mIdLast = read msgIdLast :: Int deleteIds = intercalate "," (map show [mIdFirst .. mIdLast]) alice `send` ("/_delete item @2 " <> deleteIds <> " broadcast") _ <- getTermLine alice - alice <## "301 messages deleted" - forM_ [(0 :: Int) .. 300] $ \i -> do + alice <## "300 messages deleted" + forM_ [(1 :: Int) .. 300] $ \i -> do bob <# ("alice> [marked deleted] message " <> show i) testDirectLiveMessage :: HasCallStack => FilePath -> IO () @@ -839,6 +870,112 @@ testLargeMessage = bob <## "contact alice changed to alice2" bob <## "use @alice2 to send messages" +testSendMulti :: HasCallStack => FilePath -> IO () +testSendMulti = + testChat2 aliceProfile bobProfile $ + \alice bob -> do + connectUsers alice bob + + alice ##> "/_send @2 json [{\"msgContent\": {\"type\": \"text\", \"text\": \"test 1\"}}, {\"msgContent\": {\"type\": \"text\", \"text\": \"test 2\"}}]" + alice <# "@bob test 1" + alice <# "@bob test 2" + bob <# "alice> test 1" + bob <# "alice> test 2" + +testSendMultiTimed :: HasCallStack => FilePath -> IO () +testSendMultiTimed = + testChat2 aliceProfile bobProfile $ + \alice bob -> do + connectUsers alice bob + + alice ##> "/_send @2 ttl=1 json [{\"msgContent\": {\"type\": \"text\", \"text\": \"test 1\"}}, {\"msgContent\": {\"type\": \"text\", \"text\": \"test 2\"}}]" + alice <# "@bob test 1" + alice <# "@bob test 2" + bob <# "alice> test 1" + bob <# "alice> test 2" + + alice + <### [ "timed message deleted: test 1", + "timed message deleted: test 2" + ] + bob + <### [ "timed message deleted: test 1", + "timed message deleted: test 2" + ] + +testSendMultiWithQuote :: HasCallStack => FilePath -> IO () +testSendMultiWithQuote = + testChat2 aliceProfile bobProfile $ + \alice bob -> do + connectUsers alice bob + + alice #> "@bob hello" + bob <# "alice> hello" + msgId1 <- lastItemId alice + + threadDelay 1000000 + + bob #> "@alice hi" + alice <# "bob> hi" + msgId2 <- lastItemId alice + + let cm1 = "{\"msgContent\": {\"type\": \"text\", \"text\": \"message 1\"}}" + cm2 = "{\"quotedItemId\": " <> msgId1 <> ", \"msgContent\": {\"type\": \"text\", \"text\": \"message 2\"}}" + cm3 = "{\"quotedItemId\": " <> msgId2 <> ", \"msgContent\": {\"type\": \"text\", \"text\": \"message 3\"}}" + + alice ##> ("/_send @2 json [" <> cm1 <> ", " <> cm2 <> ", " <> cm3 <> "]") + alice <## "bad chat command: invalid multi send: live and more than one quote not supported" + + alice ##> ("/_send @2 json [" <> cm1 <> ", " <> cm2 <> "]") + + alice <# "@bob message 1" + alice <# "@bob >> hello" + alice <## " message 2" + + bob <# "alice> message 1" + bob <# "alice> >> hello" + bob <## " message 2" + + alice ##> ("/_send @2 json [" <> cm3 <> ", " <> cm1 <> "]") + + alice <# "@bob > hi" + alice <## " message 3" + alice <# "@bob message 1" + + bob <# "alice> > hi" + bob <## " message 3" + bob <# "alice> message 1" + +testSendMultiManyBatches :: HasCallStack => FilePath -> IO () +testSendMultiManyBatches = + testChat2 aliceProfile bobProfile $ + \alice bob -> do + connectUsers alice bob + + threadDelay 1000000 + + msgIdAlice <- lastItemId alice + msgIdBob <- lastItemId bob + + let cm i = "{\"msgContent\": {\"type\": \"text\", \"text\": \"message " <> show i <> "\"}}" + cms = intercalate ", " (map cm [1 .. 300 :: Int]) + + alice `send` ("/_send @2 json [" <> cms <> "]") + _ <- getTermLine alice + + alice <## "300 messages sent" + + forM_ [(1 :: Int) .. 300] $ \i -> + bob <# ("alice> message " <> show i) + + aliceItemsCount <- withCCTransaction alice $ \db -> + DB.query db "SELECT count(1) FROM chat_items WHERE chat_item_id > ?" (Only msgIdAlice) :: IO [[Int]] + aliceItemsCount `shouldBe` [[300]] + + bobItemsCount <- withCCTransaction bob $ \db -> + DB.query db "SELECT count(1) FROM chat_items WHERE chat_item_id > ?" (Only msgIdBob) :: IO [[Int]] + bobItemsCount `shouldBe` [[300]] + testGetSetSMPServers :: HasCallStack => FilePath -> IO () testGetSetSMPServers = testChat2 aliceProfile bobProfile $ @@ -2162,7 +2299,7 @@ testSetChatItemTTL = -- chat item with file alice #$> ("/_files_folder ./tests/tmp/app_files", id, "ok") copyFile "./tests/fixtures/test.jpg" "./tests/tmp/app_files/test.jpg" - alice ##> "/_send @2 json {\"filePath\": \"test.jpg\", \"msgContent\": {\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}" + alice ##> "/_send @2 json [{\"filePath\": \"test.jpg\", \"msgContent\": {\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}]" alice <# "/f @bob test.jpg" alice <## "use /fc 1 to cancel sending" bob <# "alice> sends file test.jpg (136.5 KiB / 139737 bytes)" @@ -2410,7 +2547,7 @@ setupDesynchronizedRatchet tmp alice = do (bob "/tail @alice 1" bob <# "alice> decryption error, possibly due to the device change (header, 3 messages)" - bob ##> "@alice 1" + bob `send` "@alice 1" bob <## "error: command is prohibited, sendMessagesB: send prohibited" (alice FilePath -> IO () runTestMessageWithFile = testChat2 aliceProfile bobProfile $ \alice bob -> withXFTPServer $ do connectUsers alice bob - alice ##> "/_send @2 json {\"filePath\": \"./tests/fixtures/test.jpg\", \"msgContent\": {\"type\": \"text\", \"text\": \"hi, sending a file\"}}" + alice ##> "/_send @2 json [{\"filePath\": \"./tests/fixtures/test.jpg\", \"msgContent\": {\"type\": \"text\", \"text\": \"hi, sending a file\"}}]" alice <# "@bob hi, sending a file" alice <# "/f @bob ./tests/fixtures/test.jpg" alice <## "use /fc 1 to cancel sending" @@ -91,7 +94,7 @@ testSendImage = testChat2 aliceProfile bobProfile $ \alice bob -> withXFTPServer $ do connectUsers alice bob - alice ##> "/_send @2 json {\"filePath\": \"./tests/fixtures/test.jpg\", \"msgContent\": {\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}" + alice ##> "/_send @2 json [{\"filePath\": \"./tests/fixtures/test.jpg\", \"msgContent\": {\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}]" alice <# "/f @bob ./tests/fixtures/test.jpg" alice <## "use /fc 1 to cancel sending" bob <# "alice> sends file test.jpg (136.5 KiB / 139737 bytes)" @@ -122,7 +125,7 @@ testSenderMarkItemDeleted = testChat2 aliceProfile bobProfile $ \alice bob -> withXFTPServer $ do connectUsers alice bob - alice ##> "/_send @2 json {\"filePath\": \"./tests/fixtures/test_1MB.pdf\", \"msgContent\": {\"type\": \"text\", \"text\": \"hi, sending a file\"}}" + alice ##> "/_send @2 json [{\"filePath\": \"./tests/fixtures/test_1MB.pdf\", \"msgContent\": {\"type\": \"text\", \"text\": \"hi, sending a file\"}}]" alice <# "@bob hi, sending a file" alice <# "/f @bob ./tests/fixtures/test_1MB.pdf" alice <## "use /fc 1 to cancel sending" @@ -147,7 +150,7 @@ testFilesFoldersSendImage = connectUsers alice bob alice #$> ("/_files_folder ./tests/fixtures", id, "ok") bob #$> ("/_files_folder ./tests/tmp/app_files", id, "ok") - alice ##> "/_send @2 json {\"filePath\": \"test.jpg\", \"msgContent\": {\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}" + alice ##> "/_send @2 json [{\"filePath\": \"test.jpg\", \"msgContent\": {\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}]" alice <# "/f @bob test.jpg" alice <## "use /fc 1 to cancel sending" bob <# "alice> sends file test.jpg (136.5 KiB / 139737 bytes)" @@ -180,7 +183,7 @@ testFilesFoldersImageSndDelete = alice #$> ("/_files_folder ./tests/tmp/alice_app_files", id, "ok") copyFile "./tests/fixtures/test_1MB.pdf" "./tests/tmp/alice_app_files/test_1MB.pdf" bob #$> ("/_files_folder ./tests/tmp/bob_app_files", id, "ok") - alice ##> "/_send @2 json {\"filePath\": \"test_1MB.pdf\", \"msgContent\": {\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}" + alice ##> "/_send @2 json [{\"filePath\": \"test_1MB.pdf\", \"msgContent\": {\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}]" alice <# "/f @bob test_1MB.pdf" alice <## "use /fc 1 to cancel sending" bob <# "alice> sends file test_1MB.pdf (1017.7 KiB / 1042157 bytes)" @@ -212,7 +215,7 @@ testFilesFoldersImageRcvDelete = connectUsers alice bob alice #$> ("/_files_folder ./tests/fixtures", id, "ok") bob #$> ("/_files_folder ./tests/tmp/app_files", id, "ok") - alice ##> "/_send @2 json {\"filePath\": \"test.jpg\", \"msgContent\": {\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}" + alice ##> "/_send @2 json [{\"filePath\": \"test.jpg\", \"msgContent\": {\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}]" alice <# "/f @bob test.jpg" alice <## "use /fc 1 to cancel sending" bob <# "alice> sends file test.jpg (136.5 KiB / 139737 bytes)" @@ -239,7 +242,7 @@ testSendImageWithTextAndQuote = connectUsers alice bob bob #> "@alice hi alice" alice <# "bob> hi alice" - alice ##> ("/_send @2 json {\"filePath\": \"./tests/fixtures/test.jpg\", \"quotedItemId\": " <> itemId 1 <> ", \"msgContent\": {\"text\":\"hey bob\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}") + alice ##> ("/_send @2 json [{\"filePath\": \"./tests/fixtures/test.jpg\", \"quotedItemId\": " <> itemId 1 <> ", \"msgContent\": {\"text\":\"hey bob\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}]") alice <# "@bob > hi alice" alice <## " hey bob" alice <# "/f @bob ./tests/fixtures/test.jpg" @@ -265,7 +268,7 @@ testSendImageWithTextAndQuote = bob @@@ [("@alice", "hey bob")] -- quoting (file + text) with file uses quoted text - bob ##> ("/_send @2 json {\"filePath\": \"./tests/fixtures/test.pdf\", \"quotedItemId\": " <> itemId 2 <> ", \"msgContent\": {\"text\":\"\",\"type\":\"file\"}}") + bob ##> ("/_send @2 json [{\"filePath\": \"./tests/fixtures/test.pdf\", \"quotedItemId\": " <> itemId 2 <> ", \"msgContent\": {\"text\":\"\",\"type\":\"file\"}}]") bob <# "@alice > hey bob" bob <## " test.pdf" bob <# "/f @alice ./tests/fixtures/test.pdf" @@ -287,7 +290,7 @@ testSendImageWithTextAndQuote = B.readFile "./tests/tmp/test.pdf" `shouldReturn` txtSrc -- quoting (file without text) with file uses file name - alice ##> ("/_send @2 json {\"filePath\": \"./tests/fixtures/test.jpg\", \"quotedItemId\": " <> itemId 3 <> ", \"msgContent\": {\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}") + alice ##> ("/_send @2 json [{\"filePath\": \"./tests/fixtures/test.jpg\", \"quotedItemId\": " <> itemId 3 <> ", \"msgContent\": {\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}]") alice <# "@bob > test.pdf" alice <## " test.jpg" alice <# "/f @bob ./tests/fixtures/test.jpg" @@ -313,7 +316,7 @@ testGroupSendImage = \alice bob cath -> withXFTPServer $ do createGroup3 "team" alice bob cath threadDelay 1000000 - alice ##> "/_send #1 json {\"filePath\": \"./tests/fixtures/test.jpg\", \"msgContent\": {\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}" + alice ##> "/_send #1 json [{\"filePath\": \"./tests/fixtures/test.jpg\", \"msgContent\": {\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}]" alice <# "/f #team ./tests/fixtures/test.jpg" alice <## "use /fc 1 to cancel sending" concurrentlyN_ @@ -361,7 +364,7 @@ testGroupSendImageWithTextAndQuote = (cath <# "#team bob> hi team") threadDelay 1000000 msgItemId <- lastItemId alice - alice ##> ("/_send #1 json {\"filePath\": \"./tests/fixtures/test.jpg\", \"quotedItemId\": " <> msgItemId <> ", \"msgContent\": {\"text\":\"hey bob\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}") + alice ##> ("/_send #1 json [{\"filePath\": \"./tests/fixtures/test.jpg\", \"quotedItemId\": " <> msgItemId <> ", \"msgContent\": {\"text\":\"hey bob\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}]") alice <# "#team > bob hi team" alice <## " hey bob" alice <# "/f #team ./tests/fixtures/test.jpg" @@ -406,6 +409,166 @@ testGroupSendImageWithTextAndQuote = cath #$> ("/_get chat #1 count=2", chat'', [((0, "hi team"), Nothing, Nothing), ((0, "hey bob"), Just (0, "hi team"), Just "./tests/tmp/test_1.jpg")]) cath @@@ [("#team", "hey bob"), ("@alice", "received invitation to join group team as admin")] +testSendMultiFilesDirect :: HasCallStack => FilePath -> IO () +testSendMultiFilesDirect = + testChat2 aliceProfile bobProfile $ \alice bob -> do + withXFTPServer $ do + connectUsers alice bob + + alice #$> ("/_files_folder ./tests/tmp/alice_app_files", id, "ok") + copyFile "./tests/fixtures/test.jpg" "./tests/tmp/alice_app_files/test.jpg" + copyFile "./tests/fixtures/test.pdf" "./tests/tmp/alice_app_files/test.pdf" + bob #$> ("/_files_folder ./tests/tmp/bob_app_files", id, "ok") + + let cm1 = "{\"msgContent\": {\"type\": \"text\", \"text\": \"message without file\"}}" + cm2 = "{\"filePath\": \"test.jpg\", \"msgContent\": {\"type\": \"text\", \"text\": \"sending file 1\"}}" + cm3 = "{\"filePath\": \"test.pdf\", \"msgContent\": {\"type\": \"text\", \"text\": \"sending file 2\"}}" + alice ##> ("/_send @2 json [" <> cm1 <> "," <> cm2 <> "," <> cm3 <> "]") + + alice <# "@bob message without file" + + alice <# "@bob sending file 1" + alice <# "/f @bob test.jpg" + alice <## "use /fc 1 to cancel sending" + + alice <# "@bob sending file 2" + alice <# "/f @bob test.pdf" + alice <## "use /fc 2 to cancel sending" + + bob <# "alice> message without file" + + bob <# "alice> sending file 1" + bob <# "alice> sends file test.jpg (136.5 KiB / 139737 bytes)" + bob <## "use /fr 1 [/ | ] to receive it" + + bob <# "alice> sending file 2" + bob <# "alice> sends file test.pdf (266.0 KiB / 272376 bytes)" + bob <## "use /fr 2 [/ | ] to receive it" + + alice <## "completed uploading file 1 (test.jpg) for bob" + alice <## "completed uploading file 2 (test.pdf) for bob" + + bob ##> "/fr 1" + bob + <### [ "saving file 1 from alice to test.jpg", + "started receiving file 1 (test.jpg) from alice" + ] + bob <## "completed receiving file 1 (test.jpg) from alice" + + bob ##> "/fr 2" + bob + <### [ "saving file 2 from alice to test.pdf", + "started receiving file 2 (test.pdf) from alice" + ] + bob <## "completed receiving file 2 (test.pdf) from alice" + + src1 <- B.readFile "./tests/tmp/alice_app_files/test.jpg" + dest1 <- B.readFile "./tests/tmp/bob_app_files/test.jpg" + dest1 `shouldBe` src1 + + src2 <- B.readFile "./tests/tmp/alice_app_files/test.pdf" + dest2 <- B.readFile "./tests/tmp/bob_app_files/test.pdf" + dest2 `shouldBe` src2 + + alice #$> ("/_get chat @2 count=3", chatF, [((1, "message without file"), Nothing), ((1, "sending file 1"), Just "test.jpg"), ((1, "sending file 2"), Just "test.pdf")]) + bob #$> ("/_get chat @2 count=3", chatF, [((0, "message without file"), Nothing), ((0, "sending file 1"), Just "test.jpg"), ((0, "sending file 2"), Just "test.pdf")]) + +testSendMultiFilesGroup :: HasCallStack => FilePath -> IO () +testSendMultiFilesGroup = + testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do + withXFTPServer $ do + createGroup3 "team" alice bob cath + + threadDelay 1000000 + + alice #$> ("/_files_folder ./tests/tmp/alice_app_files", id, "ok") + copyFile "./tests/fixtures/test.jpg" "./tests/tmp/alice_app_files/test.jpg" + copyFile "./tests/fixtures/test.pdf" "./tests/tmp/alice_app_files/test.pdf" + bob #$> ("/_files_folder ./tests/tmp/bob_app_files", id, "ok") + cath #$> ("/_files_folder ./tests/tmp/cath_app_files", id, "ok") + + let cm1 = "{\"msgContent\": {\"type\": \"text\", \"text\": \"message without file\"}}" + cm2 = "{\"filePath\": \"test.jpg\", \"msgContent\": {\"type\": \"text\", \"text\": \"sending file 1\"}}" + cm3 = "{\"filePath\": \"test.pdf\", \"msgContent\": {\"type\": \"text\", \"text\": \"sending file 2\"}}" + alice ##> ("/_send #1 json [" <> cm1 <> "," <> cm2 <> "," <> cm3 <> "]") + + alice <# "#team message without file" + + alice <# "#team sending file 1" + alice <# "/f #team test.jpg" + alice <## "use /fc 1 to cancel sending" + + alice <# "#team sending file 2" + alice <# "/f #team test.pdf" + alice <## "use /fc 2 to cancel sending" + + bob <# "#team alice> message without file" + + bob <# "#team alice> sending file 1" + bob <# "#team alice> sends file test.jpg (136.5 KiB / 139737 bytes)" + bob <## "use /fr 1 [/ | ] to receive it" + + bob <# "#team alice> sending file 2" + bob <# "#team alice> sends file test.pdf (266.0 KiB / 272376 bytes)" + bob <## "use /fr 2 [/ | ] to receive it" + + cath <# "#team alice> message without file" + + cath <# "#team alice> sending file 1" + cath <# "#team alice> sends file test.jpg (136.5 KiB / 139737 bytes)" + cath <## "use /fr 1 [/ | ] to receive it" + + cath <# "#team alice> sending file 2" + cath <# "#team alice> sends file test.pdf (266.0 KiB / 272376 bytes)" + cath <## "use /fr 2 [/ | ] to receive it" + + alice <## "completed uploading file 1 (test.jpg) for #team" + alice <## "completed uploading file 2 (test.pdf) for #team" + + bob ##> "/fr 1" + bob + <### [ "saving file 1 from alice to test.jpg", + "started receiving file 1 (test.jpg) from alice" + ] + bob <## "completed receiving file 1 (test.jpg) from alice" + + bob ##> "/fr 2" + bob + <### [ "saving file 2 from alice to test.pdf", + "started receiving file 2 (test.pdf) from alice" + ] + bob <## "completed receiving file 2 (test.pdf) from alice" + + cath ##> "/fr 1" + cath + <### [ "saving file 1 from alice to test.jpg", + "started receiving file 1 (test.jpg) from alice" + ] + cath <## "completed receiving file 1 (test.jpg) from alice" + + cath ##> "/fr 2" + cath + <### [ "saving file 2 from alice to test.pdf", + "started receiving file 2 (test.pdf) from alice" + ] + cath <## "completed receiving file 2 (test.pdf) from alice" + + src1 <- B.readFile "./tests/tmp/alice_app_files/test.jpg" + dest1_1 <- B.readFile "./tests/tmp/bob_app_files/test.jpg" + dest1_2 <- B.readFile "./tests/tmp/cath_app_files/test.jpg" + dest1_1 `shouldBe` src1 + dest1_2 `shouldBe` src1 + + src2 <- B.readFile "./tests/tmp/alice_app_files/test.pdf" + dest2_1 <- B.readFile "./tests/tmp/bob_app_files/test.pdf" + dest2_2 <- B.readFile "./tests/tmp/cath_app_files/test.pdf" + dest2_1 `shouldBe` src2 + dest2_2 `shouldBe` src2 + + alice #$> ("/_get chat #1 count=3", chatF, [((1, "message without file"), Nothing), ((1, "sending file 1"), Just "test.jpg"), ((1, "sending file 2"), Just "test.pdf")]) + bob #$> ("/_get chat #1 count=3", chatF, [((0, "message without file"), Nothing), ((0, "sending file 1"), Just "test.jpg"), ((0, "sending file 2"), Just "test.pdf")]) + cath #$> ("/_get chat #1 count=3", chatF, [((0, "message without file"), Nothing), ((0, "sending file 1"), Just "test.jpg"), ((0, "sending file 2"), Just "test.pdf")]) + testXFTPRoundFDCount :: Expectation testXFTPRoundFDCount = do roundedFDCount (-100) `shouldBe` 4 @@ -460,7 +623,7 @@ testXFTPFileTransferEncrypted = let fileJSON = LB.unpack $ J.encode $ CryptoFile srcPath $ Just cfArgs withXFTPServer $ do connectUsers alice bob - alice ##> ("/_send @2 json {\"msgContent\":{\"type\":\"file\", \"text\":\"\"}, \"fileSource\": " <> fileJSON <> "}") + alice ##> ("/_send @2 json [{\"msgContent\":{\"type\":\"file\", \"text\":\"\"}, \"fileSource\": " <> fileJSON <> "}]") alice <# "/f @bob ./tests/tmp/alice/test.pdf" alice <## "use /fc 1 to cancel sending" bob <# "alice> sends file test.pdf (266.0 KiB / 272376 bytes)" diff --git a/tests/ChatTests/Forward.hs b/tests/ChatTests/Forward.hs index d49c6df955..3b861a8417 100644 --- a/tests/ChatTests/Forward.hs +++ b/tests/ChatTests/Forward.hs @@ -7,7 +7,11 @@ import ChatClient import ChatTests.Utils import Control.Concurrent (threadDelay) import qualified Data.ByteString.Char8 as B -import System.Directory (copyFile, doesFileExist) +import Data.List (intercalate) +import qualified Data.Text as T +import System.Directory (copyFile, doesFileExist, removeFile) +import Simplex.Chat (fixedImagePreview) +import Simplex.Chat.Types (ImageData (..)) import Test.Hspec hiding (it) chatForwardTests :: SpecWith FilePath @@ -33,6 +37,10 @@ chatForwardTests = do it "with relative paths: from contact to contact" testForwardFileContactToContact it "with relative paths: from group to notes" testForwardFileGroupToNotes it "with relative paths: from notes to group" testForwardFileNotesToGroup + describe "multi forward api" $ do + it "from contact to contact" testForwardContactToContactMulti + it "from group to group" testForwardGroupToGroupMulti + it "with relative paths: multiple files from contact to contact" testMultiForwardFiles testForwardContactToContact :: HasCallStack => FilePath -> IO () testForwardContactToContact = @@ -384,7 +392,7 @@ testForwardFileNoFilesFolder = connectUsers bob cath -- send original file - alice ##> "/_send @2 json {\"filePath\": \"./tests/fixtures/test.pdf\", \"msgContent\": {\"type\": \"text\", \"text\": \"hi\"}}" + alice ##> "/_send @2 json [{\"filePath\": \"./tests/fixtures/test.pdf\", \"msgContent\": {\"type\": \"text\", \"text\": \"hi\"}}]" alice <# "@bob hi" alice <# "/f @bob ./tests/fixtures/test.pdf" alice <## "use /fc 1 to cancel sending" @@ -441,7 +449,7 @@ testForwardFileContactToContact = connectUsers bob cath -- send original file - alice ##> "/_send @2 json {\"filePath\": \"test.pdf\", \"msgContent\": {\"type\": \"text\", \"text\": \"hi\"}}" + alice ##> "/_send @2 json [{\"filePath\": \"test.pdf\", \"msgContent\": {\"type\": \"text\", \"text\": \"hi\"}}]" alice <# "@bob hi" alice <# "/f @bob test.pdf" alice <## "use /fc 1 to cancel sending" @@ -506,7 +514,7 @@ testForwardFileGroupToNotes = createCCNoteFolder cath -- send original file - alice ##> "/_send #1 json {\"filePath\": \"test.pdf\", \"msgContent\": {\"type\": \"text\", \"text\": \"hi\"}}" + alice ##> "/_send #1 json [{\"filePath\": \"test.pdf\", \"msgContent\": {\"type\": \"text\", \"text\": \"hi\"}}]" alice <# "#team hi" alice <# "/f #team test.pdf" alice <## "use /fc 1 to cancel sending" @@ -555,7 +563,7 @@ testForwardFileNotesToGroup = createGroup2 "team" alice cath -- create original file - alice ##> "/_create *1 json {\"filePath\": \"test.pdf\", \"msgContent\": {\"type\": \"text\", \"text\": \"hi\"}}" + alice ##> "/_create *1 json [{\"filePath\": \"test.pdf\", \"msgContent\": {\"type\": \"text\", \"text\": \"hi\"}}]" alice <# "* hi" alice <# "* file 1 (test.pdf)" @@ -590,3 +598,293 @@ testForwardFileNotesToGroup = alice <## "notes: all messages are removed" fwdFileExists <- doesFileExist "./tests/tmp/alice_files/test_1.pdf" fwdFileExists `shouldBe` True + +testForwardContactToContactMulti :: HasCallStack => FilePath -> IO () +testForwardContactToContactMulti = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + connectUsers alice bob + connectUsers alice cath + connectUsers bob cath + + alice #> "@bob hi" + bob <# "alice> hi" + msgId1 <- lastItemId alice + + threadDelay 1000000 + + bob #> "@alice hey" + alice <# "bob> hey" + msgId2 <- lastItemId alice + + alice ##> ("/_forward plan @2 " <> msgId1 <> "," <> msgId2) + alice <## "all messages can be forwarded" + alice ##> ("/_forward @3 @2 " <> msgId1 <> "," <> msgId2) + alice <# "@cath <- you @bob" + alice <## " hi" + alice <# "@cath <- @bob" + alice <## " hey" + cath <# "alice> -> forwarded" + cath <## " hi" + cath <# "alice> -> forwarded" + cath <## " hey" + +testForwardGroupToGroupMulti :: HasCallStack => FilePath -> IO () +testForwardGroupToGroupMulti = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + createGroup2 "team" alice bob + createGroup2 "club" alice cath + + threadDelay 1000000 + + alice #> "#team hi" + bob <# "#team alice> hi" + msgId1 <- lastItemId alice + + threadDelay 1000000 + + bob #> "#team hey" + alice <# "#team bob> hey" + msgId2 <- lastItemId alice + + alice ##> ("/_forward plan #1 " <> msgId1 <> "," <> msgId2) + alice <## "all messages can be forwarded" + alice ##> ("/_forward #2 #1 " <> msgId1 <> "," <> msgId2) + alice <# "#club <- you #team" + alice <## " hi" + alice <# "#club <- #team" + alice <## " hey" + cath <# "#club alice> -> forwarded" + cath <## " hi" + cath <# "#club alice> -> forwarded" + cath <## " hey" + + -- read chat + alice ##> "/tail #club 2" + alice <# "#club <- you #team" + alice <## " hi" + alice <# "#club <- #team" + alice <## " hey" + + cath ##> "/tail #club 2" + cath <# "#club alice> -> forwarded" + cath <## " hi" + cath <# "#club alice> -> forwarded" + cath <## " hey" + +testMultiForwardFiles :: HasCallStack => FilePath -> IO () +testMultiForwardFiles = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> withXFTPServer $ do + setRelativePaths alice "./tests/tmp/alice_app_files" "./tests/tmp/alice_xftp" + copyFile "./tests/fixtures/test.jpg" "./tests/tmp/alice_app_files/test.jpg" + copyFile "./tests/fixtures/test.pdf" "./tests/tmp/alice_app_files/test.pdf" + copyFile "./tests/fixtures/test_1MB.pdf" "./tests/tmp/alice_app_files/test_1MB.pdf" + copyFile "./tests/fixtures/logo.jpg" "./tests/tmp/alice_app_files/logo.jpg" + setRelativePaths bob "./tests/tmp/bob_app_files" "./tests/tmp/bob_xftp" + setRelativePaths cath "./tests/tmp/cath_app_files" "./tests/tmp/cath_xftp" + connectUsers alice bob + connectUsers bob cath + + threadDelay 1000000 + + msgIdZero <- lastItemId bob + + bob #> "@alice hi" + alice <# "bob> hi" + + -- send original files + let cm1 = "{\"msgContent\": {\"type\": \"text\", \"text\": \"message without file\"}}" + ImageData img = fixedImagePreview + cm2 = "{\"filePath\": \"test.jpg\", \"msgContent\": {\"type\": \"image\", \"image\":\"" <> T.unpack img <> "\", \"text\": \"\"}}" + cm3 = "{\"filePath\": \"test.pdf\", \"msgContent\": {\"type\": \"file\", \"text\": \"\"}}" + cm4 = "{\"filePath\": \"test_1MB.pdf\", \"msgContent\": {\"type\": \"file\", \"text\": \"message with large file\"}}" + cm5 = "{\"filePath\": \"logo.jpg\", \"msgContent\": {\"type\": \"image\", \"image\":\"" <> T.unpack img <> "\", \"text\": \"\"}}" + alice ##> ("/_send @2 json [" <> intercalate "," [cm1, cm2, cm3, cm4, cm5] <> "]") + + alice <# "@bob message without file" + + alice <# "/f @bob test.jpg" + alice <## "use /fc 1 to cancel sending" + + alice <# "/f @bob test.pdf" + alice <## "use /fc 2 to cancel sending" + + alice <# "@bob message with large file" + alice <# "/f @bob test_1MB.pdf" + alice <## "use /fc 3 to cancel sending" + + alice <# "/f @bob logo.jpg" + alice <## "use /fc 4 to cancel sending" + + bob <# "alice> message without file" + + bob <# "alice> sends file test.jpg (136.5 KiB / 139737 bytes)" + bob <## "use /fr 1 [/ | ] to receive it" + + bob <# "alice> sends file test.pdf (266.0 KiB / 272376 bytes)" + bob <## "use /fr 2 [/ | ] to receive it" + + bob <# "alice> message with large file" + bob <# "alice> sends file test_1MB.pdf (1017.7 KiB / 1042157 bytes)" + bob <## "use /fr 3 [/ | ] to receive it" + + bob <# "alice> sends file logo.jpg (31.3 KiB / 32080 bytes)" + bob <## "use /fr 4 [/ | ] to receive it" + + alice <## "completed uploading file 1 (test.jpg) for bob" + alice <## "completed uploading file 2 (test.pdf) for bob" + alice <## "completed uploading file 3 (test_1MB.pdf) for bob" + alice <## "completed uploading file 4 (logo.jpg) for bob" + + -- IDs to forward + let msgId1 = (read msgIdZero :: Int) + 1 + msgIds = intercalate "," $ map (show . (msgId1 +)) [0..5] + bob ##> ("/_forward plan @2 " <> msgIds) + bob <## "Files can be received: 1, 2, 3, 4" + bob <## "5 message(s) out of 6 can be forwarded" + + bob ##> "/fr 1" + bob + <### [ "saving file 1 from alice to test.jpg", + "started receiving file 1 (test.jpg) from alice" + ] + bob <## "completed receiving file 1 (test.jpg) from alice" + + bob ##> ("/_forward plan @2 " <> msgIds) + bob <## "Files can be received: 2, 3, 4" + bob <## "5 message(s) out of 6 can be forwarded" + + bob ##> "/fr 2" + bob + <### [ "saving file 2 from alice to test.pdf", + "started receiving file 2 (test.pdf) from alice" + ] + bob <## "completed receiving file 2 (test.pdf) from alice" + + src1 <- B.readFile "./tests/tmp/alice_app_files/test.jpg" + dest1 <- B.readFile "./tests/tmp/bob_app_files/test.jpg" + dest1 `shouldBe` src1 + + src2 <- B.readFile "./tests/tmp/alice_app_files/test.pdf" + dest2 <- B.readFile "./tests/tmp/bob_app_files/test.pdf" + dest2 `shouldBe` src2 + + -- forward file + bob ##> ("/_forward plan @2 " <> msgIds) + bob <## "Files can be received: 3, 4" + bob <## "all messages can be forwarded" + bob ##> ("/_forward @3 @2 " <> msgIds) + + -- messages printed for bob + bob <# "@cath <- you @alice" + bob <## " hi" + + bob <# "@cath <- @alice" + bob <## " message without file" + + bob <# "@cath <- @alice" + bob <## " test_1.jpg" + bob <# "/f @cath test_1.jpg" + bob <## "use /fc 5 to cancel sending" + + bob <# "@cath <- @alice" + bob <## " test_1.pdf" + bob <# "/f @cath test_1.pdf" + bob <## "use /fc 6 to cancel sending" + + bob <# "@cath <- @alice" + bob <## " message with large file" + + bob <# "@cath <- @alice" + bob <## "" + + -- messages printed for cath + cath <# "bob> -> forwarded" + cath <## " hi" + + cath <# "bob> -> forwarded" + cath <## " message without file" + + cath <# "bob> -> forwarded" + cath <## " test_1.jpg" + cath <# "bob> sends file test_1.jpg (136.5 KiB / 139737 bytes)" + cath <## "use /fr 1 [/ | ] to receive it" + + cath <# "bob> -> forwarded" + cath <## " test_1.pdf" + cath <# "bob> sends file test_1.pdf (266.0 KiB / 272376 bytes)" + cath <## "use /fr 2 [/ | ] to receive it" + + cath <# "bob> -> forwarded" + cath <## " message with large file" + + cath <# "bob> -> forwarded" + cath <## "" + + -- file transfer + bob <## "completed uploading file 5 (test_1.jpg) for cath" + bob <## "completed uploading file 6 (test_1.pdf) for cath" + + cath ##> "/fr 1" + cath + <### [ "saving file 1 from bob to test_1.jpg", + "started receiving file 1 (test_1.jpg) from bob" + ] + cath <## "completed receiving file 1 (test_1.jpg) from bob" + + cath ##> "/fr 2" + cath + <### [ "saving file 2 from bob to test_1.pdf", + "started receiving file 2 (test_1.pdf) from bob" + ] + cath <## "completed receiving file 2 (test_1.pdf) from bob" + + src1B <- B.readFile "./tests/tmp/bob_app_files/test_1.jpg" + src1B `shouldBe` dest1 + dest1C <- B.readFile "./tests/tmp/cath_app_files/test_1.jpg" + dest1C `shouldBe` src1B + + src2B <- B.readFile "./tests/tmp/bob_app_files/test_1.pdf" + src2B `shouldBe` dest2 + dest2C <- B.readFile "./tests/tmp/cath_app_files/test_1.pdf" + dest2C `shouldBe` src2B + + bob ##> "/fr 3" + bob + <### [ "saving file 3 from alice to test_1MB.pdf", + "started receiving file 3 (test_1MB.pdf) from alice" + ] + bob <## "completed receiving file 3 (test_1MB.pdf) from alice" + + bob ##> ("/_forward plan @2 " <> msgIds) + bob <## "Files can be received: 4" + bob <## "all messages can be forwarded" + + bob ##> "/fr 4" + bob + <### [ "saving file 4 from alice to logo.jpg", + "started receiving file 4 (logo.jpg) from alice" + ] + bob <## "completed receiving file 4 (logo.jpg) from alice" + + bob ##> ("/_forward plan @2 " <> msgIds) + bob <## "all messages can be forwarded" + + removeFile "./tests/tmp/bob_app_files/test_1MB.pdf" + bob ##> ("/_forward plan @2 " <> msgIds) + bob <## "1 file(s) are missing" + bob <## "all messages can be forwarded" + + removeFile "./tests/tmp/bob_app_files/test.pdf" + bob ##> ("/_forward plan @2 " <> msgIds) + bob <## "2 file(s) are missing" + bob <## "5 message(s) out of 6 can be forwarded" + + -- deleting original file doesn't delete forwarded file + checkActionDeletesFile "./tests/tmp/bob_app_files/test.jpg" $ do + bob ##> "/clear alice" + bob <## "alice: all messages are removed locally ONLY" + fwdFileExists <- doesFileExist "./tests/tmp/bob_app_files/test_1.jpg" + fwdFileExists `shouldBe` True diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index d6849d3074..c65c7b8085 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -14,7 +14,9 @@ import Control.Monad (forM_, void, when) import qualified Data.ByteString.Char8 as B import Data.List (intercalate, isInfixOf) import qualified Data.Text as T +import Database.SQLite.Simple (Only (..)) import Simplex.Chat.Controller (ChatConfig (..)) +import Simplex.Chat.Messages (ChatItemId) import Simplex.Chat.Options import Simplex.Chat.Protocol (supportedChatVRange) import Simplex.Chat.Store (agentStoreFile, chatStoreFile) @@ -33,6 +35,7 @@ chatGroupTests :: SpecWith FilePath chatGroupTests = do describe "chat groups" $ do describe "add contacts, create group and send/receive messages" testGroupMatrix + it "mark multiple messages as read" testMarkReadGroup 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 @@ -64,6 +67,10 @@ chatGroupTests = do it "moderate message of another group member (full delete)" testGroupModerateFullDelete it "moderate message that arrives after the event of moderation" testGroupDelayedModeration it "moderate message that arrives after the event of moderation (full delete)" testGroupDelayedModerationFullDelete + describe "batch send messages" $ do + it "send multiple messages api" testSendMulti + it "send multiple timed messages" testSendMultiTimed + it "send multiple messages (many chat batches)" testSendMultiManyBatches describe "async group connections" $ do xit "create and join group when clients go offline" testGroupAsync describe "group links" $ do @@ -350,6 +357,22 @@ testGroupShared alice bob cath checkMessages directConnections = do alice #$> ("/_unread chat #1 on", id, "ok") alice #$> ("/_unread chat #1 off", id, "ok") +testMarkReadGroup :: HasCallStack => FilePath -> IO () +testMarkReadGroup = testChat2 aliceProfile bobProfile $ \alice bob -> do + createGroup2 "team" alice bob + alice #> "#team 1" + alice #> "#team 2" + alice #> "#team 3" + alice #> "#team 4" + bob <# "#team alice> 1" + bob <# "#team alice> 2" + bob <# "#team alice> 3" + bob <# "#team alice> 4" + bob ##> "/last_item_id" + i :: ChatItemId <- read <$> getTermLine bob + let itemIds = intercalate "," $ map show [i - 3 .. i] + bob #$> ("/_read chat items #1 " <> itemIds, id, "ok") + testGroupLargeMessage :: HasCallStack => FilePath -> IO () testGroupLargeMessage = testChat2 aliceProfile bobProfile $ @@ -1304,26 +1327,29 @@ testGroupMessageDeleteMultipleManyBatches = cath ##> "/set receipts all off" cath <## "ok" - alice #> "#team message 0" - concurrently_ - (bob <# "#team alice> message 0") - (cath <# "#team alice> message 0") - msgIdFirst <- lastItemId alice + msgIdZero <- lastItemId alice + + let cm i = "{\"msgContent\": {\"type\": \"text\", \"text\": \"message " <> show i <> "\"}}" + cms = intercalate ", " (map cm [1 .. 300 :: Int]) + + alice `send` ("/_send #1 json [" <> cms <> "]") + _ <- getTermLine alice + + alice <## "300 messages sent" forM_ [(1 :: Int) .. 300] $ \i -> do - alice #> ("#team message " <> show i) concurrently_ (bob <# ("#team alice> message " <> show i)) (cath <# ("#team alice> message " <> show i)) msgIdLast <- lastItemId alice - let mIdFirst = read msgIdFirst :: Int + let mIdFirst = (read msgIdZero :: Int) + 1 mIdLast = read msgIdLast :: Int deleteIds = intercalate "," (map show [mIdFirst .. mIdLast]) alice `send` ("/_delete item #1 " <> deleteIds <> " broadcast") _ <- getTermLine alice - alice <## "301 messages deleted" - forM_ [(0 :: Int) .. 300] $ \i -> + alice <## "300 messages deleted" + forM_ [(1 :: Int) .. 300] $ \i -> concurrently_ (bob <# ("#team alice> [marked deleted] message " <> show i)) (cath <# ("#team alice> [marked deleted] message " <> show i)) @@ -1818,6 +1844,92 @@ testGroupDelayedModerationFullDelete tmp = do where cfg = testCfgCreateGroupDirect +testSendMulti :: HasCallStack => FilePath -> IO () +testSendMulti = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + createGroup3 "team" alice bob cath + + alice ##> "/_send #1 json [{\"msgContent\": {\"type\": \"text\", \"text\": \"test 1\"}}, {\"msgContent\": {\"type\": \"text\", \"text\": \"test 2\"}}]" + alice <# "#team test 1" + alice <# "#team test 2" + bob <# "#team alice> test 1" + bob <# "#team alice> test 2" + cath <# "#team alice> test 1" + cath <# "#team alice> test 2" + +testSendMultiTimed :: HasCallStack => FilePath -> IO () +testSendMultiTimed = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + createGroup3 "team" alice bob cath + + alice ##> "/set disappear #team on 1" + alice <## "updated group preferences:" + alice <## "Disappearing messages: on (1 sec)" + bob <## "alice updated group #team:" + bob <## "updated group preferences:" + bob <## "Disappearing messages: on (1 sec)" + cath <## "alice updated group #team:" + cath <## "updated group preferences:" + cath <## "Disappearing messages: on (1 sec)" + + alice ##> "/_send #1 json [{\"msgContent\": {\"type\": \"text\", \"text\": \"test 1\"}}, {\"msgContent\": {\"type\": \"text\", \"text\": \"test 2\"}}]" + alice <# "#team test 1" + alice <# "#team test 2" + bob <# "#team alice> test 1" + bob <# "#team alice> test 2" + cath <# "#team alice> test 1" + cath <# "#team alice> test 2" + + alice + <### [ "timed message deleted: test 1", + "timed message deleted: test 2" + ] + bob + <### [ "timed message deleted: test 1", + "timed message deleted: test 2" + ] + cath + <### [ "timed message deleted: test 1", + "timed message deleted: test 2" + ] + +testSendMultiManyBatches :: HasCallStack => FilePath -> IO () +testSendMultiManyBatches = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + createGroup3 "team" alice bob cath + + msgIdAlice <- lastItemId alice + msgIdBob <- lastItemId bob + msgIdCath <- lastItemId cath + + let cm i = "{\"msgContent\": {\"type\": \"text\", \"text\": \"message " <> show i <> "\"}}" + cms = intercalate ", " (map cm [1 .. 300 :: Int]) + + alice `send` ("/_send #1 json [" <> cms <> "]") + _ <- getTermLine alice + + alice <## "300 messages sent" + + forM_ [(1 :: Int) .. 300] $ \i -> do + concurrently_ + (bob <# ("#team alice> message " <> show i)) + (cath <# ("#team alice> message " <> show i)) + + aliceItemsCount <- withCCTransaction alice $ \db -> + DB.query db "SELECT count(1) FROM chat_items WHERE chat_item_id > ?" (Only msgIdAlice) :: IO [[Int]] + aliceItemsCount `shouldBe` [[300]] + + bobItemsCount <- withCCTransaction bob $ \db -> + DB.query db "SELECT count(1) FROM chat_items WHERE chat_item_id > ?" (Only msgIdBob) :: IO [[Int]] + bobItemsCount `shouldBe` [[300]] + + cathItemsCount <- withCCTransaction cath $ \db -> + DB.query db "SELECT count(1) FROM chat_items WHERE chat_item_id > ?" (Only msgIdCath) :: IO [[Int]] + cathItemsCount `shouldBe` [[300]] + testGroupAsync :: HasCallStack => FilePath -> IO () testGroupAsync tmp = do withNewTestChat tmp "alice" aliceProfile $ \alice -> do @@ -3468,7 +3580,8 @@ testGroupSyncRatchet tmp = bob <## "1 contacts connected (use /cs for the list)" bob <## "#team: connected to server(s)" bob `send` "#team 1" - bob <## "error: command is prohibited, sendMessagesB: send prohibited" -- silence? + -- "send prohibited" error is not printed in group as SndMessage is created, + -- but it should be displayed in per member snd statuses bob <# "#team 1" (alice "/_send #1 json {\"filePath\": \"./tests/tmp/testfile\", \"msgContent\": {\"text\":\"hello\",\"type\":\"file\"}}" + bob ##> "/_send #1 json [{\"filePath\": \"./tests/tmp/testfile\", \"msgContent\": {\"text\":\"hello\",\"type\":\"file\"}}]" bob <# "#team hello" bob <# "/f #team ./tests/tmp/testfile" bob <## "use /fc 1 to cancel sending" @@ -4969,7 +5082,7 @@ testGroupHistoryMultipleFiles = threadDelay 1000000 - bob ##> "/_send #1 json {\"filePath\": \"./tests/tmp/testfile_bob\", \"msgContent\": {\"text\":\"hi alice\",\"type\":\"file\"}}" + bob ##> "/_send #1 json [{\"filePath\": \"./tests/tmp/testfile_bob\", \"msgContent\": {\"text\":\"hi alice\",\"type\":\"file\"}}]" bob <# "#team hi alice" bob <# "/f #team ./tests/tmp/testfile_bob" bob <## "use /fc 1 to cancel sending" @@ -4981,7 +5094,7 @@ testGroupHistoryMultipleFiles = threadDelay 1000000 - alice ##> "/_send #1 json {\"filePath\": \"./tests/tmp/testfile_alice\", \"msgContent\": {\"text\":\"hey bob\",\"type\":\"file\"}}" + alice ##> "/_send #1 json [{\"filePath\": \"./tests/tmp/testfile_alice\", \"msgContent\": {\"text\":\"hey bob\",\"type\":\"file\"}}]" alice <# "#team hey bob" alice <# "/f #team ./tests/tmp/testfile_alice" alice <## "use /fc 2 to cancel sending" @@ -5047,7 +5160,7 @@ testGroupHistoryFileCancel = createGroup2 "team" alice bob - bob ##> "/_send #1 json {\"filePath\": \"./tests/tmp/testfile_bob\", \"msgContent\": {\"text\":\"hi alice\",\"type\":\"file\"}}" + bob ##> "/_send #1 json [{\"filePath\": \"./tests/tmp/testfile_bob\", \"msgContent\": {\"text\":\"hi alice\",\"type\":\"file\"}}]" bob <# "#team hi alice" bob <# "/f #team ./tests/tmp/testfile_bob" bob <## "use /fc 1 to cancel sending" @@ -5063,7 +5176,7 @@ testGroupHistoryFileCancel = threadDelay 1000000 - alice ##> "/_send #1 json {\"filePath\": \"./tests/tmp/testfile_alice\", \"msgContent\": {\"text\":\"hey bob\",\"type\":\"file\"}}" + alice ##> "/_send #1 json [{\"filePath\": \"./tests/tmp/testfile_alice\", \"msgContent\": {\"text\":\"hey bob\",\"type\":\"file\"}}]" alice <# "#team hey bob" alice <# "/f #team ./tests/tmp/testfile_alice" alice <## "use /fc 2 to cancel sending" diff --git a/tests/ChatTests/Local.hs b/tests/ChatTests/Local.hs index 5562d517ac..da9c043648 100644 --- a/tests/ChatTests/Local.hs +++ b/tests/ChatTests/Local.hs @@ -22,6 +22,9 @@ chatLocalChatsTests = do it "chat pagination" testChatPagination it "stores files" testFiles it "deleting files does not interfere with other chat types" testOtherFiles + describe "batch create messages" $ do + it "create multiple messages api" testCreateMulti + it "create multiple messages with files" testCreateMultiFiles testNotes :: FilePath -> IO () testNotes tmp = withNewTestChat tmp "alice" aliceProfile $ \alice -> do @@ -120,7 +123,7 @@ testFiles tmp = withNewTestChat tmp "alice" aliceProfile $ \alice -> do let source = "./tests/fixtures/test.jpg" let stored = files "test.jpg" copyFile source stored - alice ##> "/_create *1 json {\"filePath\": \"test.jpg\", \"msgContent\": {\"text\":\"hi myself\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}" + alice ##> "/_create *1 json [{\"filePath\": \"test.jpg\", \"msgContent\": {\"text\":\"hi myself\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}]" alice <# "* hi myself" alice <# "* file 1 (test.jpg)" @@ -141,7 +144,7 @@ testFiles tmp = withNewTestChat tmp "alice" aliceProfile $ \alice -> do -- one more file let stored2 = files "another_test.jpg" copyFile source stored2 - alice ##> "/_create *1 json {\"filePath\": \"another_test.jpg\", \"msgContent\": {\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}" + alice ##> "/_create *1 json [{\"filePath\": \"another_test.jpg\", \"msgContent\": {\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}]" alice <# "* file 2 (another_test.jpg)" alice ##> "/_delete item *1 2 internal" @@ -173,8 +176,8 @@ testOtherFiles = bob ##> "/fr 1" bob <### [ "saving file 1 from alice to test.jpg", - "started receiving file 1 (test.jpg) from alice" - ] + "started receiving file 1 (test.jpg) from alice" + ] bob <## "completed receiving file 1 (test.jpg) from alice" bob /* "test" @@ -188,3 +191,36 @@ testOtherFiles = doesFileExist "./tests/tmp/test.jpg" `shouldReturn` True where cfg = testCfg {inlineFiles = defaultInlineFilesConfig {offerChunks = 100, sendChunks = 100, receiveChunks = 100}} + +testCreateMulti :: FilePath -> IO () +testCreateMulti tmp = withNewTestChat tmp "alice" aliceProfile $ \alice -> do + createCCNoteFolder alice + + alice ##> "/_create *1 json [{\"msgContent\": {\"type\": \"text\", \"text\": \"test 1\"}}, {\"msgContent\": {\"type\": \"text\", \"text\": \"test 2\"}}]" + alice <# "* test 1" + alice <# "* test 2" + +testCreateMultiFiles :: FilePath -> IO () +testCreateMultiFiles tmp = withNewTestChat tmp "alice" aliceProfile $ \alice -> do + createCCNoteFolder alice + alice #$> ("/_files_folder ./tests/tmp/alice_app_files", id, "ok") + copyFile "./tests/fixtures/test.jpg" "./tests/tmp/alice_app_files/test.jpg" + copyFile "./tests/fixtures/test.pdf" "./tests/tmp/alice_app_files/test.pdf" + + let cm1 = "{\"msgContent\": {\"type\": \"text\", \"text\": \"message without file\"}}" + cm2 = "{\"filePath\": \"test.jpg\", \"msgContent\": {\"type\": \"text\", \"text\": \"sending file 1\"}}" + cm3 = "{\"filePath\": \"test.pdf\", \"msgContent\": {\"type\": \"text\", \"text\": \"sending file 2\"}}" + alice ##> ("/_create *1 json [" <> cm1 <> "," <> cm2 <> "," <> cm3 <> "]") + + alice <# "* message without file" + alice <# "* sending file 1" + alice <# "* file 1 (test.jpg)" + alice <# "* sending file 2" + alice <# "* file 2 (test.pdf)" + + doesFileExist "./tests/tmp/alice_app_files/test.jpg" `shouldReturn` True + doesFileExist "./tests/tmp/alice_app_files/test.pdf" `shouldReturn` True + + alice ##> "/_get chat *1 count=3" + r <- chatF <$> getTermLine alice + r `shouldBe` [((1, "message without file"), Nothing), ((1, "sending file 1"), Just "test.jpg"), ((1, "sending file 2"), Just "test.pdf")] diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index 43ad5ba841..a36eef8ca9 100644 --- a/tests/ChatTests/Profiles.hs +++ b/tests/ChatTests/Profiles.hs @@ -1,6 +1,7 @@ {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PostfixOperators #-} +{-# LANGUAGE TypeApplications #-} module ChatTests.Profiles where @@ -18,6 +19,8 @@ import Simplex.Chat.Types (ConnStatus (..), Profile (..)) import Simplex.Chat.Types.Shared (GroupMemberRole (..)) import Simplex.Chat.Types.UITheme import Simplex.Messaging.Encoding.String (StrEncoding (..)) +import Simplex.Messaging.Server.Env.STM hiding (subscriptions) +import Simplex.Messaging.Transport import Simplex.Messaging.Util (encodeJSON) import System.Directory (copyFile, createDirectoryIfMissing) import Test.Hspec hiding (it) @@ -1653,34 +1656,42 @@ testChangePCCUserAndThenIncognito = testChat2 aliceProfile bobProfile $ ] testChangePCCUserDiffSrv :: HasCallStack => FilePath -> IO () -testChangePCCUserDiffSrv = testChat2 aliceProfile bobProfile $ - \alice bob -> do - -- Create a new invite - alice ##> "/connect" - _ <- getInvitation alice - alice ##> "/_set incognito :1 on" - alice <## "connection 1 changed to incognito" - -- Create new user with different servers - alice ##> "/create user alisa" - showActiveUser alice "alisa" - alice #$> ("/smp smp://2345-w==@smp2.example.im smp://3456-w==@smp3.example.im:5224", id, "ok") - alice ##> "/user alice" - showActiveUser alice "alice (Alice)" - -- Change connection to newly created user and use the newly created connection - alice ##> "/_set conn user :1 2" - alice <## "connection 1 changed from user alice to user alisa, new link:" - alice <## "" - inv <- getTermLine alice - alice <## "" - alice `hasContactProfiles` ["alice"] - alice ##> "/user alisa" - showActiveUser alice "alisa" - -- Connect - bob ##> ("/connect " <> inv) - bob <## "confirmation sent!" - concurrently_ - (alice <## "bob (Bob): contact is connected") - (bob <## "alisa: contact is connected") +testChangePCCUserDiffSrv tmp = do + withSmpServer' serverCfg' $ do + withNewTestChatCfgOpts tmp testCfg testOpts "alice" aliceProfile $ \alice -> do + withNewTestChatCfgOpts tmp testCfg testOpts "bob" bobProfile $ \bob -> do + -- Create a new invite + alice ##> "/connect" + _ <- getInvitation alice + alice ##> "/_set incognito :1 on" + alice <## "connection 1 changed to incognito" + -- 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 ##> "/user alice" + showActiveUser alice "alice (Alice)" + -- Change connection to newly created user and use the newly created connection + alice ##> "/_set conn user :1 2" + alice <## "connection 1 changed from user alice to user alisa, new link:" + alice <## "" + inv <- getTermLine alice + alice <## "" + alice `hasContactProfiles` ["alice"] + alice ##> "/user alisa" + showActiveUser alice "alisa" + -- Connect + bob ##> ("/connect " <> inv) + bob <## "confirmation sent!" + concurrently_ + (alice <## "bob (Bob): contact is connected") + (bob <## "alisa: contact is connected") + where + serverCfg' = + smpServerCfg + { transports = [("7003", transport @TLS), ("7002", transport @TLS)], + msgQueueQuota = 2 + } testSetConnectionAlias :: HasCallStack => FilePath -> IO () testSetConnectionAlias = testChat2 aliceProfile bobProfile $ @@ -1721,7 +1732,7 @@ testSetContactPrefs = testChat2 aliceProfile bobProfile $ let startFeatures = [(0, e2eeInfoPQStr), (0, "Disappearing messages: allowed"), (0, "Full deletion: off"), (0, "Message reactions: enabled"), (0, "Voice messages: off"), (0, "Audio/video calls: enabled")] alice #$> ("/_get chat @2 count=100", chat, startFeatures) bob #$> ("/_get chat @2 count=100", chat, startFeatures) - let sendVoice = "/_send @2 json {\"filePath\": \"test.txt\", \"msgContent\": {\"type\": \"voice\", \"text\": \"\", \"duration\": 10}}" + let sendVoice = "/_send @2 json [{\"filePath\": \"test.txt\", \"msgContent\": {\"type\": \"voice\", \"text\": \"\", \"duration\": 10}}]" voiceNotAllowed = "bad chat command: feature not allowed Voice messages" alice ##> sendVoice alice <## voiceNotAllowed @@ -2227,7 +2238,7 @@ testGroupPrefsSimplexLinksForRole = testChat3 aliceProfile bobProfile cathProfil inv <- getInvitation bob bob ##> ("#team \"" <> inv <> "\\ntest\"") bob <## "bad chat command: feature not allowed SimpleX links" - bob ##> ("/_send #1 json {\"msgContent\": {\"type\": \"text\", \"text\": \"" <> inv <> "\\ntest\"}}") + bob ##> ("/_send #1 json [{\"msgContent\": {\"type\": \"text\", \"text\": \"" <> inv <> "\\ntest\"}}]") bob <## "bad chat command: feature not allowed SimpleX links" (alice [SndMessage] -> [ChatError] -> [ByteString] -> IO () runBatcherTest' maxLen msgs expectedErrors expectedBatches = do - let (errors, batches) = partitionEithers $ batchMessages maxLen msgs + let (errors, batches) = partitionEithers $ batchMessages maxLen (map Right msgs) batchedStrs = map (\(MsgBatch batchBody _) -> batchBody) batches testErrors errors `shouldBe` testErrors expectedErrors batchedStrs `shouldBe` expectedBatches diff --git a/tests/RemoteTests.hs b/tests/RemoteTests.hs index 3f1bad613a..e51a938252 100644 --- a/tests/RemoteTests.hs +++ b/tests/RemoteTests.hs @@ -238,7 +238,7 @@ remoteStoreFileTest = desktop ##> "/get remote file 1 {\"userId\": 1, \"fileId\": 1, \"sent\": true, \"fileSource\": {\"filePath\": \"test_1.pdf\"}}" hostError desktop "SEFileNotFound" -- send file not encrypted locally on mobile host - desktop ##> "/_send @2 json {\"filePath\": \"test_1.pdf\", \"msgContent\": {\"type\": \"file\", \"text\": \"sending a file\"}}" + desktop ##> "/_send @2 json [{\"filePath\": \"test_1.pdf\", \"msgContent\": {\"type\": \"file\", \"text\": \"sending a file\"}}]" desktop <# "@bob sending a file" desktop <# "/f @bob test_1.pdf" desktop <## "use /fc 1 to cancel sending" @@ -268,7 +268,7 @@ remoteStoreFileTest = B.readFile (desktopHostStore "test_1.pdf") `shouldReturn` src -- send file encrypted locally on mobile host - desktop ##> ("/_send @2 json {\"fileSource\": {\"filePath\":\"test_2.pdf\", \"cryptoArgs\": " <> LB.unpack (J.encode cfArgs) <> "}, \"msgContent\": {\"type\": \"file\", \"text\": \"\"}}") + desktop ##> ("/_send @2 json [{\"fileSource\": {\"filePath\":\"test_2.pdf\", \"cryptoArgs\": " <> LB.unpack (J.encode cfArgs) <> "}, \"msgContent\": {\"type\": \"file\", \"text\": \"\"}}]") desktop <# "/f @bob test_2.pdf" desktop <## "use /fc 2 to cancel sending" bob <# "alice> sends file test_2.pdf (266.0 KiB / 272376 bytes)" diff --git a/website/langs/es.json b/website/langs/es.json index 443f04b2ac..989d89557c 100644 --- a/website/langs/es.json +++ b/website/langs/es.json @@ -29,7 +29,7 @@ "home": "Inicio", "simplex-explained-tab-3-p-1": "Para cada cola los servidores disponen de credenciales separadas y anónimas, por lo que desconocen a qué usuarios pertenecen.", "hero-p-1": "Las demás aplicaciones usan ID de usuario: Signal, Matrix, Session, Briar, Jami, Cwtch, etc.
SimpleX no los tiene, ni siquiera números aleatorios.
Esto mejora radicalmente su privacidad.", - "hero-2-header-desc": "El video muestra cómo se conecta con sus amistades a través del código QR de un solo uso, en persona o a través de videollamada. También puede conectarse compartiendo un enlace de invitación.", + "hero-2-header-desc": "El vídeo muestra cómo se conecta con sus amistades a través del código QR de un solo uso, en persona o a través de videollamada. También puede conectarse compartiendo un enlace de invitación.", "feature-7-title": "Almacenamiento portable y cifrado — podrá transferir su perfil a otro dispositivo", "simplex-private-card-4-point-2": "Para usar SimpleX a través de Tor, instala la aplicación Orbot y activa el proxy SOCKS5 (o VPN en iOS).", "simplex-private-card-3-point-1": "Para las conexiones cliente servidor se usan exclusivamente el protocolo TLS 1.2/1.3 con algoritmos robustos.", @@ -52,7 +52,7 @@ "feature-8-title": "Modo incógnito —
exclusivo de SimpleX Chat", "simplex-private-1-title": "Doble capa de
cifrado de extremo a extremo", "simplex-private-2-title": "Capa de cifrado
adicional en el servidor", - "simplex-private-3-title": "Transporte TLS
seguro y auténticado", + "simplex-private-3-title": "Transporte TLS
seguro y autenticado", "simplex-private-4-title": "Acceso opcional
a través de Tor", "simplex-private-7-title": "Verificación de la
integridad del mensaje", "feature-4-title": "Mensajes de voz cifrados E2E", @@ -252,7 +252,7 @@ "hero-overlay-card-3-p-1": "Trail of Bits es una consultora de seguridad y tecnología líder cuyos clientes incluyen grandes tecnológicas, agencias gubernamentales e importantes proyectos de blockchain.", "docs-dropdown-9": "Descargas", "please-enable-javascript": "Habilita JavaScript para ver el código QR.", - "please-use-link-in-mobile-app": "Usa el enlace en la apliación móvil,", + "please-use-link-in-mobile-app": "Usa el enlace en la apliación móvil", "docs-dropdown-10": "Transparencia", "docs-dropdown-11": "FAQ", "docs-dropdown-12": "Seguridad"