diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index c9772090e3..d973ada34e 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -336,15 +336,7 @@ final class ChatModel: ObservableObject { chats[i].chatStats.unreadCount = chats[i].chatStats.unreadCount + 1 increaseUnreadCounter(user: currentUser!) } - if i > 0 { - if chatId == nil { - withAnimation { popChat_(i) } - } else if chatId == cInfo.id { - chatToTop = cInfo.id - } else { - popChat_(i) - } - } + popChatCollector.addChat(cInfo.id) } else { addChat(Chat(chatInfo: cInfo, chatItems: [cItem])) } @@ -572,14 +564,13 @@ final class ChatModel: ObservableObject { func markChatItemRead(_ cInfo: ChatInfo, _ cItem: ChatItem) async { if chatId == cInfo.id, let itemIndex = getChatItemIndex(cItem), - let chatIndex = getChatIndex(cInfo.id), im.reversedChatItems[itemIndex].isRcvNew { await MainActor.run { withTransaction(Transaction()) { // update current chat markChatItemRead_(itemIndex) // update preview - unreadCollector.decreaseUnreadCounter(chatIndex) + unreadCollector.decreaseUnreadCounter(cInfo.id) } } } @@ -588,26 +579,59 @@ final class ChatModel: ObservableObject { private let unreadCollector = UnreadCollector() class UnreadCollector { - private let subject = PassthroughSubject() + private let subject = PassthroughSubject() private var bag = Set() - private var dictionary = Dictionary() + private var unreadCounts: [ChatId: Int] = [:] init() { subject .debounce(for: 1, scheduler: DispatchQueue.main) - .sink { _ in - self.dictionary.forEach { key, value in - ChatModel.shared.decreaseUnreadCounter(key, by: value) + .sink { + let m = ChatModel.shared + for (chatId, count) in self.unreadCounts { + if let i = m.getChatIndex(chatId) { + m.decreaseUnreadCounter(i, by: count) + } } - self.dictionary = Dictionary() + self.unreadCounts = [:] } .store(in: &bag) } // Only call from main thread - func decreaseUnreadCounter(_ chatIndex: Int) { - dictionary[chatIndex] = (dictionary[chatIndex] ?? 0) + 1 - subject.send(chatIndex) + func decreaseUnreadCounter(_ chatId: ChatId) { + unreadCounts[chatId] = (unreadCounts[chatId] ?? 0) + 1 + subject.send() + } + } + + let popChatCollector = PopChatCollector() + + class PopChatCollector { + private let subject = PassthroughSubject() + private var bag = Set() + + init() { + subject + .throttle(for: 2, scheduler: DispatchQueue.main, latest: true) + .sink { + let m = ChatModel.shared + if m.chatId == nil { + withAnimation { + m.chats = m.chats.sorted(using: KeyPathComparator(\.popTs, order: .reverse)) + } + } else { + m.chats = m.chats.sorted(using: KeyPathComparator(\.popTs, order: .reverse)) + } + } + .store(in: &bag) + } + + func addChat(_ chatId: ChatId) { + if let index = ChatModel.shared.getChatIndex(chatId) { + ChatModel.shared.chats[index].popTs = CFAbsoluteTimeGetCurrent() + subject.send() + } } } @@ -825,6 +849,7 @@ final class Chat: ObservableObject, Identifiable, ChatLike { @Published var chatItems: [ChatItem] @Published var chatStats: ChatStats var created = Date.now + var popTs: CFAbsoluteTime? init(_ cData: ChatData) { self.chatInfo = cData.chatInfo @@ -871,24 +896,6 @@ final class Chat: ObservableObject, Identifiable, ChatLike { var viewId: String { get { "\(chatInfo.id) \(created.timeIntervalSince1970)" } } - func groupFeatureEnabled(_ feature: GroupFeature) -> Bool { - if case let .group(groupInfo) = self.chatInfo { - let p = groupInfo.fullGroupPreferences - return switch feature { - case .timedMessages: p.timedMessages.on - case .directMessages: p.directMessages.on(for: groupInfo.membership) - case .fullDelete: p.fullDelete.on - case .reactions: p.reactions.on - case .voice: p.voice.on(for: groupInfo.membership) - case .files: p.files.on(for: groupInfo.membership) - case .simplexLinks: p.simplexLinks.on(for: groupInfo.membership) - case .history: p.history.on - } - } else { - return true - } - } - public static var sampleData: Chat = Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []) } diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 1d50e94f11..e33df24e06 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -279,8 +279,10 @@ func apiGetAppSettings(settings: AppSettings) throws -> AppSettings { throw r } -func apiExportArchive(config: ArchiveConfig) async throws { - try await sendCommandOkResp(.apiExportArchive(config: config)) +func apiExportArchive(config: ArchiveConfig) async throws -> [ArchiveError] { + let r = await chatSendCmd(.apiExportArchive(config: config)) + if case let .archiveExported(archiveErrors) = r { return archiveErrors } + throw r } func apiImportArchive(config: ArchiveConfig) async throws -> [ArchiveError] { @@ -431,15 +433,15 @@ func apiChatItemReaction(type: ChatType, id: Int64, itemId: Int64, add: Bool, re throw r } -func apiDeleteChatItem(type: ChatType, id: Int64, itemId: Int64, mode: CIDeleteMode) async throws -> (ChatItem, ChatItem?) { - let r = await chatSendCmd(.apiDeleteChatItem(type: type, id: id, itemId: itemId, mode: mode), bgDelay: msgDelay) - if case let .chatItemDeleted(_, deletedChatItem, toChatItem, _) = r { return (deletedChatItem.chatItem, toChatItem?.chatItem) } +func apiDeleteChatItems(type: ChatType, id: Int64, itemIds: [Int64], mode: CIDeleteMode) async throws -> [ChatItemDeletion] { + let r = await chatSendCmd(.apiDeleteChatItem(type: type, id: id, itemIds: itemIds, mode: mode), bgDelay: msgDelay) + if case let .chatItemsDeleted(_, items, _) = r { return items } throw r } -func apiDeleteMemberChatItem(groupId: Int64, groupMemberId: Int64, itemId: Int64) async throws -> (ChatItem, ChatItem?) { - let r = await chatSendCmd(.apiDeleteMemberChatItem(groupId: groupId, groupMemberId: groupMemberId, itemId: itemId), bgDelay: msgDelay) - if case let .chatItemDeleted(_, deletedChatItem, toChatItem, _) = r { return (deletedChatItem.chatItem, toChatItem?.chatItem) } +func apiDeleteMemberChatItems(groupId: Int64, itemIds: [Int64]) async throws -> [ChatItemDeletion] { + let r = await chatSendCmd(.apiDeleteMemberChatItem(groupId: groupId, itemIds: itemIds), bgDelay: msgDelay) + if case let .chatItemsDeleted(_, items, _) = r { return items } throw r } @@ -1746,21 +1748,25 @@ func processReceivedMsg(_ res: ChatResponse) async { m.updateChatItem(r.chatInfo, r.chatReaction.chatItem) } } - case let .chatItemDeleted(user, deletedChatItem, toChatItem, _): + case let .chatItemsDeleted(user, items, _): if !active(user) { - if toChatItem == nil && deletedChatItem.chatItem.isRcvNew && deletedChatItem.chatInfo.ntfsEnabled { - await MainActor.run { - m.decreaseUnreadCounter(user: user) + for item in items { + if item.toChatItem == nil && item.deletedChatItem.chatItem.isRcvNew && item.deletedChatItem.chatInfo.ntfsEnabled { + await MainActor.run { + m.decreaseUnreadCounter(user: user) + } } } return } await MainActor.run { - if let toChatItem = toChatItem { - _ = m.upsertChatItem(toChatItem.chatInfo, toChatItem.chatItem) - } else { - m.removeChatItem(deletedChatItem.chatInfo, deletedChatItem.chatItem) + for item in items { + if let toChatItem = item.toChatItem { + _ = m.upsertChatItem(toChatItem.chatInfo, toChatItem.chatItem) + } else { + m.removeChatItem(item.deletedChatItem.chatInfo, item.deletedChatItem.chatItem) + } } } case let .receivedGroupInvitation(user, groupInfo, _, _): diff --git a/apps/ios/Shared/Views/Chat/ChatItemForwardingView.swift b/apps/ios/Shared/Views/Chat/ChatItemForwardingView.swift index 0b7de32a88..7c405b6346 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemForwardingView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemForwardingView.swift @@ -21,7 +21,6 @@ struct ChatItemForwardingView: View { @State private var searchText: String = "" @FocusState private var searchFocused @State private var alert: SomeAlert? - @State private var hasSimplexLink_: Bool? private let chatsToForwardTo = filterChatsToForwardTo(chats: ChatModel.shared.chats) var body: some View { @@ -67,34 +66,6 @@ struct ChatItemForwardingView: View { } } - private func prohibitedByPref(_ chat: Chat) -> Bool { - // preference checks should match checks in compose view - let simplexLinkProhibited = hasSimplexLink && !chat.groupFeatureEnabled(.simplexLinks) - let fileProhibited = (ci.content.msgContent?.isMediaOrFileAttachment ?? false) && !chat.groupFeatureEnabled(.files) - let voiceProhibited = (ci.content.msgContent?.isVoice ?? false) && !chat.chatInfo.featureEnabled(.voice) - return switch chat.chatInfo { - case .direct: voiceProhibited - case .group: simplexLinkProhibited || fileProhibited || voiceProhibited - case .local: false - case .contactRequest: false - case .contactConnection: false - case .invalidJSON: false - } - } - - private var hasSimplexLink: Bool { - if let hasSimplexLink_ { return hasSimplexLink_ } - let r = - if let mcText = ci.content.msgContent?.text, - let parsedMsg = parseSimpleXMarkdown(mcText) { - parsedMsgHasSimplexLink(parsedMsg) - } else { - false - } - hasSimplexLink_ = r - return r - } - private func emptyList() -> some View { Text("No filtered chats") .foregroundColor(theme.colors.secondary) @@ -102,7 +73,11 @@ struct ChatItemForwardingView: View { } @ViewBuilder private func forwardListChatView(_ chat: Chat) -> some View { - let prohibited = prohibitedByPref(chat) + let prohibited = chat.prohibitedByPref( + hasSimplexLink: hasSimplexLink(ci.content.msgContent?.text), + isMediaOrFileAttachment: ci.content.msgContent?.isMediaOrFileAttachment ?? false, + isVoice: ci.content.msgContent?.isVoice ?? false + ) Button { if prohibited { alert = SomeAlert( diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index 9887865233..aa033203dc 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -43,6 +43,9 @@ struct ChatView: View { @State private var showGroupLinkSheet: Bool = false @State private var groupLink: String? @State private var groupLinkMemberRole: GroupMemberRole = .member + @State private var selectedChatItems: Set? = nil + @State private var showDeleteSelectedMessages: Bool = false + @State private var allowToDeleteSelectedMessagesForAll: Bool = false var body: some View { if #available(iOS 16.0, *) { @@ -80,25 +83,58 @@ struct ChatView: View { floatingButtons(counts: floatingButtonModel.unreadChatItemCounts) } connectingText() - ComposeView( - chat: chat, - composeState: $composeState, - keyboardVisible: $keyboardVisible - ) - .disabled(!cInfo.sendMsgEnabled) + if selectedChatItems == nil { + ComposeView( + chat: chat, + composeState: $composeState, + keyboardVisible: $keyboardVisible + ) + .disabled(!cInfo.sendMsgEnabled) + } else { + SelectedItemsBottomToolbar( + chatItems: ItemsModel.shared.reversedChatItems, + selectedChatItems: $selectedChatItems, + chatInfo: chat.chatInfo, + deleteItems: { forAll in + allowToDeleteSelectedMessagesForAll = forAll + showDeleteSelectedMessages = true + }, + moderateItems: { + if case let .group(groupInfo) = chat.chatInfo { + showModerateSelectedMessagesAlert(groupInfo) + } + } + ) + } } .navigationTitle(cInfo.chatViewName) .background(theme.colors.background) .navigationBarTitleDisplayMode(.inline) .environmentObject(theme) + .confirmationDialog(selectedChatItems?.count == 1 ? "Delete message?" : "Delete \((selectedChatItems?.count ?? 0)) messages?", isPresented: $showDeleteSelectedMessages, titleVisibility: .visible) { + Button("Delete for me", role: .destructive) { + if let selected = selectedChatItems { + deleteMessages(chat, selected.sorted(), .cidmInternal, moderate: false, deletedSelectedMessages) } + } + if allowToDeleteSelectedMessagesForAll { + Button(broadcastDeleteButtonText(chat), role: .destructive) { + if let selected = selectedChatItems { + allowToDeleteSelectedMessagesForAll = false + deleteMessages(chat, selected.sorted(), .cidmBroadcast, moderate: false, deletedSelectedMessages) + } + } + } + } .onAppear { loadChat(chat: chat) initChatView() + selectedChatItems = nil } .onChange(of: chatModel.chatId) { cId in showChatInfoSheet = false stopAudioPlayer() if let cId { + selectedChatItems = nil if let c = chatModel.getChat(cId) { chat = c } @@ -138,7 +174,9 @@ struct ChatView: View { } .toolbar { ToolbarItem(placement: .principal) { - if case let .direct(contact) = cInfo { + if selectedChatItems != nil { + SelectedItemsTopToolbar(selectedChatItems: $selectedChatItems) + } else if case let .direct(contact) = cInfo { Button { Task { do { @@ -192,66 +230,76 @@ struct ChatView: View { } } ToolbarItem(placement: .navigationBarTrailing) { - switch cInfo { - case let .direct(contact): - HStack { - let callsPrefEnabled = contact.mergedPreferences.calls.enabled.forUser - if callsPrefEnabled { - if chatModel.activeCall == nil { - callButton(contact, .audio, imageName: "phone") - .disabled(!contact.ready || !contact.active) - } else if let call = chatModel.activeCall, call.contact.id == cInfo.id { - endCallButton(call) - } + if selectedChatItems != nil { + Button { + withAnimation { + selectedChatItems = nil } - Menu { - if callsPrefEnabled && chatModel.activeCall == nil { - Button { - CallController.shared.startCall(contact, .video) - } label: { - Label("Video call", systemImage: "video") + } label: { + Text("Cancel") + } + } else { + switch cInfo { + case let .direct(contact): + HStack { + let callsPrefEnabled = contact.mergedPreferences.calls.enabled.forUser + if callsPrefEnabled { + if chatModel.activeCall == nil { + callButton(contact, .audio, imageName: "phone") + .disabled(!contact.ready || !contact.active) + } else if let call = chatModel.activeCall, call.contact.id == cInfo.id { + endCallButton(call) } - .disabled(!contact.ready || !contact.active) } - searchButton() - ToggleNtfsButton(chat: chat) - .disabled(!contact.ready || !contact.active) - } label: { - Image(systemName: "ellipsis") - } - } - case let .group(groupInfo): - HStack { - if groupInfo.canAddMembers { - if (chat.chatInfo.incognito) { - groupLinkButton() - .appSheet(isPresented: $showGroupLinkSheet) { - GroupLinkView( - groupId: groupInfo.groupId, - groupLink: $groupLink, - groupLinkMemberRole: $groupLinkMemberRole, - showTitle: true, - creatingGroup: false - ) - } - } else { - addMembersButton() - .appSheet(isPresented: $showAddMembersSheet) { - AddGroupMembersView(chat: chat, groupInfo: groupInfo) + Menu { + if callsPrefEnabled && chatModel.activeCall == nil { + Button { + CallController.shared.startCall(contact, .video) + } label: { + Label("Video call", systemImage: "video") } + .disabled(!contact.ready || !contact.active) + } + searchButton() + ToggleNtfsButton(chat: chat) + .disabled(!contact.ready || !contact.active) + } label: { + Image(systemName: "ellipsis") } } - Menu { - searchButton() - ToggleNtfsButton(chat: chat) - } label: { - Image(systemName: "ellipsis") + case let .group(groupInfo): + HStack { + if groupInfo.canAddMembers { + if (chat.chatInfo.incognito) { + groupLinkButton() + .appSheet(isPresented: $showGroupLinkSheet) { + GroupLinkView( + groupId: groupInfo.groupId, + groupLink: $groupLink, + groupLinkMemberRole: $groupLinkMemberRole, + showTitle: true, + creatingGroup: false + ) + } + } else { + addMembersButton() + .appSheet(isPresented: $showAddMembersSheet) { + AddGroupMembersView(chat: chat, groupInfo: groupInfo) + } + } + } + Menu { + searchButton() + ToggleNtfsButton(chat: chat) + } label: { + Image(systemName: "ellipsis") + } } + case .local: + searchButton() + default: + EmptyView() } - case .local: - searchButton() - default: - EmptyView() } } } @@ -553,6 +601,33 @@ struct ChatView: View { } } + private func showModerateSelectedMessagesAlert(_ groupInfo: GroupInfo) { + guard let count = selectedChatItems?.count, count > 0 else { return } + + AlertManager.shared.showAlert(Alert( + title: Text(count == 1 ? "Delete member message?" : "Delete \(count) messages of members?"), + message: Text( + groupInfo.fullGroupPreferences.fullDelete.on + ? (count == 1 ? "The message will be deleted for all members." : "The messages will be deleted for all members.") + : (count == 1 ? "The message will be marked as moderated for all members." : "The messages will be marked as moderated for all members.") + ), + primaryButton: .destructive(Text("Delete")) { + if let selected = selectedChatItems { + deleteMessages(chat, selected.sorted(), .cidmBroadcast, moderate: true, deletedSelectedMessages) + } + }, + secondaryButton: .cancel() + )) + } + + private func deletedSelectedMessages() async { + await MainActor.run { + withAnimation { + selectedChatItems = nil + } + } + } + private func loadChatItems(_ cInfo: ChatInfo) { Task { if loadingItems || firstPage { return } @@ -604,7 +679,8 @@ struct ChatView: View { maxWidth: maxWidth, composeState: $composeState, selectedMember: $selectedMember, - revealedChatItem: $revealedChatItem + revealedChatItem: $revealedChatItem, + selectedChatItems: $selectedChatItems ) } @@ -626,6 +702,8 @@ struct ChatView: View { @State private var chatItemInfo: ChatItemInfo? @State private var showForwardingSheet: Bool = false + @Binding var selectedChatItems: Set? + @State private var allowMenu: Bool = true var revealed: Bool { chatItem == revealedChatItem } @@ -642,9 +720,29 @@ struct ChatView: View { ForEach(items, 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) + } + } + } } } else { chatItemView(chatItem, range, prevItem) + .overlay { + if let selected = selectedChatItems, chatItem.canBeDeletedForSelf { + Color.clear + .contentShape(Rectangle()) + .onTapGesture { + let checked = selected.contains(chatItem.id) + selectUnselectChatItem(select: !checked, chatItem) + } + } + } } } .onAppear { @@ -689,11 +787,11 @@ struct ChatView: View { if case let .groupRcv(member) = ci.chatDir, case let .group(groupInfo) = chat.chatInfo { let (prevMember, memCount): (GroupMember?, Int) = - if let range = range { - m.getPrevHiddenMember(member, range) - } else { - (nil, 1) - } + if let range = range { + m.getPrevHiddenMember(member, range) + } else { + (nil, 1) + } if prevItem == nil || showMemberImage(member, prevItem) || prevMember != nil { VStack(alignment: .leading, spacing: 4) { if ci.content.showMemberName { @@ -706,41 +804,64 @@ struct ChatView: View { .font(.caption) .foregroundStyle(.secondary) .lineLimit(2) - .padding(.leading, memberImageSize + 14) + .padding(.leading, memberImageSize + 14 + (selectedChatItems != nil && ci.canBeDeletedForSelf ? 12 + 24 : 0)) .padding(.top, 7) } - HStack(alignment: .top, spacing: 8) { - ProfileImage(imageStr: member.memberProfile.image, size: memberImageSize, backgroundColor: theme.colors.background) - .onTapGesture { - if m.membersLoaded { - selectedMember = m.getGroupMember(member.groupMemberId) - } else { - Task { - await m.loadGroupMembers(groupInfo) { - selectedMember = m.getGroupMember(member.groupMemberId) + HStack(alignment: .center, spacing: 0) { + if selectedChatItems != nil && ci.canBeDeletedForSelf { + SelectedChatItem(ciId: ci.id, selectedChatItems: $selectedChatItems) + .padding(.trailing, 12) + } + HStack(alignment: .top, spacing: 8) { + ProfileImage(imageStr: member.memberProfile.image, size: memberImageSize, backgroundColor: theme.colors.background) + .onTapGesture { + if m.membersLoaded { + selectedMember = m.getGroupMember(member.groupMemberId) + } else { + Task { + await m.loadGroupMembers(groupInfo) { + selectedMember = m.getGroupMember(member.groupMemberId) + } } } } - } - .appSheet(item: $selectedMember) { member in - GroupMemberInfoView(groupInfo: groupInfo, groupMember: member, navigation: true) - } - chatItemWithMenu(ci, range, maxWidth) + .appSheet(item: $selectedMember) { member in + GroupMemberInfoView(groupInfo: groupInfo, groupMember: member, navigation: true) + } + chatItemWithMenu(ci, range, maxWidth) + } } } .padding(.bottom, 5) .padding(.trailing) .padding(.leading, 12) } else { - chatItemWithMenu(ci, range, maxWidth) - .padding(.bottom, 5) - .padding(.trailing) - .padding(.leading, memberImageSize + 8 + 12) + HStack(alignment: .center, spacing: 0) { + if selectedChatItems != nil && ci.canBeDeletedForSelf { + SelectedChatItem(ciId: ci.id, selectedChatItems: $selectedChatItems) + .padding(.leading, 12) + } + chatItemWithMenu(ci, range, maxWidth) + .padding(.trailing) + .padding(.leading, memberImageSize + 8 + 12) + } + .padding(.bottom, 5) } } else { - chatItemWithMenu(ci, range, maxWidth) - .padding(.horizontal) - .padding(.bottom, 5) + HStack(alignment: .center, spacing: 0) { + if selectedChatItems != nil && ci.canBeDeletedForSelf { + if chat.chatInfo.chatType == .group { + SelectedChatItem(ciId: ci.id, selectedChatItems: $selectedChatItems) + .padding(.leading, 12) + } else { + SelectedChatItem(ciId: ci.id, selectedChatItems: $selectedChatItems) + .padding(.leading) + } + } + chatItemWithMenu(ci, range, maxWidth) + .padding(.horizontal) + } + .padding(.bottom, 5) } } @@ -775,17 +896,17 @@ struct ChatView: View { } .confirmationDialog("Delete message?", isPresented: $showDeleteMessage, titleVisibility: .visible) { Button("Delete for me", role: .destructive) { - deleteMessage(.cidmInternal) + deleteMessage(.cidmInternal, moderate: false) } if let di = deletingItem, di.meta.deletable && !di.localNote { - Button(broadcastDeleteButtonText, role: .destructive) { - deleteMessage(.cidmBroadcast) + Button(broadcastDeleteButtonText(chat), role: .destructive) { + deleteMessage(.cidmBroadcast, moderate: false) } } } .confirmationDialog(deleteMessagesTitle, isPresented: $showDeleteMessages, titleVisibility: .visible) { Button("Delete for me", role: .destructive) { - deleteMessages() + deleteMessages(chat, deletingItems, moderate: false) } } .frame(maxWidth: maxWidth, maxHeight: .infinity, alignment: alignment) @@ -894,7 +1015,7 @@ struct ChatView: View { if !live || !ci.meta.isLive { deleteButton(ci) } - if let (groupInfo, _) = ci.memberToModerate(chat.chatInfo) { + if let (groupInfo, _) = ci.memberToModerate(chat.chatInfo), ci.chatDir != .groupSnd { moderateButton(ci, groupInfo) } } else if ci.meta.itemDeleted != nil { @@ -918,6 +1039,10 @@ struct ChatView: View { } else { EmptyView() } + if selectedChatItems == nil && ci.canBeDeletedForSelf { + Divider() + selectButton(ci) + } } var replyButton: Button { @@ -1090,6 +1215,21 @@ struct ChatView: View { } } + private func selectButton(_ ci: ChatItem) -> Button { + Button { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + withAnimation { + selectUnselectChatItem(select: true, ci) + } + } + } label: { + Label( + NSLocalizedString("Select", comment: "chat item action"), + systemImage: "checkmark.circle" + ) + } + } + private func viewInfoButton(_ ci: ChatItem) -> Button { Button { Task { @@ -1200,7 +1340,7 @@ struct ChatView: View { ), primaryButton: .destructive(Text("Delete")) { deletingItem = ci - deleteMessage(.cidmBroadcast) + deleteMessage(.cidmBroadcast, moderate: true) }, secondaryButton: .cancel() )) @@ -1251,72 +1391,66 @@ struct ChatView: View { } } - private var broadcastDeleteButtonText: LocalizedStringKey { - chat.chatInfo.featureEnabled(.fullDelete) ? "Delete for everyone" : "Mark deleted for everyone" - } - var deleteMessagesTitle: LocalizedStringKey { let n = deletingItems.count return n == 1 ? "Delete message?" : "Delete \(n) messages?" } - private func deleteMessages() { - let itemIds = deletingItems - if itemIds.count > 0 { - let chatInfo = chat.chatInfo - Task { - var deletedItems: [ChatItem] = [] - for itemId in itemIds { - do { - let (di, _) = try await apiDeleteChatItem( - type: chatInfo.chatType, - id: chatInfo.apiId, - itemId: itemId, - mode: .cidmInternal - ) - deletedItems.append(di) - } catch { - logger.error("ChatView.deleteMessage error: \(error.localizedDescription)") - } - } - await MainActor.run { - for di in deletedItems { - m.removeChatItem(chatInfo, di) - } + private func selectUnselectChatItem(select: Bool, _ ci: ChatItem) { + selectedChatItems = selectedChatItems ?? [] + var itemIds: [Int64] = [] + if !revealed, + let currIndex = m.getChatItemIndex(ci), + let ciCategory = ci.mergeCategory { + let (prevHidden, _) = m.getPrevShownChatItem(currIndex, ciCategory) + if let range = itemsRange(currIndex, prevHidden) { + for i in range { + itemIds.append(ItemsModel.shared.reversedChatItems[i].id) } + } else { + itemIds.append(ci.id) } + } else { + itemIds.append(ci.id) + } + if select { + if let sel = selectedChatItems { + selectedChatItems = sel.union(itemIds) + } + } else { + itemIds.forEach { selectedChatItems?.remove($0) } } } - private func deleteMessage(_ mode: CIDeleteMode) { + private func deleteMessage(_ mode: CIDeleteMode, moderate: Bool) { logger.debug("ChatView deleteMessage") Task { logger.debug("ChatView deleteMessage: in Task") do { if let di = deletingItem { - var deletedItem: ChatItem - var toItem: ChatItem? - if case .cidmBroadcast = mode, - let (groupInfo, groupMember) = di.memberToModerate(chat.chatInfo) { - (deletedItem, toItem) = try await apiDeleteMemberChatItem( + let r = if case .cidmBroadcast = mode, + moderate, + let (groupInfo, _) = di.memberToModerate(chat.chatInfo) { + try await apiDeleteMemberChatItems( groupId: groupInfo.apiId, - groupMemberId: groupMember.groupMemberId, - itemId: di.id + itemIds: [di.id] ) } else { - (deletedItem, toItem) = try await apiDeleteChatItem( + try await apiDeleteChatItems( type: chat.chatInfo.chatType, id: chat.chatInfo.apiId, - itemId: di.id, + itemIds: [di.id], mode: mode ) } - DispatchQueue.main.async { - deletingItem = nil - if let toItem = toItem { - _ = m.upsertChatItem(chat.chatInfo, toItem) - } else { - m.removeChatItem(chat.chatInfo, deletedItem) + if let itemDeletion = r.first { + await MainActor.run { + deletingItem = nil + if let toItem = itemDeletion.toChatItem { + _ = m.upsertChatItem(chat.chatInfo, toItem.chatItem) + } else { + m.removeChatItem(chat.chatInfo, itemDeletion.deletedChatItem.chatItem) + } } } } @@ -1325,6 +1459,68 @@ struct ChatView: View { } } } + + private struct SelectedChatItem: View { + @EnvironmentObject var theme: AppTheme + var ciId: Int64 + @Binding var selectedChatItems: Set? + @State var checked: Bool = false + var body: some View { + Image(systemName: checked ? "checkmark.circle.fill" : "circle") + .resizable() + .foregroundColor(checked ? theme.colors.primary : Color(uiColor: .tertiaryLabel)) + .frame(width: 24, height: 24) + .onAppear { + checked = selectedChatItems?.contains(ciId) == true + } + .onChange(of: selectedChatItems) { selected in + checked = selected?.contains(ciId) == true + } + } + } + } +} + +private func broadcastDeleteButtonText(_ chat: Chat) -> LocalizedStringKey { + chat.chatInfo.featureEnabled(.fullDelete) ? "Delete for everyone" : "Mark deleted for everyone" +} + +private func deleteMessages(_ chat: Chat, _ deletingItems: [Int64], _ mode: CIDeleteMode = .cidmInternal, moderate: Bool, _ onSuccess: @escaping () async -> Void = {}) { + let itemIds = deletingItems + if itemIds.count > 0 { + let chatInfo = chat.chatInfo + Task { + do { + let deletedItems = if case .cidmBroadcast = mode, + moderate, + case .group = chat.chatInfo { + try await apiDeleteMemberChatItems( + groupId: chatInfo.apiId, + itemIds: itemIds + ) + } else { + try await apiDeleteChatItems( + type: chatInfo.chatType, + id: chatInfo.apiId, + itemIds: itemIds, + mode: mode + ) + } + + await MainActor.run { + for di in deletedItems { + if let toItem = di.toChatItem { + _ = ChatModel.shared.upsertChatItem(chat.chatInfo, toItem.chatItem) + } else { + ChatModel.shared.removeChatItem(chatInfo, di.deletedChatItem.chatItem) + } + } + } + await onSuccess() + } catch { + logger.error("ChatView.deleteMessages error: \(error.localizedDescription)") + } + } } } diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift index e19672c883..1364958172 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift @@ -1120,10 +1120,6 @@ struct ComposeView: View { } } -func parsedMsgHasSimplexLink(_ parsedMsg: [FormattedText]) -> Bool { - parsedMsg.contains(where: { ft in ft.format?.isSimplexLink ?? false }) -} - struct ComposeView_Previews: PreviewProvider { static var previews: some View { let chat = Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []) diff --git a/apps/ios/Shared/Views/Chat/SelectableChatItemToolbars.swift b/apps/ios/Shared/Views/Chat/SelectableChatItemToolbars.swift new file mode 100644 index 0000000000..497a1bf5b5 --- /dev/null +++ b/apps/ios/Shared/Views/Chat/SelectableChatItemToolbars.swift @@ -0,0 +1,130 @@ +// +// SelectableChatItemToolbars.swift +// SimpleX (iOS) +// +// Created by Stanislav Dmitrenko on 30.07.2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +struct SelectedItemsTopToolbar: View { + @Environment(\.colorScheme) var colorScheme + @EnvironmentObject var theme: AppTheme + @Binding var selectedChatItems: Set? + + var body: some View { + let count = selectedChatItems?.count ?? 0 + return Text(count == 0 ? "Nothing selected" : "Selected \(count)").font(.headline) + .foregroundColor(theme.colors.onBackground) + .frame(width: 220) + } +} + +struct SelectedItemsBottomToolbar: View { + @Environment(\.colorScheme) var colorScheme + @EnvironmentObject var theme: AppTheme + let chatItems: [ChatItem] + @Binding var selectedChatItems: Set? + var chatInfo: ChatInfo + // Bool - delete for everyone is possible + var deleteItems: (Bool) -> Void + var moderateItems: () -> Void + //var shareItems: () -> Void + @State var deleteEnabled: Bool = false + @State var deleteForEveryoneEnabled: Bool = false + + @State var canModerate: Bool = false + @State var moderateEnabled: Bool = false + + @State var allButtonsDisabled = false + + var body: some View { + VStack(spacing: 0) { + Divider() + + HStack(alignment: .center) { + Button { + deleteItems(deleteForEveryoneEnabled) + } label: { + Image(systemName: "trash") + .resizable() + .frame(width: 20, height: 20, alignment: .center) + .foregroundColor(!deleteEnabled || allButtonsDisabled ? theme.colors.secondary: .red) + } + .disabled(!deleteEnabled || allButtonsDisabled) + + Spacer() + Button { + moderateItems() + } label: { + Image(systemName: "flag") + .resizable() + .frame(width: 20, height: 20, alignment: .center) + .foregroundColor(!moderateEnabled || allButtonsDisabled ? theme.colors.secondary : .red) + } + .disabled(!moderateEnabled || allButtonsDisabled) + .opacity(canModerate ? 1 : 0) + + + Spacer() + Button { + //shareItems() + } label: { + Image(systemName: "square.and.arrow.up") + .resizable() + .frame(width: 20, height: 20, alignment: .center) + .foregroundColor(allButtonsDisabled ? theme.colors.secondary : theme.colors.primary) + } + .disabled(allButtonsDisabled) + .opacity(0) + } + .frame(maxHeight: .infinity) + .padding([.leading, .trailing], 12) + } + .onAppear { + recheckItems(chatInfo, chatItems, selectedChatItems) + } + .onChange(of: chatInfo) { info in + recheckItems(info, chatItems, selectedChatItems) + } + .onChange(of: chatItems) { items in + recheckItems(chatInfo, items, selectedChatItems) + } + .onChange(of: selectedChatItems) { selected in + recheckItems(chatInfo, chatItems, selected) + } + .frame(height: 55.5) + .background(.thinMaterial) + } + + private func recheckItems(_ chatInfo: ChatInfo, _ chatItems: [ChatItem], _ selectedItems: Set?) { + let count = selectedItems?.count ?? 0 + allButtonsDisabled = count == 0 || count > 20 + canModerate = possibleToModerate(chatInfo) + if let selected = selectedItems { + (deleteEnabled, deleteForEveryoneEnabled, moderateEnabled, _, selectedChatItems) = chatItems.reduce((true, true, true, true, [])) { (r, ci) in + if selected.contains(ci.id) { + var (de, dee, me, onlyOwnGroupItems, sel) = r + de = de && ci.canBeDeletedForSelf + dee = dee && ci.meta.deletable && !ci.localNote + onlyOwnGroupItems = onlyOwnGroupItems && ci.chatDir == .groupSnd + me = me && !onlyOwnGroupItems && ci.content.msgContent != nil && ci.memberToModerate(chatInfo) != nil + sel.insert(ci.id) // we are collecting new selected items here to account for any changes in chat items list + return (de, dee, me, onlyOwnGroupItems, sel) + } else { + return r + } + } + } + } + + private func possibleToModerate(_ chatInfo: ChatInfo) -> Bool { + return switch chatInfo { + case let .group(groupInfo): + groupInfo.membership.memberRole >= .admin + default: false + } + } +} diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift index 82d322d6fd..dfaaf1f192 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift @@ -158,8 +158,8 @@ struct ChatListView: View { .offset(x: -8) } } - .onChange(of: chatModel.chatId) { _ in - if chatModel.chatId == nil, let chatId = chatModel.chatToTop { + .onChange(of: chatModel.chatId) { chId in + if chId == nil, let chatId = chatModel.chatToTop { chatModel.chatToTop = nil chatModel.popChat(chatId) } diff --git a/apps/ios/Shared/Views/Database/DatabaseView.swift b/apps/ios/Shared/Views/Database/DatabaseView.swift index 58000a7ee7..6b6d3796dc 100644 --- a/apps/ios/Shared/Views/Database/DatabaseView.swift +++ b/apps/ios/Shared/Views/Database/DatabaseView.swift @@ -15,6 +15,7 @@ enum DatabaseAlert: Identifiable { case importArchive case archiveImported case archiveImportedWithErrors(archiveErrors: [ArchiveError]) + case archiveExportedWithErrors(archivePath: URL, archiveErrors: [ArchiveError]) case deleteChat case chatDeleted case deleteLegacyDatabase @@ -29,6 +30,7 @@ enum DatabaseAlert: Identifiable { case .importArchive: return "importArchive" case .archiveImported: return "archiveImported" case .archiveImportedWithErrors: return "archiveImportedWithErrors" + case .archiveExportedWithErrors: return "archiveExportedWithErrors" case .deleteChat: return "deleteChat" case .chatDeleted: return "chatDeleted" case .deleteLegacyDatabase: return "deleteLegacyDatabase" @@ -265,10 +267,18 @@ struct DatabaseView: View { title: Text("Chat database imported"), message: Text("Restart the app to use imported chat database") ) - case .archiveImportedWithErrors: + case let .archiveImportedWithErrors(errs): return Alert( title: Text("Chat database imported"), - message: Text("Restart the app to use imported chat database") + Text("\n") + Text("Some non-fatal errors occurred during import - you may see Chat console for more details.") + message: Text("Restart the app to use imported chat database") + Text(verbatim: "\n\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), + dismissButton: .default(Text("Continue")) { + showShareSheet(items: [archivePath]) + } ) case .deleteChat: return Alert( @@ -349,9 +359,16 @@ struct DatabaseView: View { progressIndicator = true Task { do { - let archivePath = try await exportChatArchive() - showShareSheet(items: [archivePath]) - await MainActor.run { progressIndicator = false } + let (archivePath, archiveErrors) = try await exportChatArchive() + if archiveErrors.isEmpty { + showShareSheet(items: [archivePath]) + await MainActor.run { progressIndicator = false } + } else { + await MainActor.run { + alert = .archiveExportedWithErrors(archivePath: archivePath, archiveErrors: archiveErrors) + progressIndicator = false + } + } } catch let error { await MainActor.run { alert = .error(title: "Error exporting chat database", error: responseError(error)) @@ -486,6 +503,17 @@ struct DatabaseView: View { } } +func archiveErrorsText(_ errs: [ArchiveError]) -> Text { + return Text("\n" + errs.map(showArchiveError).joined(separator: "\n")) + + func showArchiveError(_ err: ArchiveError) -> String { + switch err { + case let .import(importError): importError + case let .fileError(file, fileError): "\(file): \(fileError)" + } + } +} + func stopChatAsync() async throws { try await apiStopChat() ChatReceiver.shared.stop() diff --git a/apps/ios/Shared/Views/Database/MigrateToAppGroupView.swift b/apps/ios/Shared/Views/Database/MigrateToAppGroupView.swift index 6d3026f11f..e79f24c6d9 100644 --- a/apps/ios/Shared/Views/Database/MigrateToAppGroupView.swift +++ b/apps/ios/Shared/Views/Database/MigrateToAppGroupView.swift @@ -190,7 +190,7 @@ struct MigrateToAppGroupView: View { do { try apiSaveAppSettings(settings: AppSettings.current.prepareForExport()) try? FileManager.default.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true) - try await apiExportArchive(config: config) + _ = try await apiExportArchive(config: config) await MainActor.run { setV3DBMigration(.exported) } } catch let error { await MainActor.run { @@ -222,7 +222,7 @@ struct MigrateToAppGroupView: View { } } -func exportChatArchive(_ storagePath: URL? = nil) async throws -> URL { +func exportChatArchive(_ storagePath: URL? = nil) async throws -> (URL, [ArchiveError]) { let archiveTime = Date.now let ts = archiveTime.ISO8601Format(Date.ISO8601FormatStyle(timeSeparator: .omitted)) let archiveName = "simplex-chat.\(ts).zip" @@ -233,13 +233,13 @@ func exportChatArchive(_ storagePath: URL? = nil) async throws -> URL { try apiSaveAppSettings(settings: AppSettings.current.prepareForExport()) } try? FileManager.default.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true) - try await apiExportArchive(config: config) + let errs = try await apiExportArchive(config: config) if storagePath == nil { deleteOldArchive() UserDefaults.standard.set(archiveName, forKey: DEFAULT_CHAT_ARCHIVE_NAME) chatArchiveTimeDefault.set(archiveTime) } - return archivePath + return (archivePath, errs) } func deleteOldArchive() { diff --git a/apps/ios/Shared/Views/Helpers/ProfileImage.swift b/apps/ios/Shared/Views/Helpers/ProfileImage.swift index e7145711af..4e6d63ffe0 100644 --- a/apps/ios/Shared/Views/Helpers/ProfileImage.swift +++ b/apps/ios/Shared/Views/Helpers/ProfileImage.swift @@ -9,8 +9,6 @@ import SwiftUI import SimpleXChat -let defaultProfileImageCorner = 22.5 - struct ProfileImage: View { @EnvironmentObject var theme: AppTheme var imageStr: String? = nil @@ -34,26 +32,6 @@ struct ProfileImage: View { } } -private let squareToCircleRatio = 0.935 - -private let radiusFactor = (1 - squareToCircleRatio) / 50 - -@ViewBuilder func clipProfileImage(_ img: Image, size: CGFloat, radius: Double) -> some View { - let v = img.resizable() - if radius >= 50 { - v.frame(width: size, height: size).clipShape(Circle()) - } else if radius <= 0 { - let sz = size * squareToCircleRatio - v.frame(width: sz, height: sz).padding((size - sz) / 2) - } else { - let sz = size * (squareToCircleRatio + radius * radiusFactor) - v.frame(width: sz, height: sz) - .clipShape(RoundedRectangle(cornerRadius: sz * radius / 100, style: .continuous)) - .padding((size - sz) / 2) - } -} - - extension Color { func asAnotherColorFromSecondary(_ theme: AppTheme) -> Color { return self diff --git a/apps/ios/Shared/Views/Migration/MigrateFromDevice.swift b/apps/ios/Shared/Views/Migration/MigrateFromDevice.swift index 028a6d179f..9cc229ba80 100644 --- a/apps/ios/Shared/Views/Migration/MigrateFromDevice.swift +++ b/apps/ios/Shared/Views/Migration/MigrateFromDevice.swift @@ -32,6 +32,7 @@ private enum MigrateFromDeviceViewAlert: Identifiable { case keychainError(_ title: LocalizedStringKey = "Keychain error") case databaseError(_ title: LocalizedStringKey = "Database error", message: String) case unknownError(_ title: LocalizedStringKey = "Unknown error", message: String) + case archiveExportedWithErrors(archivePath: URL, archiveErrors: [ArchiveError]) case error(title: LocalizedStringKey, error: String = "") @@ -45,6 +46,7 @@ private enum MigrateFromDeviceViewAlert: Identifiable { case .keychainError: return "keychainError" case let .databaseError(title, message): return "\(title) \(message)" case let .unknownError(title, message): return "\(title) \(message)" + case let .archiveExportedWithErrors(path, _): return "archiveExportedWithErrors \(path)" case let .error(title, _): return "error \(title)" } @@ -166,6 +168,14 @@ struct MigrateFromDevice: View { return Alert(title: Text(title), message: Text(message)) case let .unknownError(title, message): return Alert(title: Text(title), message: Text(message)) + 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), + dismissButton: .default(Text("Continue")) { + Task { await uploadArchive(path: archivePath) } + } + ) case let .error(title, error): return Alert(title: Text(title), message: Text(error)) } @@ -449,15 +459,12 @@ struct MigrateFromDevice: View { Task { do { try? FileManager.default.createDirectory(at: getMigrationTempFilesDirectory(), withIntermediateDirectories: true) - let archivePath = try await exportChatArchive(getMigrationTempFilesDirectory()) - if let attrs = try? FileManager.default.attributesOfItem(atPath: archivePath.path), - let totalBytes = attrs[.size] as? Int64 { - await MainActor.run { - migrationState = .uploadProgress(uploadedBytes: 0, totalBytes: totalBytes, fileId: 0, archivePath: archivePath, ctrl: nil) - } + let (archivePath, errs) = try await exportChatArchive(getMigrationTempFilesDirectory()) + if errs.isEmpty { + await uploadArchive(path: archivePath) } else { await MainActor.run { - alert = .error(title: "Exported file doesn't exist") + alert = .archiveExportedWithErrors(archivePath: archivePath, archiveErrors: errs) migrationState = .uploadConfirmation } } @@ -469,6 +476,20 @@ struct MigrateFromDevice: View { } } } + + private func uploadArchive(path archivePath: URL) async { + if let attrs = try? FileManager.default.attributesOfItem(atPath: archivePath.path), + let totalBytes = attrs[.size] as? Int64 { + await MainActor.run { + migrationState = .uploadProgress(uploadedBytes: 0, totalBytes: totalBytes, fileId: 0, archivePath: archivePath, ctrl: nil) + } + } else { + await MainActor.run { + alert = .error(title: "Exported file doesn't exist") + migrationState = .uploadConfirmation + } + } + } private func initTemporaryDatabase() -> (chat_ctrl, User)? { let (status, ctrl) = chatInitTemporaryDatabase(url: tempDatabaseUrl) diff --git a/apps/ios/Shared/Views/UserSettings/AppSettings.swift b/apps/ios/Shared/Views/UserSettings/AppSettings.swift index ac81c42b2e..ab5da53a9c 100644 --- a/apps/ios/Shared/Views/UserSettings/AppSettings.swift +++ b/apps/ios/Shared/Views/UserSettings/AppSettings.swift @@ -76,7 +76,7 @@ extension AppSettings { c.androidCallOnLockScreen = AppSettingsLockScreenCalls(rawValue: def.string(forKey: ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN)!) c.iosCallKitEnabled = callKitEnabledGroupDefault.get() c.iosCallKitCallsInRecents = def.bool(forKey: DEFAULT_CALL_KIT_CALLS_IN_RECENTS) - c.uiProfileImageCornerRadius = def.float(forKey: DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) + c.uiProfileImageCornerRadius = def.double(forKey: DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) c.uiColorScheme = currentThemeDefault.get() c.uiDarkColorScheme = systemDarkThemeDefault.get() c.uiCurrentThemeIds = currentThemeIdsDefault.get() diff --git a/apps/ios/Shared/Views/UserSettings/AppearanceSettings.swift b/apps/ios/Shared/Views/UserSettings/AppearanceSettings.swift index 18cf5fa199..5d667655e3 100644 --- a/apps/ios/Shared/Views/UserSettings/AppearanceSettings.swift +++ b/apps/ios/Shared/Views/UserSettings/AppearanceSettings.swift @@ -143,7 +143,8 @@ struct AppearanceSettings: View { Text("Themes") .foregroundColor(theme.colors.secondary) } - .onChange(of: profileImageCornerRadius) { _ in + .onChange(of: profileImageCornerRadius) { cornerRadius in + profileImageCornerRadiusGroupDefault.set(cornerRadius) saveThemeToDatabase(nil) } .onChange(of: colorMode) { mode in diff --git a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift index 6b1a619c18..8bd9072f7f 100644 --- a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift +++ b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift @@ -70,6 +70,9 @@ struct PrivacySettings: View { Section { settingsRow("network", color: theme.colors.secondary) { Toggle("Send link previews", isOn: $useLinkPreviews) + .onChange(of: useLinkPreviews) { linkPreviews in + privacyLinkPreviewsGroupDefault.set(linkPreviews) + } } settingsRow("message", color: theme.colors.secondary) { Toggle("Show last messages", isOn: $showChatPreviews) @@ -365,6 +368,7 @@ struct SimplexLockView: View { @State private var selfDestruct: Bool = UserDefaults.standard.bool(forKey: DEFAULT_LA_SELF_DESTRUCT) @State private var currentSelfDestruct: Bool = UserDefaults.standard.bool(forKey: DEFAULT_LA_SELF_DESTRUCT) @AppStorage(DEFAULT_LA_SELF_DESTRUCT_DISPLAY_NAME) private var selfDestructDisplayName = "" + @AppStorage(GROUP_DEFAULT_ALLOW_SHARE_EXTENSION, store: groupDefaults) private var allowShareExtension = false @State private var performLAToggleReset = false @State private var performLAModeReset = false @State private var performLASelfDestructReset = false @@ -436,6 +440,12 @@ struct SimplexLockView: View { } } + if performLA { + Section("Share to SimpleX") { + Toggle("Allow sharing", isOn: $allowShareExtension) + } + } + if performLA && laMode == .passcode { Section(header: Text("Self-destruct passcode").foregroundColor(theme.colors.secondary)) { Toggle(isOn: $selfDestruct) { @@ -460,6 +470,7 @@ struct SimplexLockView: View { } } .onChange(of: performLA) { performLAToggle in + performLAGroupDefault.set(performLAToggle) prefLANoticeShown = true if performLAToggleReset { performLAToggleReset = false diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift index 0a83db1e5c..fcbef61888 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift @@ -18,7 +18,7 @@ let appBuild = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? let DEFAULT_SHOW_LA_NOTICE = "showLocalAuthenticationNotice" let DEFAULT_LA_NOTICE_SHOWN = "localAuthenticationNoticeShown" -let DEFAULT_PERFORM_LA = "performLocalAuthentication" +let DEFAULT_PERFORM_LA = "performLocalAuthentication" // deprecated, moved to app group let DEFAULT_LA_MODE = "localAuthenticationMode" let DEFAULT_LA_LOCK_DELAY = "localAuthenticationLockDelay" let DEFAULT_LA_SELF_DESTRUCT = "localAuthenticationSelfDestruct" @@ -28,7 +28,7 @@ let DEFAULT_WEBRTC_POLICY_RELAY = "webrtcPolicyRelay" let DEFAULT_WEBRTC_ICE_SERVERS = "webrtcICEServers" let DEFAULT_CALL_KIT_CALLS_IN_RECENTS = "callKitCallsInRecents" let DEFAULT_PRIVACY_ACCEPT_IMAGES = "privacyAcceptImages" // unused. Use GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES instead -let DEFAULT_PRIVACY_LINK_PREVIEWS = "privacyLinkPreviews" +let DEFAULT_PRIVACY_LINK_PREVIEWS = "privacyLinkPreviews" // deprecated, moved to app group let DEFAULT_PRIVACY_SIMPLEX_LINK_MODE = "privacySimplexLinkMode" let DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS = "privacyShowChatPreviews" let DEFAULT_PRIVACY_SAVE_LAST_DRAFT = "privacySaveLastDraft" @@ -165,6 +165,9 @@ let themeOverridesDefault: CodableDefault<[ThemeOverrides]> = CodableDefault(def func setGroupDefaults() { privacyAcceptImagesGroupDefault.set(UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_ACCEPT_IMAGES)) + performLAGroupDefault.set(UserDefaults.standard.bool(forKey: DEFAULT_PERFORM_LA)) + privacyLinkPreviewsGroupDefault.set(UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_LINK_PREVIEWS)) + profileImageCornerRadiusGroupDefault.set(UserDefaults.standard.double(forKey: DEFAULT_PROFILE_IMAGE_CORNER_RADIUS)) } public class StringDefault { 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 9ac4d8ced4..6f1ed52623 100644 --- a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff @@ -772,6 +772,10 @@ Разреши изпращането на изчезващи съобщения. No comment provided by engineer. + + Allow sharing + No comment provided by engineer. + Allow to irreversibly delete sent messages. (24 hours) Позволи необратимо изтриване на изпратените съобщения. (24 часа) @@ -1058,6 +1062,10 @@ Блокиран от админ No comment provided by engineer. + + Blur media + No comment provided by engineer. + Both you and your contact can add message reactions. И вие, и вашият контакт можете да добавяте реакции към съобщението. @@ -1509,6 +1517,10 @@ This is your own one-time link! Грешка при свързване (AUTH) No comment provided by engineer. + + Connection notifications + No comment provided by engineer. + Connection request sent! Заявката за връзка е изпратена! @@ -1849,6 +1861,10 @@ This is your own one-time link! Изтрий chat item action + + Delete %lld messages of members? + No comment provided by engineer. + Delete %lld messages? Изтриване на %lld съобщения? @@ -2089,10 +2105,18 @@ This cannot be undone! Настолни устройства No comment provided by engineer. + + 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. @@ -2156,6 +2180,10 @@ This cannot be undone! Деактивиране за всички No comment provided by engineer. + + Disabled + No comment provided by engineer. + Disappearing message Изчезващо съобщение @@ -2376,6 +2404,10 @@ This cannot be undone! Активирай kод за достъп за самоунищожение set passcode view + + Enabled + No comment provided by engineer. + Enabled for Активирано за @@ -2546,6 +2578,10 @@ This cannot be undone! Грешка при промяна на настройката No comment provided by engineer. + + Error connecting to forwarding server %@. Please try later. + No comment provided by engineer. + Error creating address Грешка при създаване на адрес @@ -3040,6 +3076,18 @@ This cannot be undone! Препратено от 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: %@. + No comment provided by engineer. + Forwarding server: %1$@ Destination server error: %2$@ @@ -3851,6 +3899,10 @@ This is your link for group %@! Макс. 30 секунди, получено незабавно. No comment provided by engineer. + + Medium + blur media + Member Член @@ -4260,6 +4312,10 @@ This is your link for group %@! Несъвместим! No comment provided by engineer. + + Nothing selected + No comment provided by engineer. + Notifications Известия @@ -4287,7 +4343,7 @@ This is your link for group %@! Off Изключено - No comment provided by engineer. + blur media Ok @@ -4638,10 +4694,6 @@ Error: %@ Моля, съхранявайте паролата на сигурно място, НЯМА да можете да я промените, ако я загубите. No comment provided by engineer. - - Please try later. - No comment provided by engineer. - Polish interface Полски интерфейс @@ -5401,6 +5453,10 @@ Enable in *Network & servers* settings. Select Избери + chat item action + + + Selected %lld No comment provided by engineer. @@ -5639,10 +5695,6 @@ Enable in *Network & servers* settings. Server version is incompatible with network settings. 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. @@ -5754,6 +5806,10 @@ Enable in *Network & servers* settings. Сподели този еднократен линк за връзка No comment provided by engineer. + + Share to SimpleX + No comment provided by engineer. + Share with contacts Сподели с контактите @@ -5899,6 +5955,10 @@ Enable in *Network & servers* settings. Малки групи (максимум 20) No comment provided by engineer. + + Soft + blur media + Some non-fatal errors occurred during import - you may see Chat console for more details. Някои не-фатални грешки са възникнали по време на импортиране - може да видите конзолата за повече подробности. @@ -5997,6 +6057,10 @@ Enable in *Network & servers* settings. Спиране на чата No comment provided by engineer. + + Strong + blur media + Submit Изпрати @@ -6199,6 +6263,14 @@ It can happen because of some bug or when the connection is compromised.Съобщението ще бъде маркирано като модерирано за всички членове. 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 Ново поколение поверителни съобщения @@ -8354,4 +8426,178 @@ last received msg: %2$@ + +
+ +
+ + + SimpleX SE + Bundle display name + + + SimpleX SE + Bundle name + + + Copyright © 2024 SimpleX Chat. All rights reserved. + Copyright (human-readable) + + +
+ +
+ +
+ + + %@ + 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. + + + Dismiss Sheet + 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. + + + Keep Trying + 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. + + + No network connection + 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 + No comment provided by engineer. + + + Selected chat preferences prohibit this message. + No comment provided by engineer. + + + Sending File + No comment provided by engineer. + + + Share + No comment provided by engineer. + + + Unknown database error: %@ + No comment provided by engineer. + + + Unsupported format + No comment provided by engineer. + + + Wrong database passphrase + No comment provided by engineer. + + + You can allow sharing in Privacy & Security / SimpleX Lock settings. + No comment provided by engineer. + + +
diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings index 124ddbcc33..9c675514f4 100644 --- a/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings @@ -1,6 +1,9 @@ /* Bundle display name */ "CFBundleDisplayName" = "SimpleX NSE"; + /* Bundle name */ "CFBundleName" = "SimpleX NSE"; + /* Copyright (human-readable) */ "NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ 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 c37ee1038e..15d80de7b5 100644 --- a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff @@ -749,6 +749,10 @@ Povolit odesílání mizících zpráv. No comment provided by engineer.
+ + Allow sharing + No comment provided by engineer. + Allow to irreversibly delete sent messages. (24 hours) Povolit nevratné smazání odeslaných zpráv. (24 hodin) @@ -1019,6 +1023,10 @@ Blocked by admin No comment provided by engineer. + + Blur media + No comment provided by engineer. + Both you and your contact can add message reactions. Vy i váš kontakt můžete přidávat reakce na zprávy. @@ -1448,6 +1456,10 @@ This is your own one-time link! Chyba spojení (AUTH) No comment provided by engineer. + + Connection notifications + No comment provided by engineer. + Connection request sent! Požadavek na připojení byl odeslán! @@ -1779,6 +1791,10 @@ This is your own one-time link! Smazat chat item action + + Delete %lld messages of members? + No comment provided by engineer. + Delete %lld messages? No comment provided by engineer. @@ -2011,10 +2027,18 @@ This cannot be undone! Desktop devices No comment provided by engineer. + + 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. @@ -2078,6 +2102,10 @@ This cannot be undone! Vypnout pro všechny No comment provided by engineer. + + Disabled + No comment provided by engineer. + Disappearing message Mizící zpráva @@ -2289,6 +2317,10 @@ This cannot be undone! Povolit sebedestrukční heslo set passcode view + + Enabled + No comment provided by engineer. + Enabled for No comment provided by engineer. @@ -2451,6 +2483,10 @@ This cannot be undone! Chyba změny nastavení No comment provided by engineer. + + Error connecting to forwarding server %@. Please try later. + No comment provided by engineer. + Error creating address Chyba při vytváření adresy @@ -2928,6 +2964,18 @@ This cannot be undone! 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: %@. + No comment provided by engineer. + Forwarding server: %1$@ Destination server error: %2$@ @@ -3708,6 +3756,10 @@ This is your link for group %@! Max 30 vteřin, přijato okamžitě. No comment provided by engineer. + + Medium + blur media + Member Člen @@ -4099,6 +4151,10 @@ This is your link for group %@! Not compatible! No comment provided by engineer. + + Nothing selected + No comment provided by engineer. + Notifications Oznámení @@ -4125,7 +4181,7 @@ This is your link for group %@! Off Vypnout - No comment provided by engineer. + blur media Ok @@ -4460,10 +4516,6 @@ Error: %@ Heslo uložte bezpečně, v případě jeho ztráty jej NEBUDE možné změnit. No comment provided by engineer. - - Please try later. - No comment provided by engineer. - Polish interface Polské rozhraní @@ -5200,6 +5252,10 @@ Enable in *Network & servers* settings. Select Vybrat + chat item action + + + Selected %lld No comment provided by engineer. @@ -5437,10 +5493,6 @@ Enable in *Network & servers* settings. Server version is incompatible with network settings. 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. @@ -5548,6 +5600,10 @@ Enable in *Network & servers* settings. Share this 1-time invite link No comment provided by engineer. + + Share to SimpleX + No comment provided by engineer. + Share with contacts Sdílet s kontakty @@ -5690,6 +5746,10 @@ Enable in *Network & servers* settings. Malé skupiny (max. 20) No comment provided by engineer. + + Soft + blur media + Some non-fatal errors occurred during import - you may see Chat console for more details. Během importu došlo k nezávažným chybám - podrobnosti naleznete v chat konzoli. @@ -5784,6 +5844,10 @@ Enable in *Network & servers* settings. Stopping chat No comment provided by engineer. + + Strong + blur media + Submit Odeslat @@ -5982,6 +6046,14 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován Zpráva bude pro všechny členy označena jako moderovaná. 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 Nová generace soukromých zpráv @@ -8051,4 +8123,178 @@ last received msg: %2$@ + +
+ +
+ + + SimpleX SE + Bundle display name + + + SimpleX SE + Bundle name + + + Copyright © 2024 SimpleX Chat. All rights reserved. + Copyright (human-readable) + + +
+ +
+ +
+ + + %@ + 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. + + + Dismiss Sheet + 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. + + + Keep Trying + 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. + + + No network connection + 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 + No comment provided by engineer. + + + Selected chat preferences prohibit this message. + No comment provided by engineer. + + + Sending File + No comment provided by engineer. + + + Share + No comment provided by engineer. + + + Unknown database error: %@ + No comment provided by engineer. + + + Unsupported format + No comment provided by engineer. + + + Wrong database passphrase + No comment provided by engineer. + + + You can allow sharing in Privacy & Security / SimpleX Lock settings. + No comment provided by engineer. + + +
diff --git a/apps/ios/SimpleX Localizations/cs.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/cs.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings index 124ddbcc33..9c675514f4 100644 --- a/apps/ios/SimpleX Localizations/cs.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings @@ -1,6 +1,9 @@ /* Bundle display name */ "CFBundleDisplayName" = "SimpleX NSE"; + /* Bundle name */ "CFBundleName" = "SimpleX NSE"; + /* Copyright (human-readable) */ "NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/cs.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/cs.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/cs.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/cs.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/cs.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/cs.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ 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 e3d36cf09e..95df9edf6d 100644 --- a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff +++ b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff @@ -590,6 +590,7 @@
Active connections + Aktive Verbindungen No comment provided by engineer. @@ -684,7 +685,7 @@ All chats and messages will be deleted - this cannot be undone! - Alle Chats und Nachrichten werden gelöscht. Dies kann nicht rückgängig gemacht werden! + Es werden alle Chats und Nachrichten gelöscht. Dies kann nicht rückgängig gemacht werden! No comment provided by engineer. @@ -694,7 +695,7 @@ All data is private to your device. - Alle Daten sind auf Ihrem Gerät geschützt. + Alle Daten werden nur auf Ihrem Gerät gespeichert. No comment provided by engineer. @@ -709,7 +710,7 @@ All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you. - Alle Nachrichten werden gelöscht . Dies kann nicht rückgängig gemacht werden! Die Nachrichten werden NUR bei Ihnen gelöscht. + Es werden alle Nachrichten gelöscht. Dies kann nicht rückgängig gemacht werden! Die Nachrichten werden NUR bei Ihnen gelöscht. No comment provided by engineer. @@ -782,6 +783,10 @@ Das Senden von verschwindenden Nachrichten erlauben. No comment provided by engineer. + + Allow sharing + No comment provided by engineer. + Allow to irreversibly delete sent messages. (24 hours) Unwiederbringliches löschen von gesendeten Nachrichten erlauben. (24 Stunden) @@ -1072,6 +1077,10 @@ wurde vom Administrator blockiert No comment provided by engineer. + + Blur media + No comment provided by engineer. + Both you and your contact can add message reactions. Sowohl Sie, als auch Ihr Kontakt können Reaktionen auf Nachrichten geben. @@ -1375,6 +1384,7 @@ Configured %@ servers + Konfigurierte %@ Server No comment provided by engineer. @@ -1536,6 +1546,10 @@ Das ist Ihr eigener Einmal-Link! Verbindungsfehler (AUTH) No comment provided by engineer. + + Connection notifications + No comment provided by engineer. + Connection request sent! Verbindungsanfrage wurde gesendet! @@ -1884,6 +1898,10 @@ Das ist Ihr eigener Einmal-Link! Löschen chat item action + + Delete %lld messages of members? + No comment provided by engineer. + Delete %lld messages? %lld Nachrichten löschen? @@ -2126,11 +2144,19 @@ Dies kann nicht rückgängig gemacht werden! Desktop-Geräte No comment provided by engineer. + + Destination server address of %@ is incompatible with forwarding server %@ settings. + No comment provided by engineer. + Destination server error: %@ Zielserver-Fehler: %@ snd error text + + Destination server version of %@ is incompatible with forwarding server %@. + No comment provided by engineer. + Detailed statistics Detaillierte Statistiken @@ -2196,6 +2222,10 @@ Dies kann nicht rückgängig gemacht werden! Für Alle deaktivieren No comment provided by engineer. + + Disabled + No comment provided by engineer. + Disappearing message Verschwindende Nachricht @@ -2421,6 +2451,10 @@ Dies kann nicht rückgängig gemacht werden! Selbstzerstörungs-Zugangscode aktivieren set passcode view + + Enabled + No comment provided by engineer. + Enabled for Aktiviert für @@ -2591,6 +2625,10 @@ Dies kann nicht rückgängig gemacht werden! Fehler beim Ändern der Einstellung No comment provided by engineer. + + Error connecting to forwarding server %@. Please try later. + No comment provided by engineer. + Error creating address Fehler beim Erstellen der Adresse @@ -3097,6 +3135,18 @@ Dies kann nicht rückgängig gemacht werden! Weitergeleitet aus 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: %@. + No comment provided by engineer. + Forwarding server: %1$@ Destination server error: %2$@ @@ -3523,12 +3573,12 @@ Fehler: %2$@ Incompatible database version - Inkompatible Datenbank-Version + Datenbank-Version nicht kompatibel No comment provided by engineer. Incompatible version - Inkompatible Version + Version nicht kompatibel No comment provided by engineer. @@ -3916,6 +3966,10 @@ Das ist Ihr Link für die Gruppe %@! Max. 30 Sekunden, sofort erhalten. No comment provided by engineer. + + Medium + blur media + Member Mitglied @@ -3998,6 +4052,7 @@ Das ist Ihr Link für die Gruppe %@! Message reception + Nachrichtenempfang No comment provided by engineer. @@ -4340,6 +4395,10 @@ Das ist Ihr Link für die Gruppe %@! Nicht kompatibel! No comment provided by engineer. + + Nothing selected + No comment provided by engineer. + Notifications Benachrichtigungen @@ -4367,7 +4426,7 @@ Das ist Ihr Link für die Gruppe %@! Off Aus - No comment provided by engineer. + blur media Ok @@ -4551,6 +4610,7 @@ Das ist Ihr Link für die Gruppe %@! Other %@ servers + Andere %@ Server No comment provided by engineer. @@ -4722,10 +4782,6 @@ Fehler: %@ Bitte bewahren Sie das Passwort sicher auf, Sie können es NICHT mehr ändern, wenn Sie es vergessen haben oder verlieren. No comment provided by engineer. - - Please try later. - No comment provided by engineer. - Polish interface Polnische Bedienoberfläche @@ -4798,6 +4854,7 @@ Fehler: %@ Private routing error + Fehler beim privaten Routing No comment provided by engineer. @@ -4919,7 +4976,7 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Proxied - Proxy + Proxied No comment provided by engineer. @@ -5515,6 +5572,10 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Select Auswählen + chat item action + + + Selected %lld No comment provided by engineer. @@ -5734,11 +5795,12 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Server address is incompatible with network settings. - Die Server-Adresse ist nicht mit den Netzwerk-Einstellungen kompatibel. + Die Server-Adresse ist nicht mit den Netzwerkeinstellungen kompatibel. srv error text. Server address is incompatible with network settings: %@. + Die Server-Adresse ist nicht mit den Netzwerkeinstellungen kompatibel: %@. No comment provided by engineer. @@ -5763,15 +5825,12 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Server version is incompatible with network settings. - Die Server-Version ist nicht mit den Netzwerk-Einstellungen kompatibel. + Die Server-Version ist nicht mit den Netzwerkeinstellungen kompatibel. srv error text - - Server version is incompatible with network settings: %@. - No comment provided by engineer. - Server version is incompatible with your app: %@. + Die Server-Version ist nicht mit Ihrer App kompatibel: %@. No comment provided by engineer. @@ -5884,6 +5943,10 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Teilen Sie diesen Einmal-Einladungslink No comment provided by engineer. + + Share to SimpleX + No comment provided by engineer. + Share with contacts Mit Kontakten teilen @@ -5916,6 +5979,7 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Show percentage + Prozentualen Anteil anzeigen No comment provided by engineer. @@ -6033,6 +6097,10 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Kleine Gruppen (max. 20) No comment provided by engineer. + + Soft + blur media + Some non-fatal errors occurred during import - you may see Chat console for more details. Während des Imports sind einige nicht schwerwiegende Fehler aufgetreten - in der Chat-Konsole finden Sie weitere Einzelheiten. @@ -6133,6 +6201,10 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Chat wird beendet No comment provided by engineer. + + Strong + blur media + Submit Bestätigen @@ -6340,6 +6412,14 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro Diese Nachricht wird für alle Mitglieder als moderiert gekennzeichnet. 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 Die nächste Generation von privatem Messaging @@ -6392,12 +6472,12 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain. - Diese Aktion kann nicht rückgängig gemacht werden! Alle empfangenen und gesendeten Dateien und Medien werden gelöscht. Bilder mit niedriger Auflösung bleiben erhalten. + Diese Aktion kann nicht rückgängig gemacht werden! Es werden alle empfangenen und gesendeten Dateien und Medien gelöscht. Bilder mit niedriger Auflösung bleiben erhalten. No comment provided by engineer. This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes. - Diese Aktion kann nicht rückgängig gemacht werden! Alle empfangenen und gesendeten Nachrichten, die über den ausgewählten Zeitraum hinaus gehen, werden gelöscht. Dieser Vorgang kann mehrere Minuten dauern. + Diese Aktion kann nicht rückgängig gemacht werden! Es werden alle empfangenen und gesendeten Nachrichten, die über den ausgewählten Zeitraum hinaus gehen, gelöscht. Dieser Vorgang kann mehrere Minuten dauern. No comment provided by engineer. @@ -8529,4 +8609,178 @@ Zuletzt empfangene Nachricht: %2$@ + +
+ +
+ + + SimpleX SE + Bundle display name + + + SimpleX SE + Bundle name + + + Copyright © 2024 SimpleX Chat. All rights reserved. + Copyright (human-readable) + + +
+ +
+ +
+ + + %@ + 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. + + + Dismiss Sheet + 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. + + + Keep Trying + 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. + + + No network connection + 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 + No comment provided by engineer. + + + Selected chat preferences prohibit this message. + No comment provided by engineer. + + + Sending File + No comment provided by engineer. + + + Share + No comment provided by engineer. + + + Unknown database error: %@ + No comment provided by engineer. + + + Unsupported format + No comment provided by engineer. + + + Wrong database passphrase + No comment provided by engineer. + + + You can allow sharing in Privacy & Security / SimpleX Lock settings. + No comment provided by engineer. + + +
diff --git a/apps/ios/SimpleX Localizations/de.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/de.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings index 124ddbcc33..9c675514f4 100644 --- a/apps/ios/SimpleX Localizations/de.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/de.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings @@ -1,6 +1,9 @@ /* Bundle display name */ "CFBundleDisplayName" = "SimpleX NSE"; + /* Bundle name */ "CFBundleName" = "SimpleX NSE"; + /* Copyright (human-readable) */ "NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/de.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/de.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/de.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/de.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/de.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX Localizations/de.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/de.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/de.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/de.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ 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 a0ec9aca4a..3fc3e6037b 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff +++ b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff @@ -783,6 +783,11 @@ Allow sending disappearing messages. No comment provided by engineer.
+ + Allow sharing + Allow sharing + No comment provided by engineer. + Allow to irreversibly delete sent messages. (24 hours) Allow to irreversibly delete sent messages. (24 hours) @@ -1073,6 +1078,11 @@ Blocked by admin No comment provided by engineer. + + Blur media + Blur media + No comment provided by engineer. + Both you and your contact can add message reactions. Both you and your contact can add message reactions. @@ -1538,6 +1548,11 @@ This is your own one-time link! Connection error (AUTH) No comment provided by engineer. + + Connection notifications + Connection notifications + No comment provided by engineer. + Connection request sent! Connection request sent! @@ -1886,6 +1901,11 @@ This is your own one-time link! Delete chat item action + + Delete %lld messages of members? + Delete %lld messages of members? + No comment provided by engineer. + Delete %lld messages? Delete %lld messages? @@ -2128,11 +2148,21 @@ This cannot be undone! Desktop devices No comment provided by engineer. + + Destination server address of %@ is incompatible with forwarding server %@ settings. + Destination server address of %@ is incompatible with forwarding server %@ settings. + No comment provided by engineer. + Destination server error: %@ Destination server error: %@ snd error text + + Destination server version of %@ is incompatible with forwarding server %@. + Destination server version of %@ is incompatible with forwarding server %@. + No comment provided by engineer. + Detailed statistics Detailed statistics @@ -2198,6 +2228,11 @@ This cannot be undone! Disable for all No comment provided by engineer. + + Disabled + Disabled + No comment provided by engineer. + Disappearing message Disappearing message @@ -2423,6 +2458,11 @@ This cannot be undone! Enable self-destruct passcode set passcode view + + Enabled + Enabled + No comment provided by engineer. + Enabled for Enabled for @@ -2593,6 +2633,11 @@ This cannot be undone! Error changing setting No comment provided by engineer. + + Error connecting to forwarding server %@. Please try later. + Error connecting to forwarding server %@. Please try later. + No comment provided by engineer. + Error creating address Error creating address @@ -3099,6 +3144,21 @@ This cannot be undone! Forwarded from No comment provided by engineer. + + Forwarding server %@ failed to connect to destination server %@. Please try later. + Forwarding server %@ failed to connect to destination server %@. Please try later. + No comment provided by engineer. + + + Forwarding server address is incompatible with network settings: %@. + Forwarding server address is incompatible with network settings: %@. + No comment provided by engineer. + + + Forwarding server version is incompatible with network settings: %@. + Forwarding server version is incompatible with network settings: %@. + No comment provided by engineer. + Forwarding server: %1$@ Destination server error: %2$@ @@ -3918,6 +3978,11 @@ This is your link for group %@! Max 30 seconds, received instantly. No comment provided by engineer. + + Medium + Medium + blur media + Member Member @@ -4343,6 +4408,11 @@ This is your link for group %@! Not compatible! No comment provided by engineer. + + Nothing selected + Nothing selected + No comment provided by engineer. + Notifications Notifications @@ -4370,7 +4440,7 @@ This is your link for group %@! Off Off - No comment provided by engineer. + blur media Ok @@ -4726,11 +4796,6 @@ Error: %@ Please store passphrase securely, you will NOT be able to change it if you lose it. No comment provided by engineer. - - Please try later. - Please try later. - No comment provided by engineer. - Polish interface Polish interface @@ -5521,6 +5586,11 @@ Enable in *Network & servers* settings. Select Select + chat item action + + + Selected %lld + Selected %lld No comment provided by engineer. @@ -5773,11 +5843,6 @@ Enable in *Network & servers* settings. Server version is incompatible with network settings. srv error text - - Server version is incompatible with network settings: %@. - Server version is incompatible with network settings: %@. - No comment provided by engineer. - Server version is incompatible with your app: %@. Server version is incompatible with your app: %@. @@ -5893,6 +5958,11 @@ Enable in *Network & servers* settings. Share this 1-time invite link No comment provided by engineer. + + Share to SimpleX + Share to SimpleX + No comment provided by engineer. + Share with contacts Share with contacts @@ -6043,6 +6113,11 @@ Enable in *Network & servers* settings. Small groups (max 20) No comment provided by engineer. + + Soft + Soft + blur media + Some non-fatal errors occurred during import - you may see Chat console for more details. Some non-fatal errors occurred during import - you may see Chat console for more details. @@ -6143,6 +6218,11 @@ Enable in *Network & servers* settings. Stopping chat No comment provided by engineer. + + Strong + Strong + blur media + Submit Submit @@ -6350,6 +6430,16 @@ It can happen because of some bug or when the connection is compromised.The message will be marked as moderated for all members. No comment provided by engineer. + + The messages will be deleted for all members. + The messages will be deleted for all members. + No comment provided by engineer. + + + The messages will be marked as moderated for all members. + The messages will be marked as moderated for all members. + No comment provided by engineer. + The next generation of private messaging The next generation of private messaging @@ -8539,4 +8629,218 @@ last received msg: %2$@ + +
+ +
+ + + SimpleX SE + SimpleX SE + Bundle display name + + + SimpleX SE + SimpleX SE + Bundle name + + + Copyright © 2024 SimpleX Chat. All rights reserved. + Copyright © 2024 SimpleX Chat. All rights reserved. + Copyright (human-readable) + + +
+ +
+ +
+ + + %@ + %@ + No comment provided by engineer. + + + App is locked! + App is locked! + No comment provided by engineer. + + + Cancel + Cancel + No comment provided by engineer. + + + Cannot access keychain to save database password + Cannot access keychain to save database password + No comment provided by engineer. + + + Cannot forward message + Cannot forward message + No comment provided by engineer. + + + Comment + Comment + No comment provided by engineer. + + + Currently maximum supported file size is %@. + Currently maximum supported file size is %@. + No comment provided by engineer. + + + Database downgrade required + Database downgrade required + No comment provided by engineer. + + + Database encrypted! + Database encrypted! + No comment provided by engineer. + + + Database error + Database error + No comment provided by engineer. + + + Database passphrase is different from saved in the keychain. + Database passphrase is different from saved in the keychain. + No comment provided by engineer. + + + Database passphrase is required to open chat. + Database passphrase is required to open chat. + No comment provided by engineer. + + + Database upgrade required + Database upgrade required + No comment provided by engineer. + + + Dismiss Sheet + Dismiss Sheet + No comment provided by engineer. + + + Error preparing file + Error preparing file + No comment provided by engineer. + + + Error preparing message + Error preparing message + No comment provided by engineer. + + + Error: %@ + Error: %@ + No comment provided by engineer. + + + File error + File error + No comment provided by engineer. + + + Incompatible database version + Incompatible database version + No comment provided by engineer. + + + Invalid migration confirmation + Invalid migration confirmation + No comment provided by engineer. + + + Keep Trying + Keep Trying + No comment provided by engineer. + + + Keychain error + Keychain error + No comment provided by engineer. + + + Large file! + Large file! + No comment provided by engineer. + + + No active profile + No active profile + No comment provided by engineer. + + + No network connection + No network connection + No comment provided by engineer. + + + Ok + Ok + No comment provided by engineer. + + + Open the app to downgrade the database. + Open the app to downgrade the database. + No comment provided by engineer. + + + Open the app to upgrade the database. + Open the app to upgrade the database. + No comment provided by engineer. + + + Passphrase + Passphrase + No comment provided by engineer. + + + Please create a profile in the SimpleX app + Please create a profile in the SimpleX app + No comment provided by engineer. + + + Selected chat preferences prohibit this message. + Selected chat preferences prohibit this message. + No comment provided by engineer. + + + Sending File + Sending File + No comment provided by engineer. + + + Share + Share + No comment provided by engineer. + + + Unknown database error: %@ + Unknown database error: %@ + No comment provided by engineer. + + + Unsupported format + Unsupported format + No comment provided by engineer. + + + Wrong database passphrase + Wrong database passphrase + No comment provided by engineer. + + + You can allow sharing in Privacy & Security / SimpleX Lock settings. + You can allow sharing in Privacy & Security / SimpleX Lock settings. + No comment provided by engineer. + + +
diff --git a/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings index 124ddbcc33..9c675514f4 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings @@ -1,6 +1,9 @@ /* Bundle display name */ "CFBundleDisplayName" = "SimpleX NSE"; + /* Bundle name */ "CFBundleName" = "SimpleX NSE"; + /* Copyright (human-readable) */ "NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ 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 837ea06e96..3e33bb5d39 100644 --- a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff +++ b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff @@ -554,6 +554,7 @@
Accent + Color No comment provided by engineer. @@ -579,14 +580,17 @@ Acknowledged + Recibido No comment provided by engineer. Acknowledgement errors + Errores de reconocimiento No comment provided by engineer. Active connections + Conexiones activas No comment provided by engineer. @@ -631,14 +635,17 @@ Additional accent + Acento adicional No comment provided by engineer. Additional accent 2 + Color adicional 2 No comment provided by engineer. Additional secondary + Secundario adicional No comment provided by engineer. @@ -668,6 +675,7 @@ Advanced settings + Configuración avanzada No comment provided by engineer. @@ -687,6 +695,7 @@ All data is private to your device. + Todos los datos son privados para tu dispositivo. No comment provided by engineer. @@ -711,6 +720,7 @@ All profiles + Todos los perfiles No comment provided by engineer. @@ -773,6 +783,10 @@ Permites el envío de mensajes temporales. No comment provided by engineer. + + Allow sharing + No comment provided by engineer. + Allow to irreversibly delete sent messages. (24 hours) Se permite la eliminación irreversible de mensajes. (24 horas) @@ -915,6 +929,7 @@ Apply to + Aplicar a No comment provided by engineer. @@ -994,6 +1009,7 @@ Background + Fondo No comment provided by engineer. @@ -1023,6 +1039,7 @@ Black + Negro No comment provided by engineer. @@ -1060,6 +1077,10 @@ Bloqueado por administrador No comment provided by engineer. + + Blur media + No comment provided by engineer. + Both you and your contact can add message reactions. Tanto tú como tu contacto podéis añadir reacciones a los mensajes. @@ -1137,6 +1158,7 @@ Cannot forward message + No se puede reenviar el mensaje No comment provided by engineer. @@ -1212,6 +1234,7 @@ Chat colors + Colores del chat No comment provided by engineer. @@ -1261,6 +1284,7 @@ Chat theme + Tema de chat No comment provided by engineer. @@ -1295,14 +1319,17 @@ Chunks deleted + Bloques eliminados No comment provided by engineer. Chunks downloaded + Bloques descargados No comment provided by engineer. Chunks uploaded + Bloques subidos No comment provided by engineer. @@ -1332,6 +1359,7 @@ Color mode + Modo de color No comment provided by engineer. @@ -1346,6 +1374,7 @@ Completed + Completado No comment provided by engineer. @@ -1355,6 +1384,7 @@ Configured %@ servers + %@ servidores configurados No comment provided by engineer. @@ -1463,6 +1493,7 @@ This is your own one-time link! Connected + Conectado No comment provided by engineer. @@ -1472,6 +1503,7 @@ This is your own one-time link! Connected servers + Servidores conectados No comment provided by engineer. @@ -1481,6 +1513,7 @@ This is your own one-time link! Connecting + Conectando No comment provided by engineer. @@ -1513,6 +1546,10 @@ This is your own one-time link! Error de conexión (Autenticación) No comment provided by engineer. + + Connection notifications + No comment provided by engineer. + Connection request sent! ¡Solicitud de conexión enviada! @@ -1530,10 +1567,12 @@ This is your own one-time link! Connection with desktop stopped + La conexión con el escritorio (desktop) se ha parado No comment provided by engineer. Connections + Conexiones No comment provided by engineer. @@ -1593,6 +1632,7 @@ This is your own one-time link! Copy error + Copiar error No comment provided by engineer. @@ -1672,6 +1712,7 @@ This is your own one-time link! Created + Creado No comment provided by engineer. @@ -1711,6 +1752,7 @@ This is your own one-time link! Current profile + Perfil actual No comment provided by engineer. @@ -1725,6 +1767,7 @@ This is your own one-time link! Customize theme + Personalizar tema No comment provided by engineer. @@ -1734,6 +1777,7 @@ This is your own one-time link! Dark mode colors + Colores en modo oscuro No comment provided by engineer. @@ -1854,6 +1898,10 @@ This is your own one-time link! Eliminar chat item action + + Delete %lld messages of members? + No comment provided by engineer. + Delete %lld messages? ¿Elimina %lld mensajes? @@ -2043,6 +2091,7 @@ This cannot be undone! Deleted + Eliminado No comment provided by engineer. @@ -2057,6 +2106,7 @@ This cannot be undone! Deletion errors + Errores de borrado No comment provided by engineer. @@ -2094,17 +2144,27 @@ This cannot be undone! Ordenadores No comment provided by engineer. + + Destination server address of %@ is incompatible with forwarding server %@ settings. + No comment provided by engineer. + Destination server error: %@ Error del servidor de destino: %@ snd error text + + Destination server version of %@ is incompatible with forwarding server %@. + No comment provided by engineer. + Detailed statistics + Estadísticas detalladas No comment provided by engineer. Details + Detalles No comment provided by engineer. @@ -2162,6 +2222,10 @@ This cannot be undone! Desactivar para todos No comment provided by engineer. + + Disabled + No comment provided by engineer. + Disappearing message Mensaje temporal @@ -2264,6 +2328,7 @@ This cannot be undone! Download errors + Errores en la descarga No comment provided by engineer. @@ -2278,10 +2343,12 @@ This cannot be undone! Downloaded + Descargado No comment provided by engineer. Downloaded files + Archivos descargados No comment provided by engineer. @@ -2384,6 +2451,10 @@ This cannot be undone! Activar código de autodestrucción set passcode view + + Enabled + No comment provided by engineer. + Enabled for Activado para @@ -2554,6 +2625,10 @@ This cannot be undone! Error cambiando configuración No comment provided by engineer. + + Error connecting to forwarding server %@. Please try later. + No comment provided by engineer. + Error creating address Error al crear dirección @@ -2656,6 +2731,7 @@ This cannot be undone! Error exporting theme: %@ + Error al exportar tema: %@ No comment provided by engineer. @@ -2685,10 +2761,12 @@ This cannot be undone! Error reconnecting server + Error al reconectar con el servidor No comment provided by engineer. Error reconnecting servers + Error al reconectar con los servidores No comment provided by engineer. @@ -2698,6 +2776,7 @@ This cannot be undone! Error resetting statistics + Error al restablecer las estadísticas No comment provided by engineer. @@ -2833,6 +2912,7 @@ This cannot be undone! Errors + Errores No comment provided by engineer. @@ -2862,6 +2942,7 @@ This cannot be undone! Export theme + Exportar tema No comment provided by engineer. @@ -2901,22 +2982,27 @@ This cannot be undone! File error + Error de archivo No comment provided by engineer. File not found - most likely file was deleted or cancelled. + Archivo no encontrado, probablemente haya sido borrado o cancelado. file error text File server error: %@ + Error del servidor de archivos: %@ file error text File status + Estado del archivo No comment provided by engineer. File status: %@ + Estado del archivo: %@ copied message info @@ -3049,6 +3135,18 @@ This cannot be undone! Reenviado por 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: %@. + No comment provided by engineer. + Forwarding server: %1$@ Destination server error: %2$@ @@ -3110,10 +3208,12 @@ Error: %2$@ Good afternoon! + ¡Buenas tardes! message preview Good morning! + ¡Buenos días! message preview @@ -3398,6 +3498,7 @@ Error: %2$@ Import theme + Importar tema No comment provided by engineer. @@ -3524,6 +3625,7 @@ Error: %2$@ Interface colors + Colores del interfaz No comment provided by engineer. @@ -3864,6 +3966,10 @@ This is your link for group %@! Máximo 30 segundos, recibido al instante. No comment provided by engineer. + + Medium + blur media + Member Miembro @@ -3871,6 +3977,7 @@ This is your link for group %@! Member inactive + Miembro inactivo item status text @@ -3890,6 +3997,7 @@ This is your link for group %@! Menus + Menus No comment provided by engineer. @@ -3914,10 +4022,12 @@ This is your link for group %@! Message forwarded + Mensaje reenviado item status text Message may be delivered later if member becomes active. + El mensaje puede ser entregado más tarde si el miembro vuelve a estar activo. item status description @@ -3942,6 +4052,7 @@ This is your link for group %@! Message reception + Recepción de mensaje No comment provided by engineer. @@ -3961,10 +4072,12 @@ This is your link for group %@! Message status + Estado del mensaje No comment provided by engineer. Message status: %@ + Estado del mensaje: %@ copied message info @@ -3994,10 +4107,12 @@ This is your link for group %@! Messages received + Mensajes recibidos No comment provided by engineer. Messages sent + Mensajes enviados No comment provided by engineer. @@ -4237,6 +4352,7 @@ This is your link for group %@! No direct connection yet, message is forwarded by admin. + Aún no hay conexión directa, el mensaje es reenviado por el administrador. item status description @@ -4256,6 +4372,7 @@ This is your link for group %@! No info, try to reload + No hay información, intenta recargar No comment provided by engineer. @@ -4278,6 +4395,10 @@ This is your link for group %@! ¡No compatible! No comment provided by engineer. + + Nothing selected + No comment provided by engineer. + Notifications Notificaciones @@ -4305,7 +4426,7 @@ This is your link for group %@! Off Desactivado - No comment provided by engineer. + blur media Ok @@ -4444,6 +4565,7 @@ This is your link for group %@! Open server settings + Abrir configuración del servidor No comment provided by engineer. @@ -4488,6 +4610,7 @@ This is your link for group %@! Other %@ servers + Otros servidores %@ No comment provided by engineer. @@ -4557,6 +4680,7 @@ This is your link for group %@! Pending + Pendiente No comment provided by engineer. @@ -4587,6 +4711,8 @@ This is your link for group %@! 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. + Comprueba que el móvil y el ordenador están conectados a la misma red local y que el cortafuegos del ordenador permite la conexión. +Por favor, comparte cualquier otro problema con los desarrolladores. No comment provided by engineer. @@ -4656,10 +4782,6 @@ Error: %@ Guarda la contraseña de forma segura, NO podrás cambiarla si la pierdes. No comment provided by engineer. - - Please try later. - No comment provided by engineer. - Polish interface Interfaz en polaco @@ -4692,6 +4814,7 @@ Error: %@ Previously connected servers + Servidores conectados previamente No comment provided by engineer. @@ -4731,6 +4854,7 @@ Error: %@ Private routing error + Error de enrutamiento privado No comment provided by engineer. @@ -4765,6 +4889,7 @@ Error: %@ Profile theme + Tema del perfil No comment provided by engineer. @@ -4851,10 +4976,12 @@ Actívalo en ajustes de *Servidores y Redes*. Proxied + Con proxy No comment provided by engineer. Proxied servers + Servidores con proxy No comment provided by engineer. @@ -4924,6 +5051,7 @@ Actívalo en ajustes de *Servidores y Redes*. Receive errors + Errores de recepción No comment provided by engineer. @@ -4948,14 +5076,17 @@ Actívalo en ajustes de *Servidores y Redes*. Received messages + Mensajes recibidos No comment provided by engineer. Received reply + Respuesta recibida No comment provided by engineer. Received total + Total recibido No comment provided by engineer. @@ -4990,6 +5121,7 @@ Actívalo en ajustes de *Servidores y Redes*. Reconnect + Reconectar No comment provided by engineer. @@ -4999,18 +5131,22 @@ Actívalo en ajustes de *Servidores y Redes*. Reconnect all servers + Reconectar todos los servidores No comment provided by engineer. Reconnect all servers? + ¿Reconectar todos los servidores? No comment provided by engineer. Reconnect server to force message delivery. It uses additional traffic. + Reconectar el servidor para forzar la entrega de mensajes. Usa tráfico adicional. No comment provided by engineer. Reconnect server? + ¿Reconectar servidor? No comment provided by engineer. @@ -5065,6 +5201,7 @@ Actívalo en ajustes de *Servidores y Redes*. Remove image + Eliminar imagen No comment provided by engineer. @@ -5139,10 +5276,12 @@ Actívalo en ajustes de *Servidores y Redes*. Reset all statistics + Restablecer estadísticas No comment provided by engineer. Reset all statistics? + ¿Restablecer todas las estadísticas? No comment provided by engineer. @@ -5152,6 +5291,7 @@ Actívalo en ajustes de *Servidores y Redes*. Reset to app theme + Restablecer al tema de la aplicación No comment provided by engineer. @@ -5161,6 +5301,7 @@ Actívalo en ajustes de *Servidores y Redes*. Reset to user theme + Restablecer al tema del usuario No comment provided by engineer. @@ -5235,6 +5376,7 @@ Actívalo en ajustes de *Servidores y Redes*. SMP server + Servidor SMP No comment provided by engineer. @@ -5354,10 +5496,12 @@ Actívalo en ajustes de *Servidores y Redes*. Scale + Escala No comment provided by engineer. Scan / Paste link + Escanear / Pegar enlace No comment provided by engineer. @@ -5402,6 +5546,7 @@ Actívalo en ajustes de *Servidores y Redes*. Secondary + Secundario No comment provided by engineer. @@ -5411,6 +5556,7 @@ Actívalo en ajustes de *Servidores y Redes*. Secured + Seguro No comment provided by engineer. @@ -5426,10 +5572,15 @@ Actívalo en ajustes de *Servidores y Redes*. Select Seleccionar + chat item action + + + Selected %lld No comment provided by engineer. Selected chat preferences prohibit this message. + Las preferencias seleccionadas no permiten este mensaje. No comment provided by engineer. @@ -5484,6 +5635,7 @@ Actívalo en ajustes de *Servidores y Redes*. Send errors + Errores de envío No comment provided by engineer. @@ -5598,6 +5750,7 @@ Actívalo en ajustes de *Servidores y Redes*. Sent directly + Enviado directamente No comment provided by engineer. @@ -5612,6 +5765,7 @@ Actívalo en ajustes de *Servidores y Redes*. Sent messages + Mensajes enviados No comment provided by engineer. @@ -5621,18 +5775,22 @@ Actívalo en ajustes de *Servidores y Redes*. Sent reply + Respuesta enviada No comment provided by engineer. Sent total + Total enviado No comment provided by engineer. Sent via proxy + Enviado mediante proxy No comment provided by engineer. Server address + Dirección del servidor No comment provided by engineer. @@ -5642,6 +5800,7 @@ Actívalo en ajustes de *Servidores y Redes*. Server address is incompatible with network settings: %@. + La dirección del servidor es incompatible con la configuración de la red: %@. No comment provided by engineer. @@ -5661,6 +5820,7 @@ Actívalo en ajustes de *Servidores y Redes*. Server type + Tipo de servidor No comment provided by engineer. @@ -5668,12 +5828,9 @@ Actívalo en ajustes de *Servidores y Redes*. La versión del servidor es incompatible con la configuración de red. srv error text - - Server version is incompatible with network settings: %@. - No comment provided by engineer. - Server version is incompatible with your app: %@. + La versión del servidor es incompatible con tu aplicación: %@. No comment provided by engineer. @@ -5683,10 +5840,12 @@ Actívalo en ajustes de *Servidores y Redes*. Servers info + Info servidores No comment provided by engineer. Servers statistics will be reset - this cannot be undone! + Las estadísticas de los servidores serán restablecidas. ¡No podrá deshacerse! No comment provided by engineer. @@ -5706,6 +5865,7 @@ Actívalo en ajustes de *Servidores y Redes*. Set default theme + Establecer tema predeterminado No comment provided by engineer. @@ -5783,6 +5943,10 @@ Actívalo en ajustes de *Servidores y Redes*. Comparte este enlace de un solo uso No comment provided by engineer. + + Share to SimpleX + No comment provided by engineer. + Share with contacts Compartir con contactos @@ -5815,6 +5979,7 @@ Actívalo en ajustes de *Servidores y Redes*. Show percentage + Mostrar porcentaje No comment provided by engineer. @@ -5834,6 +5999,7 @@ Actívalo en ajustes de *Servidores y Redes*. SimpleX + SimpleX No comment provided by engineer. @@ -5913,6 +6079,7 @@ Actívalo en ajustes de *Servidores y Redes*. Size + Tamaño No comment provided by engineer. @@ -5930,6 +6097,10 @@ Actívalo en ajustes de *Servidores y Redes*. Grupos pequeños (máx. 20) No comment provided by engineer. + + Soft + blur media + Some non-fatal errors occurred during import - you may see Chat console for more details. Algunos errores no críticos ocurrieron durante la importación - para más detalles puedes ver la consola de Chat. @@ -5962,10 +6133,12 @@ Actívalo en ajustes de *Servidores y Redes*. Starting from %@. + Iniciado desde %@. No comment provided by engineer. Statistics + Estadísticas No comment provided by engineer. @@ -6028,6 +6201,10 @@ Actívalo en ajustes de *Servidores y Redes*. Parando chat No comment provided by engineer. + + Strong + blur media + Submit Enviar @@ -6035,14 +6212,17 @@ Actívalo en ajustes de *Servidores y Redes*. Subscribed + Suscrito No comment provided by engineer. Subscription errors + Errores de suscripción No comment provided by engineer. Subscriptions ignored + Suscripciones ignoradas No comment provided by engineer. @@ -6127,6 +6307,7 @@ Actívalo en ajustes de *Servidores y Redes*. Temporary file error + Error en archivo temporal No comment provided by engineer. @@ -6231,6 +6412,14 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. El mensaje será marcado como moderado para todos los miembros. 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 La nueva generación de mensajería privada @@ -6268,6 +6457,7 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. Themes + Temas No comment provided by engineer. @@ -6337,6 +6527,7 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. This link was used with another mobile device, please create a new link on the desktop. + Este enlace ha sido usado en otro dispositivo móvil, por favor crea un enlace nuevo en el ordenador. No comment provided by engineer. @@ -6346,6 +6537,7 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. Title + Título No comment provided by engineer. @@ -6417,6 +6609,7 @@ Se te pedirá que completes la autenticación antes de activar esta función. Total + Total No comment provided by engineer. @@ -6426,6 +6619,7 @@ Se te pedirá que completes la autenticación antes de activar esta función. Transport sessions + Sesiones de transporte No comment provided by engineer. @@ -6622,6 +6816,7 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión Upload errors + Errores en subida No comment provided by engineer. @@ -6636,10 +6831,12 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión Uploaded + Subido No comment provided by engineer. Uploaded files + Archivos subidos No comment provided by engineer. @@ -6719,6 +6916,7 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión User selection + Selección de usuarios No comment provided by engineer. @@ -6858,10 +7056,12 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión Wallpaper accent + Color imagen de fondo No comment provided by engineer. Wallpaper background + Color de fondo No comment provided by engineer. @@ -6971,6 +7171,7 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión Wrong key or unknown file chunk address - most likely file is deleted. + Clave incorrecta o dirección del bloque del archivo desconocida. Es probable que el archivo se haya eliminado. file error text @@ -6980,6 +7181,7 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión XFTP server + Servidor XFTP No comment provided by engineer. @@ -7066,6 +7268,7 @@ Repeat join request? You are not connected to these servers. Private routing is used to deliver messages to them. + No estás conectado a estos servidores. Para enviarles mensajes se usa el enrutamiento privado. No comment provided by engineer. @@ -7105,7 +7308,7 @@ Repeat join request? You can now chat with %@ - Ya puedes enviar mensajes a %@ + Ya puedes chatear con %@ notification body @@ -7476,6 +7679,7 @@ Los servidores SimpleX no pueden ver tu perfil. attempts + intentos No comment provided by engineer. @@ -7670,6 +7874,7 @@ Los servidores SimpleX no pueden ver tu perfil. decryption errors + errores de descifrado No comment provided by engineer. @@ -7724,6 +7929,7 @@ Los servidores SimpleX no pueden ver tu perfil. duplicates + duplicados No comment provided by engineer. @@ -7808,6 +8014,7 @@ Los servidores SimpleX no pueden ver tu perfil. expired + expirado No comment provided by engineer. @@ -7842,6 +8049,7 @@ Los servidores SimpleX no pueden ver tu perfil. inactive + inactivo No comment provided by engineer. @@ -8023,10 +8231,12 @@ Los servidores SimpleX no pueden ver tu perfil. other + otro No comment provided by engineer. other errors + otros errores No comment provided by engineer. @@ -8399,4 +8609,178 @@ last received msg: %2$@ + +
+ +
+ + + SimpleX SE + Bundle display name + + + SimpleX SE + Bundle name + + + Copyright © 2024 SimpleX Chat. All rights reserved. + Copyright (human-readable) + + +
+ +
+ +
+ + + %@ + 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. + + + Dismiss Sheet + 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. + + + Keep Trying + 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. + + + No network connection + 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 + No comment provided by engineer. + + + Selected chat preferences prohibit this message. + No comment provided by engineer. + + + Sending File + No comment provided by engineer. + + + Share + No comment provided by engineer. + + + Unknown database error: %@ + No comment provided by engineer. + + + Unsupported format + No comment provided by engineer. + + + Wrong database passphrase + No comment provided by engineer. + + + You can allow sharing in Privacy & Security / SimpleX Lock settings. + No comment provided by engineer. + + +
diff --git a/apps/ios/SimpleX Localizations/es.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/es.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings index 124ddbcc33..9c675514f4 100644 --- a/apps/ios/SimpleX Localizations/es.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/es.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings @@ -1,6 +1,9 @@ /* Bundle display name */ "CFBundleDisplayName" = "SimpleX NSE"; + /* Bundle name */ "CFBundleName" = "SimpleX NSE"; + /* Copyright (human-readable) */ "NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/es.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/es.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/es.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/es.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/es.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX Localizations/es.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/es.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/es.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/es.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ 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 07673d911c..5b6a1fab80 100644 --- a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff @@ -744,6 +744,10 @@ Salli katoavien viestien lähettäminen. No comment provided by engineer.
+ + Allow sharing + No comment provided by engineer. + Allow to irreversibly delete sent messages. (24 hours) Salli lähetettyjen viestien peruuttamaton poistaminen. (24 tuntia) @@ -1013,6 +1017,10 @@ Blocked by admin No comment provided by engineer. + + Blur media + No comment provided by engineer. + Both you and your contact can add message reactions. Sekä sinä että kontaktisi voivat käyttää viestireaktioita. @@ -1441,6 +1449,10 @@ This is your own one-time link! Yhteysvirhe (AUTH) No comment provided by engineer. + + Connection notifications + No comment provided by engineer. + Connection request sent! Yhteyspyyntö lähetetty! @@ -1772,6 +1784,10 @@ This is your own one-time link! Poista chat item action + + Delete %lld messages of members? + No comment provided by engineer. + Delete %lld messages? No comment provided by engineer. @@ -2004,10 +2020,18 @@ This cannot be undone! Desktop devices No comment provided by engineer. + + 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. @@ -2071,6 +2095,10 @@ This cannot be undone! Poista käytöstä kaikilta No comment provided by engineer. + + Disabled + No comment provided by engineer. + Disappearing message Tuhoutuva viesti @@ -2282,6 +2310,10 @@ This cannot be undone! Ota itsetuhoava pääsykoodi käyttöön set passcode view + + Enabled + No comment provided by engineer. + Enabled for No comment provided by engineer. @@ -2443,6 +2475,10 @@ This cannot be undone! Virhe asetuksen muuttamisessa No comment provided by engineer. + + Error connecting to forwarding server %@. Please try later. + No comment provided by engineer. + Error creating address Virhe osoitteen luomisessa @@ -2918,6 +2954,18 @@ This cannot be undone! 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: %@. + No comment provided by engineer. + Forwarding server: %1$@ Destination server error: %2$@ @@ -3698,6 +3746,10 @@ This is your link for group %@! Enintään 30 sekuntia, vastaanotetaan välittömästi. No comment provided by engineer. + + Medium + blur media + Member Jäsen @@ -4088,6 +4140,10 @@ This is your link for group %@! Not compatible! No comment provided by engineer. + + Nothing selected + No comment provided by engineer. + Notifications Ilmoitukset @@ -4114,7 +4170,7 @@ This is your link for group %@! Off Pois - No comment provided by engineer. + blur media Ok @@ -4448,10 +4504,6 @@ Error: %@ Säilytä tunnuslause turvallisesti, ET voi muuttaa sitä, jos kadotat sen. No comment provided by engineer. - - Please try later. - No comment provided by engineer. - Polish interface Puolalainen käyttöliittymä @@ -5188,6 +5240,10 @@ Enable in *Network & servers* settings. Select Valitse + chat item action + + + Selected %lld No comment provided by engineer. @@ -5424,10 +5480,6 @@ Enable in *Network & servers* settings. Server version is incompatible with network settings. 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. @@ -5535,6 +5587,10 @@ Enable in *Network & servers* settings. Share this 1-time invite link No comment provided by engineer. + + Share to SimpleX + No comment provided by engineer. + Share with contacts Jaa kontaktien kanssa @@ -5676,6 +5732,10 @@ Enable in *Network & servers* settings. Pienryhmät (max 20) No comment provided by engineer. + + Soft + blur media + Some non-fatal errors occurred during import - you may see Chat console for more details. Tuonnin aikana tapahtui joitakin ei-vakavia virheitä – saatat nähdä Chat-konsolissa lisätietoja. @@ -5770,6 +5830,10 @@ Enable in *Network & servers* settings. Stopping chat No comment provided by engineer. + + Strong + blur media + Submit Lähetä @@ -5968,6 +6032,14 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.Viesti merkitään moderoiduksi kaikille jäsenille. 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 Seuraavan sukupolven yksityisviestit @@ -8035,4 +8107,178 @@ last received msg: %2$@ + +
+ +
+ + + SimpleX SE + Bundle display name + + + SimpleX SE + Bundle name + + + Copyright © 2024 SimpleX Chat. All rights reserved. + Copyright (human-readable) + + +
+ +
+ +
+ + + %@ + 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. + + + Dismiss Sheet + 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. + + + Keep Trying + 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. + + + No network connection + 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 + No comment provided by engineer. + + + Selected chat preferences prohibit this message. + No comment provided by engineer. + + + Sending File + No comment provided by engineer. + + + Share + No comment provided by engineer. + + + Unknown database error: %@ + No comment provided by engineer. + + + Unsupported format + No comment provided by engineer. + + + Wrong database passphrase + No comment provided by engineer. + + + You can allow sharing in Privacy & Security / SimpleX Lock settings. + No comment provided by engineer. + + +
diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings index 124ddbcc33..9c675514f4 100644 --- a/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings @@ -1,6 +1,9 @@ /* Bundle display name */ "CFBundleDisplayName" = "SimpleX NSE"; + /* Bundle name */ "CFBundleName" = "SimpleX NSE"; + /* Copyright (human-readable) */ "NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ 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 5c23f58e9c..e3c45aeebf 100644 --- a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff @@ -554,6 +554,7 @@
Accent + Principale No comment provided by engineer. @@ -579,14 +580,17 @@ Acknowledged + Reçu avec accusé de réception No comment provided by engineer. Acknowledgement errors + Erreur d'accusé de réception No comment provided by engineer. Active connections + Connections actives No comment provided by engineer. @@ -631,14 +635,17 @@ Additional accent + Accent additionnel No comment provided by engineer. Additional accent 2 + Accent additionnel 2 No comment provided by engineer. Additional secondary + Accent secondaire No comment provided by engineer. @@ -668,6 +675,7 @@ Advanced settings + Paramètres avancés No comment provided by engineer. @@ -687,6 +695,7 @@ All data is private to your device. + Toutes les données restent confinées dans votre appareil. No comment provided by engineer. @@ -711,6 +720,7 @@ All profiles + Tous les profiles No comment provided by engineer. @@ -773,6 +783,10 @@ Autorise l’envoi de messages éphémères. No comment provided by engineer. + + Allow sharing + No comment provided by engineer. + Allow to irreversibly delete sent messages. (24 hours) Autoriser la suppression irréversible de messages envoyés. (24 heures) @@ -915,6 +929,7 @@ Apply to + Appliquer à No comment provided by engineer. @@ -994,6 +1009,7 @@ Background + Fond No comment provided by engineer. @@ -1023,6 +1039,7 @@ Black + Noir No comment provided by engineer. @@ -1060,6 +1077,10 @@ Bloqué par l'administrateur No comment provided by engineer. + + Blur media + No comment provided by engineer. + Both you and your contact can add message reactions. Vous et votre contact pouvez ajouter des réactions aux messages. @@ -1137,6 +1158,7 @@ Cannot forward message + Impossible de transférer le message No comment provided by engineer. @@ -1212,6 +1234,7 @@ Chat colors + Couleurs de chat No comment provided by engineer. @@ -1261,6 +1284,7 @@ Chat theme + Thème de chat No comment provided by engineer. @@ -1295,14 +1319,17 @@ Chunks deleted + Chunks supprimés No comment provided by engineer. Chunks downloaded + Chunks téléchargés No comment provided by engineer. Chunks uploaded + Chunks téléversés No comment provided by engineer. @@ -1332,6 +1359,7 @@ Color mode + Mode de couleur No comment provided by engineer. @@ -1346,6 +1374,7 @@ Completed + Complété No comment provided by engineer. @@ -1355,6 +1384,7 @@ Configured %@ servers + %@ serveurs configurés No comment provided by engineer. @@ -1463,6 +1493,7 @@ Il s'agit de votre propre lien unique ! Connected + Connecté No comment provided by engineer. @@ -1472,6 +1503,7 @@ Il s'agit de votre propre lien unique ! Connected servers + Serveurs connectés No comment provided by engineer. @@ -1481,6 +1513,7 @@ Il s'agit de votre propre lien unique ! Connecting + Connexion No comment provided by engineer. @@ -1513,6 +1546,10 @@ Il s'agit de votre propre lien unique ! Erreur de connexion (AUTH) No comment provided by engineer. + + Connection notifications + No comment provided by engineer. + Connection request sent! Demande de connexion envoyée ! @@ -1530,10 +1567,12 @@ Il s'agit de votre propre lien unique ! Connection with desktop stopped + La connexion avec le bureau s'est arrêtée No comment provided by engineer. Connections + Connexions No comment provided by engineer. @@ -1593,6 +1632,7 @@ Il s'agit de votre propre lien unique ! Copy error + Erreur de copie No comment provided by engineer. @@ -1672,6 +1712,7 @@ Il s'agit de votre propre lien unique ! Created + Créé No comment provided by engineer. @@ -1711,6 +1752,7 @@ Il s'agit de votre propre lien unique ! Current profile + Profil actuel No comment provided by engineer. @@ -1725,6 +1767,7 @@ Il s'agit de votre propre lien unique ! Customize theme + Personnaliser le thème No comment provided by engineer. @@ -1734,6 +1777,7 @@ Il s'agit de votre propre lien unique ! Dark mode colors + Couleurs en mode sombre No comment provided by engineer. @@ -1854,6 +1898,10 @@ Il s'agit de votre propre lien unique ! Supprimer chat item action + + Delete %lld messages of members? + No comment provided by engineer. + Delete %lld messages? Supprimer %lld messages ? @@ -2043,6 +2091,7 @@ Cette opération ne peut être annulée ! Deleted + Supprimé No comment provided by engineer. @@ -2057,6 +2106,7 @@ Cette opération ne peut être annulée ! Deletion errors + Erreurs de suppression No comment provided by engineer. @@ -2094,17 +2144,27 @@ Cette opération ne peut être annulée ! Appareils de bureau No comment provided by engineer. + + Destination server address of %@ is incompatible with forwarding server %@ settings. + No comment provided by engineer. + Destination server error: %@ Erreur du serveur de destination : %@ snd error text + + Destination server version of %@ is incompatible with forwarding server %@. + No comment provided by engineer. + Detailed statistics + Statistiques détaillées No comment provided by engineer. Details + Détails No comment provided by engineer. @@ -2162,6 +2222,10 @@ Cette opération ne peut être annulée ! Désactiver pour tous No comment provided by engineer. + + Disabled + No comment provided by engineer. + Disappearing message Message éphémère @@ -2264,6 +2328,7 @@ Cette opération ne peut être annulée ! Download errors + Erreurs de téléchargement No comment provided by engineer. @@ -2278,10 +2343,12 @@ Cette opération ne peut être annulée ! Downloaded + Téléchargé No comment provided by engineer. Downloaded files + Fichiers téléchargés No comment provided by engineer. @@ -2384,6 +2451,10 @@ Cette opération ne peut être annulée ! Activer le code d'autodestruction set passcode view + + Enabled + No comment provided by engineer. + Enabled for Activé pour @@ -2554,6 +2625,10 @@ Cette opération ne peut être annulée ! Erreur de changement de paramètre No comment provided by engineer. + + Error connecting to forwarding server %@. Please try later. + No comment provided by engineer. + Error creating address Erreur lors de la création de l'adresse @@ -2656,6 +2731,7 @@ Cette opération ne peut être annulée ! Error exporting theme: %@ + Erreur d'exportation du thème : %@ No comment provided by engineer. @@ -2685,10 +2761,12 @@ Cette opération ne peut être annulée ! Error reconnecting server + Erreur de reconnexion du serveur No comment provided by engineer. Error reconnecting servers + Erreur de reconnexion des serveurs No comment provided by engineer. @@ -2698,6 +2776,7 @@ Cette opération ne peut être annulée ! Error resetting statistics + Erreur de réinitialisation des statistiques No comment provided by engineer. @@ -2833,6 +2912,7 @@ Cette opération ne peut être annulée ! Errors + Erreurs No comment provided by engineer. @@ -2862,6 +2942,7 @@ Cette opération ne peut être annulée ! Export theme + Exporter le thème No comment provided by engineer. @@ -2901,22 +2982,27 @@ Cette opération ne peut être annulée ! File error + Erreur de fichier No comment provided by engineer. File not found - most likely file was deleted or cancelled. + Fichier introuvable - le fichier a probablement été supprimé ou annulé. file error text File server error: %@ + Erreur de serveur de fichiers : %@ file error text File status + Statut du fichier No comment provided by engineer. File status: %@ + Statut du fichier : %@ copied message info @@ -3049,6 +3135,18 @@ Cette opération ne peut être annulée ! Transféré depuis 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: %@. + No comment provided by engineer. + Forwarding server: %1$@ Destination server error: %2$@ @@ -3110,10 +3208,12 @@ Erreur : %2$@ Good afternoon! + Bonjour ! message preview Good morning! + Bonjour ! message preview @@ -3398,6 +3498,7 @@ Erreur : %2$@ Import theme + Importer un thème No comment provided by engineer. @@ -3524,6 +3625,7 @@ Erreur : %2$@ Interface colors + Couleurs d'interface No comment provided by engineer. @@ -3864,6 +3966,10 @@ Voici votre lien pour le groupe %@ ! Max 30 secondes, réception immédiate. No comment provided by engineer. + + Medium + blur media + Member Membre @@ -3871,6 +3977,7 @@ Voici votre lien pour le groupe %@ ! Member inactive + Membre inactif item status text @@ -3890,6 +3997,7 @@ Voici votre lien pour le groupe %@ ! Menus + Menus No comment provided by engineer. @@ -3914,10 +4022,12 @@ Voici votre lien pour le groupe %@ ! Message forwarded + Message transféré item status text Message may be delivered later if member becomes active. + Le message peut être transmis plus tard si le membre devient actif. item status description @@ -3942,6 +4052,7 @@ Voici votre lien pour le groupe %@ ! Message reception + Réception de message No comment provided by engineer. @@ -3961,10 +4072,12 @@ Voici votre lien pour le groupe %@ ! Message status + Statut du message No comment provided by engineer. Message status: %@ + Statut du message : %@ copied message info @@ -3994,10 +4107,12 @@ Voici votre lien pour le groupe %@ ! Messages received + Messages reçus No comment provided by engineer. Messages sent + Messages envoyés No comment provided by engineer. @@ -4237,6 +4352,7 @@ Voici votre lien pour le groupe %@ ! No direct connection yet, message is forwarded by admin. + Pas de connexion directe pour l'instant, le message est transmis par l'administrateur. item status description @@ -4256,6 +4372,7 @@ Voici votre lien pour le groupe %@ ! No info, try to reload + Pas d'info, essayez de recharger No comment provided by engineer. @@ -4278,6 +4395,10 @@ Voici votre lien pour le groupe %@ ! Non compatible ! No comment provided by engineer. + + Nothing selected + No comment provided by engineer. + Notifications Notifications @@ -4305,7 +4426,7 @@ Voici votre lien pour le groupe %@ ! Off Off - No comment provided by engineer. + blur media Ok @@ -4444,6 +4565,7 @@ Voici votre lien pour le groupe %@ ! Open server settings + Ouvrir les paramètres du serveur No comment provided by engineer. @@ -4488,6 +4610,7 @@ Voici votre lien pour le groupe %@ ! Other %@ servers + Autres serveurs %@ No comment provided by engineer. @@ -4557,6 +4680,7 @@ Voici votre lien pour le groupe %@ ! Pending + En attente No comment provided by engineer. @@ -4587,6 +4711,8 @@ Voici votre lien pour le groupe %@ ! 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. + Veuillez vérifier que le téléphone portable et l'ordinateur de bureau sont connectés au même réseau local et que le pare-feu de l'ordinateur de bureau autorise la connexion. +Veuillez faire part de tout autre problème aux développeurs. No comment provided by engineer. @@ -4656,10 +4782,6 @@ Erreur : %@ Veuillez conserver votre phrase secrète en lieu sûr, vous NE pourrez PAS la changer si vous la perdez. No comment provided by engineer. - - Please try later. - No comment provided by engineer. - Polish interface Interface en polonais @@ -4692,6 +4814,7 @@ Erreur : %@ Previously connected servers + Serveurs précédemment connectés No comment provided by engineer. @@ -4731,6 +4854,7 @@ Erreur : %@ Private routing error + Erreur de routage privé No comment provided by engineer. @@ -4765,6 +4889,7 @@ Erreur : %@ Profile theme + Thème de profil No comment provided by engineer. @@ -4851,10 +4976,12 @@ Activez-le dans les paramètres *Réseau et serveurs*. Proxied + Routé via un proxy No comment provided by engineer. Proxied servers + Serveurs routés via des proxy No comment provided by engineer. @@ -4924,6 +5051,7 @@ Activez-le dans les paramètres *Réseau et serveurs*. Receive errors + Erreurs reçues No comment provided by engineer. @@ -4948,14 +5076,17 @@ Activez-le dans les paramètres *Réseau et serveurs*. Received messages + Messages reçus No comment provided by engineer. Received reply + Réponse reçue No comment provided by engineer. Received total + Total reçu No comment provided by engineer. @@ -4990,6 +5121,7 @@ Activez-le dans les paramètres *Réseau et serveurs*. Reconnect + Reconnecter No comment provided by engineer. @@ -4999,18 +5131,22 @@ Activez-le dans les paramètres *Réseau et serveurs*. Reconnect all servers + Reconnecter tous les serveurs No comment provided by engineer. Reconnect all servers? + Reconnecter tous les serveurs ? No comment provided by engineer. Reconnect server to force message delivery. It uses additional traffic. + Reconnecter le serveur pour forcer la livraison des messages. Utilise du trafic supplémentaire. No comment provided by engineer. Reconnect server? + Reconnecter le serveur ? No comment provided by engineer. @@ -5065,6 +5201,7 @@ Activez-le dans les paramètres *Réseau et serveurs*. Remove image + Enlever l'image No comment provided by engineer. @@ -5139,10 +5276,12 @@ Activez-le dans les paramètres *Réseau et serveurs*. Reset all statistics + Réinitialiser toutes les statistiques No comment provided by engineer. Reset all statistics? + Réinitialiser toutes les statistiques ? No comment provided by engineer. @@ -5152,6 +5291,7 @@ Activez-le dans les paramètres *Réseau et serveurs*. Reset to app theme + Réinitialisation au thème de l'appli No comment provided by engineer. @@ -5161,6 +5301,7 @@ Activez-le dans les paramètres *Réseau et serveurs*. Reset to user theme + Réinitialisation au thème de l'utilisateur No comment provided by engineer. @@ -5235,6 +5376,7 @@ Activez-le dans les paramètres *Réseau et serveurs*. SMP server + Serveur SMP No comment provided by engineer. @@ -5354,10 +5496,12 @@ Activez-le dans les paramètres *Réseau et serveurs*. Scale + Échelle No comment provided by engineer. Scan / Paste link + Scanner / Coller le lien No comment provided by engineer. @@ -5402,6 +5546,7 @@ Activez-le dans les paramètres *Réseau et serveurs*. Secondary + Secondaire No comment provided by engineer. @@ -5411,6 +5556,7 @@ Activez-le dans les paramètres *Réseau et serveurs*. Secured + Sécurisé No comment provided by engineer. @@ -5426,10 +5572,15 @@ Activez-le dans les paramètres *Réseau et serveurs*. Select Choisir + chat item action + + + Selected %lld No comment provided by engineer. Selected chat preferences prohibit this message. + Les préférences de chat sélectionnées interdisent ce message. No comment provided by engineer. @@ -5484,6 +5635,7 @@ Activez-le dans les paramètres *Réseau et serveurs*. Send errors + Erreurs d'envoi No comment provided by engineer. @@ -5598,6 +5750,7 @@ Activez-le dans les paramètres *Réseau et serveurs*. Sent directly + Envoyé directement No comment provided by engineer. @@ -5612,6 +5765,7 @@ Activez-le dans les paramètres *Réseau et serveurs*. Sent messages + Messages envoyés No comment provided by engineer. @@ -5621,18 +5775,22 @@ Activez-le dans les paramètres *Réseau et serveurs*. Sent reply + Réponse envoyée No comment provided by engineer. Sent total + Total envoyé No comment provided by engineer. Sent via proxy + Envoyé via le proxy No comment provided by engineer. Server address + Adresse du serveur No comment provided by engineer. @@ -5642,6 +5800,7 @@ Activez-le dans les paramètres *Réseau et serveurs*. Server address is incompatible with network settings: %@. + L'adresse du serveur est incompatible avec les paramètres réseau : %@. No comment provided by engineer. @@ -5661,6 +5820,7 @@ Activez-le dans les paramètres *Réseau et serveurs*. Server type + Type de serveur No comment provided by engineer. @@ -5668,12 +5828,9 @@ Activez-le dans les paramètres *Réseau et serveurs*. La version du serveur est incompatible avec les paramètres du réseau. srv error text - - Server version is incompatible with network settings: %@. - No comment provided by engineer. - Server version is incompatible with your app: %@. + La version du serveur est incompatible avec votre appli : %@. No comment provided by engineer. @@ -5683,10 +5840,12 @@ Activez-le dans les paramètres *Réseau et serveurs*. Servers info + Infos serveurs No comment provided by engineer. Servers statistics will be reset - this cannot be undone! + Les statistiques des serveurs seront réinitialisées - il n'est pas possible de revenir en arrière ! No comment provided by engineer. @@ -5706,6 +5865,7 @@ Activez-le dans les paramètres *Réseau et serveurs*. Set default theme + Définir le thème par défaut No comment provided by engineer. @@ -5783,6 +5943,10 @@ Activez-le dans les paramètres *Réseau et serveurs*. Partager ce lien d'invitation unique No comment provided by engineer. + + Share to SimpleX + No comment provided by engineer. + Share with contacts Partager avec vos contacts @@ -5815,6 +5979,7 @@ Activez-le dans les paramètres *Réseau et serveurs*. Show percentage + Afficher le pourcentage No comment provided by engineer. @@ -5913,6 +6078,7 @@ Activez-le dans les paramètres *Réseau et serveurs*. Size + Taille No comment provided by engineer. @@ -5930,6 +6096,10 @@ Activez-le dans les paramètres *Réseau et serveurs*. Petits groupes (max 20) No comment provided by engineer. + + Soft + blur media + Some non-fatal errors occurred during import - you may see Chat console for more details. Des erreurs non fatales se sont produites lors de l'importation - vous pouvez consulter la console de chat pour plus de détails. @@ -5962,10 +6132,12 @@ Activez-le dans les paramètres *Réseau et serveurs*. Starting from %@. + À partir de %@. No comment provided by engineer. Statistics + Statistiques No comment provided by engineer. @@ -6028,6 +6200,10 @@ Activez-le dans les paramètres *Réseau et serveurs*. Arrêt du chat No comment provided by engineer. + + Strong + blur media + Submit Soumettre @@ -6035,14 +6211,17 @@ Activez-le dans les paramètres *Réseau et serveurs*. Subscribed + Inscrit No comment provided by engineer. Subscription errors + Erreurs d'inscription No comment provided by engineer. Subscriptions ignored + Inscriptions ignorées No comment provided by engineer. @@ -6127,6 +6306,7 @@ Activez-le dans les paramètres *Réseau et serveurs*. Temporary file error + Erreur de fichier temporaire No comment provided by engineer. @@ -6231,6 +6411,14 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. Le message sera marqué comme modéré pour tous les membres. 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 La nouvelle génération de messagerie privée @@ -6268,6 +6456,7 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. Themes + Thèmes No comment provided by engineer. @@ -6337,6 +6526,7 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. This link was used with another mobile device, please create a new link on the desktop. + Ce lien a été utilisé avec un autre appareil mobile, veuillez créer un nouveau lien sur le bureau. No comment provided by engineer. @@ -6346,6 +6536,7 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. Title + Titre No comment provided by engineer. @@ -6417,6 +6608,7 @@ Vous serez invité à confirmer l'authentification avant que cette fonction ne s Total + Total No comment provided by engineer. @@ -6426,6 +6618,7 @@ Vous serez invité à confirmer l'authentification avant que cette fonction ne s Transport sessions + Sessions de transport No comment provided by engineer. @@ -6622,6 +6815,7 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Upload errors + Erreurs de téléversement No comment provided by engineer. @@ -6636,10 +6830,12 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Uploaded + Téléversé No comment provided by engineer. Uploaded files + Fichiers téléversés No comment provided by engineer. @@ -6719,6 +6915,7 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien User selection + Sélection de l'utilisateur No comment provided by engineer. @@ -6858,10 +7055,12 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Wallpaper accent + Accentuation du papier-peint No comment provided by engineer. Wallpaper background + Fond d'écran No comment provided by engineer. @@ -6971,6 +7170,7 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Wrong key or unknown file chunk address - most likely file is deleted. + Mauvaise clé ou adresse inconnue du bloc de données du fichier - le fichier est probablement supprimé. file error text @@ -6980,6 +7180,7 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien XFTP server + Serveur XFTP No comment provided by engineer. @@ -7066,6 +7267,7 @@ Répéter la demande d'adhésion ? You are not connected to these servers. Private routing is used to deliver messages to them. + Vous n'êtes pas connecté à ces serveurs. Le routage privé est utilisé pour leur délivrer des messages. No comment provided by engineer. @@ -7476,6 +7678,7 @@ Les serveurs SimpleX ne peuvent pas voir votre profil. attempts + tentatives No comment provided by engineer. @@ -7670,6 +7873,7 @@ Les serveurs SimpleX ne peuvent pas voir votre profil. decryption errors + Erreurs de déchiffrement No comment provided by engineer. @@ -7724,6 +7928,7 @@ Les serveurs SimpleX ne peuvent pas voir votre profil. duplicates + doublons No comment provided by engineer. @@ -7808,6 +8013,7 @@ Les serveurs SimpleX ne peuvent pas voir votre profil. expired + expiré No comment provided by engineer. @@ -7842,6 +8048,7 @@ Les serveurs SimpleX ne peuvent pas voir votre profil. inactive + inactif No comment provided by engineer. @@ -8023,10 +8230,12 @@ Les serveurs SimpleX ne peuvent pas voir votre profil. other + autre No comment provided by engineer. other errors + autres erreurs No comment provided by engineer. @@ -8399,4 +8608,178 @@ dernier message reçu : %2$@ + +
+ +
+ + + SimpleX SE + Bundle display name + + + SimpleX SE + Bundle name + + + Copyright © 2024 SimpleX Chat. All rights reserved. + Copyright (human-readable) + + +
+ +
+ +
+ + + %@ + 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. + + + Dismiss Sheet + 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. + + + Keep Trying + 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. + + + No network connection + 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 + No comment provided by engineer. + + + Selected chat preferences prohibit this message. + No comment provided by engineer. + + + Sending File + No comment provided by engineer. + + + Share + No comment provided by engineer. + + + Unknown database error: %@ + No comment provided by engineer. + + + Unsupported format + No comment provided by engineer. + + + Wrong database passphrase + No comment provided by engineer. + + + You can allow sharing in Privacy & Security / SimpleX Lock settings. + No comment provided by engineer. + + +
diff --git a/apps/ios/SimpleX Localizations/fr.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/fr.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings index 124ddbcc33..9c675514f4 100644 --- a/apps/ios/SimpleX Localizations/fr.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings @@ -1,6 +1,9 @@ /* Bundle display name */ "CFBundleDisplayName" = "SimpleX NSE"; + /* Bundle name */ "CFBundleName" = "SimpleX NSE"; + /* Copyright (human-readable) */ "NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/fr.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/fr.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/fr.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/fr.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/fr.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/fr.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ 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 2dbd206209..d6ac48d974 100644 --- a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff @@ -329,7 +329,7 @@
**Add new contact**: to create your one-time QR Code or link for your contact. - **Új ismerős hozzáadása**: egyszer használatos QR-kód vagy hivatkozás létrehozása a kapcsolattartóhoz. + **Új ismerős hozzáadása**: egyszer használatos QR-kód vagy hivatkozás létrehozása az ismerőse számára. No comment provided by engineer. @@ -349,7 +349,7 @@ **Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection. - **Megjegyzés**: ha két eszközön is ugyanazt az adatbázist használja, akkor biztonsági védelemként megszakítja a kapcsolataiból érkező üzenetek visszafejtését. + **Megjegyzés**: ha két eszközön is ugyanazt az adatbázist használja, akkor biztonsági védelemként megszakítja az ismerőseitől érkező üzenetek visszafejtését. No comment provided by engineer. @@ -590,6 +590,7 @@ Active connections + Aktív kapcsolatok száma No comment provided by engineer. @@ -782,6 +783,10 @@ Az eltűnő üzenetek küldése engedélyezve van. No comment provided by engineer. + + Allow sharing + No comment provided by engineer. + Allow to irreversibly delete sent messages. (24 hours) Elküldött üzenetek végleges törlésének engedélyezése. (24 óra) @@ -994,7 +999,7 @@ Auto-accept images - Fotók automatikus elfogadása + Képek automatikus elfogadása No comment provided by engineer. @@ -1072,6 +1077,10 @@ Letiltva az admin által No comment provided by engineer. + + Blur media + No comment provided by engineer. + Both you and your contact can add message reactions. Mindkét fél is hozzáadhat üzenetreakciókat. @@ -1375,6 +1384,7 @@ Configured %@ servers + Beállított %@ kiszolgálók No comment provided by engineer. @@ -1536,6 +1546,10 @@ Ez az egyszer használatos hivatkozása! Kapcsolódási hiba (AUTH) No comment provided by engineer. + + Connection notifications + No comment provided by engineer. + Connection request sent! Kapcsolódási kérés elküldve! @@ -1884,6 +1898,10 @@ Ez az egyszer használatos hivatkozása! Törlés chat item action + + Delete %lld messages of members? + No comment provided by engineer. + Delete %lld messages? Töröl %lld üzenetet? @@ -1916,7 +1934,7 @@ Ez az egyszer használatos hivatkozása! Delete and notify contact - Törlés és ismerős értesítése + Törlés, és az ismerős értesítése No comment provided by engineer. @@ -1988,7 +2006,7 @@ Ez a művelet nem vonható vissza! Delete for me - Törlés nálam + Csak nálam No comment provided by engineer. @@ -2098,7 +2116,7 @@ Ez a művelet nem vonható vissza! Delivery receipts are disabled! - Kézbesítési igazolások kikapcsolva! + A kézbesítési jelentések ki vannak kapcsolva! No comment provided by engineer. @@ -2126,11 +2144,19 @@ Ez a művelet nem vonható vissza! Számítógépek No comment provided by engineer. + + Destination server address of %@ is incompatible with forwarding server %@ settings. + No comment provided by engineer. + Destination server error: %@ Célkiszolgáló hiba: %@ snd error text + + Destination server version of %@ is incompatible with forwarding server %@. + No comment provided by engineer. + Detailed statistics Részletes statisztikák @@ -2158,12 +2184,12 @@ Ez a művelet nem vonható vissza! Device authentication is disabled. Turning off SimpleX Lock. - Eszközhitelesítés kikapcsolva. SimpleX zárolás kikapcsolása. + A készüléken nincs beállítva a képernyőzár. A SimpleX zár ki van kapcsolva. No comment provided by engineer. Device authentication is not enabled. You can turn on SimpleX Lock via Settings, once you enable device authentication. - Eszközhitelesítés nem engedélyezett.A SimpleX zárolás bekapcsolható a Beállításokon keresztül, miután az eszköz hitelesítés engedélyezésre került. + A készüléken nincs beállítva a képernyőzár. A SimpleX zár az „Adatvédelem és biztonság” menüben kapcsolható be, miután beállította a képernyőzárat az eszközén. No comment provided by engineer. @@ -2188,7 +2214,7 @@ Ez a művelet nem vonható vissza! Disable SimpleX Lock - SimpleX zárolás kikapcsolása + SimpleX zár kikapcsolása authentication reason @@ -2196,6 +2222,10 @@ Ez a művelet nem vonható vissza! Letiltás mindenki számára No comment provided by engineer. + + Disabled + No comment provided by engineer. + Disappearing message Eltűnő üzenet @@ -2363,7 +2393,7 @@ Ez a művelet nem vonható vissza! Enable SimpleX Lock - SimpleX zárolás engedélyezése + SimpleX zár bekapcsolása authentication reason @@ -2421,6 +2451,10 @@ Ez a művelet nem vonható vissza! Önmegsemmisítő jelkód engedélyezése set passcode view + + Enabled + No comment provided by engineer. + Enabled for Engedélyezve @@ -2591,6 +2625,10 @@ Ez a művelet nem vonható vissza! Hiba a beállítás megváltoztatásakor No comment provided by engineer. + + Error connecting to forwarding server %@. Please try later. + No comment provided by engineer. + Error creating address Hiba a cím létrehozásakor @@ -3097,6 +3135,18 @@ Ez a művelet nem vonható vissza! Továbbítva innen: 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: %@. + No comment provided by engineer. + Forwarding server: %1$@ Destination server error: %2$@ @@ -3665,7 +3715,7 @@ Hiba: %2$@ It can happen when you or your connection used the old database backup. - Ez akkor fordulhat elő, ha ön vagy a kapcsolata régi adatbázis biztonsági mentést használt. + Ez akkor fordulhat elő, ha ön vagy az ismerőse régi adatbázis biztonsági mentést használt. No comment provided by engineer. @@ -3916,6 +3966,10 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Max. 30 másodperc, azonnal érkezett. No comment provided by engineer. + + Medium + blur media + Member Tag @@ -3998,6 +4052,7 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Message reception + Üzenetjelentés No comment provided by engineer. @@ -4340,6 +4395,10 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Nem kompatibilis! No comment provided by engineer. + + Nothing selected + No comment provided by engineer. + Notifications Értesítések @@ -4366,8 +4425,8 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Off - Ki - No comment provided by engineer. + Kikapcsolva + blur media Ok @@ -4551,6 +4610,7 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Other %@ servers + További %@ kiszolgálók No comment provided by engineer. @@ -4722,10 +4782,6 @@ Hiba: %@ Tárolja el biztonságosan jelmondatát, mert ha elveszíti azt, NEM tudja megváltoztatni. No comment provided by engineer. - - Please try later. - No comment provided by engineer. - Polish interface Lengyel kezelőfelület @@ -4798,6 +4854,7 @@ Hiba: %@ Private routing error + Privát útválasztási hiba No comment provided by engineer. @@ -5515,6 +5572,10 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.< Select Választás + chat item action + + + Selected %lld No comment provided by engineer. @@ -5739,6 +5800,7 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.< Server address is incompatible with network settings: %@. + A kiszolgáló címe nem kompatibilis a hálózati beállításokkal: %@. No comment provided by engineer. @@ -5766,12 +5828,9 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.< A kiszolgáló verziója nem kompatibilis a hálózati beállításokkal. srv error text - - Server version is incompatible with network settings: %@. - No comment provided by engineer. - Server version is incompatible with your app: %@. + A kiszolgáló verziója nem kompatibilis az alkalmazással: %@. No comment provided by engineer. @@ -5884,6 +5943,10 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.< Egyszer használatos meghívó hivatkozás megosztása No comment provided by engineer. + + Share to SimpleX + No comment provided by engineer. + Share with contacts Megosztás ismerősökkel @@ -5916,6 +5979,7 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.< Show percentage + Százalék megjelenítése No comment provided by engineer. @@ -5950,22 +6014,22 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.< SimpleX Lock - SimpleX zárolás + SimpleX zár No comment provided by engineer. SimpleX Lock mode - SimpleX zárolási mód + Zárolási mód No comment provided by engineer. SimpleX Lock not enabled! - SimpleX zárolás nincs engedélyezve! + A SimpleX zár nincs bekapcsolva! No comment provided by engineer. SimpleX Lock turned on - SimpleX zárolás bekapcsolva + SimpleX zár bekapcsolva No comment provided by engineer. @@ -6033,6 +6097,10 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.< Kis csoportok (max. 20 tag) No comment provided by engineer. + + Soft + blur media + Some non-fatal errors occurred during import - you may see Chat console for more details. Néhány nem végzetes hiba történt az importálás során – további részletekért a csevegési konzolban olvashat. @@ -6133,6 +6201,10 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.< Csevegés megállítása folyamatban No comment provided by engineer. + + Strong + blur media + Submit Elküldés @@ -6190,7 +6262,7 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.< Take picture - Fotó készítése + Kép készítése No comment provided by engineer. @@ -6340,6 +6412,14 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. Az üzenet minden tag számára moderáltként lesz megjelölve. 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 A privát üzenetküldés következő generációja @@ -6392,7 +6472,7 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain. - 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ú fotók viszont megmaradnak. + 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. No comment provided by engineer. @@ -6498,8 +6578,8 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled. - Az adatavédelem érdekében kapcsolja be a SimpleX zárolás funkciót. -A funkció engedélyezése előtt a rendszer felszólítja a hitelesítés befejezésére. + A biztonsága érdekében kapcsolja be a SimpleX zár funkciót. +A funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beállítására az eszközén. No comment provided by engineer. @@ -6509,7 +6589,7 @@ A funkció engedélyezése előtt a rendszer felszólítja a hitelesítés befej To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page. - Rejtett profilja feltárásához írja be a teljes jelszót a keresőmezőbe a **Csevegési profiljai** oldalon. + Rejtett profilja megjelenítéséhez írja be a teljes jelszavát a keresőmezőbe a **Csevegési profilok** menüben. No comment provided by engineer. @@ -7218,7 +7298,7 @@ Csatlakozási kérés megismétlése? You can hide or mute a user profile - swipe it to the right. - Elrejthet vagy némíthat egy felhasználói profilt – csúsztasson jobbra. + Elrejtheti vagy lenémíthatja a felhasználó profiljait - csúsztassa jobbra a profilt. No comment provided by engineer. @@ -7258,7 +7338,7 @@ Csatlakozási kérés megismétlése? You can turn on SimpleX Lock via Settings. - A SimpleX zárolás a Beállításokon keresztül kapcsolható be. + A SimpleX zár az „Adatvédelem és biztonság” menüben kapcsolható be. No comment provided by engineer. @@ -7440,7 +7520,7 @@ Kapcsolódási kérés megismétlése? Your chat profiles - Csevegési profiljai + Csevegési profilok No comment provided by engineer. @@ -8146,7 +8226,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. on - be + bekapcsolva group pref value @@ -8211,7 +8291,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. removed profile picture - törölt profilkép + törölte a profilképét profile update event chat item @@ -8529,4 +8609,178 @@ utoljára fogadott üzenet: %2$@ + +
+ +
+ + + SimpleX SE + Bundle display name + + + SimpleX SE + Bundle name + + + Copyright © 2024 SimpleX Chat. All rights reserved. + Copyright (human-readable) + + +
+ +
+ +
+ + + %@ + 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. + + + Dismiss Sheet + 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. + + + Keep Trying + 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. + + + No network connection + 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 + No comment provided by engineer. + + + Selected chat preferences prohibit this message. + No comment provided by engineer. + + + Sending File + No comment provided by engineer. + + + Share + No comment provided by engineer. + + + Unknown database error: %@ + No comment provided by engineer. + + + Unsupported format + No comment provided by engineer. + + + Wrong database passphrase + No comment provided by engineer. + + + You can allow sharing in Privacy & Security / SimpleX Lock settings. + No comment provided by engineer. + + +
diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings index 124ddbcc33..9c675514f4 100644 --- a/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings @@ -1,6 +1,9 @@ /* Bundle display name */ "CFBundleDisplayName" = "SimpleX NSE"; + /* Bundle name */ "CFBundleName" = "SimpleX NSE"; + /* Copyright (human-readable) */ "NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ 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 bb2241c336..e196830ac4 100644 --- a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff +++ b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff @@ -590,6 +590,7 @@
Active connections + Connessioni attive No comment provided by engineer. @@ -782,6 +783,10 @@ Permetti l'invio di messaggi a tempo. No comment provided by engineer. + + Allow sharing + No comment provided by engineer. + Allow to irreversibly delete sent messages. (24 hours) Permetti di eliminare irreversibilmente i messaggi inviati. (24 ore) @@ -1072,6 +1077,10 @@ Bloccato dall'amministratore No comment provided by engineer. + + Blur media + No comment provided by engineer. + Both you and your contact can add message reactions. Sia tu che il tuo contatto potete aggiungere reazioni ai messaggi. @@ -1375,6 +1384,7 @@ Configured %@ servers + Configurati %@ server No comment provided by engineer. @@ -1536,6 +1546,10 @@ Questo è il tuo link una tantum! Errore di connessione (AUTH) No comment provided by engineer. + + Connection notifications + No comment provided by engineer. + Connection request sent! Richiesta di connessione inviata! @@ -1884,6 +1898,10 @@ Questo è il tuo link una tantum! Elimina chat item action + + Delete %lld messages of members? + No comment provided by engineer. + Delete %lld messages? Eliminare %lld messaggi? @@ -2126,11 +2144,19 @@ Non è reversibile! Dispositivi desktop No comment provided by engineer. + + Destination server address of %@ is incompatible with forwarding server %@ settings. + No comment provided by engineer. + Destination server error: %@ Errore del server di destinazione: %@ snd error text + + Destination server version of %@ is incompatible with forwarding server %@. + No comment provided by engineer. + Detailed statistics Statistiche dettagliate @@ -2196,6 +2222,10 @@ Non è reversibile! Disattiva per tutti No comment provided by engineer. + + Disabled + No comment provided by engineer. + Disappearing message Messaggio a tempo @@ -2421,6 +2451,10 @@ Non è reversibile! Attiva il codice di autodistruzione set passcode view + + Enabled + No comment provided by engineer. + Enabled for Attivo per @@ -2591,6 +2625,10 @@ Non è reversibile! Errore nella modifica dell'impostazione No comment provided by engineer. + + Error connecting to forwarding server %@. Please try later. + No comment provided by engineer. + Error creating address Errore nella creazione dell'indirizzo @@ -3097,6 +3135,18 @@ Non è reversibile! Inoltrato da 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: %@. + No comment provided by engineer. + Forwarding server: %1$@ Destination server error: %2$@ @@ -3916,6 +3966,10 @@ Questo è il tuo link per il gruppo %@! Max 30 secondi, ricevuto istantaneamente. No comment provided by engineer. + + Medium + blur media + Member Membro @@ -3998,6 +4052,7 @@ Questo è il tuo link per il gruppo %@! Message reception + Ricezione messaggi No comment provided by engineer. @@ -4340,6 +4395,10 @@ Questo è il tuo link per il gruppo %@! Non compatibile! No comment provided by engineer. + + Nothing selected + No comment provided by engineer. + Notifications Notifiche @@ -4367,7 +4426,7 @@ Questo è il tuo link per il gruppo %@! Off Off - No comment provided by engineer. + blur media Ok @@ -4551,6 +4610,7 @@ Questo è il tuo link per il gruppo %@! Other %@ servers + Altri %@ server No comment provided by engineer. @@ -4722,10 +4782,6 @@ Errore: %@ Conserva la password in modo sicuro, NON potrai cambiarla se la perdi. No comment provided by engineer. - - Please try later. - No comment provided by engineer. - Polish interface Interfaccia polacca @@ -4798,6 +4854,7 @@ Errore: %@ Private routing error + Errore di instradamento privato No comment provided by engineer. @@ -5515,6 +5572,10 @@ Attivalo nelle impostazioni *Rete e server*. Select Seleziona + chat item action + + + Selected %lld No comment provided by engineer. @@ -5739,6 +5800,7 @@ Attivalo nelle impostazioni *Rete e server*. Server address is incompatible with network settings: %@. + L'indirizzo del server è incompatibile con le impostazioni di rete: %@. No comment provided by engineer. @@ -5766,12 +5828,9 @@ Attivalo nelle impostazioni *Rete e server*. La versione del server non è compatibile con le impostazioni di rete. srv error text - - Server version is incompatible with network settings: %@. - No comment provided by engineer. - Server version is incompatible with your app: %@. + La versione del server è incompatibile con la tua app: %@. No comment provided by engineer. @@ -5884,6 +5943,10 @@ Attivalo nelle impostazioni *Rete e server*. Condividi questo link di invito una tantum No comment provided by engineer. + + Share to SimpleX + No comment provided by engineer. + Share with contacts Condividi con i contatti @@ -5916,6 +5979,7 @@ Attivalo nelle impostazioni *Rete e server*. Show percentage + Mostra percentuale No comment provided by engineer. @@ -6033,6 +6097,10 @@ Attivalo nelle impostazioni *Rete e server*. Piccoli gruppi (max 20) No comment provided by engineer. + + Soft + blur media + Some non-fatal errors occurred during import - you may see Chat console for more details. Si sono verificati alcuni errori non gravi durante l'importazione: vedi la console della chat per i dettagli. @@ -6133,6 +6201,10 @@ Attivalo nelle impostazioni *Rete e server*. Arresto della chat No comment provided by engineer. + + Strong + blur media + Submit Invia @@ -6340,6 +6412,14 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa.Il messaggio sarà segnato come moderato per tutti i membri. 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 La nuova generazione di messaggistica privata @@ -8529,4 +8609,178 @@ ultimo msg ricevuto: %2$@ + +
+ +
+ + + SimpleX SE + Bundle display name + + + SimpleX SE + Bundle name + + + Copyright © 2024 SimpleX Chat. All rights reserved. + Copyright (human-readable) + + +
+ +
+ +
+ + + %@ + 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. + + + Dismiss Sheet + 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. + + + Keep Trying + 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. + + + No network connection + 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 + No comment provided by engineer. + + + Selected chat preferences prohibit this message. + No comment provided by engineer. + + + Sending File + No comment provided by engineer. + + + Share + No comment provided by engineer. + + + Unknown database error: %@ + No comment provided by engineer. + + + Unsupported format + No comment provided by engineer. + + + Wrong database passphrase + No comment provided by engineer. + + + You can allow sharing in Privacy & Security / SimpleX Lock settings. + No comment provided by engineer. + + +
diff --git a/apps/ios/SimpleX Localizations/it.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/it.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings index 124ddbcc33..9c675514f4 100644 --- a/apps/ios/SimpleX Localizations/it.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/it.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings @@ -1,6 +1,9 @@ /* Bundle display name */ "CFBundleDisplayName" = "SimpleX NSE"; + /* Bundle name */ "CFBundleName" = "SimpleX NSE"; + /* Copyright (human-readable) */ "NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/it.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/it.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/it.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/it.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/it.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX Localizations/it.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/it.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/it.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/it.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ 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 2f94e9c141..46ef837f9f 100644 --- a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff +++ b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff @@ -762,6 +762,10 @@ 消えるメッセージの送信を許可する。 No comment provided by engineer.
+ + Allow sharing + No comment provided by engineer. + Allow to irreversibly delete sent messages. (24 hours) 送信済みメッセージの永久削除を許可する。(24時間) @@ -1036,6 +1040,10 @@ Blocked by admin No comment provided by engineer. + + Blur media + No comment provided by engineer. + Both you and your contact can add message reactions. 自分も相手もメッセージへのリアクションを追加できます。 @@ -1465,6 +1473,10 @@ This is your own one-time link! 接続エラー (AUTH) No comment provided by engineer. + + Connection notifications + No comment provided by engineer. + Connection request sent! 接続リクエストを送信しました! @@ -1796,6 +1808,10 @@ This is your own one-time link! 削除 chat item action + + Delete %lld messages of members? + No comment provided by engineer. + Delete %lld messages? No comment provided by engineer. @@ -2028,10 +2044,18 @@ This cannot be undone! Desktop devices No comment provided by engineer. + + 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. @@ -2095,6 +2119,10 @@ This cannot be undone! すべて無効 No comment provided by engineer. + + Disabled + No comment provided by engineer. + Disappearing message 消えるメッセージ @@ -2306,6 +2334,10 @@ This cannot be undone! 自己破壊パスコードを有効にする set passcode view + + Enabled + No comment provided by engineer. + Enabled for No comment provided by engineer. @@ -2468,6 +2500,10 @@ This cannot be undone! 設定変更にエラー発生 No comment provided by engineer. + + Error connecting to forwarding server %@. Please try later. + No comment provided by engineer. + Error creating address アドレス作成にエラー発生 @@ -2943,6 +2979,18 @@ This cannot be undone! 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: %@. + No comment provided by engineer. + Forwarding server: %1$@ Destination server error: %2$@ @@ -3723,6 +3771,10 @@ This is your link for group %@! 最大 30 秒で即時受信します。 No comment provided by engineer. + + Medium + blur media + Member メンバー @@ -4113,6 +4165,10 @@ This is your link for group %@! Not compatible! No comment provided by engineer. + + Nothing selected + No comment provided by engineer. + Notifications 通知 @@ -4139,7 +4195,7 @@ This is your link for group %@! Off オフ - No comment provided by engineer. + blur media Ok @@ -4474,10 +4530,6 @@ Error: %@ パスフレーズを失くさないように保管してください。失くすと変更できなくなります。 No comment provided by engineer. - - Please try later. - No comment provided by engineer. - Polish interface ポーランド語UI @@ -5213,6 +5265,10 @@ Enable in *Network & servers* settings. Select 選択 + chat item action + + + Selected %lld No comment provided by engineer. @@ -5442,10 +5498,6 @@ Enable in *Network & servers* settings. Server version is incompatible with network settings. 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. @@ -5553,6 +5605,10 @@ Enable in *Network & servers* settings. Share this 1-time invite link No comment provided by engineer. + + Share to SimpleX + No comment provided by engineer. + Share with contacts 連絡先と共有する @@ -5695,6 +5751,10 @@ Enable in *Network & servers* settings. 小グループ(最大20名) No comment provided by engineer. + + Soft + blur media + Some non-fatal errors occurred during import - you may see Chat console for more details. インポート中に致命的でないエラーが発生しました - 詳細はチャットコンソールを参照してください。 @@ -5789,6 +5849,10 @@ Enable in *Network & servers* settings. Stopping chat No comment provided by engineer. + + Strong + blur media + Submit 送信 @@ -5987,6 +6051,14 @@ It can happen because of some bug or when the connection is compromised.メッセージは、すべてのメンバーに対してモデレートされたものとして表示されます。 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 次世代のプライバシー・メッセンジャー @@ -8053,4 +8125,178 @@ last received msg: %2$@ + +
+ +
+ + + SimpleX SE + Bundle display name + + + SimpleX SE + Bundle name + + + Copyright © 2024 SimpleX Chat. All rights reserved. + Copyright (human-readable) + + +
+ +
+ +
+ + + %@ + 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. + + + Dismiss Sheet + 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. + + + Keep Trying + 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. + + + No network connection + 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 + No comment provided by engineer. + + + Selected chat preferences prohibit this message. + No comment provided by engineer. + + + Sending File + No comment provided by engineer. + + + Share + No comment provided by engineer. + + + Unknown database error: %@ + No comment provided by engineer. + + + Unsupported format + No comment provided by engineer. + + + Wrong database passphrase + No comment provided by engineer. + + + You can allow sharing in Privacy & Security / SimpleX Lock settings. + No comment provided by engineer. + + +
diff --git a/apps/ios/SimpleX Localizations/ja.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/ja.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings index 124ddbcc33..9c675514f4 100644 --- a/apps/ios/SimpleX Localizations/ja.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/ja.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings @@ -1,6 +1,9 @@ /* Bundle display name */ "CFBundleDisplayName" = "SimpleX NSE"; + /* Bundle name */ "CFBundleName" = "SimpleX NSE"; + /* Copyright (human-readable) */ "NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/ja.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/ja.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/ja.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/ja.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/ja.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX Localizations/ja.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/ja.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/ja.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/ja.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ 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 1f7adaeca4..6a5eb34a47 100644 --- a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff @@ -554,6 +554,7 @@
Accent + Accent No comment provided by engineer. @@ -579,14 +580,17 @@ Acknowledged + Erkend No comment provided by engineer. Acknowledgement errors + Bevestigingsfouten No comment provided by engineer. Active connections + Actieve verbindingen No comment provided by engineer. @@ -626,19 +630,22 @@ Add welcome message - Welkomst bericht toevoegen + Welkom bericht toevoegen No comment provided by engineer. Additional accent + Extra accent No comment provided by engineer. Additional accent 2 + Extra accent 2 No comment provided by engineer. Additional secondary + Extra secundair No comment provided by engineer. @@ -668,6 +675,7 @@ Advanced settings + Geavanceerde instellingen No comment provided by engineer. @@ -687,6 +695,7 @@ All data is private to your device. + Alle gegevens zijn privé op uw apparaat. No comment provided by engineer. @@ -711,6 +720,7 @@ All profiles + Alle profielen No comment provided by engineer. @@ -755,12 +765,12 @@ Allow message reactions only if your contact allows them. - Sta berichtreacties alleen toe als uw contact dit toestaat. + Sta bericht reacties alleen toe als uw contact dit toestaat. No comment provided by engineer. Allow message reactions. - Sta berichtreacties toe. + Sta bericht reacties toe. No comment provided by engineer. @@ -773,6 +783,10 @@ Toestaan dat verdwijnende berichten worden verzonden. No comment provided by engineer. + + Allow sharing + No comment provided by engineer. + Allow to irreversibly delete sent messages. (24 hours) Sta toe om verzonden berichten onomkeerbaar te verwijderen. (24 uur) @@ -805,7 +819,7 @@ Allow your contacts adding message reactions. - Sta uw contactpersonen toe om berichtreacties toe te voegen. + Sta uw contactpersonen toe om bericht reacties toe te voegen. No comment provided by engineer. @@ -915,6 +929,7 @@ Apply to + Toepassen op No comment provided by engineer. @@ -994,6 +1009,7 @@ Background + Achtergrond No comment provided by engineer. @@ -1023,6 +1039,7 @@ Black + Zwart No comment provided by engineer. @@ -1060,9 +1077,13 @@ Geblokkeerd door beheerder No comment provided by engineer. + + Blur media + No comment provided by engineer. + Both you and your contact can add message reactions. - Zowel u als uw contact kunnen berichtreacties toevoegen. + Zowel u als uw contact kunnen bericht reacties toevoegen. No comment provided by engineer. @@ -1137,6 +1158,7 @@ Cannot forward message + Kan bericht niet doorsturen No comment provided by engineer. @@ -1212,6 +1234,7 @@ Chat colors + Chat kleuren No comment provided by engineer. @@ -1261,6 +1284,7 @@ Chat theme + Chat thema No comment provided by engineer. @@ -1295,14 +1319,17 @@ Chunks deleted + Stukken verwijderd No comment provided by engineer. Chunks downloaded + Stukken gedownload No comment provided by engineer. Chunks uploaded + Stukken geüpload No comment provided by engineer. @@ -1332,6 +1359,7 @@ Color mode + Kleur mode No comment provided by engineer. @@ -1346,6 +1374,7 @@ Completed + voltooid No comment provided by engineer. @@ -1355,6 +1384,7 @@ Configured %@ servers + %@ servers geconfigureerd No comment provided by engineer. @@ -1463,6 +1493,7 @@ Dit is uw eigen eenmalige link! Connected + Verbonden No comment provided by engineer. @@ -1472,6 +1503,7 @@ Dit is uw eigen eenmalige link! Connected servers + Verbonden servers No comment provided by engineer. @@ -1481,6 +1513,7 @@ Dit is uw eigen eenmalige link! Connecting + Verbinden No comment provided by engineer. @@ -1513,6 +1546,10 @@ Dit is uw eigen eenmalige link! Verbindingsfout (AUTH) No comment provided by engineer. + + Connection notifications + No comment provided by engineer. + Connection request sent! Verbindingsverzoek verzonden! @@ -1530,10 +1567,12 @@ Dit is uw eigen eenmalige link! Connection with desktop stopped + Verbinding met desktop is gestopt No comment provided by engineer. Connections + Verbindingen No comment provided by engineer. @@ -1593,6 +1632,7 @@ Dit is uw eigen eenmalige link! Copy error + Kopieerfout No comment provided by engineer. @@ -1672,6 +1712,7 @@ Dit is uw eigen eenmalige link! Created + Gemaakt No comment provided by engineer. @@ -1711,6 +1752,7 @@ Dit is uw eigen eenmalige link! Current profile + Huidig profiel No comment provided by engineer. @@ -1725,6 +1767,7 @@ Dit is uw eigen eenmalige link! Customize theme + Thema aanpassen No comment provided by engineer. @@ -1734,6 +1777,7 @@ Dit is uw eigen eenmalige link! Dark mode colors + Kleuren in donkere modus No comment provided by engineer. @@ -1854,6 +1898,10 @@ Dit is uw eigen eenmalige link! Verwijderen chat item action + + Delete %lld messages of members? + No comment provided by engineer. + Delete %lld messages? %lld berichten verwijderen? @@ -2043,6 +2091,7 @@ Dit kan niet ongedaan gemaakt worden! Deleted + Verwijderd No comment provided by engineer. @@ -2057,6 +2106,7 @@ Dit kan niet ongedaan gemaakt worden! Deletion errors + Verwijderingsfouten No comment provided by engineer. @@ -2094,17 +2144,27 @@ Dit kan niet ongedaan gemaakt worden! Desktop apparaten No comment provided by engineer. + + Destination server address of %@ is incompatible with forwarding server %@ settings. + No comment provided by engineer. + Destination server error: %@ Bestemmingsserverfout: %@ snd error text + + Destination server version of %@ is incompatible with forwarding server %@. + No comment provided by engineer. + Detailed statistics + Gedetailleerde statistieken No comment provided by engineer. Details + Details No comment provided by engineer. @@ -2162,6 +2222,10 @@ Dit kan niet ongedaan gemaakt worden! Uitschakelen voor iedereen No comment provided by engineer. + + Disabled + No comment provided by engineer. + Disappearing message Verdwijnend bericht @@ -2264,6 +2328,7 @@ Dit kan niet ongedaan gemaakt worden! Download errors + Downloadfouten No comment provided by engineer. @@ -2278,10 +2343,12 @@ Dit kan niet ongedaan gemaakt worden! Downloaded + Gedownload No comment provided by engineer. Downloaded files + Gedownloade bestanden No comment provided by engineer. @@ -2384,6 +2451,10 @@ Dit kan niet ongedaan gemaakt worden! Zelfvernietigings wachtwoord inschakelen set passcode view + + Enabled + No comment provided by engineer. + Enabled for Ingeschakeld voor @@ -2501,12 +2572,12 @@ Dit kan niet ongedaan gemaakt worden! Enter welcome message… - Welkomst bericht invoeren… + Welkom bericht invoeren… placeholder Enter welcome message… (optional) - Voer welkomst bericht in... (optioneel) + Voer welkom bericht in... (optioneel) placeholder @@ -2554,6 +2625,10 @@ Dit kan niet ongedaan gemaakt worden! Fout bij wijzigen van instelling No comment provided by engineer. + + Error connecting to forwarding server %@. Please try later. + No comment provided by engineer. + Error creating address Fout bij aanmaken van adres @@ -2656,6 +2731,7 @@ Dit kan niet ongedaan gemaakt worden! Error exporting theme: %@ + Fout bij exporteren van thema: %@ No comment provided by engineer. @@ -2685,10 +2761,12 @@ Dit kan niet ongedaan gemaakt worden! Error reconnecting server + Fout bij opnieuw verbinding maken met de server No comment provided by engineer. Error reconnecting servers + Fout bij opnieuw verbinden van servers No comment provided by engineer. @@ -2698,6 +2776,7 @@ Dit kan niet ongedaan gemaakt worden! Error resetting statistics + Fout bij het resetten van statistieken No comment provided by engineer. @@ -2833,6 +2912,7 @@ Dit kan niet ongedaan gemaakt worden! Errors + Fouten No comment provided by engineer. @@ -2862,6 +2942,7 @@ Dit kan niet ongedaan gemaakt worden! Export theme + Exporteer thema No comment provided by engineer. @@ -2901,22 +2982,27 @@ Dit kan niet ongedaan gemaakt worden! File error + Bestandsfout No comment provided by engineer. File not found - most likely file was deleted or cancelled. + Bestand niet gevonden - hoogstwaarschijnlijk is het bestand verwijderd of geannuleerd. file error text File server error: %@ + Bestandsserverfout: %@ file error text File status + Bestandsstatus No comment provided by engineer. File status: %@ + Bestandsstatus: %@ copied message info @@ -3049,6 +3135,18 @@ Dit kan niet ongedaan gemaakt worden! Doorgestuurd vanuit 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: %@. + No comment provided by engineer. + Forwarding server: %1$@ Destination server error: %2$@ @@ -3110,10 +3208,12 @@ Fout: %2$@ Good afternoon! + Goedemiddag! message preview Good morning! + Goedemorgen! message preview @@ -3173,7 +3273,7 @@ Fout: %2$@ Group members can add message reactions. - Groepsleden kunnen berichtreacties toevoegen. + Groepsleden kunnen bericht reacties toevoegen. No comment provided by engineer. @@ -3233,7 +3333,7 @@ Fout: %2$@ Group welcome message - Groep welkomst bericht + Groep welkom bericht No comment provided by engineer. @@ -3398,6 +3498,7 @@ Fout: %2$@ Import theme + Thema importeren No comment provided by engineer. @@ -3524,6 +3625,7 @@ Fout: %2$@ Interface colors + Interface kleuren No comment provided by engineer. @@ -3864,6 +3966,10 @@ Dit is jouw link voor groep %@! Max 30 seconden, direct ontvangen. No comment provided by engineer. + + Medium + blur media + Member Lid @@ -3871,6 +3977,7 @@ Dit is jouw link voor groep %@! Member inactive + Lid inactief item status text @@ -3890,6 +3997,7 @@ Dit is jouw link voor groep %@! Menus + Menu's No comment provided by engineer. @@ -3914,10 +4022,12 @@ Dit is jouw link voor groep %@! Message forwarded + Bericht doorgestuurd item status text Message may be delivered later if member becomes active. + Het bericht kan later worden bezorgd als het lid actief wordt. item status description @@ -3942,6 +4052,7 @@ Dit is jouw link voor groep %@! Message reception + Bericht ontvangst No comment provided by engineer. @@ -3961,10 +4072,12 @@ Dit is jouw link voor groep %@! Message status + Berichtstatus No comment provided by engineer. Message status: %@ + Berichtstatus: %@ copied message info @@ -3994,10 +4107,12 @@ Dit is jouw link voor groep %@! Messages received + Berichten ontvangen No comment provided by engineer. Messages sent + Berichten verzonden No comment provided by engineer. @@ -4237,6 +4352,7 @@ Dit is jouw link voor groep %@! No direct connection yet, message is forwarded by admin. + Nog geen directe verbinding, bericht wordt doorgestuurd door beheerder. item status description @@ -4256,6 +4372,7 @@ Dit is jouw link voor groep %@! No info, try to reload + Geen info, probeer opnieuw te laden No comment provided by engineer. @@ -4278,6 +4395,10 @@ Dit is jouw link voor groep %@! Niet compatibel! No comment provided by engineer. + + Nothing selected + No comment provided by engineer. + Notifications Meldingen @@ -4305,7 +4426,7 @@ Dit is jouw link voor groep %@! Off Uit - No comment provided by engineer. + blur media Ok @@ -4364,7 +4485,7 @@ Dit is jouw link voor groep %@! Only you can add message reactions. - Alleen jij kunt berichtreacties toevoegen. + Alleen jij kunt bericht reacties toevoegen. No comment provided by engineer. @@ -4389,7 +4510,7 @@ Dit is jouw link voor groep %@! Only your contact can add message reactions. - Alleen uw contact kan berichtreacties toevoegen. + Alleen uw contact kan bericht reacties toevoegen. No comment provided by engineer. @@ -4444,6 +4565,7 @@ Dit is jouw link voor groep %@! Open server settings + Server instellingen openen No comment provided by engineer. @@ -4488,6 +4610,7 @@ Dit is jouw link voor groep %@! Other %@ servers + Andere %@ servers No comment provided by engineer. @@ -4557,6 +4680,7 @@ Dit is jouw link voor groep %@! Pending + in behandeling No comment provided by engineer. @@ -4587,6 +4711,8 @@ Dit is jouw link voor groep %@! 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. + Controleer of mobiel en desktop met hetzelfde lokale netwerk zijn verbonden en of de desktopfirewall de verbinding toestaat. +Deel eventuele andere problemen met de ontwikkelaars. No comment provided by engineer. @@ -4656,10 +4782,6 @@ Fout: %@ Bewaar het wachtwoord veilig, u kunt deze NIET wijzigen als u het kwijtraakt. No comment provided by engineer. - - Please try later. - No comment provided by engineer. - Polish interface Poolse interface @@ -4692,6 +4814,7 @@ Fout: %@ Previously connected servers + Eerder verbonden servers No comment provided by engineer. @@ -4731,6 +4854,7 @@ Fout: %@ Private routing error + Fout in privéroutering No comment provided by engineer. @@ -4765,6 +4889,7 @@ Fout: %@ Profile theme + Profiel thema No comment provided by engineer. @@ -4784,7 +4909,7 @@ Fout: %@ Prohibit message reactions. - Berichtreacties verbieden. + Bericht reacties verbieden. No comment provided by engineer. @@ -4851,10 +4976,12 @@ Schakel dit in in *Netwerk en servers*-instellingen. Proxied + Proxied No comment provided by engineer. Proxied servers + Proxied servers No comment provided by engineer. @@ -4924,6 +5051,7 @@ Schakel dit in in *Netwerk en servers*-instellingen. Receive errors + Fouten ontvangen No comment provided by engineer. @@ -4948,14 +5076,17 @@ Schakel dit in in *Netwerk en servers*-instellingen. Received messages + Ontvangen berichten No comment provided by engineer. Received reply + Antwoord ontvangen No comment provided by engineer. Received total + Totaal ontvangen No comment provided by engineer. @@ -4990,6 +5121,7 @@ Schakel dit in in *Netwerk en servers*-instellingen. Reconnect + opnieuw verbinden No comment provided by engineer. @@ -4999,18 +5131,22 @@ Schakel dit in in *Netwerk en servers*-instellingen. Reconnect all servers + Maak opnieuw verbinding met alle servers No comment provided by engineer. Reconnect all servers? + Alle servers opnieuw verbinden? No comment provided by engineer. Reconnect server to force message delivery. It uses additional traffic. + Maak opnieuw verbinding met de server om de bezorging van berichten te forceren. Er wordt gebruik gemaakt van extra verkeer.Maak opnieuw verbinding met de server om de bezorging van berichten te forceren. Er wordt gebruik gemaakt van extra data. No comment provided by engineer. Reconnect server? + Server opnieuw verbinden? No comment provided by engineer. @@ -5065,6 +5201,7 @@ Schakel dit in in *Netwerk en servers*-instellingen. Remove image + Verwijder afbeelding No comment provided by engineer. @@ -5139,10 +5276,12 @@ Schakel dit in in *Netwerk en servers*-instellingen. Reset all statistics + Reset alle statistieken No comment provided by engineer. Reset all statistics? + Alle statistieken resetten? No comment provided by engineer. @@ -5152,6 +5291,7 @@ Schakel dit in in *Netwerk en servers*-instellingen. Reset to app theme + Terugzetten naar app thema No comment provided by engineer. @@ -5161,6 +5301,7 @@ Schakel dit in in *Netwerk en servers*-instellingen. Reset to user theme + Terugzetten naar gebruikersthema No comment provided by engineer. @@ -5235,6 +5376,7 @@ Schakel dit in in *Netwerk en servers*-instellingen. SMP server + SMP server No comment provided by engineer. @@ -5329,7 +5471,7 @@ Schakel dit in in *Netwerk en servers*-instellingen. Save welcome message? - Welkomst bericht opslaan? + Welkom bericht opslaan? No comment provided by engineer. @@ -5354,10 +5496,12 @@ Schakel dit in in *Netwerk en servers*-instellingen. Scale + Schaal No comment provided by engineer. Scan / Paste link + Link scannen/plakken No comment provided by engineer. @@ -5402,6 +5546,7 @@ Schakel dit in in *Netwerk en servers*-instellingen. Secondary + Secundair No comment provided by engineer. @@ -5411,6 +5556,7 @@ Schakel dit in in *Netwerk en servers*-instellingen. Secured + Beveiligd No comment provided by engineer. @@ -5426,10 +5572,15 @@ Schakel dit in in *Netwerk en servers*-instellingen. Select Selecteer + chat item action + + + Selected %lld No comment provided by engineer. Selected chat preferences prohibit this message. + Geselecteerde chat voorkeuren verbieden dit bericht. No comment provided by engineer. @@ -5484,6 +5635,7 @@ Schakel dit in in *Netwerk en servers*-instellingen. Send errors + Verzend fouten No comment provided by engineer. @@ -5598,6 +5750,7 @@ Schakel dit in in *Netwerk en servers*-instellingen. Sent directly + Direct verzonden No comment provided by engineer. @@ -5612,6 +5765,7 @@ Schakel dit in in *Netwerk en servers*-instellingen. Sent messages + Verzonden berichten No comment provided by engineer. @@ -5621,18 +5775,22 @@ Schakel dit in in *Netwerk en servers*-instellingen. Sent reply + Antwoord verzonden No comment provided by engineer. Sent total + Totaal verzonden No comment provided by engineer. Sent via proxy + Verzonden via proxy No comment provided by engineer. Server address + Server adres No comment provided by engineer. @@ -5642,6 +5800,7 @@ Schakel dit in in *Netwerk en servers*-instellingen. Server address is incompatible with network settings: %@. + Serveradres is incompatibel met netwerkinstellingen: %@. No comment provided by engineer. @@ -5661,6 +5820,7 @@ Schakel dit in in *Netwerk en servers*-instellingen. Server type + Server type No comment provided by engineer. @@ -5668,12 +5828,9 @@ Schakel dit in in *Netwerk en servers*-instellingen. Serverversie is incompatibel met netwerkinstellingen. srv error text - - Server version is incompatible with network settings: %@. - No comment provided by engineer. - Server version is incompatible with your app: %@. + Serverversie is incompatibel met uw app: %@. No comment provided by engineer. @@ -5683,10 +5840,12 @@ Schakel dit in in *Netwerk en servers*-instellingen. Servers info + Server informatie No comment provided by engineer. Servers statistics will be reset - this cannot be undone! + Serverstatistieken worden gereset - dit kan niet ongedaan worden gemaakt! No comment provided by engineer. @@ -5706,6 +5865,7 @@ Schakel dit in in *Netwerk en servers*-instellingen. Set default theme + Stel het standaard thema in No comment provided by engineer. @@ -5783,6 +5943,10 @@ Schakel dit in in *Netwerk en servers*-instellingen. Deel deze eenmalige uitnodigingslink No comment provided by engineer. + + Share to SimpleX + No comment provided by engineer. + Share with contacts Delen met contacten @@ -5815,6 +5979,7 @@ Schakel dit in in *Netwerk en servers*-instellingen. Show percentage + Percentage weergeven No comment provided by engineer. @@ -5834,6 +5999,7 @@ Schakel dit in in *Netwerk en servers*-instellingen. SimpleX + SimpleX No comment provided by engineer. @@ -5913,6 +6079,7 @@ Schakel dit in in *Netwerk en servers*-instellingen. Size + Maat No comment provided by engineer. @@ -5930,6 +6097,10 @@ Schakel dit in in *Netwerk en servers*-instellingen. Kleine groepen (max 20) No comment provided by engineer. + + Soft + blur media + Some non-fatal errors occurred during import - you may see Chat console for more details. Er zijn enkele niet-fatale fouten opgetreden tijdens het importeren - u kunt de Chat console raadplegen voor meer details. @@ -5962,10 +6133,12 @@ Schakel dit in in *Netwerk en servers*-instellingen. Starting from %@. + Beginnend vanaf %@. No comment provided by engineer. Statistics + Statistieken No comment provided by engineer. @@ -6028,6 +6201,10 @@ Schakel dit in in *Netwerk en servers*-instellingen. Chat stoppen No comment provided by engineer. + + Strong + blur media + Submit Indienen @@ -6035,14 +6212,17 @@ Schakel dit in in *Netwerk en servers*-instellingen. Subscribed + Ingeschreven No comment provided by engineer. Subscription errors + Inschrijving fouten No comment provided by engineer. Subscriptions ignored + Inschrijvingen genegeerd No comment provided by engineer. @@ -6127,6 +6307,7 @@ Schakel dit in in *Netwerk en servers*-instellingen. Temporary file error + Tijdelijke bestandsfout No comment provided by engineer. @@ -6231,6 +6412,14 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast. Het bericht wordt gemarkeerd als gemodereerd voor alle leden. 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 De volgende generatie privéberichten @@ -6268,6 +6457,7 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast. Themes + Thema's No comment provided by engineer. @@ -6337,6 +6527,7 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast. This link was used with another mobile device, please create a new link on the desktop. + Deze link is gebruikt met een ander mobiel apparaat. Maak een nieuwe link op de desktop. No comment provided by engineer. @@ -6346,6 +6537,7 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast. Title + Titel No comment provided by engineer. @@ -6417,6 +6609,7 @@ U wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingesc Total + Totaal No comment provided by engineer. @@ -6426,6 +6619,7 @@ U wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingesc Transport sessions + Transportsessies No comment provided by engineer. @@ -6622,6 +6816,7 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Upload errors + Upload fouten No comment provided by engineer. @@ -6636,10 +6831,12 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Uploaded + Geüpload No comment provided by engineer. Uploaded files + Geüploade bestanden No comment provided by engineer. @@ -6719,6 +6916,7 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak User selection + Gebruikersselectie No comment provided by engineer. @@ -6858,10 +7056,12 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Wallpaper accent + Achtergrond accent No comment provided by engineer. Wallpaper background + Wallpaper achtergrond No comment provided by engineer. @@ -6886,12 +7086,12 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Welcome message - Welkomst bericht + Welkom bericht No comment provided by engineer. Welcome message is too long - Welkomstbericht is te lang + Welkom bericht is te lang No comment provided by engineer. @@ -6941,7 +7141,7 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak With optional welcome message. - Met optioneel welkomst bericht. + Met optioneel welkom bericht. No comment provided by engineer. @@ -6971,6 +7171,7 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Wrong key or unknown file chunk address - most likely file is deleted. + Verkeerde sleutel of onbekend bestanddeeladres - hoogstwaarschijnlijk is het bestand verwijderd. file error text @@ -6980,6 +7181,7 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak XFTP server + XFTP server No comment provided by engineer. @@ -7066,6 +7268,7 @@ Deelnameverzoek herhalen? You are not connected to these servers. Private routing is used to deliver messages to them. + U bent niet verbonden met deze servers. Privéroutering wordt gebruikt om berichten bij hen af te leveren. No comment provided by engineer. @@ -7476,6 +7679,7 @@ SimpleX servers kunnen uw profiel niet zien. attempts + pogingen No comment provided by engineer. @@ -7670,6 +7874,7 @@ SimpleX servers kunnen uw profiel niet zien. decryption errors + decoderingsfouten No comment provided by engineer. @@ -7724,6 +7929,7 @@ SimpleX servers kunnen uw profiel niet zien. duplicates + duplicaten No comment provided by engineer. @@ -7808,6 +8014,7 @@ SimpleX servers kunnen uw profiel niet zien. expired + verlopen No comment provided by engineer. @@ -7842,6 +8049,7 @@ SimpleX servers kunnen uw profiel niet zien. inactive + inactief No comment provided by engineer. @@ -8023,10 +8231,12 @@ SimpleX servers kunnen uw profiel niet zien. other + overig No comment provided by engineer. other errors + overige fouten No comment provided by engineer. @@ -8399,4 +8609,178 @@ laatst ontvangen bericht: %2$@ + +
+ +
+ + + SimpleX SE + Bundle display name + + + SimpleX SE + Bundle name + + + Copyright © 2024 SimpleX Chat. All rights reserved. + Copyright (human-readable) + + +
+ +
+ +
+ + + %@ + 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. + + + Dismiss Sheet + 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. + + + Keep Trying + 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. + + + No network connection + 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 + No comment provided by engineer. + + + Selected chat preferences prohibit this message. + No comment provided by engineer. + + + Sending File + No comment provided by engineer. + + + Share + No comment provided by engineer. + + + Unknown database error: %@ + No comment provided by engineer. + + + Unsupported format + No comment provided by engineer. + + + Wrong database passphrase + No comment provided by engineer. + + + You can allow sharing in Privacy & Security / SimpleX Lock settings. + No comment provided by engineer. + + +
diff --git a/apps/ios/SimpleX Localizations/nl.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/nl.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings index 124ddbcc33..9c675514f4 100644 --- a/apps/ios/SimpleX Localizations/nl.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings @@ -1,6 +1,9 @@ /* Bundle display name */ "CFBundleDisplayName" = "SimpleX NSE"; + /* Bundle name */ "CFBundleName" = "SimpleX NSE"; + /* Copyright (human-readable) */ "NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/nl.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/nl.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/nl.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/nl.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/nl.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/nl.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ 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 731095dea8..a9d342d549 100644 --- a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff @@ -554,6 +554,7 @@
Accent + Akcent No comment provided by engineer. @@ -579,14 +580,17 @@ Acknowledged + Potwierdzono No comment provided by engineer. Acknowledgement errors + Błędy potwierdzenia No comment provided by engineer. Active connections + Aktywne połączenia No comment provided by engineer. @@ -631,14 +635,17 @@ Additional accent + Dodatkowy akcent No comment provided by engineer. Additional accent 2 + Dodatkowy akcent 2 No comment provided by engineer. Additional secondary + Dodatkowy drugorzędny No comment provided by engineer. @@ -668,6 +675,7 @@ Advanced settings + Zaawansowane ustawienia No comment provided by engineer. @@ -687,6 +695,7 @@ All data is private to your device. + Wszystkie dane są prywatne na Twoim urządzeniu. No comment provided by engineer. @@ -711,6 +720,7 @@ All profiles + Wszystkie profile No comment provided by engineer. @@ -773,6 +783,10 @@ Zezwól na wysyłanie znikających wiadomości. No comment provided by engineer. + + Allow sharing + No comment provided by engineer. + Allow to irreversibly delete sent messages. (24 hours) Zezwól na nieodwracalne usunięcie wysłanych wiadomości. (24 godziny) @@ -915,6 +929,7 @@ Apply to + Zastosuj dla No comment provided by engineer. @@ -994,6 +1009,7 @@ Background + Tło No comment provided by engineer. @@ -1023,6 +1039,7 @@ Black + Czarny No comment provided by engineer. @@ -1060,6 +1077,10 @@ Zablokowany przez admina No comment provided by engineer. + + Blur media + No comment provided by engineer. + Both you and your contact can add message reactions. Zarówno Ty, jak i Twój kontakt możecie dodawać reakcje wiadomości. @@ -1137,6 +1158,7 @@ Cannot forward message + Nie można przekazać wiadomości No comment provided by engineer. @@ -1212,6 +1234,7 @@ Chat colors + Kolory czatu No comment provided by engineer. @@ -1261,6 +1284,7 @@ Chat theme + Motyw czatu No comment provided by engineer. @@ -1295,14 +1319,17 @@ Chunks deleted + Fragmenty usunięte No comment provided by engineer. Chunks downloaded + Fragmenty pobrane No comment provided by engineer. Chunks uploaded + Fragmenty przesłane No comment provided by engineer. @@ -1332,6 +1359,7 @@ Color mode + Tryb koloru No comment provided by engineer. @@ -1346,6 +1374,7 @@ Completed + Zakończono No comment provided by engineer. @@ -1355,6 +1384,7 @@ Configured %@ servers + Skonfigurowano %@ serwerów No comment provided by engineer. @@ -1463,6 +1493,7 @@ To jest twój jednorazowy link! Connected + Połączony No comment provided by engineer. @@ -1472,6 +1503,7 @@ To jest twój jednorazowy link! Connected servers + Połączone serwery No comment provided by engineer. @@ -1481,6 +1513,7 @@ To jest twój jednorazowy link! Connecting + Łączenie No comment provided by engineer. @@ -1513,6 +1546,10 @@ To jest twój jednorazowy link! Błąd połączenia (UWIERZYTELNIANIE) No comment provided by engineer. + + Connection notifications + No comment provided by engineer. + Connection request sent! Prośba o połączenie wysłana! @@ -1530,10 +1567,12 @@ To jest twój jednorazowy link! Connection with desktop stopped + Połączenie z komputerem zakończone No comment provided by engineer. Connections + Połączenia No comment provided by engineer. @@ -1593,6 +1632,7 @@ To jest twój jednorazowy link! Copy error + Kopiuj błąd No comment provided by engineer. @@ -1672,6 +1712,7 @@ To jest twój jednorazowy link! Created + Utworzono No comment provided by engineer. @@ -1711,6 +1752,7 @@ To jest twój jednorazowy link! Current profile + Bieżący profil No comment provided by engineer. @@ -1725,6 +1767,7 @@ To jest twój jednorazowy link! Customize theme + Dostosuj motyw No comment provided by engineer. @@ -1734,6 +1777,7 @@ To jest twój jednorazowy link! Dark mode colors + Kolory ciemnego trybu No comment provided by engineer. @@ -1854,6 +1898,10 @@ To jest twój jednorazowy link! Usuń chat item action + + Delete %lld messages of members? + No comment provided by engineer. + Delete %lld messages? Usunąć %lld wiadomości? @@ -2043,6 +2091,7 @@ To nie może być cofnięte! Deleted + Usunięto No comment provided by engineer. @@ -2057,6 +2106,7 @@ To nie może być cofnięte! Deletion errors + Błędy usuwania No comment provided by engineer. @@ -2094,17 +2144,27 @@ To nie może być cofnięte! Urządzenia komputerowe No comment provided by engineer. + + Destination server address of %@ is incompatible with forwarding server %@ settings. + No comment provided by engineer. + Destination server error: %@ Błąd docelowego serwera: %@ snd error text + + Destination server version of %@ is incompatible with forwarding server %@. + No comment provided by engineer. + Detailed statistics + Szczegółowe statystyki No comment provided by engineer. Details + Szczegóły No comment provided by engineer. @@ -2162,6 +2222,10 @@ To nie może być cofnięte! Wyłącz dla wszystkich No comment provided by engineer. + + Disabled + No comment provided by engineer. + Disappearing message Znikająca wiadomość @@ -2264,6 +2328,7 @@ To nie może być cofnięte! Download errors + Błędy pobierania No comment provided by engineer. @@ -2278,10 +2343,12 @@ To nie może być cofnięte! Downloaded + Pobrane No comment provided by engineer. Downloaded files + Pobrane pliki No comment provided by engineer. @@ -2384,6 +2451,10 @@ To nie może być cofnięte! Włącz pin samodestrukcji set passcode view + + Enabled + No comment provided by engineer. + Enabled for Włączony dla @@ -2554,6 +2625,10 @@ To nie może być cofnięte! Błąd zmiany ustawienia No comment provided by engineer. + + Error connecting to forwarding server %@. Please try later. + No comment provided by engineer. + Error creating address Błąd tworzenia adresu @@ -2656,6 +2731,7 @@ To nie może być cofnięte! Error exporting theme: %@ + Błąd eksportowania motywu: %@ No comment provided by engineer. @@ -2685,10 +2761,12 @@ To nie może być cofnięte! Error reconnecting server + Błąd ponownego łączenia z serwerem No comment provided by engineer. Error reconnecting servers + Błąd ponownego łączenia serwerów No comment provided by engineer. @@ -2698,6 +2776,7 @@ To nie może być cofnięte! Error resetting statistics + Błąd resetowania statystyk No comment provided by engineer. @@ -2833,6 +2912,7 @@ To nie może być cofnięte! Errors + Błędy No comment provided by engineer. @@ -2862,6 +2942,7 @@ To nie może być cofnięte! Export theme + Eksportuj motyw No comment provided by engineer. @@ -2901,22 +2982,27 @@ To nie może być cofnięte! File error + Błąd pliku No comment provided by engineer. File not found - most likely file was deleted or cancelled. + Nie odnaleziono pliku - najprawdopodobniej plik został usunięty lub anulowany. file error text File server error: %@ + Błąd serwera plików: %@ file error text File status + Status pliku No comment provided by engineer. File status: %@ + Status pliku: %@ copied message info @@ -3049,6 +3135,18 @@ To nie może być cofnięte! Przekazane dalej od 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: %@. + No comment provided by engineer. + Forwarding server: %1$@ Destination server error: %2$@ @@ -3110,10 +3208,12 @@ Błąd: %2$@ Good afternoon! + Dzień dobry! message preview Good morning! + Dzień dobry! message preview @@ -3398,6 +3498,7 @@ Błąd: %2$@ Import theme + Importuj motyw No comment provided by engineer. @@ -3524,6 +3625,7 @@ Błąd: %2$@ Interface colors + Kolory interfejsu No comment provided by engineer. @@ -3864,6 +3966,10 @@ To jest twój link do grupy %@! Maksymalnie 30 sekund, odbierane natychmiast. No comment provided by engineer. + + Medium + blur media + Member Członek @@ -3871,6 +3977,7 @@ To jest twój link do grupy %@! Member inactive + Członek nieaktywny item status text @@ -3890,6 +3997,7 @@ To jest twój link do grupy %@! Menus + Menu No comment provided by engineer. @@ -3914,10 +4022,12 @@ To jest twój link do grupy %@! Message forwarded + Wiadomość przekazana item status text Message may be delivered later if member becomes active. + Wiadomość może zostać dostarczona później jeśli członek stanie się aktywny. item status description @@ -3942,6 +4052,7 @@ To jest twój link do grupy %@! Message reception + Odebranie wiadomości No comment provided by engineer. @@ -3961,10 +4072,12 @@ To jest twój link do grupy %@! Message status + Status wiadomości No comment provided by engineer. Message status: %@ + Status wiadomości: %@ copied message info @@ -3994,10 +4107,12 @@ To jest twój link do grupy %@! Messages received + Otrzymane wiadomości No comment provided by engineer. Messages sent + Wysłane wiadomości No comment provided by engineer. @@ -4237,6 +4352,7 @@ To jest twój link do grupy %@! No direct connection yet, message is forwarded by admin. + Brak bezpośredniego połączenia, wiadomość została przekazana przez administratora. item status description @@ -4256,6 +4372,7 @@ To jest twój link do grupy %@! No info, try to reload + Brak informacji, spróbuj przeładować No comment provided by engineer. @@ -4278,6 +4395,10 @@ To jest twój link do grupy %@! Nie kompatybilny! No comment provided by engineer. + + Nothing selected + No comment provided by engineer. + Notifications Powiadomienia @@ -4305,7 +4426,7 @@ To jest twój link do grupy %@! Off Wyłączony - No comment provided by engineer. + blur media Ok @@ -4444,6 +4565,7 @@ To jest twój link do grupy %@! Open server settings + Otwórz ustawienia serwera No comment provided by engineer. @@ -4488,6 +4610,7 @@ To jest twój link do grupy %@! Other %@ servers + Inne %@ serwery No comment provided by engineer. @@ -4557,6 +4680,7 @@ To jest twój link do grupy %@! Pending + Oczekujące No comment provided by engineer. @@ -4587,6 +4711,8 @@ To jest twój link do grupy %@! 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. + Sprawdź, czy telefon i komputer są podłączone do tej samej sieci lokalnej i czy zapora sieciowa komputera umożliwia połączenie. +Proszę podzielić się innymi problemami z deweloperami. No comment provided by engineer. @@ -4656,10 +4782,6 @@ Błąd: %@ Przechowuj kod dostępu w bezpieczny sposób, w przypadku jego utraty NIE będzie można go zmienić. No comment provided by engineer. - - Please try later. - No comment provided by engineer. - Polish interface Polski interfejs @@ -4692,6 +4814,7 @@ Błąd: %@ Previously connected servers + Wcześniej połączone serwery No comment provided by engineer. @@ -4731,6 +4854,7 @@ Błąd: %@ Private routing error + Błąd prywatnego trasowania No comment provided by engineer. @@ -4765,6 +4889,7 @@ Błąd: %@ Profile theme + Motyw profilu No comment provided by engineer. @@ -4851,10 +4976,12 @@ Włącz w ustawianiach *Sieć i serwery* . Proxied + Trasowane przez proxy No comment provided by engineer. Proxied servers + Serwery trasowane przez proxy No comment provided by engineer. @@ -4924,6 +5051,7 @@ Włącz w ustawianiach *Sieć i serwery* . Receive errors + Błędy otrzymania No comment provided by engineer. @@ -4948,14 +5076,17 @@ Włącz w ustawianiach *Sieć i serwery* . Received messages + Otrzymane wiadomości No comment provided by engineer. Received reply + Otrzymano odpowiedź No comment provided by engineer. Received total + Otrzymano łącznie No comment provided by engineer. @@ -4990,6 +5121,7 @@ Włącz w ustawianiach *Sieć i serwery* . Reconnect + Połącz ponownie No comment provided by engineer. @@ -4999,18 +5131,22 @@ Włącz w ustawianiach *Sieć i serwery* . Reconnect all servers + Połącz ponownie wszystkie serwery No comment provided by engineer. Reconnect all servers? + Połączyć ponownie wszystkie serwery? No comment provided by engineer. Reconnect server to force message delivery. It uses additional traffic. + Ponownie połącz z serwerem w celu wymuszenia dostarczenia wiadomości. Wykorzystuje to dodatkowy ruch. No comment provided by engineer. Reconnect server? + Połączyć ponownie serwer? No comment provided by engineer. @@ -5065,6 +5201,7 @@ Włącz w ustawianiach *Sieć i serwery* . Remove image + Usuń obraz No comment provided by engineer. @@ -5139,10 +5276,12 @@ Włącz w ustawianiach *Sieć i serwery* . Reset all statistics + Resetuj wszystkie statystyki No comment provided by engineer. Reset all statistics? + Zresetować wszystkie statystyki? No comment provided by engineer. @@ -5152,6 +5291,7 @@ Włącz w ustawianiach *Sieć i serwery* . Reset to app theme + Zresetuj do motywu aplikacji No comment provided by engineer. @@ -5161,6 +5301,7 @@ Włącz w ustawianiach *Sieć i serwery* . Reset to user theme + Zresetuj do motywu użytkownika No comment provided by engineer. @@ -5235,6 +5376,7 @@ Włącz w ustawianiach *Sieć i serwery* . SMP server + Serwer SMP No comment provided by engineer. @@ -5354,10 +5496,12 @@ Włącz w ustawianiach *Sieć i serwery* . Scale + Skaluj No comment provided by engineer. Scan / Paste link + Skanuj / Wklej link No comment provided by engineer. @@ -5402,6 +5546,7 @@ Włącz w ustawianiach *Sieć i serwery* . Secondary + Drugorzędny No comment provided by engineer. @@ -5411,6 +5556,7 @@ Włącz w ustawianiach *Sieć i serwery* . Secured + Zabezpieczone No comment provided by engineer. @@ -5426,10 +5572,15 @@ Włącz w ustawianiach *Sieć i serwery* . Select Wybierz + chat item action + + + Selected %lld No comment provided by engineer. Selected chat preferences prohibit this message. + Wybrane preferencje czatu zabraniają tej wiadomości. No comment provided by engineer. @@ -5484,6 +5635,7 @@ Włącz w ustawianiach *Sieć i serwery* . Send errors + Wyślij błędy No comment provided by engineer. @@ -5598,6 +5750,7 @@ Włącz w ustawianiach *Sieć i serwery* . Sent directly + Wysłano bezpośrednio No comment provided by engineer. @@ -5612,6 +5765,7 @@ Włącz w ustawianiach *Sieć i serwery* . Sent messages + Wysłane wiadomości No comment provided by engineer. @@ -5621,18 +5775,22 @@ Włącz w ustawianiach *Sieć i serwery* . Sent reply + Wyślij odpowiedź No comment provided by engineer. Sent total + Wysłano łącznie No comment provided by engineer. Sent via proxy + Wysłano przez proxy No comment provided by engineer. Server address + Adres serwera No comment provided by engineer. @@ -5642,6 +5800,7 @@ Włącz w ustawianiach *Sieć i serwery* . Server address is incompatible with network settings: %@. + Adres serwera jest niekompatybilny z ustawieniami sieci: %@. No comment provided by engineer. @@ -5661,6 +5820,7 @@ Włącz w ustawianiach *Sieć i serwery* . Server type + Typ serwera No comment provided by engineer. @@ -5668,12 +5828,9 @@ Włącz w ustawianiach *Sieć i serwery* . Wersja serwera jest niekompatybilna z ustawieniami sieciowymi. srv error text - - Server version is incompatible with network settings: %@. - No comment provided by engineer. - Server version is incompatible with your app: %@. + Wersja serwera jest niekompatybilna z aplikacją: %@. No comment provided by engineer. @@ -5683,10 +5840,12 @@ Włącz w ustawianiach *Sieć i serwery* . Servers info + Informacje o serwerach No comment provided by engineer. Servers statistics will be reset - this cannot be undone! + Statystyki serwerów zostaną zresetowane - nie można tego cofnąć! No comment provided by engineer. @@ -5706,6 +5865,7 @@ Włącz w ustawianiach *Sieć i serwery* . Set default theme + Ustaw domyślny motyw No comment provided by engineer. @@ -5783,6 +5943,10 @@ Włącz w ustawianiach *Sieć i serwery* . Udostępnij ten jednorazowy link No comment provided by engineer. + + Share to SimpleX + No comment provided by engineer. + Share with contacts Udostępnij kontaktom @@ -5815,6 +5979,7 @@ Włącz w ustawianiach *Sieć i serwery* . Show percentage + Pokaż procent No comment provided by engineer. @@ -5834,6 +5999,7 @@ Włącz w ustawianiach *Sieć i serwery* . SimpleX + SimpleX No comment provided by engineer. @@ -5913,6 +6079,7 @@ Włącz w ustawianiach *Sieć i serwery* . Size + Rozmiar No comment provided by engineer. @@ -5930,6 +6097,10 @@ Włącz w ustawianiach *Sieć i serwery* . Małe grupy (maks. 20) No comment provided by engineer. + + Soft + blur media + Some non-fatal errors occurred during import - you may see Chat console for more details. Podczas importu wystąpiły niekrytyczne błędy - więcej szczegółów można znaleźć w konsoli czatu. @@ -5962,10 +6133,12 @@ Włącz w ustawianiach *Sieć i serwery* . Starting from %@. + Zaczynanie od %@. No comment provided by engineer. Statistics + Statystyki No comment provided by engineer. @@ -6028,6 +6201,10 @@ Włącz w ustawianiach *Sieć i serwery* . Zatrzymywanie czatu No comment provided by engineer. + + Strong + blur media + Submit Zatwierdź @@ -6035,14 +6212,17 @@ Włącz w ustawianiach *Sieć i serwery* . Subscribed + Zasubskrybowano No comment provided by engineer. Subscription errors + Błędy subskrypcji No comment provided by engineer. Subscriptions ignored + Subskrypcje zignorowane No comment provided by engineer. @@ -6127,6 +6307,7 @@ Włącz w ustawianiach *Sieć i serwery* . Temporary file error + Tymczasowy błąd pliku No comment provided by engineer. @@ -6231,6 +6412,14 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom Wiadomość zostanie oznaczona jako moderowana dla wszystkich członków. 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 Następna generacja prywatnych wiadomości @@ -6268,6 +6457,7 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom Themes + Motywy No comment provided by engineer. @@ -6337,6 +6527,7 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom This link was used with another mobile device, please create a new link on the desktop. + Ten link dostał użyty z innym urządzeniem mobilnym, proszę stworzyć nowy link na komputerze. No comment provided by engineer. @@ -6346,6 +6537,7 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom Title + Tytuł No comment provided by engineer. @@ -6417,6 +6609,7 @@ Przed włączeniem tej funkcji zostanie wyświetlony monit uwierzytelniania. Total + Łącznie No comment provided by engineer. @@ -6426,6 +6619,7 @@ Przed włączeniem tej funkcji zostanie wyświetlony monit uwierzytelniania. Transport sessions + Sesje transportowe No comment provided by engineer. @@ -6622,6 +6816,7 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Upload errors + Błędy przesłania No comment provided by engineer. @@ -6636,10 +6831,12 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Uploaded + Przesłane No comment provided by engineer. Uploaded files + Przesłane pliki No comment provided by engineer. @@ -6719,6 +6916,7 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc User selection + Wybór użytkownika No comment provided by engineer. @@ -6858,10 +7056,12 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Wallpaper accent + Akcent tapety No comment provided by engineer. Wallpaper background + Tło tapety No comment provided by engineer. @@ -6971,6 +7171,7 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Wrong key or unknown file chunk address - most likely file is deleted. + Zły klucz lub nieznany adres fragmentu pliku - najprawdopodobniej plik został usunięty. file error text @@ -6980,6 +7181,7 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc XFTP server + Serwer XFTP No comment provided by engineer. @@ -7066,6 +7268,7 @@ Powtórzyć prośbę dołączenia? You are not connected to these servers. Private routing is used to deliver messages to them. + Nie jesteś połączony z tymi serwerami. Prywatne trasowanie jest używane do dostarczania do nich wiadomości. No comment provided by engineer. @@ -7476,6 +7679,7 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu. attempts + próby No comment provided by engineer. @@ -7670,6 +7874,7 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu. decryption errors + błąd odszyfrowywania No comment provided by engineer. @@ -7724,6 +7929,7 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu. duplicates + duplikaty No comment provided by engineer. @@ -7808,6 +8014,7 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu. expired + wygasły No comment provided by engineer. @@ -7842,6 +8049,7 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu. inactive + nieaktywny No comment provided by engineer. @@ -8023,10 +8231,12 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu. other + inne No comment provided by engineer. other errors + inne błędy No comment provided by engineer. @@ -8399,4 +8609,178 @@ ostatnia otrzymana wiadomość: %2$@ + +
+ +
+ + + SimpleX SE + Bundle display name + + + SimpleX SE + Bundle name + + + Copyright © 2024 SimpleX Chat. All rights reserved. + Copyright (human-readable) + + +
+ +
+ +
+ + + %@ + 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. + + + Dismiss Sheet + 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. + + + Keep Trying + 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. + + + No network connection + 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 + No comment provided by engineer. + + + Selected chat preferences prohibit this message. + No comment provided by engineer. + + + Sending File + No comment provided by engineer. + + + Share + No comment provided by engineer. + + + Unknown database error: %@ + No comment provided by engineer. + + + Unsupported format + No comment provided by engineer. + + + Wrong database passphrase + No comment provided by engineer. + + + You can allow sharing in Privacy & Security / SimpleX Lock settings. + No comment provided by engineer. + + +
diff --git a/apps/ios/SimpleX Localizations/pl.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/pl.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings index 124ddbcc33..9c675514f4 100644 --- a/apps/ios/SimpleX Localizations/pl.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings @@ -1,6 +1,9 @@ /* Bundle display name */ "CFBundleDisplayName" = "SimpleX NSE"; + /* Bundle name */ "CFBundleName" = "SimpleX NSE"; + /* Copyright (human-readable) */ "NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/pl.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/pl.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/pl.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/pl.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/pl.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/pl.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ 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 2fe659c6d8..7789f9ddcf 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff @@ -773,6 +773,10 @@ Разрешить посылать исчезающие сообщения. No comment provided by engineer.
+ + Allow sharing + No comment provided by engineer. + Allow to irreversibly delete sent messages. (24 hours) Разрешить необратимо удалять отправленные сообщения. (24 часа) @@ -1060,6 +1064,10 @@ Заблокирован администратором No comment provided by engineer. + + Blur media + No comment provided by engineer. + Both you and your contact can add message reactions. И Вы, и Ваш контакт можете добавлять реакции на сообщения. @@ -1513,6 +1521,10 @@ This is your own one-time link! Ошибка соединения (AUTH) No comment provided by engineer. + + Connection notifications + No comment provided by engineer. + Connection request sent! Запрос на соединение отправлен! @@ -1853,6 +1865,10 @@ This is your own one-time link! Удалить chat item action + + Delete %lld messages of members? + No comment provided by engineer. + Delete %lld messages? Удалить %lld сообщений? @@ -2093,11 +2109,19 @@ This cannot be undone! Компьютеры No comment provided by engineer. + + 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. @@ -2161,6 +2185,10 @@ This cannot be undone! Выключить для всех No comment provided by engineer. + + Disabled + No comment provided by engineer. + Disappearing message Исчезающее сообщение @@ -2383,6 +2411,10 @@ This cannot be undone! Включить код самоуничтожения set passcode view + + Enabled + No comment provided by engineer. + Enabled for Включено для @@ -2553,6 +2585,10 @@ This cannot be undone! Ошибка при изменении настройки No comment provided by engineer. + + Error connecting to forwarding server %@. Please try later. + No comment provided by engineer. + Error creating address Ошибка при создании адреса @@ -3048,6 +3084,18 @@ This cannot be undone! Переслано из 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: %@. + No comment provided by engineer. + Forwarding server: %1$@ Destination server error: %2$@ @@ -3863,6 +3911,10 @@ This is your link for group %@! Макс. 30 секунд, доставляются мгновенно. No comment provided by engineer. + + Medium + blur media + Member Член группы @@ -4276,6 +4328,10 @@ This is your link for group %@! Несовместимая версия! No comment provided by engineer. + + Nothing selected + No comment provided by engineer. + Notifications Уведомления @@ -4303,7 +4359,7 @@ This is your link for group %@! Off Выключено - No comment provided by engineer. + blur media Ok @@ -4654,10 +4710,6 @@ Error: %@ Пожалуйста, надежно сохраните пароль, Вы НЕ сможете его поменять, если потеряете. No comment provided by engineer. - - Please try later. - No comment provided by engineer. - Polish interface Польский интерфейс @@ -5424,6 +5476,10 @@ Enable in *Network & servers* settings. Select Выбрать + chat item action + + + Selected %lld No comment provided by engineer. @@ -5666,10 +5722,6 @@ Enable in *Network & servers* settings. Версия сервера несовместима с настройками сети. 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. @@ -5781,6 +5833,10 @@ Enable in *Network & servers* settings. Поделиться одноразовой ссылкой-приглашением No comment provided by engineer. + + Share to SimpleX + No comment provided by engineer. + Share with contacts Поделиться с контактами @@ -5928,6 +5984,10 @@ Enable in *Network & servers* settings. Маленькие группы (до 20) No comment provided by engineer. + + Soft + blur media + Some non-fatal errors occurred during import - you may see Chat console for more details. Во время импорта произошли некоторые ошибки - для получения более подробной информации вы можете обратиться к консоли. @@ -6026,6 +6086,10 @@ Enable in *Network & servers* settings. Остановка чата No comment provided by engineer. + + Strong + blur media + Submit Продолжить @@ -6229,6 +6293,14 @@ It can happen because of some bug or when the connection is compromised.Сообщение будет помечено как удаленное для всех членов группы. 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 Новое поколение приватных сообщений @@ -8394,4 +8466,179 @@ last received msg: %2$@ + +
+ +
+ + + SimpleX SE + Bundle display name + + + SimpleX SE + Bundle name + + + Copyright © 2024 SimpleX Chat. All rights reserved. + Copyright (human-readable) + + +
+ +
+ +
+ + + %@ + 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. + + + Dismiss Sheet + 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. + + + Keep Trying + 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. + + + No network connection + 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 + No comment provided by engineer. + + + Selected chat preferences prohibit this message. + No comment provided by engineer. + + + Sending File + No comment provided by engineer. + + + Share + Поделиться + No comment provided by engineer. + + + Unknown database error: %@ + No comment provided by engineer. + + + Unsupported format + No comment provided by engineer. + + + Wrong database passphrase + No comment provided by engineer. + + + You can allow sharing in Privacy & Security / SimpleX Lock settings. + No comment provided by engineer. + + +
diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings index 124ddbcc33..9c675514f4 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings @@ -1,6 +1,9 @@ /* Bundle display name */ "CFBundleDisplayName" = "SimpleX NSE"; + /* Bundle name */ "CFBundleName" = "SimpleX NSE"; + /* Copyright (human-readable) */ "NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ 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 2fb86f9a76..0735f1b288 100644 --- a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff +++ b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff @@ -736,6 +736,10 @@ อนุญาตให้ส่งข้อความที่จะหายไปหลังปิดแชท (disappearing message) No comment provided by engineer.
+ + Allow sharing + No comment provided by engineer. + Allow to irreversibly delete sent messages. (24 hours) อนุญาตให้ลบข้อความที่ส่งไปแล้วอย่างถาวร @@ -1005,6 +1009,10 @@ Blocked by admin No comment provided by engineer. + + Blur media + No comment provided by engineer. + Both you and your contact can add message reactions. ทั้งคุณและผู้ติดต่อของคุณสามารถเพิ่มปฏิกิริยาของข้อความได้ @@ -1431,6 +1439,10 @@ This is your own one-time link! การเชื่อมต่อผิดพลาด (AUTH) No comment provided by engineer. + + Connection notifications + No comment provided by engineer. + Connection request sent! ส่งคําขอเชื่อมต่อแล้ว! @@ -1761,6 +1773,10 @@ This is your own one-time link! ลบ chat item action + + Delete %lld messages of members? + No comment provided by engineer. + Delete %lld messages? No comment provided by engineer. @@ -1992,10 +2008,18 @@ This cannot be undone! Desktop devices No comment provided by engineer. + + 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. @@ -2059,6 +2083,10 @@ This cannot be undone! ปิดการใช้งานสำหรับทุกคน No comment provided by engineer. + + Disabled + No comment provided by engineer. + Disappearing message ข้อความที่จะหายไปหลังเวลาที่กําหนด (disappearing message) @@ -2269,6 +2297,10 @@ This cannot be undone! เปิดใช้งานรหัสผ่านแบบทําลายตัวเอง set passcode view + + Enabled + No comment provided by engineer. + Enabled for No comment provided by engineer. @@ -2429,6 +2461,10 @@ This cannot be undone! เกิดข้อผิดพลาดในการเปลี่ยนการตั้งค่า No comment provided by engineer. + + Error connecting to forwarding server %@. Please try later. + No comment provided by engineer. + Error creating address เกิดข้อผิดพลาดในการสร้างที่อยู่ @@ -2903,6 +2939,18 @@ This cannot be undone! 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: %@. + No comment provided by engineer. + Forwarding server: %1$@ Destination server error: %2$@ @@ -3681,6 +3729,10 @@ This is your link for group %@! สูงสุด 30 วินาที รับทันที No comment provided by engineer. + + Medium + blur media + Member สมาชิก @@ -4069,6 +4121,10 @@ This is your link for group %@! Not compatible! No comment provided by engineer. + + Nothing selected + No comment provided by engineer. + Notifications การแจ้งเตือน @@ -4095,7 +4151,7 @@ This is your link for group %@! Off ปิด - No comment provided by engineer. + blur media Ok @@ -4429,10 +4485,6 @@ Error: %@ โปรดจัดเก็บรหัสผ่านอย่างปลอดภัย คุณจะไม่สามารถเปลี่ยนรหัสผ่านได้หากคุณทำรหัสผ่านหาย No comment provided by engineer. - - Please try later. - No comment provided by engineer. - Polish interface อินเตอร์เฟซภาษาโปแลนด์ @@ -5167,6 +5219,10 @@ Enable in *Network & servers* settings. Select เลือก + chat item action + + + Selected %lld No comment provided by engineer. @@ -5401,10 +5457,6 @@ Enable in *Network & servers* settings. Server version is incompatible with network settings. 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. @@ -5512,6 +5564,10 @@ Enable in *Network & servers* settings. Share this 1-time invite link No comment provided by engineer. + + Share to SimpleX + No comment provided by engineer. + Share with contacts แชร์กับผู้ติดต่อ @@ -5651,6 +5707,10 @@ Enable in *Network & servers* settings. Small groups (max 20) No comment provided by engineer. + + Soft + blur media + Some non-fatal errors occurred during import - you may see Chat console for more details. ข้อผิดพลาดที่ไม่ร้ายแรงบางอย่างเกิดขึ้นระหว่างการนำเข้า - คุณอาจดูรายละเอียดเพิ่มเติมได้ที่คอนโซล Chat @@ -5745,6 +5805,10 @@ Enable in *Network & servers* settings. Stopping chat No comment provided by engineer. + + Strong + blur media + Submit ส่ง @@ -5944,6 +6008,14 @@ It can happen because of some bug or when the connection is compromised.ข้อความจะถูกทำเครื่องหมายว่ากลั่นกรองสำหรับสมาชิกทุกคน 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 การส่งข้อความส่วนตัวรุ่นต่อไป @@ -8003,4 +8075,178 @@ last received msg: %2$@ + +
+ +
+ + + SimpleX SE + Bundle display name + + + SimpleX SE + Bundle name + + + Copyright © 2024 SimpleX Chat. All rights reserved. + Copyright (human-readable) + + +
+ +
+ +
+ + + %@ + 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. + + + Dismiss Sheet + 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. + + + Keep Trying + 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. + + + No network connection + 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 + No comment provided by engineer. + + + Selected chat preferences prohibit this message. + No comment provided by engineer. + + + Sending File + No comment provided by engineer. + + + Share + No comment provided by engineer. + + + Unknown database error: %@ + No comment provided by engineer. + + + Unsupported format + No comment provided by engineer. + + + Wrong database passphrase + No comment provided by engineer. + + + You can allow sharing in Privacy & Security / SimpleX Lock settings. + No comment provided by engineer. + + +
diff --git a/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings index 124ddbcc33..9c675514f4 100644 --- a/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings @@ -1,6 +1,9 @@ /* Bundle display name */ "CFBundleDisplayName" = "SimpleX NSE"; + /* Bundle name */ "CFBundleName" = "SimpleX NSE"; + /* Copyright (human-readable) */ "NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ 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 4b4c804fa6..2781891aa6 100644 --- a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff @@ -773,6 +773,10 @@ Kendiliğinden yok olan mesajlar göndermeye izin ver. No comment provided by engineer.
+ + Allow sharing + No comment provided by engineer. + Allow to irreversibly delete sent messages. (24 hours) Gönderilen mesajların kalıcı olarak silinmesine izin ver. (24 saat içinde) @@ -1060,6 +1064,10 @@ Yönetici tarafından engellendi No comment provided by engineer. + + Blur media + No comment provided by engineer. + Both you and your contact can add message reactions. Sen ve konuştuğun kişi mesaj tepkileri ekleyebilir. @@ -1513,6 +1521,10 @@ Bu senin kendi tek kullanımlık bağlantın! Bağlantı hatası (DOĞRULAMA) No comment provided by engineer. + + Connection notifications + No comment provided by engineer. + Connection request sent! Bağlantı daveti gönderildi! @@ -1854,6 +1866,10 @@ Bu senin kendi tek kullanımlık bağlantın! Sil chat item action + + Delete %lld messages of members? + No comment provided by engineer. + Delete %lld messages? %lld mesaj silinsin mi? @@ -2094,11 +2110,19 @@ Bu geri alınamaz! Bilgisayar cihazları No comment provided by engineer. + + Destination server address of %@ is incompatible with forwarding server %@ settings. + No comment provided by engineer. + Destination server error: %@ Hedef sunucu hatası: %@ snd error text + + Destination server version of %@ is incompatible with forwarding server %@. + No comment provided by engineer. + Detailed statistics No comment provided by engineer. @@ -2162,6 +2186,10 @@ Bu geri alınamaz! Herkes için devre dışı bırak No comment provided by engineer. + + Disabled + No comment provided by engineer. + Disappearing message Kaybolan mesaj @@ -2384,6 +2412,10 @@ Bu geri alınamaz! Kendini imha şifresini etkinleştir set passcode view + + Enabled + No comment provided by engineer. + Enabled for Şunlar için etkinleştirildi @@ -2554,6 +2586,10 @@ Bu geri alınamaz! Ayar değiştirilirken hata oluştu No comment provided by engineer. + + Error connecting to forwarding server %@. Please try later. + No comment provided by engineer. + Error creating address Adres oluşturulurken hata oluştu @@ -3049,6 +3085,18 @@ Bu geri alınamaz! Şuradan iletildi 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: %@. + No comment provided by engineer. + Forwarding server: %1$@ Destination server error: %2$@ @@ -3864,6 +3912,10 @@ Bu senin grup için bağlantın %@! Maksimum 30 saniye, anında alındı. No comment provided by engineer. + + Medium + blur media + Member Kişi @@ -4278,6 +4330,10 @@ Bu senin grup için bağlantın %@! Uyumlu değil! No comment provided by engineer. + + Nothing selected + No comment provided by engineer. + Notifications Bildirimler @@ -4305,7 +4361,7 @@ Bu senin grup için bağlantın %@! Off Kapalı - No comment provided by engineer. + blur media Ok @@ -4656,10 +4712,6 @@ Hata: %@ Lütfen parolayı güvenli bir şekilde saklayın, kaybederseniz parolayı DEĞİŞTİREMEZSİNİZ. No comment provided by engineer. - - Please try later. - No comment provided by engineer. - Polish interface Lehçe arayüz @@ -5426,6 +5478,10 @@ Enable in *Network & servers* settings. Select Seç + chat item action + + + Selected %lld No comment provided by engineer. @@ -5668,10 +5724,6 @@ Enable in *Network & servers* settings. Sunucu sürümü ağ ayarlarıyla uyumlu değil. 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. @@ -5783,6 +5835,10 @@ Enable in *Network & servers* settings. Bu tek kullanımlık bağlantı davetini paylaş No comment provided by engineer. + + Share to SimpleX + No comment provided by engineer. + Share with contacts Kişilerle paylaş @@ -5930,6 +5986,10 @@ Enable in *Network & servers* settings. Küçük gruplar (en fazla 20 kişi) No comment provided by engineer. + + Soft + blur media + Some non-fatal errors occurred during import - you may see Chat console for more details. İçe aktarma sırasında bazı ölümcül olmayan hatalar oluştu - daha fazla ayrıntı için Sohbet konsoluna bakabilirsiniz. @@ -6028,6 +6088,10 @@ Enable in *Network & servers* settings. Sohbeti durdurma No comment provided by engineer. + + Strong + blur media + Submit Gönder @@ -6231,6 +6295,14 @@ Bazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir. Mesaj tüm üyeler için yönetilmiş olarak işaretlenecektir. 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 Gizli mesajlaşmanın yeni nesli @@ -8399,4 +8471,178 @@ son alınan msj: %2$@ + +
+ +
+ + + SimpleX SE + Bundle display name + + + SimpleX SE + Bundle name + + + Copyright © 2024 SimpleX Chat. All rights reserved. + Copyright (human-readable) + + +
+ +
+ +
+ + + %@ + 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. + + + Dismiss Sheet + 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. + + + Keep Trying + 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. + + + No network connection + 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 + No comment provided by engineer. + + + Selected chat preferences prohibit this message. + No comment provided by engineer. + + + Sending File + No comment provided by engineer. + + + Share + No comment provided by engineer. + + + Unknown database error: %@ + No comment provided by engineer. + + + Unsupported format + No comment provided by engineer. + + + Wrong database passphrase + No comment provided by engineer. + + + You can allow sharing in Privacy & Security / SimpleX Lock settings. + No comment provided by engineer. + + +
diff --git a/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings index 124ddbcc33..9c675514f4 100644 --- a/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings @@ -1,6 +1,9 @@ /* Bundle display name */ "CFBundleDisplayName" = "SimpleX NSE"; + /* Bundle name */ "CFBundleName" = "SimpleX NSE"; + /* Copyright (human-readable) */ "NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ 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 8056f0753f..3d95496a67 100644 --- a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff @@ -773,6 +773,10 @@ Дозволити надсилання зникаючих повідомлень. No comment provided by engineer.
+ + Allow sharing + No comment provided by engineer. + Allow to irreversibly delete sent messages. (24 hours) Дозволяє безповоротно видаляти надіслані повідомлення. (24 години) @@ -1060,6 +1064,10 @@ Заблокований адміністратором No comment provided by engineer. + + Blur media + No comment provided by engineer. + Both you and your contact can add message reactions. Реакції на повідомлення можете додавати як ви, так і ваш контакт. @@ -1513,6 +1521,10 @@ This is your own one-time link! Помилка підключення (AUTH) No comment provided by engineer. + + Connection notifications + No comment provided by engineer. + Connection request sent! Запит на підключення відправлено! @@ -1854,6 +1866,10 @@ This is your own one-time link! Видалити chat item action + + Delete %lld messages of members? + No comment provided by engineer. + Delete %lld messages? Видалити %lld повідомлень? @@ -2094,11 +2110,19 @@ This cannot be undone! Настільні пристрої No comment provided by engineer. + + 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. @@ -2162,6 +2186,10 @@ This cannot be undone! Вимкнути для всіх No comment provided by engineer. + + Disabled + No comment provided by engineer. + Disappearing message Зникаюче повідомлення @@ -2384,6 +2412,10 @@ This cannot be undone! Увімкнути пароль самознищення set passcode view + + Enabled + No comment provided by engineer. + Enabled for Увімкнено для @@ -2554,6 +2586,10 @@ This cannot be undone! Помилка зміни налаштування No comment provided by engineer. + + Error connecting to forwarding server %@. Please try later. + No comment provided by engineer. + Error creating address Помилка створення адреси @@ -3049,6 +3085,18 @@ This cannot be undone! Переслано з 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: %@. + No comment provided by engineer. + Forwarding server: %1$@ Destination server error: %2$@ @@ -3864,6 +3912,10 @@ This is your link for group %@! Максимум 30 секунд, отримується миттєво. No comment provided by engineer. + + Medium + blur media + Member Учасник @@ -4278,6 +4330,10 @@ This is your link for group %@! Не сумісні! No comment provided by engineer. + + Nothing selected + No comment provided by engineer. + Notifications Сповіщення @@ -4305,7 +4361,7 @@ This is your link for group %@! Off Вимкнено - No comment provided by engineer. + blur media Ok @@ -4656,10 +4712,6 @@ Error: %@ Будь ласка, зберігайте пароль надійно, ви НЕ зможете змінити його, якщо втратите. No comment provided by engineer. - - Please try later. - No comment provided by engineer. - Polish interface Польський інтерфейс @@ -5426,6 +5478,10 @@ Enable in *Network & servers* settings. Select Виберіть + chat item action + + + Selected %lld No comment provided by engineer. @@ -5668,10 +5724,6 @@ Enable in *Network & servers* settings. Серверна версія несумісна з мережевими налаштуваннями. 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. @@ -5783,6 +5835,10 @@ Enable in *Network & servers* settings. Поділіться цим одноразовим посиланням-запрошенням No comment provided by engineer. + + Share to SimpleX + No comment provided by engineer. + Share with contacts Поділіться з контактами @@ -5930,6 +5986,10 @@ Enable in *Network & servers* settings. Невеликі групи (максимум 20 осіб) No comment provided by engineer. + + Soft + blur media + Some non-fatal errors occurred during import - you may see Chat console for more details. Під час імпорту виникли деякі нефатальні помилки – ви можете переглянути консоль чату, щоб дізнатися більше. @@ -6028,6 +6088,10 @@ Enable in *Network & servers* settings. Зупинка чату No comment provided by engineer. + + Strong + blur media + Submit Надіслати @@ -6231,6 +6295,14 @@ It can happen because of some bug or when the connection is compromised.Повідомлення буде позначено як модероване для всіх учасників. 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 Наступне покоління приватних повідомлень @@ -8399,4 +8471,178 @@ last received msg: %2$@ + +
+ +
+ + + SimpleX SE + Bundle display name + + + SimpleX SE + Bundle name + + + Copyright © 2024 SimpleX Chat. All rights reserved. + Copyright (human-readable) + + +
+ +
+ +
+ + + %@ + 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. + + + Dismiss Sheet + 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. + + + Keep Trying + 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. + + + No network connection + 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 + No comment provided by engineer. + + + Selected chat preferences prohibit this message. + No comment provided by engineer. + + + Sending File + No comment provided by engineer. + + + Share + No comment provided by engineer. + + + Unknown database error: %@ + No comment provided by engineer. + + + Unsupported format + No comment provided by engineer. + + + Wrong database passphrase + No comment provided by engineer. + + + You can allow sharing in Privacy & Security / SimpleX Lock settings. + No comment provided by engineer. + + +
diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings index 124ddbcc33..9c675514f4 100644 --- a/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings @@ -1,6 +1,9 @@ /* Bundle display name */ "CFBundleDisplayName" = "SimpleX NSE"; + /* Bundle name */ "CFBundleName" = "SimpleX NSE"; + /* Copyright (human-readable) */ "NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ 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 0a55e89f1a..097de15cb3 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 @@ -759,6 +759,10 @@ 允许发送限时消息。 No comment provided by engineer.
+ + Allow sharing + No comment provided by engineer. + Allow to irreversibly delete sent messages. (24 hours) 允许不可撤回地删除已发送消息。 @@ -1045,6 +1049,10 @@ 由管理员封禁 No comment provided by engineer. + + Blur media + No comment provided by engineer. + Both you and your contact can add message reactions. 您和您的联系人都可以添加消息回应。 @@ -1489,6 +1497,10 @@ This is your own one-time link! 连接错误(AUTH) No comment provided by engineer. + + Connection notifications + No comment provided by engineer. + Connection request sent! 已发送连接请求! @@ -1827,6 +1839,10 @@ This is your own one-time link! 删除 chat item action + + Delete %lld messages of members? + No comment provided by engineer. + Delete %lld messages? No comment provided by engineer. @@ -2063,10 +2079,18 @@ This cannot be undone! 桌面设备 No comment provided by engineer. + + 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. @@ -2130,6 +2154,10 @@ This cannot be undone! 全部禁用 No comment provided by engineer. + + Disabled + No comment provided by engineer. + Disappearing message 限时消息 @@ -2350,6 +2378,10 @@ This cannot be undone! 启用自毁密码 set passcode view + + Enabled + No comment provided by engineer. + Enabled for 启用对象 @@ -2517,6 +2549,10 @@ This cannot be undone! 更改设置错误 No comment provided by engineer. + + Error connecting to forwarding server %@. Please try later. + No comment provided by engineer. + Error creating address 创建地址错误 @@ -3009,6 +3045,18 @@ This cannot be undone! 转发自 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: %@. + No comment provided by engineer. + Forwarding server: %1$@ Destination server error: %2$@ @@ -3813,6 +3861,10 @@ This is your link for group %@! 最长30秒,立即接收。 No comment provided by engineer. + + Medium + blur media + Member 成员 @@ -4219,6 +4271,10 @@ This is your link for group %@! 不兼容! No comment provided by engineer. + + Nothing selected + No comment provided by engineer. + Notifications 通知 @@ -4246,7 +4302,7 @@ This is your link for group %@! Off 关闭 - No comment provided by engineer. + blur media Ok @@ -4592,10 +4648,6 @@ Error: %@ 请安全地保存密码,如果您丢失了密码,您将无法更改它。 No comment provided by engineer. - - Please try later. - No comment provided by engineer. - Polish interface 波兰语界面 @@ -5351,6 +5403,10 @@ Enable in *Network & servers* settings. Select 选择 + chat item action + + + Selected %lld No comment provided by engineer. @@ -5589,10 +5645,6 @@ Enable in *Network & servers* settings. Server version is incompatible with network settings. 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. @@ -5704,6 +5756,10 @@ Enable in *Network & servers* settings. 分享此一次性邀请链接 No comment provided by engineer. + + Share to SimpleX + No comment provided by engineer. + Share with contacts 与联系人分享 @@ -5849,6 +5905,10 @@ Enable in *Network & servers* settings. 小群组(最多 20 人) No comment provided by engineer. + + Soft + blur media + Some non-fatal errors occurred during import - you may see Chat console for more details. 导入过程中发生了一些非致命错误——您可以查看聊天控制台了解更多详细信息。 @@ -5947,6 +6007,10 @@ Enable in *Network & servers* settings. 正在停止聊天 No comment provided by engineer. + + Strong + blur media + Submit 提交 @@ -6149,6 +6213,14 @@ It can happen because of some bug or when the connection is compromised.该消息将对所有成员标记为已被管理员移除。 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 下一代私密通讯软件 @@ -8281,4 +8353,178 @@ last received msg: %2$@ + +
+ +
+ + + SimpleX SE + Bundle display name + + + SimpleX SE + Bundle name + + + Copyright © 2024 SimpleX Chat. All rights reserved. + Copyright (human-readable) + + +
+ +
+ +
+ + + %@ + 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. + + + Dismiss Sheet + 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. + + + Keep Trying + 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. + + + No network connection + 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 + No comment provided by engineer. + + + Selected chat preferences prohibit this message. + No comment provided by engineer. + + + Sending File + No comment provided by engineer. + + + Share + No comment provided by engineer. + + + Unknown database error: %@ + No comment provided by engineer. + + + Unsupported format + No comment provided by engineer. + + + Wrong database passphrase + No comment provided by engineer. + + + You can allow sharing in Privacy & Security / SimpleX Lock settings. + No comment provided by engineer. + + +
diff --git a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings index 124ddbcc33..9c675514f4 100644 --- a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings @@ -1,6 +1,9 @@ /* Bundle display name */ "CFBundleDisplayName" = "SimpleX NSE"; + /* Bundle name */ "CFBundleName" = "SimpleX NSE"; + /* Copyright (human-readable) */ "NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX NSE/bg.lproj/Localizable.strings b/apps/ios/SimpleX NSE/bg.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX NSE/bg.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX NSE/cs.lproj/Localizable.strings b/apps/ios/SimpleX NSE/cs.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX NSE/cs.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX NSE/de.lproj/Localizable.strings b/apps/ios/SimpleX NSE/de.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX NSE/de.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX NSE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..9c675514f4 --- /dev/null +++ b/apps/ios/SimpleX NSE/en.lproj/InfoPlist.strings @@ -0,0 +1,9 @@ +/* Bundle display name */ +"CFBundleDisplayName" = "SimpleX NSE"; + +/* Bundle name */ +"CFBundleName" = "SimpleX NSE"; + +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX NSE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX NSE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX NSE/es.lproj/Localizable.strings b/apps/ios/SimpleX NSE/es.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX NSE/es.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX NSE/fi.lproj/Localizable.strings b/apps/ios/SimpleX NSE/fi.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX NSE/fi.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX NSE/fr.lproj/Localizable.strings b/apps/ios/SimpleX NSE/fr.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX NSE/fr.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX NSE/hu.lproj/Localizable.strings b/apps/ios/SimpleX NSE/hu.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX NSE/hu.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX NSE/it.lproj/Localizable.strings b/apps/ios/SimpleX NSE/it.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX NSE/it.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX NSE/ja.lproj/Localizable.strings b/apps/ios/SimpleX NSE/ja.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX NSE/ja.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX NSE/nl.lproj/Localizable.strings b/apps/ios/SimpleX NSE/nl.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX NSE/nl.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX NSE/pl.lproj/Localizable.strings b/apps/ios/SimpleX NSE/pl.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX NSE/pl.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX NSE/ru.lproj/Localizable.strings b/apps/ios/SimpleX NSE/ru.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX NSE/ru.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX NSE/th.lproj/Localizable.strings b/apps/ios/SimpleX NSE/th.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX NSE/th.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX NSE/tr.lproj/Localizable.strings b/apps/ios/SimpleX NSE/tr.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX NSE/tr.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX NSE/uk.lproj/Localizable.strings b/apps/ios/SimpleX NSE/uk.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX NSE/uk.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX NSE/zh-Hans.lproj/Localizable.strings b/apps/ios/SimpleX NSE/zh-Hans.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX NSE/zh-Hans.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/ShareModel.swift b/apps/ios/SimpleX SE/ShareModel.swift index 35d26dea35..0297aa6557 100644 --- a/apps/ios/SimpleX SE/ShareModel.swift +++ b/apps/ios/SimpleX SE/ShareModel.swift @@ -19,14 +19,16 @@ private let MAX_DOWNSAMPLE_SIZE: Int64 = 2000 class ShareModel: ObservableObject { @Published var sharedContent: SharedContent? - @Published var chats = Array() - @Published var profileImages = Dictionary() - @Published var search = String() - @Published var comment = String() + @Published var chats: [ChatData] = [] + @Published var profileImages: [ChatInfo.ID: UIImage] = [:] + @Published var search = "" + @Published var comment = "" @Published var selected: ChatData? @Published var isLoaded = false @Published var bottomBar: BottomBar = .loadingSpinner @Published var errorAlert: ErrorAlert? + @Published var hasSimplexLink = false + @Published var alertRequiresPassword = false enum BottomBar { case sendButton @@ -48,9 +50,22 @@ class ShareModel: ObservableObject { private var itemProvider: NSItemProvider? - var isSendDisbled: Bool { sharedContent == nil || selected == nil } + var isSendDisbled: Bool { sharedContent == nil || selected == nil || isProhibited(selected) } + + var isLinkPreview: Bool { + switch sharedContent { + case .url: true + default: false + } + } - var filteredChats: Array { + func isProhibited(_ chat: ChatData?) -> Bool { + if let chat, let sharedContent { + sharedContent.prohibited(in: chat, hasSimplexLink: hasSimplexLink) + } else { false } + } + + var filteredChats: [ChatData] { search.isEmpty ? filterChatsToForwardTo(chats: chats) : filterChatsToForwardTo(chats: chats) @@ -58,6 +73,10 @@ class ShareModel: ObservableObject { } func setup(context: NSExtensionContext) { + if performLAGroupDefault.get() && !allowShareExtensionGroupDefault.get() { + errorAlert = ErrorAlert(title: "App is locked!", message: "You can allow sharing in Privacy & Security / SimpleX Lock settings.") + return + } if let item = context.inputItems.first as? NSExtensionItem, let itemProvider = item.attachments?.first { self.itemProvider = itemProvider @@ -67,43 +86,47 @@ class ShareModel: ObservableObject { apiSuspendChat(expired: $0) } } - // Init Chat - Task { - if let e = initChat() { - await MainActor.run { errorAlert = e } - } else { - // Load Chats - Task { - switch fetchChats() { - case let .success(chats): - // 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) { - dict[chatData.id] = uiImage - } + setup() + } + } + + func setup(with dbKey: String? = nil) { + // Init Chat + Task { + if let e = initChat(with: dbKey) { + await MainActor.run { errorAlert = e } + } else { + // Load Chats + Task { + switch fetchChats() { + case let .success(chats): + // 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) { + dict[chatData.id] = uiImage } - await MainActor.run { - self.chats = chats - self.profileImages = profileImages - withAnimation { isLoaded = true } - } - case let .failure(error): - await MainActor.run { errorAlert = error } } + await MainActor.run { + self.chats = chats + self.profileImages = profileImages + withAnimation { isLoaded = true } + } + case let .failure(error): + await MainActor.run { errorAlert = error } } - // Process Attachment - Task { - switch await self.itemProvider!.sharedContent() { - case let .success(chatItemContent): - await MainActor.run { - self.sharedContent = chatItemContent - self.bottomBar = .sendButton - if case let .text(string) = chatItemContent { comment = string } - } - case let .failure(errorAlert): - await MainActor.run { self.errorAlert = errorAlert } + } + // Process Attachment + Task { + switch await getSharedContent(self.itemProvider!) { + case let .success(chatItemContent): + await MainActor.run { + self.sharedContent = chatItemContent + self.bottomBar = .sendButton + if case let .text(string) = chatItemContent { comment = string } } + case let .failure(errorAlert): + await MainActor.run { self.errorAlert = errorAlert } } } } @@ -145,14 +168,15 @@ class ShareModel: ObservableObject { } } - private func initChat() -> ErrorAlert? { + private func initChat(with dbKey: String? = nil) -> ErrorAlert? { do { - if hasChatCtrl() { + if hasChatCtrl() && dbKey == nil { try apiActivateChat() } else { + resetChatCtrl() // Clears retained migration result registerGroupDefaults() haskell_init_se() - let (_, result) = chatMigrateInit(confirmMigrations: defaultMigrationConfirmation()) + let (_, result) = chatMigrateInit(dbKey, confirmMigrations: defaultMigrationConfirmation()) if let e = migrationError(result) { return e } try apiSetAppFilePaths( filesFolder: getAppFilesDirectory().path, @@ -171,8 +195,9 @@ class ShareModel: ObservableObject { private func migrationError(_ r: DBMigrationResult) -> ErrorAlert? { let useKeychain = storeDBPassphraseGroupDefault.get() let storedDBKey = kcDatabasePassword.get() - // This switch duplicates DatabaseErrorView. - // TODO allow entering passphrase and make messages the same as in DatabaseErrorView. + if case .errorNotADatabase = r { + Task { await MainActor.run { self.alertRequiresPassword = true } } + } return switch r { case .errorNotADatabase: if useKeychain && storedDBKey != nil && storedDBKey != "" { @@ -182,8 +207,8 @@ class ShareModel: ObservableObject { ) } else { ErrorAlert( - title: "Encrypted database", - message: "Sharing is not supported when passphrase is not stored in KeyChain." + title: "Database encrypted!", + message: "Database passphrase is required to open chat." ) } case let .errorMigration(_, migrationError): @@ -363,100 +388,104 @@ enum SharedContent { switch self { case let .image(preview, _): .image(text: comment, image: preview) case let .movie(preview, duration, _): .video(text: comment, image: preview, duration: duration) - case let .url(preview): .link(text: comment, preview: preview) + case let .url(preview): .link(text: preview.uri.absoluteString + (comment == "" ? "" : "\n" + comment), preview: preview) case .text: .text(comment) case .data: .file(comment) } } + + func prohibited(in chatData: ChatData, hasSimplexLink: Bool) -> Bool { + chatData.prohibitedByPref( + hasSimplexLink: hasSimplexLink, + isMediaOrFileAttachment: cryptoFile != nil, + isVoice: false + ) + } } -extension NSItemProvider { - fileprivate func sharedContent() async -> Result { - if let type = firstMatching(of: [.image, .movie, .fileURL, .url, .text]) { - switch type { - // Prepare Image message - case .image: - - // Animated - return if hasItemConformingToTypeIdentifier(UTType.gif.identifier) { - if let url = try? await inPlaceUrl(type: type), - let data = try? Data(contentsOf: url), - let image = UIImage(data: data), - let cryptoFile = saveFile(data, generateNewFileName("IMG", "gif"), encrypted: privacyEncryptLocalFilesGroupDefault.get()), - let preview = resizeImageToStrSize(image, maxDataSize: MAX_DATA_SIZE) { - .success(.image(preview: preview, cryptoFile: cryptoFile)) - } else { .failure(ErrorAlert("Error preparing message")) } - - // Static - } else { - if let url = try? await inPlaceUrl(type: type), - let image = downsampleImage(at: url, to: MAX_DOWNSAMPLE_SIZE), - let cryptoFile = saveImage(image), - let preview = resizeImageToStrSize(image, maxDataSize: MAX_DATA_SIZE) { - .success(.image(preview: preview, cryptoFile: cryptoFile)) - } else { .failure(ErrorAlert("Error preparing message")) } - } - - // Prepare Movie message - case .movie: +fileprivate func getSharedContent(_ ip: NSItemProvider) async -> Result { + if let type = firstMatching(of: [.image, .movie, .fileURL, .url, .text]) { + switch type { + // Prepare Image message + case .image: + // Animated + return if ip.hasItemConformingToTypeIdentifier(UTType.gif.identifier) { if let url = try? await inPlaceUrl(type: type), - let trancodedUrl = await transcodeVideo(from: url), - let (image, duration) = AVAsset(url: trancodedUrl).generatePreview(), - let preview = resizeImageToStrSize(image, maxDataSize: MAX_DATA_SIZE), - let cryptoFile = moveTempFileFromURL(trancodedUrl) { - try? FileManager.default.removeItem(at: trancodedUrl) - return .success(.movie(preview: preview, duration: duration, cryptoFile: cryptoFile)) - } else { return .failure(ErrorAlert("Error preparing message")) } - - // Prepare Data message - case .fileURL: - if let url = try? await inPlaceUrl(type: .data) { - if isFileTooLarge(for: url) { - let sizeString = ByteCountFormatter.string( - fromByteCount: Int64(getMaxFileSize(.xftp)), - countStyle: .binary - ) - return .failure( - ErrorAlert( - title: "Large file!", - message: "Currently maximum supported file size is \(sizeString)." - ) - ) - } - if let file = saveFileFromURL(url) { - return .success(.data(cryptoFile: file)) - } - } - return .failure(ErrorAlert("Error preparing file")) - - // Prepare Link message - case .url: - if let url = try? await loadItem(forTypeIdentifier: type.identifier) as? URL { - let content: SharedContent = -// Option to disable previews needs to be taken into account -// if let linkPreview = await getLinkPreview(for: url) { -// .url(preview: linkPreview) -// } else { - .text(string: url.absoluteString) -// } - return .success(content) - } else { return .failure(ErrorAlert("Error preparing message")) } - - // Prepare Text message - case .text: - return if let text = try? await loadItem(forTypeIdentifier: type.identifier) as? String { - .success(.text(string: text)) + let data = try? Data(contentsOf: url), + let image = UIImage(data: data), + let cryptoFile = saveFile(data, generateNewFileName("IMG", "gif"), encrypted: privacyEncryptLocalFilesGroupDefault.get()), + let preview = resizeImageToStrSize(image, maxDataSize: MAX_DATA_SIZE) { + .success(.image(preview: preview, cryptoFile: cryptoFile)) + } else { .failure(ErrorAlert("Error preparing message")) } + + // Static + } else { + if let image = await staticImage(), + let cryptoFile = saveImage(image), + let preview = resizeImageToStrSize(image, maxDataSize: MAX_DATA_SIZE) { + .success(.image(preview: preview, cryptoFile: cryptoFile)) } else { .failure(ErrorAlert("Error preparing message")) } - default: return .failure(ErrorAlert("Unsupported format")) } - } else { - return .failure(ErrorAlert("Unsupported format")) + + // Prepare Movie message + case .movie: + if let url = try? await inPlaceUrl(type: type), + let trancodedUrl = await transcodeVideo(from: url), + let (image, duration) = AVAsset(url: trancodedUrl).generatePreview(), + let preview = resizeImageToStrSize(image, maxDataSize: MAX_DATA_SIZE), + let cryptoFile = moveTempFileFromURL(trancodedUrl) { + try? FileManager.default.removeItem(at: trancodedUrl) + return .success(.movie(preview: preview, duration: duration, cryptoFile: cryptoFile)) + } else { return .failure(ErrorAlert("Error preparing message")) } + + // Prepare Data message + case .fileURL: + if let url = try? await inPlaceUrl(type: .data) { + if isFileTooLarge(for: url) { + let sizeString = ByteCountFormatter.string( + fromByteCount: Int64(getMaxFileSize(.xftp)), + countStyle: .binary + ) + return .failure( + ErrorAlert( + title: "Large file!", + message: "Currently maximum supported file size is \(sizeString)." + ) + ) + } + if let file = saveFileFromURL(url) { + return .success(.data(cryptoFile: file)) + } + } + return .failure(ErrorAlert("Error preparing file")) + + // Prepare Link message + case .url: + if let url = try? await ip.loadItem(forTypeIdentifier: type.identifier) as? URL { + let content: SharedContent = + if privacyLinkPreviewsGroupDefault.get(), let linkPreview = await getLinkPreview(for: url) { + .url(preview: linkPreview) + } else { + .text(string: url.absoluteString) + } + return .success(content) + } else { return .failure(ErrorAlert("Error preparing message")) } + + // Prepare Text message + case .text: + return if let text = try? await ip.loadItem(forTypeIdentifier: type.identifier) as? String { + .success(.text(string: text)) + } else { .failure(ErrorAlert("Error preparing message")) } + default: return .failure(ErrorAlert("Unsupported format")) } + } else { + return .failure(ErrorAlert("Unsupported format")) } - private func inPlaceUrl(type: UTType) async throws -> URL { + + func inPlaceUrl(type: UTType) async throws -> URL { try await withCheckedThrowingContinuation { cont in - let _ = loadInPlaceFileRepresentation(forTypeIdentifier: type.identifier) { url, bool, error in + let _ = ip.loadInPlaceFileRepresentation(forTypeIdentifier: type.identifier) { url, bool, error in if let url = url { cont.resume(returning: url) } else if let error = error { @@ -468,12 +497,23 @@ extension NSItemProvider { } } - private func firstMatching(of types: Array) -> UTType? { + func firstMatching(of types: Array) -> UTType? { for type in types { - if hasItemConformingToTypeIdentifier(type.identifier) { return type } + if ip.hasItemConformingToTypeIdentifier(type.identifier) { return type } } return nil } + + func staticImage() async -> UIImage? { + if let url = try? await inPlaceUrl(type: .image), + let downsampledImage = downsampleImage(at: url, to: MAX_DOWNSAMPLE_SIZE) { + downsampledImage + } else { + /// Fallback to loading image directly from `ItemProvider` + /// in case loading from disk is not possible. Required for sharing screenshots. + try? await ip.loadItem(forTypeIdentifier: UTType.image.identifier) as? UIImage + } + } } diff --git a/apps/ios/SimpleX SE/ShareView.swift b/apps/ios/SimpleX SE/ShareView.swift index 20e6450b99..dcdeae2b82 100644 --- a/apps/ios/SimpleX SE/ShareView.swift +++ b/apps/ios/SimpleX SE/ShareView.swift @@ -12,24 +12,39 @@ import SimpleXChat struct ShareView: View { @ObservedObject var model: ShareModel @Environment(\.colorScheme) var colorScheme + @State private var password = String() + @AppStorage(GROUP_DEFAULT_PROFILE_IMAGE_CORNER_RADIUS, store: groupDefaults) private var radius = defaultProfileImageCorner var body: some View { NavigationView { ZStack(alignment: .bottom) { if model.isLoaded { List(model.filteredChats) { chat in + let isProhibited = model.isProhibited(chat) + let isSelected = model.selected == chat HStack { profileImage( chatInfoId: chat.chatInfo.id, - systemFallback: chatIconName(chat.chatInfo), + iconName: chatIconName(chat.chatInfo), size: 30 ) - Text(chat.chatInfo.displayName) + Text(chat.chatInfo.displayName).foregroundStyle( + isProhibited ? .secondary : .primary + ) Spacer() - radioButton(selected: chat == model.selected) + radioButton(selected: isSelected && !isProhibited) } .contentShape(Rectangle()) - .onTapGesture { model.selected = model.selected == chat ? nil : chat } + .onTapGesture { + if isProhibited { + model.errorAlert = ErrorAlert( + title: "Cannot forward message", + message: "Selected chat preferences prohibit this message." + ) { Button("Ok", role: .cancel) { } } + } else { + model.selected = isSelected ? nil : chat + } + } .tag(chat) } } else { @@ -53,7 +68,19 @@ struct ShareView: View { placement: .navigationBarDrawer(displayMode: .always) ) .alert($model.errorAlert) { alert in - Button("Ok") { model.completion() } + if model.alertRequiresPassword { + SecureField("Passphrase", text: $password) + Button("Ok") { + model.setup(with: password) + password = String() + } + Button("Cancel", role: .cancel) { model.completion() } + } else { + Button("Ok") { model.completion() } + } + } + .onChange(of: model.comment) { + model.hasSimplexLink = hasSimplexLink($0) } } @@ -66,7 +93,7 @@ struct ShareView: View { HStack { Group { if #available(iOSApplicationExtension 16.0, *) { - TextField("Comment", text: $model.comment, axis: .vertical) + TextField("Comment", text: $model.comment, axis: .vertical).lineLimit(6) } else { TextField("Comment", text: $model.comment) } @@ -159,17 +186,17 @@ struct ShareView: View { .background(Material.ultraThin) } - private func profileImage(chatInfoId: ChatInfo.ID, systemFallback: String, size: Double) -> some View { - Group { - if let uiImage = model.profileImages[chatInfoId] { - Image(uiImage: uiImage).resizable() - } else { - Image(systemName: systemFallback).resizable() - } + @ViewBuilder private func profileImage(chatInfoId: ChatInfo.ID, iconName: String, size: Double) -> some View { + if let uiImage = model.profileImages[chatInfoId] { + clipProfileImage(Image(uiImage: uiImage), size: size, radius: radius) + } else { + Image(systemName: iconName) + .resizable() + .foregroundColor(Color(uiColor: .tertiaryLabel)) + .frame(width: size, height: size) +// add background when adding themes to SE +// .background(Circle().fill(backgroundColor != nil ? backgroundColor! : .clear)) } - .foregroundStyle(Color(.tertiaryLabel)) - .frame(width: size, height: size) - .clipShape(RoundedRectangle(cornerRadius: size * 0.225, style: .continuous)) } private func radioButton(selected: Bool) -> some View { diff --git a/apps/ios/SimpleX SE/bg.lproj/InfoPlist.strings b/apps/ios/SimpleX SE/bg.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX SE/bg.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/bg.lproj/Localizable.strings b/apps/ios/SimpleX SE/bg.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX SE/bg.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/cs.lproj/InfoPlist.strings b/apps/ios/SimpleX SE/cs.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX SE/cs.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/cs.lproj/Localizable.strings b/apps/ios/SimpleX SE/cs.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX SE/cs.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/de.lproj/InfoPlist.strings b/apps/ios/SimpleX SE/de.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX SE/de.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/de.lproj/Localizable.strings b/apps/ios/SimpleX SE/de.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX SE/de.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX SE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX SE/en.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/en.lproj/Localizable.strings b/apps/ios/SimpleX SE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX SE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/es.lproj/InfoPlist.strings b/apps/ios/SimpleX SE/es.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX SE/es.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/es.lproj/Localizable.strings b/apps/ios/SimpleX SE/es.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX SE/es.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/fi.lproj/InfoPlist.strings b/apps/ios/SimpleX SE/fi.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX SE/fi.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/fi.lproj/Localizable.strings b/apps/ios/SimpleX SE/fi.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX SE/fi.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/fr.lproj/InfoPlist.strings b/apps/ios/SimpleX SE/fr.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX SE/fr.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/fr.lproj/Localizable.strings b/apps/ios/SimpleX SE/fr.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX SE/fr.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/hu.lproj/InfoPlist.strings b/apps/ios/SimpleX SE/hu.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX SE/hu.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/hu.lproj/Localizable.strings b/apps/ios/SimpleX SE/hu.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX SE/hu.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/it.lproj/InfoPlist.strings b/apps/ios/SimpleX SE/it.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX SE/it.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/it.lproj/Localizable.strings b/apps/ios/SimpleX SE/it.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX SE/it.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/ja.lproj/InfoPlist.strings b/apps/ios/SimpleX SE/ja.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX SE/ja.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/ja.lproj/Localizable.strings b/apps/ios/SimpleX SE/ja.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX SE/ja.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/nl.lproj/InfoPlist.strings b/apps/ios/SimpleX SE/nl.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX SE/nl.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/nl.lproj/Localizable.strings b/apps/ios/SimpleX SE/nl.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX SE/nl.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/pl.lproj/InfoPlist.strings b/apps/ios/SimpleX SE/pl.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX SE/pl.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/pl.lproj/Localizable.strings b/apps/ios/SimpleX SE/pl.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX SE/pl.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/ru.lproj/InfoPlist.strings b/apps/ios/SimpleX SE/ru.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX SE/ru.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/ru.lproj/Localizable.strings b/apps/ios/SimpleX SE/ru.lproj/Localizable.strings new file mode 100644 index 0000000000..30a26c9920 --- /dev/null +++ b/apps/ios/SimpleX SE/ru.lproj/Localizable.strings @@ -0,0 +1,3 @@ +/* No comment provided by engineer. */ +"Share" = "Поделиться"; + diff --git a/apps/ios/SimpleX SE/th.lproj/InfoPlist.strings b/apps/ios/SimpleX SE/th.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX SE/th.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/th.lproj/Localizable.strings b/apps/ios/SimpleX SE/th.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX SE/th.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/tr.lproj/InfoPlist.strings b/apps/ios/SimpleX SE/tr.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX SE/tr.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/tr.lproj/Localizable.strings b/apps/ios/SimpleX SE/tr.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX SE/tr.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/uk.lproj/InfoPlist.strings b/apps/ios/SimpleX SE/uk.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX SE/uk.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/uk.lproj/Localizable.strings b/apps/ios/SimpleX SE/uk.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX SE/uk.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/zh-Hans.lproj/InfoPlist.strings b/apps/ios/SimpleX SE/zh-Hans.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX SE/zh-Hans.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + 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 new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX SE/zh-Hans.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + 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 58eaf0a7ae..1acd6a57dc 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -198,6 +198,7 @@ 8C9BC2652C240D5200875A27 /* ThemeModeEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C9BC2642C240D5100875A27 /* ThemeModeEditor.swift */; }; 8CC4ED902BD7B8530078AEE8 /* CallAudioDeviceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CC4ED8F2BD7B8530078AEE8 /* CallAudioDeviceManager.swift */; }; 8CC956EE2BC0041000412A11 /* NetworkObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CC956ED2BC0041000412A11 /* NetworkObserver.swift */; }; + 8CE848A32C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CE848A22C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift */; }; 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 */; }; @@ -217,12 +218,15 @@ D77B92DC2952372200A5A1CC /* SwiftyGif in Frameworks */ = {isa = PBXBuildFile; productRef = D77B92DB2952372200A5A1CC /* SwiftyGif */; }; D7F0E33929964E7E0068AF69 /* LZString in Frameworks */ = {isa = PBXBuildFile; productRef = D7F0E33829964E7E0068AF69 /* LZString */; }; E50581062C3DDD9D009C3F71 /* Yams in Frameworks */ = {isa = PBXBuildFile; productRef = E50581052C3DDD9D009C3F71 /* Yams */; }; - E5DCF8D52C56F7EF007928CC /* libHSsimplex-chat-6.0.0.2-B4oiZFZeYN0AY2321yyqdp-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5DCF8D02C56F7EF007928CC /* libHSsimplex-chat-6.0.0.2-B4oiZFZeYN0AY2321yyqdp-ghc9.6.3.a */; }; - E5DCF8D62C56F7EF007928CC /* libHSsimplex-chat-6.0.0.2-B4oiZFZeYN0AY2321yyqdp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5DCF8D12C56F7EF007928CC /* libHSsimplex-chat-6.0.0.2-B4oiZFZeYN0AY2321yyqdp.a */; }; - E5DCF8D72C56F7EF007928CC /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5DCF8D22C56F7EF007928CC /* libffi.a */; }; - E5DCF8D82C56F7EF007928CC /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5DCF8D32C56F7EF007928CC /* libgmp.a */; }; - E5DCF8D92C56F7EF007928CC /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5DCF8D42C56F7EF007928CC /* libgmpxx.a */; }; E5DCF8DB2C56FAC1007928CC /* SimpleXChat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; }; + E5DCF8E12C5847EB007928CC /* libHSsimplex-chat-6.0.0.3-5yF58NJ20MV4vZ7jQKFAxB.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5DCF8DC2C5847EB007928CC /* libHSsimplex-chat-6.0.0.3-5yF58NJ20MV4vZ7jQKFAxB.a */; }; + E5DCF8E22C5847EB007928CC /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5DCF8DD2C5847EB007928CC /* libgmp.a */; }; + E5DCF8E32C5847EB007928CC /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5DCF8DE2C5847EB007928CC /* libgmpxx.a */; }; + E5DCF8E42C5847EB007928CC /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5DCF8DF2C5847EB007928CC /* libffi.a */; }; + E5DCF8E52C5847EB007928CC /* libHSsimplex-chat-6.0.0.3-5yF58NJ20MV4vZ7jQKFAxB-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5DCF8E02C5847EB007928CC /* libHSsimplex-chat-6.0.0.3-5yF58NJ20MV4vZ7jQKFAxB-ghc9.6.3.a */; }; + E5DCF9712C590272007928CC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E5DCF96F2C590272007928CC /* Localizable.strings */; }; + E5DCF9842C5902CE007928CC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E5DCF9822C5902CE007928CC /* Localizable.strings */; }; + E5DCF9982C5906FF007928CC /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = E5DCF9962C5906FF007928CC /* InfoPlist.strings */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -536,6 +540,7 @@ 8C9BC2642C240D5100875A27 /* ThemeModeEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeModeEditor.swift; sourceTree = ""; }; 8CC4ED8F2BD7B8530078AEE8 /* CallAudioDeviceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallAudioDeviceManager.swift; sourceTree = ""; }; 8CC956ED2BC0041000412A11 /* NetworkObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkObserver.swift; sourceTree = ""; }; + 8CE848A22C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableChatItemToolbars.swift; sourceTree = ""; }; 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 = ""; }; @@ -552,11 +557,63 @@ D741547729AF89AF0022400A /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/StoreKit.framework; sourceTree = DEVELOPER_DIR; }; D741547929AF90B00022400A /* PushKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PushKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/PushKit.framework; sourceTree = DEVELOPER_DIR; }; D7AA2C3429A936B400737B40 /* MediaEncryption.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; name = MediaEncryption.playground; path = Shared/MediaEncryption.playground; sourceTree = SOURCE_ROOT; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; - E5DCF8D02C56F7EF007928CC /* libHSsimplex-chat-6.0.0.2-B4oiZFZeYN0AY2321yyqdp-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.0.0.2-B4oiZFZeYN0AY2321yyqdp-ghc9.6.3.a"; sourceTree = ""; }; - E5DCF8D12C56F7EF007928CC /* libHSsimplex-chat-6.0.0.2-B4oiZFZeYN0AY2321yyqdp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.0.0.2-B4oiZFZeYN0AY2321yyqdp.a"; sourceTree = ""; }; - E5DCF8D22C56F7EF007928CC /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - E5DCF8D32C56F7EF007928CC /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; - E5DCF8D42C56F7EF007928CC /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; + E5DCF8DC2C5847EB007928CC /* libHSsimplex-chat-6.0.0.3-5yF58NJ20MV4vZ7jQKFAxB.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.0.0.3-5yF58NJ20MV4vZ7jQKFAxB.a"; sourceTree = ""; }; + E5DCF8DD2C5847EB007928CC /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; + E5DCF8DE2C5847EB007928CC /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; + E5DCF8DF2C5847EB007928CC /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; + E5DCF8E02C5847EB007928CC /* libHSsimplex-chat-6.0.0.3-5yF58NJ20MV4vZ7jQKFAxB-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.0.0.3-5yF58NJ20MV4vZ7jQKFAxB-ghc9.6.3.a"; sourceTree = ""; }; + E5DCF9702C590272007928CC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9722C590274007928CC /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9732C590275007928CC /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; + E5DCF9742C590276007928CC /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9752C590277007928CC /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9762C590278007928CC /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9772C590279007928CC /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9782C590279007928CC /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9792C59027A007928CC /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF97A2C59027A007928CC /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF97B2C59027B007928CC /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF97C2C59027B007928CC /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF97D2C59027C007928CC /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF97E2C59027C007928CC /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF97F2C59027D007928CC /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = th; path = th.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9802C59027D007928CC /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9812C59027D007928CC /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9832C5902CE007928CC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9852C5902D4007928CC /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9862C5902D5007928CC /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; + E5DCF9872C5902D8007928CC /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9882C5902DC007928CC /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9892C5902DC007928CC /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF98A2C5902DD007928CC /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF98B2C5902DD007928CC /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF98C2C5902DE007928CC /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF98D2C5902DE007928CC /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF98E2C5902E0007928CC /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF98F2C5902E0007928CC /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9902C5902E1007928CC /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9912C5902E1007928CC /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9922C5902E2007928CC /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = th; path = th.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9932C5902E2007928CC /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9942C5902E3007928CC /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9952C59067B007928CC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; + E5DCF9972C5906FF007928CC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; + E5DCF9992C59072A007928CC /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/InfoPlist.strings; sourceTree = ""; }; + E5DCF99A2C59072B007928CC /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/InfoPlist.strings"; sourceTree = ""; }; + E5DCF99B2C59072B007928CC /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/InfoPlist.strings; sourceTree = ""; }; + E5DCF99C2C59072C007928CC /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/InfoPlist.strings; sourceTree = ""; }; + E5DCF99D2C59072D007928CC /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/InfoPlist.strings; sourceTree = ""; }; + E5DCF99E2C59072E007928CC /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/InfoPlist.strings; sourceTree = ""; }; + E5DCF99F2C59072E007928CC /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/InfoPlist.strings; sourceTree = ""; }; + E5DCF9A02C59072F007928CC /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/InfoPlist.strings; sourceTree = ""; }; + E5DCF9A12C59072F007928CC /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/InfoPlist.strings; sourceTree = ""; }; + E5DCF9A22C590730007928CC /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/InfoPlist.strings; sourceTree = ""; }; + E5DCF9A32C590730007928CC /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/InfoPlist.strings; sourceTree = ""; }; + E5DCF9A42C590731007928CC /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/InfoPlist.strings; sourceTree = ""; }; + E5DCF9A52C590731007928CC /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/InfoPlist.strings; sourceTree = ""; }; + E5DCF9A62C590731007928CC /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = th; path = th.lproj/InfoPlist.strings; sourceTree = ""; }; + E5DCF9A72C590732007928CC /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/InfoPlist.strings; sourceTree = ""; }; + E5DCF9A82C590732007928CC /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/InfoPlist.strings; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -597,13 +654,13 @@ files = ( 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, - E5DCF8D82C56F7EF007928CC /* libgmp.a in Frameworks */, E50581062C3DDD9D009C3F71 /* Yams in Frameworks */, - E5DCF8D62C56F7EF007928CC /* libHSsimplex-chat-6.0.0.2-B4oiZFZeYN0AY2321yyqdp.a in Frameworks */, - E5DCF8D72C56F7EF007928CC /* libffi.a in Frameworks */, - E5DCF8D92C56F7EF007928CC /* libgmpxx.a in Frameworks */, - E5DCF8D52C56F7EF007928CC /* libHSsimplex-chat-6.0.0.2-B4oiZFZeYN0AY2321yyqdp-ghc9.6.3.a in Frameworks */, + E5DCF8E52C5847EB007928CC /* libHSsimplex-chat-6.0.0.3-5yF58NJ20MV4vZ7jQKFAxB-ghc9.6.3.a in Frameworks */, + E5DCF8E42C5847EB007928CC /* libffi.a in Frameworks */, + E5DCF8E12C5847EB007928CC /* libHSsimplex-chat-6.0.0.3-5yF58NJ20MV4vZ7jQKFAxB.a in Frameworks */, + E5DCF8E22C5847EB007928CC /* libgmp.a in Frameworks */, CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */, + E5DCF8E32C5847EB007928CC /* libgmpxx.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -671,6 +728,7 @@ 5CBE6C132944CC12002D9531 /* ScanCodeView.swift */, 64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */, 648679AA2BC96A74006456E7 /* ChatItemForwardingView.swift */, + 8CE848A22C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift */, ); path = Chat; sourceTree = ""; @@ -678,11 +736,11 @@ 5C764E5C279C70B7000C6508 /* Libraries */ = { isa = PBXGroup; children = ( - E5DCF8D22C56F7EF007928CC /* libffi.a */, - E5DCF8D32C56F7EF007928CC /* libgmp.a */, - E5DCF8D42C56F7EF007928CC /* libgmpxx.a */, - E5DCF8D02C56F7EF007928CC /* libHSsimplex-chat-6.0.0.2-B4oiZFZeYN0AY2321yyqdp-ghc9.6.3.a */, - E5DCF8D12C56F7EF007928CC /* libHSsimplex-chat-6.0.0.2-B4oiZFZeYN0AY2321yyqdp.a */, + E5DCF8DF2C5847EB007928CC /* libffi.a */, + E5DCF8DD2C5847EB007928CC /* libgmp.a */, + E5DCF8DE2C5847EB007928CC /* libgmpxx.a */, + E5DCF8E02C5847EB007928CC /* libHSsimplex-chat-6.0.0.3-5yF58NJ20MV4vZ7jQKFAxB-ghc9.6.3.a */, + E5DCF8DC2C5847EB007928CC /* libHSsimplex-chat-6.0.0.3-5yF58NJ20MV4vZ7jQKFAxB.a */, ); path = Libraries; sourceTree = ""; @@ -895,6 +953,7 @@ 5CDCAD472818589900503DA2 /* NotificationService.swift */, 5CDCAD492818589900503DA2 /* Info.plist */, 5CB0BA862826CB3A00B3292C /* InfoPlist.strings */, + E5DCF9822C5902CE007928CC /* Localizable.strings */, ); path = "SimpleX NSE"; sourceTree = ""; @@ -1037,6 +1096,8 @@ CEE723F12C3D25ED0009AE93 /* ShareModel.swift */, CEE723EF2C3D25C70009AE93 /* ShareView.swift */, CEE723A92C3BD3D70009AE93 /* ShareViewController.swift */, + E5DCF96F2C590272007928CC /* Localizable.strings */, + E5DCF9962C5906FF007928CC /* InfoPlist.strings */, ); path = "SimpleX SE"; sourceTree = ""; @@ -1263,6 +1324,7 @@ buildActionMask = 2147483647; files = ( 5CB0BA882826CB3A00B3292C /* InfoPlist.strings in Resources */, + E5DCF9842C5902CE007928CC /* Localizable.strings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1277,6 +1339,8 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + E5DCF9712C590272007928CC /* Localizable.strings in Resources */, + E5DCF9982C5906FF007928CC /* InfoPlist.strings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1375,6 +1439,7 @@ 5CF937232B2503D000E1D781 /* NSESubscriber.swift in Sources */, 6419EC582AB97507004A607A /* CIMemberCreatedContactView.swift in Sources */, 64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */, + 8CE848A32C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift in Sources */, 64466DCC29FFE3E800E3D48D /* MailView.swift in Sources */, 5C971E2127AEBF8300C8A3CE /* ChatInfoImage.swift in Sources */, 5C55A921283CCCB700C4E99E /* IncomingCallView.swift in Sources */, @@ -1554,6 +1619,7 @@ 5C5B67932ABAF56000DA9412 /* bg */, 5C245F3E2B501F13001CC39F /* tr */, 5C371E502BA9AB6400100AD3 /* hu */, + E5DCF9952C59067B007928CC /* en */, ); name = InfoPlist.strings; sourceTree = ""; @@ -1605,6 +1671,78 @@ name = "SimpleX--iOS--InfoPlist.strings"; sourceTree = ""; }; + E5DCF96F2C590272007928CC /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + E5DCF9702C590272007928CC /* en */, + E5DCF9722C590274007928CC /* bg */, + E5DCF9732C590275007928CC /* zh-Hans */, + E5DCF9742C590276007928CC /* nl */, + E5DCF9752C590277007928CC /* cs */, + E5DCF9762C590278007928CC /* fi */, + E5DCF9772C590279007928CC /* fr */, + E5DCF9782C590279007928CC /* de */, + E5DCF9792C59027A007928CC /* hu */, + E5DCF97A2C59027A007928CC /* it */, + E5DCF97B2C59027B007928CC /* ja */, + E5DCF97C2C59027B007928CC /* pl */, + E5DCF97D2C59027C007928CC /* ru */, + E5DCF97E2C59027C007928CC /* es */, + E5DCF97F2C59027D007928CC /* th */, + E5DCF9802C59027D007928CC /* uk */, + E5DCF9812C59027D007928CC /* tr */, + ); + name = Localizable.strings; + sourceTree = ""; + }; + E5DCF9822C5902CE007928CC /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + E5DCF9832C5902CE007928CC /* en */, + E5DCF9852C5902D4007928CC /* bg */, + E5DCF9862C5902D5007928CC /* zh-Hans */, + E5DCF9872C5902D8007928CC /* cs */, + E5DCF9882C5902DC007928CC /* nl */, + E5DCF9892C5902DC007928CC /* fi */, + E5DCF98A2C5902DD007928CC /* de */, + E5DCF98B2C5902DD007928CC /* fr */, + E5DCF98C2C5902DE007928CC /* it */, + E5DCF98D2C5902DE007928CC /* hu */, + E5DCF98E2C5902E0007928CC /* ja */, + E5DCF98F2C5902E0007928CC /* pl */, + E5DCF9902C5902E1007928CC /* ru */, + E5DCF9912C5902E1007928CC /* es */, + E5DCF9922C5902E2007928CC /* th */, + E5DCF9932C5902E2007928CC /* tr */, + E5DCF9942C5902E3007928CC /* uk */, + ); + name = Localizable.strings; + sourceTree = ""; + }; + E5DCF9962C5906FF007928CC /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + E5DCF9972C5906FF007928CC /* en */, + E5DCF9992C59072A007928CC /* bg */, + E5DCF99A2C59072B007928CC /* zh-Hans */, + E5DCF99B2C59072B007928CC /* cs */, + E5DCF99C2C59072C007928CC /* nl */, + E5DCF99D2C59072D007928CC /* fi */, + E5DCF99E2C59072E007928CC /* fr */, + E5DCF99F2C59072E007928CC /* de */, + E5DCF9A02C59072F007928CC /* hu */, + E5DCF9A12C59072F007928CC /* it */, + E5DCF9A22C590730007928CC /* ja */, + E5DCF9A32C590730007928CC /* pl */, + E5DCF9A42C590731007928CC /* ru */, + E5DCF9A52C590731007928CC /* es */, + E5DCF9A62C590731007928CC /* th */, + E5DCF9A72C590732007928CC /* tr */, + E5DCF9A82C590732007928CC /* uk */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ @@ -1738,7 +1876,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 228; + CURRENT_PROJECT_VERSION = 229; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -1787,7 +1925,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 228; + CURRENT_PROJECT_VERSION = 229; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -1873,7 +2011,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 228; + CURRENT_PROJECT_VERSION = 229; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GCC_OPTIMIZATION_LEVEL = s; @@ -1910,7 +2048,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 228; + CURRENT_PROJECT_VERSION = 229; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_CODE_COVERAGE = NO; @@ -1947,7 +2085,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 228; + CURRENT_PROJECT_VERSION = 229; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -1998,7 +2136,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 228; + CURRENT_PROJECT_VERSION = 229; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 4a45892ee5..17b4c2d6ad 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -46,8 +46,8 @@ public enum ChatCommand { 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 apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, msg: MsgContent, live: Bool) - case apiDeleteChatItem(type: ChatType, id: Int64, itemId: Int64, mode: CIDeleteMode) - case apiDeleteMemberChatItem(groupId: Int64, groupMemberId: Int64, itemId: Int64) + 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 apiGetNtfToken @@ -199,8 +199,8 @@ public enum ChatCommand { let msg = encodeJSON(ComposedMessage(fileSource: file, msgContent: mc)) return "/_create *\(noteFolderId) json \(msg)" 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, itemId, mode): return "/_delete item \(ref(type, id)) \(itemId) \(mode.rawValue)" - case let .apiDeleteMemberChatItem(groupId, groupMemberId, itemId): return "/_delete member item #\(groupId) \(groupMemberId) \(itemId)" + 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): let ttlStr = ttl != nil ? "\(ttl!)" : "default" @@ -598,7 +598,7 @@ public enum ChatResponse: Decodable, Error { case chatItemUpdated(user: UserRef, chatItem: AChatItem) case chatItemNotChanged(user: UserRef, chatItem: AChatItem) case chatItemReaction(user: UserRef, added: Bool, reaction: ACIReaction) - case chatItemDeleted(user: UserRef, deletedChatItem: AChatItem, toChatItem: AChatItem?, byUser: Bool) + case chatItemsDeleted(user: UserRef, chatItemDeletions: [ChatItemDeletion], byUser: Bool) case contactsList(user: UserRef, contacts: [Contact]) // group events case groupCreated(user: UserRef, groupInfo: GroupInfo) @@ -689,6 +689,7 @@ public enum ChatResponse: Decodable, Error { case agentSubsSummary(user: UserRef, subsSummary: SMPServerSubs) case chatCmdError(user_: UserRef?, chatError: ChatError) case chatError(user_: UserRef?, chatError: ChatError) + case archiveExported(archiveErrors: [ArchiveError]) case archiveImported(archiveErrors: [ArchiveError]) case appSettings(appSettings: AppSettings) @@ -767,7 +768,7 @@ public enum ChatResponse: Decodable, Error { case .chatItemUpdated: return "chatItemUpdated" case .chatItemNotChanged: return "chatItemNotChanged" case .chatItemReaction: return "chatItemReaction" - case .chatItemDeleted: return "chatItemDeleted" + case .chatItemsDeleted: return "chatItemsDeleted" case .contactsList: return "contactsList" case .groupCreated: return "groupCreated" case .sentGroupInvitation: return "sentGroupInvitation" @@ -851,6 +852,7 @@ public enum ChatResponse: Decodable, Error { case .agentSubsSummary: return "agentSubsSummary" case .chatCmdError: return "chatCmdError" case .chatError: return "chatError" + case .archiveExported: return "archiveExported" case .archiveImported: return "archiveImported" case .appSettings: return "appSettings" } @@ -934,7 +936,10 @@ public enum ChatResponse: Decodable, Error { case let .chatItemUpdated(u, chatItem): return withUser(u, String(describing: chatItem)) case let .chatItemNotChanged(u, chatItem): return withUser(u, String(describing: chatItem)) case let .chatItemReaction(u, added, reaction): return withUser(u, "added: \(added)\n\(String(describing: reaction))") - case let .chatItemDeleted(u, deletedChatItem, toChatItem, byUser): return withUser(u, "deletedChatItem:\n\(String(describing: deletedChatItem))\ntoChatItem:\n\(String(describing: toChatItem))\nbyUser: \(byUser)") + case let .chatItemsDeleted(u, items, byUser): + let itemsString = items.map { item in + "deletedChatItem:\n\(String(describing: item.deletedChatItem))\ntoChatItem:\n\(String(describing: item.toChatItem))" }.joined(separator: "\n") + return withUser(u, itemsString + "\nbyUser: \(byUser)") case let .contactsList(u, contacts): return withUser(u, String(describing: contacts)) case let .groupCreated(u, groupInfo): return withUser(u, String(describing: groupInfo)) case let .sentGroupInvitation(u, groupInfo, contact, member): return withUser(u, "groupInfo: \(groupInfo)\ncontact: \(contact)\nmember: \(member)") @@ -1018,6 +1023,7 @@ public enum ChatResponse: Decodable, Error { case let .agentSubsSummary(u, subsSummary): return withUser(u, String(describing: subsSummary)) case let .chatCmdError(u, chatError): return withUser(u, String(describing: chatError)) case let .chatError(u, chatError): return withUser(u, String(describing: chatError)) + case let .archiveExported(archiveErrors): return String(describing: archiveErrors) case let .archiveImported(archiveErrors): return String(describing: archiveErrors) case let .appSettings(appSettings): return String(describing: appSettings) } @@ -2033,8 +2039,8 @@ public enum SMPAgentError: Decodable, Hashable { } public enum ArchiveError: Decodable, Hashable { - case `import`(chatError: ChatError) - case importFile(file: String, chatError: ChatError) + case `import`(importError: String) + case fileError(file: String, fileError: String) } public enum RemoteCtrlError: Decodable, Hashable { @@ -2112,7 +2118,7 @@ public struct AppSettings: Codable, Equatable, Hashable { public var androidCallOnLockScreen: AppSettingsLockScreenCalls? = nil public var iosCallKitEnabled: Bool? = nil public var iosCallKitCallsInRecents: Bool? = nil - public var uiProfileImageCornerRadius: Float? = nil + public var uiProfileImageCornerRadius: Double? = nil public var uiColorScheme: String? = nil public var uiDarkColorScheme: String? = nil public var uiCurrentThemeIds: [String: String]? = nil diff --git a/apps/ios/SimpleXChat/AppGroup.swift b/apps/ios/SimpleXChat/AppGroup.swift index 6f9ad3b68e..1690c7bc73 100644 --- a/apps/ios/SimpleXChat/AppGroup.swift +++ b/apps/ios/SimpleXChat/AppGroup.swift @@ -11,6 +11,8 @@ import SwiftUI public let appSuspendTimeout: Int = 15 // seconds +public let defaultProfileImageCorner: Double = 22.5 + let GROUP_DEFAULT_APP_STATE = "appState" let GROUP_DEFAULT_NSE_STATE = "nseState" let GROUP_DEFAULT_SE_STATE = "seState" @@ -20,11 +22,18 @@ public let GROUP_DEFAULT_CHAT_LAST_BACKGROUND_RUN = "chatLastBackgroundRun" let GROUP_DEFAULT_NTF_PREVIEW_MODE = "ntfPreviewMode" public let GROUP_DEFAULT_NTF_ENABLE_LOCAL = "ntfEnableLocal" // no longer used public let GROUP_DEFAULT_NTF_ENABLE_PERIODIC = "ntfEnablePeriodic" // no longer used +// replaces DEFAULT_PERFORM_LA +let GROUP_DEFAULT_PERFORM_LA = "performLocalAuthentication" +public let GROUP_DEFAULT_ALLOW_SHARE_EXTENSION = "allowShareExtension" +// replaces DEFAULT_PRIVACY_LINK_PREVIEWS +let GROUP_DEFAULT_PRIVACY_LINK_PREVIEWS = "privacyLinkPreviews" // This setting is a main one, while having an unused duplicate from the past: DEFAULT_PRIVACY_ACCEPT_IMAGES let GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES = "privacyAcceptImages" public let GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE = "privacyTransferImagesInline" // no longer used public let GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES = "privacyEncryptLocalFiles" public let GROUP_DEFAULT_PRIVACY_ASK_TO_APPROVE_RELAYS = "privacyAskToApproveRelays" +// replaces DEFAULT_PROFILE_IMAGE_CORNER_RADIUS +public let GROUP_DEFAULT_PROFILE_IMAGE_CORNER_RADIUS = "profileImageCornerRadius" let GROUP_DEFAULT_NTF_BADGE_COUNT = "ntgBadgeCount" let GROUP_DEFAULT_NETWORK_USE_ONION_HOSTS = "networkUseOnionHosts" let GROUP_DEFAULT_NETWORK_SESSION_MODE = "networkSessionMode" @@ -72,10 +81,14 @@ public func registerGroupDefaults() { GROUP_DEFAULT_INCOGNITO: false, GROUP_DEFAULT_STORE_DB_PASSPHRASE: true, GROUP_DEFAULT_INITIAL_RANDOM_DB_PASSPHRASE: false, + GROUP_DEFAULT_PERFORM_LA: true, + GROUP_DEFAULT_ALLOW_SHARE_EXTENSION: false, + GROUP_DEFAULT_PRIVACY_LINK_PREVIEWS: false, GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES: true, GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE: false, GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES: true, GROUP_DEFAULT_PRIVACY_ASK_TO_APPROVE_RELAYS: true, + GROUP_DEFAULT_PROFILE_IMAGE_CORNER_RADIUS: defaultProfileImageCorner, GROUP_DEFAULT_CONFIRM_DB_UPGRADES: false, GROUP_DEFAULT_CALL_KIT_ENABLED: true, GROUP_DEFAULT_PQ_EXPERIMENTAL_ENABLED: false, @@ -190,6 +203,12 @@ public let ntfPreviewModeGroupDefault = EnumDefault( public let incognitoGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_INCOGNITO) +public let performLAGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PERFORM_LA) + +public let allowShareExtensionGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_ALLOW_SHARE_EXTENSION) + +public let privacyLinkPreviewsGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_LINK_PREVIEWS) + // This setting is a main one, while having an unused duplicate from the past: DEFAULT_PRIVACY_ACCEPT_IMAGES public let privacyAcceptImagesGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES) @@ -197,6 +216,8 @@ public let privacyEncryptLocalFilesGroupDefault = BoolDefault(defaults: groupDef public let privacyAskToApproveRelaysGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_ASK_TO_APPROVE_RELAYS) +public let profileImageCornerRadiusGroupDefault = Default(defaults: groupDefaults, forKey: GROUP_DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) + public let ntfBadgeCountGroupDefault = IntDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_NTF_BADGE_COUNT) public let networkUseOnionHostsGroupDefault = EnumDefault( diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 54e8a80332..f9eb0ed39e 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -2234,6 +2234,11 @@ public struct NtfMsgInfo: Decodable, Hashable { public var msgTs: Date } +public struct ChatItemDeletion: Decodable, Hashable { + public var deletedChatItem: AChatItem + public var toChatItem: AChatItem? = nil +} + public struct AChatItem: Decodable, Hashable { public var chatInfo: ChatInfo public var chatItem: ChatItem @@ -2449,13 +2454,16 @@ public struct ChatItem: Identifiable, Decodable, Hashable { } } - public func memberToModerate(_ chatInfo: ChatInfo) -> (GroupInfo, GroupMember)? { + public func memberToModerate(_ chatInfo: ChatInfo) -> (GroupInfo, GroupMember?)? { switch (chatInfo, chatDir) { case let (.group(groupInfo), .groupRcv(groupMember)): let m = groupInfo.membership return m.memberRole >= .admin && m.memberRole >= groupMember.memberRole && meta.itemDeleted == nil ? (groupInfo, groupMember) : nil + case let (.group(groupInfo), .groupSnd): + let m = groupInfo.membership + return m.memberRole >= .admin ? (groupInfo, nil) : nil default: return nil } } @@ -2470,6 +2478,10 @@ public struct ChatItem: Identifiable, Decodable, Hashable { } } + public var canBeDeletedForSelf: Bool { + (content.msgContent != nil && !meta.isLive) || meta.itemDeleted != nil || isDeletedContent || mergeCategory != nil || showLocalDelete + } + public static func getSample (_ id: Int64, _ dir: CIDirection, _ ts: Date, _ text: String, _ status: CIStatus = .sndNew, quotedItem: CIQuote? = nil, file: CIFile? = nil, itemDeleted: CIDeleted? = nil, itemEdited: Bool = false, itemLive: Bool = false, deletable: Bool = true, editable: Bool = true) -> ChatItem { ChatItem( chatDir: dir, diff --git a/apps/ios/SimpleXChat/ChatUtils.swift b/apps/ios/SimpleXChat/ChatUtils.swift index a37b6babf7..5f56180918 100644 --- a/apps/ios/SimpleXChat/ChatUtils.swift +++ b/apps/ios/SimpleXChat/ChatUtils.swift @@ -14,6 +14,45 @@ public protocol ChatLike { var chatStats: ChatStats { get } } +extension ChatLike { + public func groupFeatureEnabled(_ feature: GroupFeature) -> Bool { + if case let .group(groupInfo) = self.chatInfo { + let p = groupInfo.fullGroupPreferences + return switch feature { + case .timedMessages: p.timedMessages.on + case .directMessages: p.directMessages.on(for: groupInfo.membership) + case .fullDelete: p.fullDelete.on + case .reactions: p.reactions.on + case .voice: p.voice.on(for: groupInfo.membership) + case .files: p.files.on(for: groupInfo.membership) + case .simplexLinks: p.simplexLinks.on(for: groupInfo.membership) + case .history: p.history.on + } + } else { + return true + } + } + + public func prohibitedByPref( + hasSimplexLink: Bool, + isMediaOrFileAttachment: Bool, + isVoice: Bool + ) -> Bool { + // preference checks should match checks in compose view + let simplexLinkProhibited = hasSimplexLink && !groupFeatureEnabled(.simplexLinks) + let fileProhibited = isMediaOrFileAttachment && !groupFeatureEnabled(.files) + let voiceProhibited = isVoice && !chatInfo.featureEnabled(.voice) + return switch chatInfo { + case .direct: voiceProhibited + case .group: simplexLinkProhibited || fileProhibited || voiceProhibited + case .local: false + case .contactRequest: false + case .contactConnection: false + case .invalidJSON: false + } + } +} + public func filterChatsToForwardTo(chats: [C]) -> [C] { var filteredChats = chats.filter { c in c.chatInfo.chatType != .local && canForwardToChat(c.chatInfo) @@ -60,3 +99,15 @@ public func chatIconName(_ cInfo: ChatInfo) -> String { default: "circle.fill" } } + +public func hasSimplexLink(_ text: String?) -> Bool { + if let text, let parsedMsg = parseSimpleXMarkdown(text) { + parsedMsgHasSimplexLink(parsedMsg) + } else { + false + } +} + +public func parsedMsgHasSimplexLink(_ parsedMsg: [FormattedText]) -> Bool { + parsedMsg.contains(where: { ft in ft.format?.isSimplexLink ?? false }) +} diff --git a/apps/ios/SimpleXChat/ErrorAlert.swift b/apps/ios/SimpleXChat/ErrorAlert.swift index 65ed4c6717..5b9acc4fca 100644 --- a/apps/ios/SimpleXChat/ErrorAlert.swift +++ b/apps/ios/SimpleXChat/ErrorAlert.swift @@ -72,25 +72,25 @@ extension View { _ errorAlert: Binding, @ViewBuilder actions: (ErrorAlert) -> A = { _ in EmptyView() } ) -> some View { - if let alert = errorAlert.wrappedValue { - self.alert( - alert.title, - isPresented: Binding( - get: { errorAlert.wrappedValue != nil }, - set: { if !$0 { errorAlert.wrappedValue = nil } } - ), - actions: { - if let actions_ = alert.actions { - actions_() - } else { - actions(alert) - } - }, - message: { - if let message = alert.message { Text(message) } + alert( + errorAlert.wrappedValue?.title ?? "", + isPresented: Binding( + get: { errorAlert.wrappedValue != nil }, + set: { if !$0 { errorAlert.wrappedValue = nil } } + ), + actions: { + if let actions_ = errorAlert.wrappedValue?.actions { + actions_() + } else { + if let alert = errorAlert.wrappedValue { actions(alert) } } - ) - } else { self } + }, + message: { + if let message = errorAlert.wrappedValue?.message { + Text(message) + } + } + ) } } diff --git a/apps/ios/SimpleXChat/ImageUtils.swift b/apps/ios/SimpleXChat/ImageUtils.swift index c387c84aaa..7a62d863e6 100644 --- a/apps/ios/SimpleXChat/ImageUtils.swift +++ b/apps/ios/SimpleXChat/ImageUtils.swift @@ -428,3 +428,22 @@ public func getLinkPreview(for url: URL) async -> LinkPreview? { getLinkPreview(url: url) { cont.resume(returning: $0) } } } + +private let squareToCircleRatio = 0.935 + +private let radiusFactor = (1 - squareToCircleRatio) / 50 + +@ViewBuilder public func clipProfileImage(_ img: Image, size: CGFloat, radius: Double) -> some View { + let v = img.resizable() + if radius >= 50 { + v.frame(width: size, height: size).clipShape(Circle()) + } else if radius <= 0 { + let sz = size * squareToCircleRatio + v.frame(width: sz, height: sz).padding((size - sz) / 2) + } else { + let sz = size * (squareToCircleRatio + radius * radiusFactor) + v.frame(width: sz, height: sz) + .clipShape(RoundedRectangle(cornerRadius: sz * radius / 100, style: .continuous)) + .padding((size - sz) / 2) + } +} diff --git a/apps/ios/bg.lproj/Localizable.strings b/apps/ios/bg.lproj/Localizable.strings index 142baa5cbe..1e7b7a6314 100644 --- a/apps/ios/bg.lproj/Localizable.strings +++ b/apps/ios/bg.lproj/Localizable.strings @@ -641,7 +641,7 @@ /* rcv group event chat item */ "blocked %@" = "блокиран %@"; -/* marked deleted chat item preview text */ +/* blocked chat item */ "blocked by admin" = "блокиран от админ"; /* No comment provided by engineer. */ @@ -2691,7 +2691,7 @@ time to disappear */ "off" = "изключено"; -/* No comment provided by engineer. */ +/* blur media */ "Off" = "Изключено"; /* feature offered item */ diff --git a/apps/ios/cs.lproj/Localizable.strings b/apps/ios/cs.lproj/Localizable.strings index 1a83061c8c..c7f916408f 100644 --- a/apps/ios/cs.lproj/Localizable.strings +++ b/apps/ios/cs.lproj/Localizable.strings @@ -2190,7 +2190,7 @@ time to disappear */ "off" = "vypnuto"; -/* No comment provided by engineer. */ +/* blur media */ "Off" = "Vypnout"; /* feature offered item */ diff --git a/apps/ios/de.lproj/Localizable.strings b/apps/ios/de.lproj/Localizable.strings index d46f3243d4..b410f63f4c 100644 --- a/apps/ios/de.lproj/Localizable.strings +++ b/apps/ios/de.lproj/Localizable.strings @@ -359,6 +359,9 @@ /* No comment provided by engineer. */ "Acknowledgement errors" = "Fehler bei der Bestätigung"; +/* No comment provided by engineer. */ +"Active connections" = "Aktive Verbindungen"; + /* No comment provided by engineer. */ "Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." = "Fügen Sie die Adresse Ihrem Profil hinzu, damit Ihre Kontakte sie mit anderen Personen teilen können. Es wird eine Profilaktualisierung an Ihre Kontakte gesendet."; @@ -426,13 +429,13 @@ "All app data is deleted." = "Werden die App-Daten komplett gelöscht."; /* No comment provided by engineer. */ -"All chats and messages will be deleted - this cannot be undone!" = "Alle Chats und Nachrichten werden gelöscht. Dies kann nicht rückgängig gemacht werden!"; +"All chats and messages will be deleted - this cannot be undone!" = "Es werden alle Chats und Nachrichten gelöscht. Dies kann nicht rückgängig gemacht werden!"; /* No comment provided by engineer. */ "All data is erased when it is entered." = "Alle Daten werden gelöscht, sobald dieser eingegeben wird."; /* No comment provided by engineer. */ -"All data is private to your device." = "Alle Daten sind auf Ihrem Gerät geschützt."; +"All data is private to your device." = "Alle Daten werden nur auf Ihrem Gerät gespeichert."; /* No comment provided by engineer. */ "All group members will remain connected." = "Alle Gruppenmitglieder bleiben verbunden."; @@ -444,7 +447,7 @@ "All messages will be deleted - this cannot be undone!" = "Es werden alle Nachrichten gelöscht. Dies kann nicht rückgängig gemacht werden!"; /* No comment provided by engineer. */ -"All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." = "Alle Nachrichten werden gelöscht . Dies kann nicht rückgängig gemacht werden! Die Nachrichten werden NUR bei Ihnen gelöscht."; +"All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." = "Es werden alle Nachrichten gelöscht. Dies kann nicht rückgängig gemacht werden! Die Nachrichten werden NUR bei Ihnen gelöscht."; /* No comment provided by engineer. */ "All new messages from %@ will be hidden!" = "Von %@ werden alle neuen Nachrichten ausgeblendet!"; @@ -686,7 +689,7 @@ /* rcv group event chat item */ "blocked %@" = "%@ wurde blockiert"; -/* marked deleted chat item preview text */ +/* blocked chat item */ "blocked by admin" = "wurde vom Administrator blockiert"; /* No comment provided by engineer. */ @@ -909,6 +912,9 @@ /* No comment provided by engineer. */ "Configure ICE servers" = "ICE-Server konfigurieren"; +/* No comment provided by engineer. */ +"Configured %@ servers" = "Konfigurierte %@ Server"; + /* No comment provided by engineer. */ "Confirm" = "Bestätigen"; @@ -2354,10 +2360,10 @@ "Incoming video call" = "Eingehender Videoanruf"; /* No comment provided by engineer. */ -"Incompatible database version" = "Inkompatible Datenbank-Version"; +"Incompatible database version" = "Datenbank-Version nicht kompatibel"; /* No comment provided by engineer. */ -"Incompatible version" = "Inkompatible Version"; +"Incompatible version" = "Version nicht kompatibel"; /* PIN entry */ "Incorrect passcode" = "Zugangscode ist falsch"; @@ -2689,6 +2695,9 @@ /* notification */ "message received" = "Nachricht empfangen"; +/* No comment provided by engineer. */ +"Message reception" = "Nachrichtenempfang"; + /* No comment provided by engineer. */ "Message routing fallback" = "Fallback für das Nachrichten-Routing"; @@ -2940,7 +2949,7 @@ time to disappear */ "off" = "Aus"; -/* No comment provided by engineer. */ +/* blur media */ "Off" = "Aus"; /* feature offered item */ @@ -3066,6 +3075,9 @@ /* No comment provided by engineer. */ "Other" = "Andere"; +/* No comment provided by engineer. */ +"Other %@ servers" = "Andere %@ Server"; + /* No comment provided by engineer. */ "other errors" = "Andere Fehler"; @@ -3219,6 +3231,9 @@ /* No comment provided by engineer. */ "Private routing" = "Privates Routing"; +/* No comment provided by engineer. */ +"Private routing error" = "Fehler beim privaten Routing"; + /* No comment provided by engineer. */ "Profile and server connections" = "Profil und Serververbindungen"; @@ -3289,7 +3304,7 @@ "Protocol timeout per KB" = "Protokollzeitüberschreitung pro kB"; /* No comment provided by engineer. */ -"Proxied" = "Proxy"; +"Proxied" = "Proxied"; /* No comment provided by engineer. */ "Proxied servers" = "Proxy-Server"; @@ -3819,8 +3834,11 @@ /* No comment provided by engineer. */ "Server address" = "Server-Adresse"; +/* No comment provided by engineer. */ +"Server address is incompatible with network settings: %@." = "Die Server-Adresse ist nicht mit den Netzwerkeinstellungen kompatibel: %@."; + /* srv error text. */ -"Server address is incompatible with network settings." = "Die Server-Adresse ist nicht mit den Netzwerk-Einstellungen kompatibel."; +"Server address is incompatible with network settings." = "Die Server-Adresse ist nicht mit den Netzwerkeinstellungen kompatibel."; /* queue info */ "server queue info: %@\n\nlast received msg: %@" = "Server-Warteschlangen-Information: %1$@\n\nZuletzt empfangene Nachricht: %2$@"; @@ -3838,7 +3856,10 @@ "Server type" = "Server-Typ"; /* srv error text */ -"Server version is incompatible with network settings." = "Die Server-Version ist nicht mit den Netzwerk-Einstellungen kompatibel."; +"Server version is incompatible with network settings." = "Die Server-Version ist nicht mit den Netzwerkeinstellungen kompatibel."; + +/* No comment provided by engineer. */ +"Server version is incompatible with your app: %@." = "Die Server-Version ist nicht mit Ihrer App kompatibel: %@."; /* No comment provided by engineer. */ "Servers" = "Server"; @@ -3930,6 +3951,9 @@ /* No comment provided by engineer. */ "Show message status" = "Nachrichtenstatus anzeigen"; +/* No comment provided by engineer. */ +"Show percentage" = "Prozentualen Anteil anzeigen"; + /* No comment provided by engineer. */ "Show preview" = "Vorschau anzeigen"; @@ -4228,10 +4252,10 @@ "They can be overridden in contact and group settings." = "Sie können in den Kontakteinstellungen überschrieben werden."; /* No comment provided by engineer. */ -"This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." = "Diese Aktion kann nicht rückgängig gemacht werden! Alle empfangenen und gesendeten Dateien und Medien werden gelöscht. Bilder mit niedriger Auflösung bleiben erhalten."; +"This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." = "Diese Aktion kann nicht rückgängig gemacht werden! Es werden alle empfangenen und gesendeten Dateien und Medien gelöscht. Bilder mit niedriger Auflösung bleiben erhalten."; /* No comment provided by engineer. */ -"This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes." = "Diese Aktion kann nicht rückgängig gemacht werden! Alle empfangenen und gesendeten Nachrichten, die über den ausgewählten Zeitraum hinaus gehen, werden gelöscht. Dieser Vorgang kann mehrere Minuten dauern."; +"This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes." = "Diese Aktion kann nicht rückgängig gemacht werden! Es werden alle empfangenen und gesendeten Nachrichten, die über den ausgewählten Zeitraum hinaus gehen, gelöscht. Dieser Vorgang kann mehrere Minuten dauern."; /* No comment provided by engineer. */ "This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." = "Diese Aktion kann nicht rückgängig gemacht werden! Ihr Profil und Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren."; diff --git a/apps/ios/es.lproj/Localizable.strings b/apps/ios/es.lproj/Localizable.strings index 1a8835c976..a84f8c0433 100644 --- a/apps/ios/es.lproj/Localizable.strings +++ b/apps/ios/es.lproj/Localizable.strings @@ -334,6 +334,9 @@ /* No comment provided by engineer. */ "above, then choose:" = "y después elige:"; +/* No comment provided by engineer. */ +"Accent" = "Color"; + /* accept contact request via notification accept incoming call via notification */ "Accept" = "Aceptar"; @@ -350,6 +353,15 @@ /* call status */ "accepted call" = "llamada aceptada"; +/* No comment provided by engineer. */ +"Acknowledged" = "Recibido"; + +/* No comment provided by engineer. */ +"Acknowledgement errors" = "Errores de reconocimiento"; + +/* No comment provided by engineer. */ +"Active connections" = "Conexiones activas"; + /* No comment provided by engineer. */ "Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." = "Añade la dirección a tu perfil para que tus contactos puedan compartirla con otros. La actualización del perfil se enviará a tus contactos."; @@ -374,6 +386,15 @@ /* No comment provided by engineer. */ "Add welcome message" = "Añadir mensaje de bienvenida"; +/* No comment provided by engineer. */ +"Additional accent" = "Acento adicional"; + +/* No comment provided by engineer. */ +"Additional accent 2" = "Color adicional 2"; + +/* No comment provided by engineer. */ +"Additional secondary" = "Secundario adicional"; + /* No comment provided by engineer. */ "Address" = "Dirección"; @@ -395,6 +416,9 @@ /* No comment provided by engineer. */ "Advanced network settings" = "Configuración avanzada de red"; +/* No comment provided by engineer. */ +"Advanced settings" = "Configuración avanzada"; + /* chat item text */ "agreeing encryption for %@…" = "acordando cifrado para %@…"; @@ -410,6 +434,9 @@ /* No comment provided by engineer. */ "All data is erased when it is entered." = "Al introducirlo todos los datos son eliminados."; +/* No comment provided by engineer. */ +"All data is private to your device." = "Todos los datos son privados para tu dispositivo."; + /* No comment provided by engineer. */ "All group members will remain connected." = "Todos los miembros del grupo permanecerán conectados."; @@ -425,6 +452,9 @@ /* No comment provided by engineer. */ "All new messages from %@ will be hidden!" = "¡Los mensajes nuevos de %@ estarán ocultos!"; +/* No comment provided by engineer. */ +"All profiles" = "Todos los perfiles"; + /* No comment provided by engineer. */ "All your contacts will remain connected." = "Todos tus contactos permanecerán conectados."; @@ -551,6 +581,9 @@ /* No comment provided by engineer. */ "Apply" = "Aplicar"; +/* No comment provided by engineer. */ +"Apply to" = "Aplicar a"; + /* No comment provided by engineer. */ "Archive and upload" = "Archivar y subir"; @@ -560,6 +593,9 @@ /* No comment provided by engineer. */ "Attach" = "Adjuntar"; +/* No comment provided by engineer. */ +"attempts" = "intentos"; + /* No comment provided by engineer. */ "Audio & video calls" = "Llamadas y videollamadas"; @@ -602,6 +638,9 @@ /* No comment provided by engineer. */ "Back" = "Volver"; +/* No comment provided by engineer. */ +"Background" = "Fondo"; + /* No comment provided by engineer. */ "Bad desktop address" = "Dirección ordenador incorrecta"; @@ -623,6 +662,9 @@ /* No comment provided by engineer. */ "Better messages" = "Mensajes mejorados"; +/* No comment provided by engineer. */ +"Black" = "Negro"; + /* No comment provided by engineer. */ "Block" = "Bloquear"; @@ -647,7 +689,7 @@ /* rcv group event chat item */ "blocked %@" = "ha bloqueado a %@"; -/* marked deleted chat item preview text */ +/* blocked chat item */ "blocked by admin" = "bloqueado por administrador"; /* No comment provided by engineer. */ @@ -713,6 +755,9 @@ /* No comment provided by engineer. */ "Cannot access keychain to save database password" = "Keychain inaccesible para guardar la contraseña de la base de datos"; +/* No comment provided by engineer. */ +"Cannot forward message" = "No se puede reenviar el mensaje"; + /* No comment provided by engineer. */ "Cannot receive file" = "No se puede recibir el archivo"; @@ -771,6 +816,9 @@ /* No comment provided by engineer. */ "Chat archive" = "Archivo del chat"; +/* No comment provided by engineer. */ +"Chat colors" = "Colores del chat"; + /* No comment provided by engineer. */ "Chat console" = "Consola de Chat"; @@ -798,6 +846,9 @@ /* No comment provided by engineer. */ "Chat preferences" = "Preferencias de Chat"; +/* No comment provided by engineer. */ +"Chat theme" = "Tema de chat"; + /* No comment provided by engineer. */ "Chats" = "Chats"; @@ -816,6 +867,15 @@ /* No comment provided by engineer. */ "Choose from library" = "Elige de la biblioteca"; +/* No comment provided by engineer. */ +"Chunks deleted" = "Bloques eliminados"; + +/* No comment provided by engineer. */ +"Chunks downloaded" = "Bloques descargados"; + +/* No comment provided by engineer. */ +"Chunks uploaded" = "Bloques subidos"; + /* No comment provided by engineer. */ "Clear" = "Vaciar"; @@ -831,6 +891,9 @@ /* No comment provided by engineer. */ "Clear verification" = "Eliminar verificación"; +/* No comment provided by engineer. */ +"Color mode" = "Modo de color"; + /* No comment provided by engineer. */ "colored" = "coloreado"; @@ -843,9 +906,15 @@ /* No comment provided by engineer. */ "complete" = "completado"; +/* No comment provided by engineer. */ +"Completed" = "Completado"; + /* No comment provided by engineer. */ "Configure ICE servers" = "Configure servidores ICE"; +/* No comment provided by engineer. */ +"Configured %@ servers" = "%@ servidores configurados"; + /* No comment provided by engineer. */ "Confirm" = "Confirmar"; @@ -912,18 +981,27 @@ /* No comment provided by engineer. */ "connected" = "conectado"; +/* No comment provided by engineer. */ +"Connected" = "Conectado"; + /* No comment provided by engineer. */ "Connected desktop" = "Ordenador conectado"; /* rcv group event chat item */ "connected directly" = "conectado directamente"; +/* No comment provided by engineer. */ +"Connected servers" = "Servidores conectados"; + /* No comment provided by engineer. */ "Connected to desktop" = "Conectado con ordenador"; /* No comment provided by engineer. */ "connecting" = "conectando"; +/* No comment provided by engineer. */ +"Connecting" = "Conectando"; + /* No comment provided by engineer. */ "connecting (accepted)" = "conectando (aceptado)"; @@ -972,9 +1050,15 @@ /* No comment provided by engineer. */ "Connection timeout" = "Tiempo de conexión agotado"; +/* No comment provided by engineer. */ +"Connection with desktop stopped" = "La conexión con el escritorio (desktop) se ha parado"; + /* connection information */ "connection:%@" = "conexión: % @"; +/* No comment provided by engineer. */ +"Connections" = "Conexiones"; + /* profile update event chat item */ "contact %@ changed to %@" = "el contacto %1$@ ha cambiado a %2$@"; @@ -1017,6 +1101,9 @@ /* No comment provided by engineer. */ "Copy" = "Copiar"; +/* No comment provided by engineer. */ +"Copy error" = "Copiar error"; + /* No comment provided by engineer. */ "Core version: v%@" = "Versión Core: v%@"; @@ -1062,6 +1149,9 @@ /* No comment provided by engineer. */ "Create your profile" = "Crea tu perfil"; +/* No comment provided by engineer. */ +"Created" = "Creado"; + /* No comment provided by engineer. */ "Created at" = "Creado"; @@ -1086,6 +1176,9 @@ /* No comment provided by engineer. */ "Current passphrase…" = "Contraseña actual…"; +/* No comment provided by engineer. */ +"Current profile" = "Perfil actual"; + /* No comment provided by engineer. */ "Currently maximum supported file size is %@." = "El tamaño máximo de archivo admitido es %@."; @@ -1095,9 +1188,15 @@ /* No comment provided by engineer. */ "Custom time" = "Tiempo personalizado"; +/* No comment provided by engineer. */ +"Customize theme" = "Personalizar tema"; + /* No comment provided by engineer. */ "Dark" = "Oscuro"; +/* No comment provided by engineer. */ +"Dark mode colors" = "Colores en modo oscuro"; + /* No comment provided by engineer. */ "Database downgrade" = "Degradación de la base de datos"; @@ -1167,6 +1266,9 @@ /* message decrypt error item */ "Decryption error" = "Error descifrado"; +/* No comment provided by engineer. */ +"decryption errors" = "errores de descifrado"; + /* pref value */ "default (%@)" = "predeterminado (%@)"; @@ -1293,6 +1395,9 @@ /* deleted chat item */ "deleted" = "eliminado"; +/* No comment provided by engineer. */ +"Deleted" = "Eliminado"; + /* No comment provided by engineer. */ "Deleted at" = "Eliminado"; @@ -1305,6 +1410,9 @@ /* rcv group event chat item */ "deleted group" = "grupo eliminado"; +/* No comment provided by engineer. */ +"Deletion errors" = "Errores de borrado"; + /* No comment provided by engineer. */ "Delivery" = "Entrega"; @@ -1329,6 +1437,12 @@ /* snd error text */ "Destination server error: %@" = "Error del servidor de destino: %@"; +/* No comment provided by engineer. */ +"Detailed statistics" = "Estadísticas detalladas"; + +/* No comment provided by engineer. */ +"Details" = "Detalles"; + /* No comment provided by engineer. */ "Develop" = "Desarrollo"; @@ -1431,12 +1545,21 @@ /* chat item action */ "Download" = "Descargar"; +/* No comment provided by engineer. */ +"Download errors" = "Errores en la descarga"; + /* No comment provided by engineer. */ "Download failed" = "Descarga fallida"; /* server test step */ "Download file" = "Descargar archivo"; +/* No comment provided by engineer. */ +"Downloaded" = "Descargado"; + +/* No comment provided by engineer. */ +"Downloaded files" = "Archivos descargados"; + /* No comment provided by engineer. */ "Downloading archive" = "Descargando archivo"; @@ -1449,6 +1572,9 @@ /* integrity error chat item */ "duplicate message" = "mensaje duplicado"; +/* No comment provided by engineer. */ +"duplicates" = "duplicados"; + /* No comment provided by engineer. */ "Duration" = "Duración"; @@ -1707,6 +1833,9 @@ /* No comment provided by engineer. */ "Error exporting chat database" = "Error al exportar base de datos"; +/* No comment provided by engineer. */ +"Error exporting theme: %@" = "Error al exportar tema: %@"; + /* No comment provided by engineer. */ "Error importing chat database" = "Error al importar base de datos"; @@ -1722,9 +1851,18 @@ /* No comment provided by engineer. */ "Error receiving file" = "Error al recibir archivo"; +/* No comment provided by engineer. */ +"Error reconnecting server" = "Error al reconectar con el servidor"; + +/* No comment provided by engineer. */ +"Error reconnecting servers" = "Error al reconectar con los servidores"; + /* No comment provided by engineer. */ "Error removing member" = "Error al eliminar miembro"; +/* No comment provided by engineer. */ +"Error resetting statistics" = "Error al restablecer las estadísticas"; + /* No comment provided by engineer. */ "Error saving %@ servers" = "Error al guardar servidores %@"; @@ -1804,6 +1942,9 @@ /* No comment provided by engineer. */ "Error: URL is invalid" = "Error: la URL no es válida"; +/* No comment provided by engineer. */ +"Errors" = "Errores"; + /* No comment provided by engineer. */ "Even when disabled in the conversation." = "Incluso si está desactivado para la conversación."; @@ -1816,12 +1957,18 @@ /* chat item action */ "Expand" = "Expandir"; +/* No comment provided by engineer. */ +"expired" = "expirado"; + /* No comment provided by engineer. */ "Export database" = "Exportar base de datos"; /* No comment provided by engineer. */ "Export error:" = "Error al exportar:"; +/* No comment provided by engineer. */ +"Export theme" = "Exportar tema"; + /* No comment provided by engineer. */ "Exported database archive." = "Archivo de base de datos exportado."; @@ -1843,6 +1990,21 @@ /* No comment provided by engineer. */ "Favorite" = "Favoritos"; +/* No comment provided by engineer. */ +"File error" = "Error de archivo"; + +/* file error text */ +"File not found - most likely file was deleted or cancelled." = "Archivo no encontrado, probablemente haya sido borrado o cancelado."; + +/* file error text */ +"File server error: %@" = "Error del servidor de archivos: %@"; + +/* No comment provided by engineer. */ +"File status" = "Estado del archivo"; + +/* copied message info */ +"File status: %@" = "Estado del archivo: %@"; + /* No comment provided by engineer. */ "File will be deleted from servers." = "El archivo será eliminado de los servidores."; @@ -1957,6 +2119,12 @@ /* No comment provided by engineer. */ "GIFs and stickers" = "GIFs y stickers"; +/* message preview */ +"Good afternoon!" = "¡Buenas tardes!"; + +/* message preview */ +"Good morning!" = "¡Buenos días!"; + /* No comment provided by engineer. */ "Group" = "Grupo"; @@ -2134,6 +2302,9 @@ /* No comment provided by engineer. */ "Import failed" = "Error de importación"; +/* No comment provided by engineer. */ +"Import theme" = "Importar tema"; + /* No comment provided by engineer. */ "Importing archive" = "Importando archivo"; @@ -2155,6 +2326,9 @@ /* No comment provided by engineer. */ "In-call sounds" = "Sonido de llamada"; +/* No comment provided by engineer. */ +"inactive" = "inactivo"; + /* No comment provided by engineer. */ "Incognito" = "Incógnito"; @@ -2218,6 +2392,9 @@ /* No comment provided by engineer. */ "Interface" = "Interfaz"; +/* No comment provided by engineer. */ +"Interface colors" = "Colores del interfaz"; + /* invalid chat data */ "invalid chat" = "chat no válido"; @@ -2470,6 +2647,9 @@ /* rcv group event chat item */ "member connected" = "conectado"; +/* item status text */ +"Member inactive" = "Miembro inactivo"; + /* No comment provided by engineer. */ "Member role will be changed to \"%@\". All group members will be notified." = "El rol del miembro cambiará a \"%@\" y se notificará al grupo."; @@ -2479,6 +2659,9 @@ /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "El miembro será expulsado del grupo. ¡No podrá deshacerse!"; +/* No comment provided by engineer. */ +"Menus" = "Menus"; + /* item status text */ "Message delivery error" = "Error en la entrega del mensaje"; @@ -2491,6 +2674,12 @@ /* No comment provided by engineer. */ "Message draft" = "Borrador de mensaje"; +/* item status text */ +"Message forwarded" = "Mensaje reenviado"; + +/* item status description */ +"Message may be delivered later if member becomes active." = "El mensaje puede ser entregado más tarde si el miembro vuelve a estar activo."; + /* No comment provided by engineer. */ "Message queue info" = "Información cola de mensajes"; @@ -2506,6 +2695,9 @@ /* notification */ "message received" = "mensaje recibido"; +/* No comment provided by engineer. */ +"Message reception" = "Recepción de mensaje"; + /* No comment provided by engineer. */ "Message routing fallback" = "Enrutamiento de mensajes alternativo"; @@ -2515,6 +2707,12 @@ /* No comment provided by engineer. */ "Message source remains private." = "El autor del mensaje se mantiene privado."; +/* No comment provided by engineer. */ +"Message status" = "Estado del mensaje"; + +/* copied message info */ +"Message status: %@" = "Estado del mensaje: %@"; + /* No comment provided by engineer. */ "Message text" = "Contacto y texto"; @@ -2530,6 +2728,12 @@ /* No comment provided by engineer. */ "Messages from %@ will be shown!" = "¡Los mensajes de %@ serán mostrados!"; +/* No comment provided by engineer. */ +"Messages received" = "Mensajes recibidos"; + +/* No comment provided by engineer. */ +"Messages sent" = "Mensajes enviados"; + /* 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." = "Los mensajes, archivos y llamadas están protegidos mediante **cifrado de extremo a extremo** con secreto perfecto hacía adelante, repudio y recuperación tras ataque."; @@ -2695,6 +2899,9 @@ /* No comment provided by engineer. */ "No device token!" = "¡Sin dispositivo token!"; +/* item status description */ +"No direct connection yet, message is forwarded by admin." = "Aún no hay conexión directa, el mensaje es reenviado por el administrador."; + /* No comment provided by engineer. */ "no e2e encryption" = "sin cifrar"; @@ -2707,6 +2914,9 @@ /* No comment provided by engineer. */ "No history" = "Sin historial"; +/* No comment provided by engineer. */ +"No info, try to reload" = "No hay información, intenta recargar"; + /* No comment provided by engineer. */ "No network connection" = "Sin conexión de red"; @@ -2739,7 +2949,7 @@ time to disappear */ "off" = "desactivado"; -/* No comment provided by engineer. */ +/* blur media */ "Off" = "Desactivado"; /* feature offered item */ @@ -2832,6 +3042,9 @@ /* authentication reason */ "Open migration to another device" = "Abrir menú migración a otro dispositivo"; +/* No comment provided by engineer. */ +"Open server settings" = "Abrir configuración del servidor"; + /* No comment provided by engineer. */ "Open Settings" = "Abrir Configuración"; @@ -2856,9 +3069,18 @@ /* No comment provided by engineer. */ "Or show this code" = "O muestra este código QR"; +/* No comment provided by engineer. */ +"other" = "otro"; + /* No comment provided by engineer. */ "Other" = "Otro"; +/* No comment provided by engineer. */ +"Other %@ servers" = "Otros servidores %@"; + +/* No comment provided by engineer. */ +"other errors" = "otros errores"; + /* member role */ "owner" = "propietario"; @@ -2901,6 +3123,9 @@ /* No comment provided by engineer. */ "peer-to-peer" = "p2p"; +/* No comment provided by engineer. */ +"Pending" = "Pendiente"; + /* No comment provided by engineer. */ "People can connect to you only via the links you share." = "Las personas pueden conectarse contigo solo mediante los enlaces que compartes."; @@ -2922,6 +3147,9 @@ /* No comment provided by engineer. */ "Please ask your contact to enable sending voice messages." = "Solicita que tu contacto habilite el envío de mensajes de voz."; +/* 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." = "Comprueba que el móvil y el ordenador están conectados a la misma red local y que el cortafuegos del ordenador permite la conexión.\nPor favor, comparte cualquier otro problema con los desarrolladores."; + /* No comment provided by engineer. */ "Please check that you used the correct link or ask your contact to send you another one." = "Comprueba que has usado el enlace correcto o pide a tu contacto que te envíe otro."; @@ -2979,6 +3207,9 @@ /* No comment provided by engineer. */ "Preview" = "Vista previa"; +/* No comment provided by engineer. */ +"Previously connected servers" = "Servidores conectados previamente"; + /* No comment provided by engineer. */ "Privacy & security" = "Privacidad y Seguridad"; @@ -3000,6 +3231,9 @@ /* No comment provided by engineer. */ "Private routing" = "Enrutamiento privado"; +/* No comment provided by engineer. */ +"Private routing error" = "Error de enrutamiento privado"; + /* No comment provided by engineer. */ "Profile and server connections" = "Datos del perfil y conexiones"; @@ -3018,6 +3252,9 @@ /* No comment provided by engineer. */ "Profile password" = "Contraseña del perfil"; +/* No comment provided by engineer. */ +"Profile theme" = "Tema del perfil"; + /* No comment provided by engineer. */ "Profile update will be sent to your contacts." = "La actualización del perfil se enviará a tus contactos."; @@ -3066,6 +3303,12 @@ /* No comment provided by engineer. */ "Protocol timeout per KB" = "Timeout protocolo por KB"; +/* No comment provided by engineer. */ +"Proxied" = "Con proxy"; + +/* No comment provided by engineer. */ +"Proxied servers" = "Servidores con proxy"; + /* No comment provided by engineer. */ "Push notifications" = "Notificaciones automáticas"; @@ -3108,6 +3351,9 @@ /* No comment provided by engineer. */ "Receipts are disabled" = "Las confirmaciones están desactivadas"; +/* No comment provided by engineer. */ +"Receive errors" = "Errores de recepción"; + /* No comment provided by engineer. */ "received answer…" = "respuesta recibida…"; @@ -3126,6 +3372,15 @@ /* message info title */ "Received message" = "Mensaje entrante"; +/* No comment provided by engineer. */ +"Received messages" = "Mensajes recibidos"; + +/* No comment provided by engineer. */ +"Received reply" = "Respuesta recibida"; + +/* No comment provided by engineer. */ +"Received total" = "Total recibido"; + /* No comment provided by engineer. */ "Receiving address will be changed to a different server. Address change will complete after sender comes online." = "La dirección de recepción pasará a otro servidor. El cambio se completará cuando el remitente esté en línea."; @@ -3144,9 +3399,24 @@ /* No comment provided by engineer. */ "Recipients see updates as you type them." = "Los destinatarios ven actualizarse mientras escribes."; +/* No comment provided by engineer. */ +"Reconnect" = "Reconectar"; + /* No comment provided by engineer. */ "Reconnect all connected servers to force message delivery. It uses additional traffic." = "Reconectar todos los servidores conectados para forzar la entrega del mensaje. Se usa tráfico adicional."; +/* No comment provided by engineer. */ +"Reconnect all servers" = "Reconectar todos los servidores"; + +/* No comment provided by engineer. */ +"Reconnect all servers?" = "¿Reconectar todos los servidores?"; + +/* No comment provided by engineer. */ +"Reconnect server to force message delivery. It uses additional traffic." = "Reconectar el servidor para forzar la entrega de mensajes. Usa tráfico adicional."; + +/* No comment provided by engineer. */ +"Reconnect server?" = "¿Reconectar servidor?"; + /* No comment provided by engineer. */ "Reconnect servers?" = "¿Reconectar servidores?"; @@ -3180,6 +3450,9 @@ /* No comment provided by engineer. */ "Remove" = "Eliminar"; +/* No comment provided by engineer. */ +"Remove image" = "Eliminar imagen"; + /* No comment provided by engineer. */ "Remove member" = "Expulsar miembro"; @@ -3237,12 +3510,24 @@ /* No comment provided by engineer. */ "Reset" = "Restablecer"; +/* No comment provided by engineer. */ +"Reset all statistics" = "Restablecer estadísticas"; + +/* No comment provided by engineer. */ +"Reset all statistics?" = "¿Restablecer todas las estadísticas?"; + /* No comment provided by engineer. */ "Reset colors" = "Restablecer colores"; +/* No comment provided by engineer. */ +"Reset to app theme" = "Restablecer al tema de la aplicación"; + /* No comment provided by engineer. */ "Reset to defaults" = "Restablecer valores predetarminados"; +/* No comment provided by engineer. */ +"Reset to user theme" = "Restablecer al tema del usuario"; + /* No comment provided by engineer. */ "Restart the app to create a new chat profile" = "Reinicia la aplicación para crear un perfil nuevo"; @@ -3357,6 +3642,12 @@ /* No comment provided by engineer. */ "Saved WebRTC ICE servers will be removed" = "Los servidores WebRTC ICE guardados serán eliminados"; +/* No comment provided by engineer. */ +"Scale" = "Escala"; + +/* No comment provided by engineer. */ +"Scan / Paste link" = "Escanear / Pegar enlace"; + /* No comment provided by engineer. */ "Scan code" = "Escanear código"; @@ -3384,6 +3675,9 @@ /* network option */ "sec" = "seg"; +/* No comment provided by engineer. */ +"Secondary" = "Secundario"; + /* time unit */ "seconds" = "segundos"; @@ -3393,6 +3687,9 @@ /* server test step */ "Secure queue" = "Cola segura"; +/* No comment provided by engineer. */ +"Secured" = "Seguro"; + /* No comment provided by engineer. */ "Security assessment" = "Evaluación de la seguridad"; @@ -3405,6 +3702,9 @@ /* No comment provided by engineer. */ "Select" = "Seleccionar"; +/* No comment provided by engineer. */ +"Selected chat preferences prohibit this message." = "Las preferencias seleccionadas no permiten este mensaje."; + /* No comment provided by engineer. */ "Self-destruct" = "Autodestrucción"; @@ -3438,6 +3738,9 @@ /* No comment provided by engineer. */ "Send disappearing message" = "Enviar mensaje temporal"; +/* No comment provided by engineer. */ +"Send errors" = "Errores de envío"; + /* No comment provided by engineer. */ "Send link previews" = "Enviar previsualizacion de enlaces"; @@ -3504,15 +3807,36 @@ /* copied message info */ "Sent at: %@" = "Enviado: %@"; +/* No comment provided by engineer. */ +"Sent directly" = "Enviado directamente"; + /* notification */ "Sent file event" = "Evento de archivo enviado"; /* message info title */ "Sent message" = "Mensaje saliente"; +/* No comment provided by engineer. */ +"Sent messages" = "Mensajes enviados"; + /* No comment provided by engineer. */ "Sent messages will be deleted after set time." = "Los mensajes enviados se eliminarán una vez transcurrido el tiempo establecido."; +/* No comment provided by engineer. */ +"Sent reply" = "Respuesta enviada"; + +/* No comment provided by engineer. */ +"Sent total" = "Total enviado"; + +/* No comment provided by engineer. */ +"Sent via proxy" = "Enviado mediante proxy"; + +/* No comment provided by engineer. */ +"Server address" = "Dirección del servidor"; + +/* No comment provided by engineer. */ +"Server address is incompatible with network settings: %@." = "La dirección del servidor es incompatible con la configuración de la red: %@."; + /* srv error text. */ "Server address is incompatible with network settings." = "La dirección del servidor es incompatible con la configuración de la red."; @@ -3528,12 +3852,24 @@ /* No comment provided by engineer. */ "Server test failed!" = "¡Error en prueba del servidor!"; +/* No comment provided by engineer. */ +"Server type" = "Tipo de servidor"; + /* srv error text */ "Server version is incompatible with network settings." = "La versión del servidor es incompatible con la configuración de red."; +/* No comment provided by engineer. */ +"Server version is incompatible with your app: %@." = "La versión del servidor es incompatible con tu aplicación: %@."; + /* No comment provided by engineer. */ "Servers" = "Servidores"; +/* No comment provided by engineer. */ +"Servers info" = "Info servidores"; + +/* No comment provided by engineer. */ +"Servers statistics will be reset - this cannot be undone!" = "Las estadísticas de los servidores serán restablecidas. ¡No podrá deshacerse!"; + /* No comment provided by engineer. */ "Session code" = "Código de sesión"; @@ -3543,6 +3879,9 @@ /* No comment provided by engineer. */ "Set contact name…" = "Escribe el nombre del contacto…"; +/* No comment provided by engineer. */ +"Set default theme" = "Establecer tema predeterminado"; + /* No comment provided by engineer. */ "Set group preferences" = "Establecer preferencias de grupo"; @@ -3612,6 +3951,9 @@ /* No comment provided by engineer. */ "Show message status" = "Estado del mensaje"; +/* No comment provided by engineer. */ +"Show percentage" = "Mostrar porcentaje"; + /* No comment provided by engineer. */ "Show preview" = "Mostrar vista previa"; @@ -3621,6 +3963,9 @@ /* No comment provided by engineer. */ "Show:" = "Mostrar:"; +/* No comment provided by engineer. */ +"SimpleX" = "SimpleX"; + /* No comment provided by engineer. */ "SimpleX address" = "Dirección SimpleX"; @@ -3666,6 +4011,9 @@ /* No comment provided by engineer. */ "Simplified incognito mode" = "Modo incógnito simplificado"; +/* No comment provided by engineer. */ +"Size" = "Tamaño"; + /* No comment provided by engineer. */ "Skip" = "Omitir"; @@ -3675,6 +4023,9 @@ /* No comment provided by engineer. */ "Small groups (max 20)" = "Grupos pequeños (máx. 20)"; +/* No comment provided by engineer. */ +"SMP server" = "Servidor SMP"; + /* No comment provided by engineer. */ "SMP servers" = "Servidores SMP"; @@ -3699,9 +4050,15 @@ /* No comment provided by engineer. */ "Start migration" = "Iniciar migración"; +/* No comment provided by engineer. */ +"Starting from %@." = "Iniciado desde %@."; + /* No comment provided by engineer. */ "starting…" = "inicializando…"; +/* No comment provided by engineer. */ +"Statistics" = "Estadísticas"; + /* No comment provided by engineer. */ "Stop" = "Parar"; @@ -3744,6 +4101,15 @@ /* No comment provided by engineer. */ "Submit" = "Enviar"; +/* No comment provided by engineer. */ +"Subscribed" = "Suscrito"; + +/* No comment provided by engineer. */ +"Subscription errors" = "Errores de suscripción"; + +/* No comment provided by engineer. */ +"Subscriptions ignored" = "Suscripciones ignoradas"; + /* No comment provided by engineer. */ "Support SimpleX Chat" = "Soporte SimpleX Chat"; @@ -3792,6 +4158,9 @@ /* No comment provided by engineer. */ "TCP_KEEPINTVL" = "TCP_KEEPINTVL"; +/* No comment provided by engineer. */ +"Temporary file error" = "Error en archivo temporal"; + /* server test failure */ "Test failed at step %@." = "La prueba ha fallado en el paso %@."; @@ -3873,6 +4242,9 @@ /* No comment provided by engineer. */ "The text you pasted is not a SimpleX link." = "El texto pegado no es un enlace SimpleX."; +/* No comment provided by engineer. */ +"Themes" = "Temas"; + /* No comment provided by engineer. */ "These settings are for your current profile **%@**." = "Esta configuración afecta a tu perfil actual **%@**."; @@ -3915,9 +4287,15 @@ /* No comment provided by engineer. */ "This is your own SimpleX address!" = "¡Esta es tu propia dirección SimpleX!"; +/* No comment provided by engineer. */ +"This link was used with another mobile device, please create a new link on the desktop." = "Este enlace ha sido usado en otro dispositivo móvil, por favor crea un enlace nuevo en el ordenador."; + /* No comment provided by engineer. */ "This setting applies to messages in your current chat profile **%@**." = "Esta configuración se aplica a los mensajes del perfil actual **%@**."; +/* No comment provided by engineer. */ +"Title" = "Título"; + /* No comment provided by engineer. */ "To ask any questions and to receive updates:" = "Para consultar cualquier duda y recibir actualizaciones:"; @@ -3957,9 +4335,15 @@ /* No comment provided by engineer. */ "Toggle incognito when connecting." = "Activa incógnito al conectar."; +/* No comment provided by engineer. */ +"Total" = "Total"; + /* No comment provided by engineer. */ "Transport isolation" = "Aislamiento de transporte"; +/* No comment provided by engineer. */ +"Transport sessions" = "Sesiones de transporte"; + /* No comment provided by engineer. */ "Trying to connect to the server used to receive messages from this contact (error: %@)." = "Intentando conectar con el servidor usado para recibir mensajes de este contacto (error: %@)."; @@ -4095,12 +4479,21 @@ /* No comment provided by engineer. */ "Upgrade and open chat" = "Actualizar y abrir Chat"; +/* No comment provided by engineer. */ +"Upload errors" = "Errores en subida"; + /* No comment provided by engineer. */ "Upload failed" = "Error de subida"; /* server test step */ "Upload file" = "Subir archivo"; +/* No comment provided by engineer. */ +"Uploaded" = "Subido"; + +/* No comment provided by engineer. */ +"Uploaded files" = "Archivos subidos"; + /* No comment provided by engineer. */ "Uploading archive" = "Subiendo archivo"; @@ -4146,6 +4539,9 @@ /* No comment provided by engineer. */ "User profile" = "Perfil de usuario"; +/* No comment provided by engineer. */ +"User selection" = "Selección de usuarios"; + /* No comment provided by engineer. */ "Using .onion hosts requires compatible VPN provider." = "Usar hosts .onion requiere un proveedor VPN compatible."; @@ -4254,6 +4650,12 @@ /* No comment provided by engineer. */ "Waiting for video" = "Esperando el vídeo"; +/* No comment provided by engineer. */ +"Wallpaper accent" = "Color imagen de fondo"; + +/* No comment provided by engineer. */ +"Wallpaper background" = "Color de fondo"; + /* No comment provided by engineer. */ "wants to connect to you!" = "¡quiere contactar contigo!"; @@ -4326,9 +4728,15 @@ /* snd error text */ "Wrong key or unknown connection - most likely this connection is deleted." = "Clave incorrecta o conexión desconocida - probablemente esta conexión fue eliminada."; +/* file error text */ +"Wrong key or unknown file chunk address - most likely file is deleted." = "Clave incorrecta o dirección del bloque del archivo desconocida. Es probable que el archivo se haya eliminado."; + /* No comment provided by engineer. */ "Wrong passphrase!" = "¡Contraseña incorrecta!"; +/* No comment provided by engineer. */ +"XFTP server" = "Servidor XFTP"; + /* No comment provided by engineer. */ "XFTP servers" = "Servidores XFTP"; @@ -4386,6 +4794,9 @@ /* No comment provided by engineer. */ "You are invited to group" = "Has sido invitado a un grupo"; +/* No comment provided by engineer. */ +"You are not connected to these servers. Private routing is used to deliver messages to them." = "No estás conectado a estos servidores. Para enviarles mensajes se usa el enrutamiento privado."; + /* No comment provided by engineer. */ "you are observer" = "Tu rol es observador"; @@ -4414,7 +4825,7 @@ "You can make it visible to your SimpleX contacts via Settings." = "Puedes hacerlo visible para tus contactos de SimpleX en Configuración."; /* notification body */ -"You can now chat with %@" = "Ya puedes enviar mensajes a %@"; +"You can now chat with %@" = "Ya puedes chatear con %@"; /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "Puedes configurar las notificaciones de la pantalla de bloqueo desde Configuración."; diff --git a/apps/ios/fi.lproj/Localizable.strings b/apps/ios/fi.lproj/Localizable.strings index 84613e6a54..b9ea0024c5 100644 --- a/apps/ios/fi.lproj/Localizable.strings +++ b/apps/ios/fi.lproj/Localizable.strings @@ -2163,7 +2163,7 @@ time to disappear */ "off" = "pois"; -/* No comment provided by engineer. */ +/* blur media */ "Off" = "Pois"; /* feature offered item */ diff --git a/apps/ios/fr.lproj/Localizable.strings b/apps/ios/fr.lproj/Localizable.strings index c2777fac75..272c3bb410 100644 --- a/apps/ios/fr.lproj/Localizable.strings +++ b/apps/ios/fr.lproj/Localizable.strings @@ -334,6 +334,9 @@ /* No comment provided by engineer. */ "above, then choose:" = "ci-dessus, puis choisissez :"; +/* No comment provided by engineer. */ +"Accent" = "Principale"; + /* accept contact request via notification accept incoming call via notification */ "Accept" = "Accepter"; @@ -350,6 +353,15 @@ /* call status */ "accepted call" = "appel accepté"; +/* No comment provided by engineer. */ +"Acknowledged" = "Reçu avec accusé de réception"; + +/* No comment provided by engineer. */ +"Acknowledgement errors" = "Erreur d'accusé de réception"; + +/* No comment provided by engineer. */ +"Active connections" = "Connections actives"; + /* No comment provided by engineer. */ "Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." = "Ajoutez une adresse à votre profil, afin que vos contacts puissent la partager avec d'autres personnes. La mise à jour du profil sera envoyée à vos contacts."; @@ -374,6 +386,15 @@ /* No comment provided by engineer. */ "Add welcome message" = "Ajouter un message d'accueil"; +/* No comment provided by engineer. */ +"Additional accent" = "Accent additionnel"; + +/* No comment provided by engineer. */ +"Additional accent 2" = "Accent additionnel 2"; + +/* No comment provided by engineer. */ +"Additional secondary" = "Accent secondaire"; + /* No comment provided by engineer. */ "Address" = "Adresse"; @@ -395,6 +416,9 @@ /* No comment provided by engineer. */ "Advanced network settings" = "Paramètres réseau avancés"; +/* No comment provided by engineer. */ +"Advanced settings" = "Paramètres avancés"; + /* chat item text */ "agreeing encryption for %@…" = "négociation du chiffrement avec %@…"; @@ -410,6 +434,9 @@ /* No comment provided by engineer. */ "All data is erased when it is entered." = "Toutes les données sont effacées lorsqu'il est saisi."; +/* No comment provided by engineer. */ +"All data is private to your device." = "Toutes les données restent confinées dans votre appareil."; + /* No comment provided by engineer. */ "All group members will remain connected." = "Tous les membres du groupe resteront connectés."; @@ -425,6 +452,9 @@ /* No comment provided by engineer. */ "All new messages from %@ will be hidden!" = "Tous les nouveaux messages de %@ seront cachés !"; +/* No comment provided by engineer. */ +"All profiles" = "Tous les profiles"; + /* No comment provided by engineer. */ "All your contacts will remain connected." = "Tous vos contacts resteront connectés."; @@ -551,6 +581,9 @@ /* No comment provided by engineer. */ "Apply" = "Appliquer"; +/* No comment provided by engineer. */ +"Apply to" = "Appliquer à"; + /* No comment provided by engineer. */ "Archive and upload" = "Archiver et transférer"; @@ -560,6 +593,9 @@ /* No comment provided by engineer. */ "Attach" = "Attacher"; +/* No comment provided by engineer. */ +"attempts" = "tentatives"; + /* No comment provided by engineer. */ "Audio & video calls" = "Appels audio et vidéo"; @@ -602,6 +638,9 @@ /* No comment provided by engineer. */ "Back" = "Retour"; +/* No comment provided by engineer. */ +"Background" = "Fond"; + /* No comment provided by engineer. */ "Bad desktop address" = "Mauvaise adresse de bureau"; @@ -623,6 +662,9 @@ /* No comment provided by engineer. */ "Better messages" = "Meilleurs messages"; +/* No comment provided by engineer. */ +"Black" = "Noir"; + /* No comment provided by engineer. */ "Block" = "Bloquer"; @@ -647,7 +689,7 @@ /* rcv group event chat item */ "blocked %@" = "%@ bloqué"; -/* marked deleted chat item preview text */ +/* blocked chat item */ "blocked by admin" = "bloqué par l'administrateur"; /* No comment provided by engineer. */ @@ -713,6 +755,9 @@ /* No comment provided by engineer. */ "Cannot access keychain to save database password" = "Impossible d'accéder à la keychain pour enregistrer le mot de passe de la base de données"; +/* No comment provided by engineer. */ +"Cannot forward message" = "Impossible de transférer le message"; + /* No comment provided by engineer. */ "Cannot receive file" = "Impossible de recevoir le fichier"; @@ -771,6 +816,9 @@ /* No comment provided by engineer. */ "Chat archive" = "Archives du chat"; +/* No comment provided by engineer. */ +"Chat colors" = "Couleurs de chat"; + /* No comment provided by engineer. */ "Chat console" = "Console du chat"; @@ -798,6 +846,9 @@ /* No comment provided by engineer. */ "Chat preferences" = "Préférences de chat"; +/* No comment provided by engineer. */ +"Chat theme" = "Thème de chat"; + /* No comment provided by engineer. */ "Chats" = "Discussions"; @@ -816,6 +867,15 @@ /* No comment provided by engineer. */ "Choose from library" = "Choisir dans la photothèque"; +/* No comment provided by engineer. */ +"Chunks deleted" = "Chunks supprimés"; + +/* No comment provided by engineer. */ +"Chunks downloaded" = "Chunks téléchargés"; + +/* No comment provided by engineer. */ +"Chunks uploaded" = "Chunks téléversés"; + /* No comment provided by engineer. */ "Clear" = "Effacer"; @@ -831,6 +891,9 @@ /* No comment provided by engineer. */ "Clear verification" = "Retirer la vérification"; +/* No comment provided by engineer. */ +"Color mode" = "Mode de couleur"; + /* No comment provided by engineer. */ "colored" = "coloré"; @@ -843,9 +906,15 @@ /* No comment provided by engineer. */ "complete" = "complet"; +/* No comment provided by engineer. */ +"Completed" = "Complété"; + /* No comment provided by engineer. */ "Configure ICE servers" = "Configurer les serveurs ICE"; +/* No comment provided by engineer. */ +"Configured %@ servers" = "%@ serveurs configurés"; + /* No comment provided by engineer. */ "Confirm" = "Confirmer"; @@ -912,18 +981,27 @@ /* No comment provided by engineer. */ "connected" = "connecté"; +/* No comment provided by engineer. */ +"Connected" = "Connecté"; + /* No comment provided by engineer. */ "Connected desktop" = "Bureau connecté"; /* rcv group event chat item */ "connected directly" = "s'est connecté.e de manière directe"; +/* No comment provided by engineer. */ +"Connected servers" = "Serveurs connectés"; + /* No comment provided by engineer. */ "Connected to desktop" = "Connecté au bureau"; /* No comment provided by engineer. */ "connecting" = "connexion"; +/* No comment provided by engineer. */ +"Connecting" = "Connexion"; + /* No comment provided by engineer. */ "connecting (accepted)" = "connexion (acceptée)"; @@ -972,9 +1050,15 @@ /* No comment provided by engineer. */ "Connection timeout" = "Délai de connexion"; +/* No comment provided by engineer. */ +"Connection with desktop stopped" = "La connexion avec le bureau s'est arrêtée"; + /* connection information */ "connection:%@" = "connexion : %@"; +/* No comment provided by engineer. */ +"Connections" = "Connexions"; + /* profile update event chat item */ "contact %@ changed to %@" = "le contact %1$@ est devenu %2$@"; @@ -1017,6 +1101,9 @@ /* No comment provided by engineer. */ "Copy" = "Copier"; +/* No comment provided by engineer. */ +"Copy error" = "Erreur de copie"; + /* No comment provided by engineer. */ "Core version: v%@" = "Version du cœur : v%@"; @@ -1062,6 +1149,9 @@ /* No comment provided by engineer. */ "Create your profile" = "Créez votre profil"; +/* No comment provided by engineer. */ +"Created" = "Créé"; + /* No comment provided by engineer. */ "Created at" = "Créé à"; @@ -1086,6 +1176,9 @@ /* No comment provided by engineer. */ "Current passphrase…" = "Phrase secrète actuelle…"; +/* No comment provided by engineer. */ +"Current profile" = "Profil actuel"; + /* No comment provided by engineer. */ "Currently maximum supported file size is %@." = "Actuellement, la taille maximale des fichiers supportés est de %@."; @@ -1095,9 +1188,15 @@ /* No comment provided by engineer. */ "Custom time" = "Délai personnalisé"; +/* No comment provided by engineer. */ +"Customize theme" = "Personnaliser le thème"; + /* No comment provided by engineer. */ "Dark" = "Sombre"; +/* No comment provided by engineer. */ +"Dark mode colors" = "Couleurs en mode sombre"; + /* No comment provided by engineer. */ "Database downgrade" = "Rétrogradation de la base de données"; @@ -1167,6 +1266,9 @@ /* message decrypt error item */ "Decryption error" = "Erreur de déchiffrement"; +/* No comment provided by engineer. */ +"decryption errors" = "Erreurs de déchiffrement"; + /* pref value */ "default (%@)" = "défaut (%@)"; @@ -1293,6 +1395,9 @@ /* deleted chat item */ "deleted" = "supprimé"; +/* No comment provided by engineer. */ +"Deleted" = "Supprimé"; + /* No comment provided by engineer. */ "Deleted at" = "Supprimé à"; @@ -1305,6 +1410,9 @@ /* rcv group event chat item */ "deleted group" = "groupe supprimé"; +/* No comment provided by engineer. */ +"Deletion errors" = "Erreurs de suppression"; + /* No comment provided by engineer. */ "Delivery" = "Distribution"; @@ -1329,6 +1437,12 @@ /* snd error text */ "Destination server error: %@" = "Erreur du serveur de destination : %@"; +/* No comment provided by engineer. */ +"Detailed statistics" = "Statistiques détaillées"; + +/* No comment provided by engineer. */ +"Details" = "Détails"; + /* No comment provided by engineer. */ "Develop" = "Développer"; @@ -1431,12 +1545,21 @@ /* chat item action */ "Download" = "Télécharger"; +/* No comment provided by engineer. */ +"Download errors" = "Erreurs de téléchargement"; + /* No comment provided by engineer. */ "Download failed" = "Échec du téléchargement"; /* server test step */ "Download file" = "Télécharger le fichier"; +/* No comment provided by engineer. */ +"Downloaded" = "Téléchargé"; + +/* No comment provided by engineer. */ +"Downloaded files" = "Fichiers téléchargés"; + /* No comment provided by engineer. */ "Downloading archive" = "Téléchargement de l'archive"; @@ -1449,6 +1572,9 @@ /* integrity error chat item */ "duplicate message" = "message dupliqué"; +/* No comment provided by engineer. */ +"duplicates" = "doublons"; + /* No comment provided by engineer. */ "Duration" = "Durée"; @@ -1707,6 +1833,9 @@ /* No comment provided by engineer. */ "Error exporting chat database" = "Erreur lors de l'exportation de la base de données du chat"; +/* No comment provided by engineer. */ +"Error exporting theme: %@" = "Erreur d'exportation du thème : %@"; + /* No comment provided by engineer. */ "Error importing chat database" = "Erreur lors de l'importation de la base de données du chat"; @@ -1722,9 +1851,18 @@ /* No comment provided by engineer. */ "Error receiving file" = "Erreur lors de la réception du fichier"; +/* No comment provided by engineer. */ +"Error reconnecting server" = "Erreur de reconnexion du serveur"; + +/* No comment provided by engineer. */ +"Error reconnecting servers" = "Erreur de reconnexion des serveurs"; + /* No comment provided by engineer. */ "Error removing member" = "Erreur lors de la suppression d'un membre"; +/* No comment provided by engineer. */ +"Error resetting statistics" = "Erreur de réinitialisation des statistiques"; + /* No comment provided by engineer. */ "Error saving %@ servers" = "Erreur lors de la sauvegarde des serveurs %@"; @@ -1804,6 +1942,9 @@ /* No comment provided by engineer. */ "Error: URL is invalid" = "Erreur : URL invalide"; +/* No comment provided by engineer. */ +"Errors" = "Erreurs"; + /* No comment provided by engineer. */ "Even when disabled in the conversation." = "Même s'il est désactivé dans la conversation."; @@ -1816,12 +1957,18 @@ /* chat item action */ "Expand" = "Étendre"; +/* No comment provided by engineer. */ +"expired" = "expiré"; + /* No comment provided by engineer. */ "Export database" = "Exporter la base de données"; /* No comment provided by engineer. */ "Export error:" = "Erreur lors de l'exportation :"; +/* No comment provided by engineer. */ +"Export theme" = "Exporter le thème"; + /* No comment provided by engineer. */ "Exported database archive." = "Archive de la base de données exportée."; @@ -1843,6 +1990,21 @@ /* No comment provided by engineer. */ "Favorite" = "Favoris"; +/* No comment provided by engineer. */ +"File error" = "Erreur de fichier"; + +/* file error text */ +"File not found - most likely file was deleted or cancelled." = "Fichier introuvable - le fichier a probablement été supprimé ou annulé."; + +/* file error text */ +"File server error: %@" = "Erreur de serveur de fichiers : %@"; + +/* No comment provided by engineer. */ +"File status" = "Statut du fichier"; + +/* copied message info */ +"File status: %@" = "Statut du fichier : %@"; + /* No comment provided by engineer. */ "File will be deleted from servers." = "Le fichier sera supprimé des serveurs."; @@ -1957,6 +2119,12 @@ /* No comment provided by engineer. */ "GIFs and stickers" = "GIFs et stickers"; +/* message preview */ +"Good afternoon!" = "Bonjour !"; + +/* message preview */ +"Good morning!" = "Bonjour !"; + /* No comment provided by engineer. */ "Group" = "Groupe"; @@ -2134,6 +2302,9 @@ /* No comment provided by engineer. */ "Import failed" = "Échec de l'importation"; +/* No comment provided by engineer. */ +"Import theme" = "Importer un thème"; + /* No comment provided by engineer. */ "Importing archive" = "Importation de l'archive"; @@ -2155,6 +2326,9 @@ /* No comment provided by engineer. */ "In-call sounds" = "Sons d'appel"; +/* No comment provided by engineer. */ +"inactive" = "inactif"; + /* No comment provided by engineer. */ "Incognito" = "Incognito"; @@ -2218,6 +2392,9 @@ /* No comment provided by engineer. */ "Interface" = "Interface"; +/* No comment provided by engineer. */ +"Interface colors" = "Couleurs d'interface"; + /* invalid chat data */ "invalid chat" = "chat invalide"; @@ -2470,6 +2647,9 @@ /* rcv group event chat item */ "member connected" = "est connecté·e"; +/* item status text */ +"Member inactive" = "Membre inactif"; + /* No comment provided by engineer. */ "Member role will be changed to \"%@\". All group members will be notified." = "Le rôle du membre sera changé pour \"%@\". Tous les membres du groupe en seront informés."; @@ -2479,6 +2659,9 @@ /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "Ce membre sera retiré du groupe - impossible de revenir en arrière !"; +/* No comment provided by engineer. */ +"Menus" = "Menus"; + /* item status text */ "Message delivery error" = "Erreur de distribution du message"; @@ -2491,6 +2674,12 @@ /* No comment provided by engineer. */ "Message draft" = "Brouillon de message"; +/* item status text */ +"Message forwarded" = "Message transféré"; + +/* item status description */ +"Message may be delivered later if member becomes active." = "Le message peut être transmis plus tard si le membre devient actif."; + /* No comment provided by engineer. */ "Message queue info" = "Informations sur la file d'attente des messages"; @@ -2506,6 +2695,9 @@ /* notification */ "message received" = "message reçu"; +/* No comment provided by engineer. */ +"Message reception" = "Réception de message"; + /* No comment provided by engineer. */ "Message routing fallback" = "Rabattement du routage des messages"; @@ -2515,6 +2707,12 @@ /* No comment provided by engineer. */ "Message source remains private." = "La source du message reste privée."; +/* No comment provided by engineer. */ +"Message status" = "Statut du message"; + +/* copied message info */ +"Message status: %@" = "Statut du message : %@"; + /* No comment provided by engineer. */ "Message text" = "Texte du message"; @@ -2530,6 +2728,12 @@ /* No comment provided by engineer. */ "Messages from %@ will be shown!" = "Les messages de %@ seront affichés !"; +/* No comment provided by engineer. */ +"Messages received" = "Messages reçus"; + +/* No comment provided by engineer. */ +"Messages sent" = "Messages envoyés"; + /* No comment provided by engineer. */ "Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery." = "Les messages, fichiers et appels sont protégés par un chiffrement **de bout en bout** avec une confidentialité persistante, une répudiation et une récupération en cas d'effraction."; @@ -2695,6 +2899,9 @@ /* No comment provided by engineer. */ "No device token!" = "Pas de token d'appareil !"; +/* item status description */ +"No direct connection yet, message is forwarded by admin." = "Pas de connexion directe pour l'instant, le message est transmis par l'administrateur."; + /* No comment provided by engineer. */ "no e2e encryption" = "sans chiffrement de bout en bout"; @@ -2707,6 +2914,9 @@ /* No comment provided by engineer. */ "No history" = "Aucun historique"; +/* No comment provided by engineer. */ +"No info, try to reload" = "Pas d'info, essayez de recharger"; + /* No comment provided by engineer. */ "No network connection" = "Pas de connexion au réseau"; @@ -2739,7 +2949,7 @@ time to disappear */ "off" = "off"; -/* No comment provided by engineer. */ +/* blur media */ "Off" = "Off"; /* feature offered item */ @@ -2832,6 +3042,9 @@ /* authentication reason */ "Open migration to another device" = "Ouvrir le transfert vers un autre appareil"; +/* No comment provided by engineer. */ +"Open server settings" = "Ouvrir les paramètres du serveur"; + /* No comment provided by engineer. */ "Open Settings" = "Ouvrir les Paramètres"; @@ -2856,9 +3069,18 @@ /* No comment provided by engineer. */ "Or show this code" = "Ou présenter ce code"; +/* No comment provided by engineer. */ +"other" = "autre"; + /* No comment provided by engineer. */ "Other" = "Autres"; +/* No comment provided by engineer. */ +"Other %@ servers" = "Autres serveurs %@"; + +/* No comment provided by engineer. */ +"other errors" = "autres erreurs"; + /* member role */ "owner" = "propriétaire"; @@ -2901,6 +3123,9 @@ /* No comment provided by engineer. */ "peer-to-peer" = "pair-à-pair"; +/* No comment provided by engineer. */ +"Pending" = "En attente"; + /* No comment provided by engineer. */ "People can connect to you only via the links you share." = "On ne peut se connecter à vous qu’avec les liens que vous partagez."; @@ -2922,6 +3147,9 @@ /* No comment provided by engineer. */ "Please ask your contact to enable sending voice messages." = "Veuillez demander à votre contact de permettre l'envoi de messages vocaux."; +/* 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." = "Veuillez vérifier que le téléphone portable et l'ordinateur de bureau sont connectés au même réseau local et que le pare-feu de l'ordinateur de bureau autorise la connexion.\nVeuillez faire part de tout autre problème aux développeurs."; + /* No comment provided by engineer. */ "Please check that you used the correct link or ask your contact to send you another one." = "Veuillez vérifier que vous avez utilisé le bon lien ou demandez à votre contact de vous en envoyer un autre."; @@ -2979,6 +3207,9 @@ /* No comment provided by engineer. */ "Preview" = "Aperçu"; +/* No comment provided by engineer. */ +"Previously connected servers" = "Serveurs précédemment connectés"; + /* No comment provided by engineer. */ "Privacy & security" = "Vie privée et sécurité"; @@ -3000,6 +3231,9 @@ /* No comment provided by engineer. */ "Private routing" = "Routage privé"; +/* No comment provided by engineer. */ +"Private routing error" = "Erreur de routage privé"; + /* No comment provided by engineer. */ "Profile and server connections" = "Profil et connexions au serveur"; @@ -3018,6 +3252,9 @@ /* No comment provided by engineer. */ "Profile password" = "Mot de passe de profil"; +/* No comment provided by engineer. */ +"Profile theme" = "Thème de profil"; + /* No comment provided by engineer. */ "Profile update will be sent to your contacts." = "La mise à jour du profil sera envoyée à vos contacts."; @@ -3066,6 +3303,12 @@ /* No comment provided by engineer. */ "Protocol timeout per KB" = "Délai d'attente du protocole par KB"; +/* No comment provided by engineer. */ +"Proxied" = "Routé via un proxy"; + +/* No comment provided by engineer. */ +"Proxied servers" = "Serveurs routés via des proxy"; + /* No comment provided by engineer. */ "Push notifications" = "Notifications push"; @@ -3108,6 +3351,9 @@ /* No comment provided by engineer. */ "Receipts are disabled" = "Les accusés de réception sont désactivés"; +/* No comment provided by engineer. */ +"Receive errors" = "Erreurs reçues"; + /* No comment provided by engineer. */ "received answer…" = "réponse reçu…"; @@ -3126,6 +3372,15 @@ /* message info title */ "Received message" = "Message reçu"; +/* No comment provided by engineer. */ +"Received messages" = "Messages reçus"; + +/* No comment provided by engineer. */ +"Received reply" = "Réponse reçue"; + +/* No comment provided by engineer. */ +"Received total" = "Total reçu"; + /* No comment provided by engineer. */ "Receiving address will be changed to a different server. Address change will complete after sender comes online." = "L'adresse de réception sera changée pour un autre serveur. Le changement d'adresse sera terminé lorsque l'expéditeur sera en ligne."; @@ -3144,9 +3399,24 @@ /* No comment provided by engineer. */ "Recipients see updates as you type them." = "Les destinataires voient les mises à jour au fur et à mesure que vous leur écrivez."; +/* No comment provided by engineer. */ +"Reconnect" = "Reconnecter"; + /* No comment provided by engineer. */ "Reconnect all connected servers to force message delivery. It uses additional traffic." = "Reconnecter tous les serveurs connectés pour forcer la livraison des messages. Cette méthode utilise du trafic supplémentaire."; +/* No comment provided by engineer. */ +"Reconnect all servers" = "Reconnecter tous les serveurs"; + +/* No comment provided by engineer. */ +"Reconnect all servers?" = "Reconnecter tous les serveurs ?"; + +/* No comment provided by engineer. */ +"Reconnect server to force message delivery. It uses additional traffic." = "Reconnecter le serveur pour forcer la livraison des messages. Utilise du trafic supplémentaire."; + +/* No comment provided by engineer. */ +"Reconnect server?" = "Reconnecter le serveur ?"; + /* No comment provided by engineer. */ "Reconnect servers?" = "Reconnecter les serveurs ?"; @@ -3180,6 +3450,9 @@ /* No comment provided by engineer. */ "Remove" = "Supprimer"; +/* No comment provided by engineer. */ +"Remove image" = "Enlever l'image"; + /* No comment provided by engineer. */ "Remove member" = "Retirer le membre"; @@ -3237,12 +3510,24 @@ /* No comment provided by engineer. */ "Reset" = "Réinitialisation"; +/* No comment provided by engineer. */ +"Reset all statistics" = "Réinitialiser toutes les statistiques"; + +/* No comment provided by engineer. */ +"Reset all statistics?" = "Réinitialiser toutes les statistiques ?"; + /* No comment provided by engineer. */ "Reset colors" = "Réinitialisation des couleurs"; +/* No comment provided by engineer. */ +"Reset to app theme" = "Réinitialisation au thème de l'appli"; + /* No comment provided by engineer. */ "Reset to defaults" = "Réinitialisation des valeurs par défaut"; +/* No comment provided by engineer. */ +"Reset to user theme" = "Réinitialisation au thème de l'utilisateur"; + /* No comment provided by engineer. */ "Restart the app to create a new chat profile" = "Redémarrez l'application pour créer un nouveau profil de chat"; @@ -3357,6 +3642,12 @@ /* No comment provided by engineer. */ "Saved WebRTC ICE servers will be removed" = "Les serveurs WebRTC ICE sauvegardés seront supprimés"; +/* No comment provided by engineer. */ +"Scale" = "Échelle"; + +/* No comment provided by engineer. */ +"Scan / Paste link" = "Scanner / Coller le lien"; + /* No comment provided by engineer. */ "Scan code" = "Scanner le code"; @@ -3384,6 +3675,9 @@ /* network option */ "sec" = "sec"; +/* No comment provided by engineer. */ +"Secondary" = "Secondaire"; + /* time unit */ "seconds" = "secondes"; @@ -3393,6 +3687,9 @@ /* server test step */ "Secure queue" = "File d'attente sécurisée"; +/* No comment provided by engineer. */ +"Secured" = "Sécurisé"; + /* No comment provided by engineer. */ "Security assessment" = "Évaluation de sécurité"; @@ -3405,6 +3702,9 @@ /* No comment provided by engineer. */ "Select" = "Choisir"; +/* No comment provided by engineer. */ +"Selected chat preferences prohibit this message." = "Les préférences de chat sélectionnées interdisent ce message."; + /* No comment provided by engineer. */ "Self-destruct" = "Autodestruction"; @@ -3438,6 +3738,9 @@ /* No comment provided by engineer. */ "Send disappearing message" = "Envoyer un message éphémère"; +/* No comment provided by engineer. */ +"Send errors" = "Erreurs d'envoi"; + /* No comment provided by engineer. */ "Send link previews" = "Envoi d'aperçus de liens"; @@ -3504,15 +3807,36 @@ /* copied message info */ "Sent at: %@" = "Envoyé le : %@"; +/* No comment provided by engineer. */ +"Sent directly" = "Envoyé directement"; + /* notification */ "Sent file event" = "Événement de fichier envoyé"; /* message info title */ "Sent message" = "Message envoyé"; +/* No comment provided by engineer. */ +"Sent messages" = "Messages envoyés"; + /* No comment provided by engineer. */ "Sent messages will be deleted after set time." = "Les messages envoyés seront supprimés après une durée déterminée."; +/* No comment provided by engineer. */ +"Sent reply" = "Réponse envoyée"; + +/* No comment provided by engineer. */ +"Sent total" = "Total envoyé"; + +/* No comment provided by engineer. */ +"Sent via proxy" = "Envoyé via le proxy"; + +/* No comment provided by engineer. */ +"Server address" = "Adresse du serveur"; + +/* No comment provided by engineer. */ +"Server address is incompatible with network settings: %@." = "L'adresse du serveur est incompatible avec les paramètres réseau : %@."; + /* srv error text. */ "Server address is incompatible with network settings." = "L'adresse du serveur est incompatible avec les paramètres du réseau."; @@ -3528,12 +3852,24 @@ /* No comment provided by engineer. */ "Server test failed!" = "Échec du test du serveur !"; +/* No comment provided by engineer. */ +"Server type" = "Type de serveur"; + /* srv error text */ "Server version is incompatible with network settings." = "La version du serveur est incompatible avec les paramètres du réseau."; +/* No comment provided by engineer. */ +"Server version is incompatible with your app: %@." = "La version du serveur est incompatible avec votre appli : %@."; + /* No comment provided by engineer. */ "Servers" = "Serveurs"; +/* No comment provided by engineer. */ +"Servers info" = "Infos serveurs"; + +/* No comment provided by engineer. */ +"Servers statistics will be reset - this cannot be undone!" = "Les statistiques des serveurs seront réinitialisées - il n'est pas possible de revenir en arrière !"; + /* No comment provided by engineer. */ "Session code" = "Code de session"; @@ -3543,6 +3879,9 @@ /* No comment provided by engineer. */ "Set contact name…" = "Définir le nom du contact…"; +/* No comment provided by engineer. */ +"Set default theme" = "Définir le thème par défaut"; + /* No comment provided by engineer. */ "Set group preferences" = "Définir les préférences du groupe"; @@ -3612,6 +3951,9 @@ /* No comment provided by engineer. */ "Show message status" = "Afficher le statut du message"; +/* No comment provided by engineer. */ +"Show percentage" = "Afficher le pourcentage"; + /* No comment provided by engineer. */ "Show preview" = "Afficher l'aperçu"; @@ -3666,6 +4008,9 @@ /* No comment provided by engineer. */ "Simplified incognito mode" = "Mode incognito simplifié"; +/* No comment provided by engineer. */ +"Size" = "Taille"; + /* No comment provided by engineer. */ "Skip" = "Passer"; @@ -3675,6 +4020,9 @@ /* No comment provided by engineer. */ "Small groups (max 20)" = "Petits groupes (max 20)"; +/* No comment provided by engineer. */ +"SMP server" = "Serveur SMP"; + /* No comment provided by engineer. */ "SMP servers" = "Serveurs SMP"; @@ -3699,9 +4047,15 @@ /* No comment provided by engineer. */ "Start migration" = "Démarrer la migration"; +/* No comment provided by engineer. */ +"Starting from %@." = "À partir de %@."; + /* No comment provided by engineer. */ "starting…" = "lancement…"; +/* No comment provided by engineer. */ +"Statistics" = "Statistiques"; + /* No comment provided by engineer. */ "Stop" = "Arrêter"; @@ -3744,6 +4098,15 @@ /* No comment provided by engineer. */ "Submit" = "Soumettre"; +/* No comment provided by engineer. */ +"Subscribed" = "Inscrit"; + +/* No comment provided by engineer. */ +"Subscription errors" = "Erreurs d'inscription"; + +/* No comment provided by engineer. */ +"Subscriptions ignored" = "Inscriptions ignorées"; + /* No comment provided by engineer. */ "Support SimpleX Chat" = "Supporter SimpleX Chat"; @@ -3792,6 +4155,9 @@ /* No comment provided by engineer. */ "TCP_KEEPINTVL" = "TCP_KEEPINTVL"; +/* No comment provided by engineer. */ +"Temporary file error" = "Erreur de fichier temporaire"; + /* server test failure */ "Test failed at step %@." = "Échec du test à l'étape %@."; @@ -3873,6 +4239,9 @@ /* No comment provided by engineer. */ "The text you pasted is not a SimpleX link." = "Le texte collé n'est pas un lien SimpleX."; +/* No comment provided by engineer. */ +"Themes" = "Thèmes"; + /* No comment provided by engineer. */ "These settings are for your current profile **%@**." = "Ces paramètres s'appliquent à votre profil actuel **%@**."; @@ -3915,9 +4284,15 @@ /* No comment provided by engineer. */ "This is your own SimpleX address!" = "Voici votre propre adresse SimpleX !"; +/* No comment provided by engineer. */ +"This link was used with another mobile device, please create a new link on the desktop." = "Ce lien a été utilisé avec un autre appareil mobile, veuillez créer un nouveau lien sur le bureau."; + /* No comment provided by engineer. */ "This setting applies to messages in your current chat profile **%@**." = "Ce paramètre s'applique aux messages de votre profil de chat actuel **%@**."; +/* No comment provided by engineer. */ +"Title" = "Titre"; + /* No comment provided by engineer. */ "To ask any questions and to receive updates:" = "Si vous avez des questions et que vous souhaitez des réponses :"; @@ -3957,9 +4332,15 @@ /* No comment provided by engineer. */ "Toggle incognito when connecting." = "Basculer en mode incognito lors de la connexion."; +/* No comment provided by engineer. */ +"Total" = "Total"; + /* No comment provided by engineer. */ "Transport isolation" = "Transport isolé"; +/* No comment provided by engineer. */ +"Transport sessions" = "Sessions de transport"; + /* No comment provided by engineer. */ "Trying to connect to the server used to receive messages from this contact (error: %@)." = "Tentative de connexion au serveur utilisé pour recevoir les messages de ce contact (erreur : %@)."; @@ -4095,12 +4476,21 @@ /* No comment provided by engineer. */ "Upgrade and open chat" = "Mettre à niveau et ouvrir le chat"; +/* No comment provided by engineer. */ +"Upload errors" = "Erreurs de téléversement"; + /* No comment provided by engineer. */ "Upload failed" = "Échec de l'envoi"; /* server test step */ "Upload file" = "Transférer le fichier"; +/* No comment provided by engineer. */ +"Uploaded" = "Téléversé"; + +/* No comment provided by engineer. */ +"Uploaded files" = "Fichiers téléversés"; + /* No comment provided by engineer. */ "Uploading archive" = "Envoi de l'archive"; @@ -4146,6 +4536,9 @@ /* No comment provided by engineer. */ "User profile" = "Profil d'utilisateur"; +/* No comment provided by engineer. */ +"User selection" = "Sélection de l'utilisateur"; + /* No comment provided by engineer. */ "Using .onion hosts requires compatible VPN provider." = "L'utilisation des hôtes .onion nécessite un fournisseur VPN compatible."; @@ -4254,6 +4647,12 @@ /* No comment provided by engineer. */ "Waiting for video" = "En attente de la vidéo"; +/* No comment provided by engineer. */ +"Wallpaper accent" = "Accentuation du papier-peint"; + +/* No comment provided by engineer. */ +"Wallpaper background" = "Fond d'écran"; + /* No comment provided by engineer. */ "wants to connect to you!" = "veut établir une connexion !"; @@ -4326,9 +4725,15 @@ /* snd error text */ "Wrong key or unknown connection - most likely this connection is deleted." = "Clé erronée ou connexion non identifiée - il est très probable que cette connexion soit supprimée."; +/* file error text */ +"Wrong key or unknown file chunk address - most likely file is deleted." = "Mauvaise clé ou adresse inconnue du bloc de données du fichier - le fichier est probablement supprimé."; + /* No comment provided by engineer. */ "Wrong passphrase!" = "Mauvaise phrase secrète !"; +/* No comment provided by engineer. */ +"XFTP server" = "Serveur XFTP"; + /* No comment provided by engineer. */ "XFTP servers" = "Serveurs XFTP"; @@ -4386,6 +4791,9 @@ /* No comment provided by engineer. */ "You are invited to group" = "Vous êtes invité·e au groupe"; +/* No comment provided by engineer. */ +"You are not connected to these servers. Private routing is used to deliver messages to them." = "Vous n'êtes pas connecté à ces serveurs. Le routage privé est utilisé pour leur délivrer des messages."; + /* No comment provided by engineer. */ "you are observer" = "vous êtes observateur"; diff --git a/apps/ios/hu.lproj/Localizable.strings b/apps/ios/hu.lproj/Localizable.strings index ab732754c9..3027e1df3f 100644 --- a/apps/ios/hu.lproj/Localizable.strings +++ b/apps/ios/hu.lproj/Localizable.strings @@ -68,7 +68,7 @@ "**Add contact**: to create a new invitation link, or connect via a link you received." = "**Ismerős hozzáadása**: új meghívó hivatkozás létrehozásához, vagy egy kapott hivatkozáson keresztül történő kapcsolódáshoz."; /* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Új ismerős hozzáadása**: egyszer használatos QR-kód vagy hivatkozás létrehozása a kapcsolattartóhoz."; +"**Add new contact**: to create your one-time QR Code for your contact." = "**Új ismerős hozzáadása**: egyszer használatos QR-kód vagy hivatkozás létrehozása az ismerőse számára."; /* No comment provided by engineer. */ "**Create group**: to create a new group." = "**Csoport létrehozása**: új csoport létrehozásához."; @@ -86,7 +86,7 @@ "**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." = "**Legprivátabb**: ne használja a SimpleX Chat értesítési kiszolgálót, rendszeresen ellenőrizze az üzeneteket a háttérben (attól függően, hogy milyen gyakran használja az alkalmazást)."; /* No comment provided by engineer. */ -"**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." = "**Megjegyzés**: ha két eszközön is ugyanazt az adatbázist használja, akkor biztonsági védelemként megszakítja a kapcsolataiból érkező üzenetek visszafejtését."; +"**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." = "**Megjegyzés**: ha két eszközön is ugyanazt az adatbázist használja, akkor biztonsági védelemként megszakítja az ismerőseitől érkező üzenetek visszafejtését."; /* No comment provided by engineer. */ "**Please note**: you will NOT be able to recover or change passphrase if you lose it." = "**Figyelem**: NEM tudja visszaállítani vagy megváltoztatni jelmondatát, ha elveszíti azt."; @@ -359,6 +359,9 @@ /* No comment provided by engineer. */ "Acknowledgement errors" = "Nyugtázott hibák"; +/* No comment provided by engineer. */ +"Active connections" = "Aktív kapcsolatok száma"; + /* No comment provided by engineer. */ "Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." = "Cím hozzáadása a profilhoz, hogy az ismerősei megoszthassák másokkal. A profilfrissítés elküldésre kerül az ismerősei számára."; @@ -630,7 +633,7 @@ "Auto-accept contact requests" = "Kapcsolódási kérelmek automatikus elfogadása"; /* No comment provided by engineer. */ -"Auto-accept images" = "Fotók automatikus elfogadása"; +"Auto-accept images" = "Képek automatikus elfogadása"; /* No comment provided by engineer. */ "Back" = "Vissza"; @@ -686,7 +689,7 @@ /* rcv group event chat item */ "blocked %@" = "letiltotta őt: %@"; -/* marked deleted chat item preview text */ +/* blocked chat item */ "blocked by admin" = "letiltva az admin által"; /* No comment provided by engineer. */ @@ -909,6 +912,9 @@ /* No comment provided by engineer. */ "Configure ICE servers" = "ICE kiszolgálók beállítása"; +/* No comment provided by engineer. */ +"Configured %@ servers" = "Beállított %@ kiszolgálók"; + /* No comment provided by engineer. */ "Confirm" = "Megerősítés"; @@ -1291,7 +1297,7 @@ "Delete all files" = "Minden fájl törlése"; /* No comment provided by engineer. */ -"Delete and notify contact" = "Törlés és ismerős értesítése"; +"Delete and notify contact" = "Törlés, és az ismerős értesítése"; /* No comment provided by engineer. */ "Delete archive" = "Archívum törlése"; @@ -1336,7 +1342,7 @@ "Delete for everyone" = "Törlés mindenkinél"; /* No comment provided by engineer. */ -"Delete for me" = "Törlés nálam"; +"Delete for me" = "Csak nálam"; /* No comment provided by engineer. */ "Delete group" = "Csoport törlése"; @@ -1411,7 +1417,7 @@ "Delivery" = "Kézbesítés"; /* No comment provided by engineer. */ -"Delivery receipts are disabled!" = "Kézbesítési igazolások kikapcsolva!"; +"Delivery receipts are disabled!" = "A kézbesítési jelentések ki vannak kapcsolva!"; /* No comment provided by engineer. */ "Delivery receipts!" = "Kézbesítési igazolások!"; @@ -1447,10 +1453,10 @@ "Device" = "Eszköz"; /* No comment provided by engineer. */ -"Device authentication is disabled. Turning off SimpleX Lock." = "Eszközhitelesítés kikapcsolva. SimpleX zárolás kikapcsolása."; +"Device authentication is disabled. Turning off SimpleX Lock." = "A készüléken nincs beállítva a képernyőzár. A SimpleX zár ki van kapcsolva."; /* No comment provided by engineer. */ -"Device authentication is not enabled. You can turn on SimpleX Lock via Settings, once you enable device authentication." = "Eszközhitelesítés nem engedélyezett.A SimpleX zárolás bekapcsolható a Beállításokon keresztül, miután az eszköz hitelesítés engedélyezésre került."; +"Device authentication is not enabled. You can turn on SimpleX Lock via Settings, once you enable device authentication." = "A készüléken nincs beállítva a képernyőzár. A SimpleX zár az „Adatvédelem és biztonság” menüben kapcsolható be, miután beállította a képernyőzárat az eszközén."; /* No comment provided by engineer. */ "different migration in the app/database: %@ / %@" = "különböző átköltöztetések az alkalmazásban/adatbázisban: %@ / %@"; @@ -1474,7 +1480,7 @@ "Disable for all" = "Letiltás mindenki számára"; /* authentication reason */ -"Disable SimpleX Lock" = "SimpleX zárolás kikapcsolása"; +"Disable SimpleX Lock" = "SimpleX zár kikapcsolása"; /* No comment provided by engineer. */ "disabled" = "letiltva"; @@ -1618,7 +1624,7 @@ "Enable self-destruct passcode" = "Önmegsemmisítő jelkód engedélyezése"; /* authentication reason */ -"Enable SimpleX Lock" = "SimpleX zárolás engedélyezése"; +"Enable SimpleX Lock" = "SimpleX zár bekapcsolása"; /* No comment provided by engineer. */ "Enable TCP keep-alive" = "TCP életben tartása"; @@ -2471,7 +2477,7 @@ "It allows having many anonymous connections without any shared data between them in a single chat profile." = "Lehetővé teszi, hogy egyetlen csevegőprofilon belül több anonim kapcsolat legyen, anélkül, hogy megosztott adatok lennének közöttük."; /* No comment provided by engineer. */ -"It can happen when you or your connection used the old database backup." = "Ez akkor fordulhat elő, ha ön vagy a kapcsolata régi adatbázis biztonsági mentést használt."; +"It can happen when you or your connection used the old database backup." = "Ez akkor fordulhat elő, ha ön vagy az ismerőse régi adatbázis biztonsági mentést használt."; /* 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." = "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 vagy az ismerőse régebbi adatbázis biztonsági mentést használt.\n3. A kapcsolat sérült."; @@ -2689,6 +2695,9 @@ /* notification */ "message received" = "üzenet érkezett"; +/* No comment provided by engineer. */ +"Message reception" = "Üzenetjelentés"; + /* No comment provided by engineer. */ "Message routing fallback" = "Üzenet útválasztási tartalék"; @@ -2940,8 +2949,8 @@ time to disappear */ "off" = "kikapcsolva"; -/* No comment provided by engineer. */ -"Off" = "Ki"; +/* blur media */ +"Off" = "Kikapcsolva"; /* feature offered item */ "offered %@" = "%@ ajánlotta"; @@ -2962,7 +2971,7 @@ "Old database archive" = "Régi adatbázis archívum"; /* group pref value */ -"on" = "be"; +"on" = "bekapcsolva"; /* No comment provided by engineer. */ "One-time invitation link" = "Egyszer használatos meghívó hivatkozás"; @@ -3066,6 +3075,9 @@ /* No comment provided by engineer. */ "Other" = "További"; +/* No comment provided by engineer. */ +"Other %@ servers" = "További %@ kiszolgálók"; + /* No comment provided by engineer. */ "other errors" = "egyéb hibák"; @@ -3219,6 +3231,9 @@ /* No comment provided by engineer. */ "Private routing" = "Privát útválasztás"; +/* No comment provided by engineer. */ +"Private routing error" = "Privát útválasztási hiba"; + /* No comment provided by engineer. */ "Profile and server connections" = "Profil és kiszolgálókapcsolatok"; @@ -3457,7 +3472,7 @@ "removed contact address" = "törölt kapcsolattartási cím"; /* profile update event chat item */ -"removed profile picture" = "törölt profilkép"; +"removed profile picture" = "törölte a profilképét"; /* rcv group event chat item */ "removed you" = "eltávolítottak"; @@ -3819,6 +3834,9 @@ /* No comment provided by engineer. */ "Server address" = "Kiszolgáló címe"; +/* No comment provided by engineer. */ +"Server address is incompatible with network settings: %@." = "A kiszolgáló címe nem kompatibilis a hálózati beállításokkal: %@."; + /* srv error text. */ "Server address is incompatible with network settings." = "A kiszolgáló címe nem kompatibilis a hálózati beállításokkal."; @@ -3840,6 +3858,9 @@ /* srv error text */ "Server version is incompatible with network settings." = "A kiszolgáló verziója nem kompatibilis a hálózati beállításokkal."; +/* No comment provided by engineer. */ +"Server version is incompatible with your app: %@." = "A kiszolgáló verziója nem kompatibilis az alkalmazással: %@."; + /* No comment provided by engineer. */ "Servers" = "Kiszolgálók"; @@ -3930,6 +3951,9 @@ /* No comment provided by engineer. */ "Show message status" = "Üzenet állapot megjelenítése"; +/* No comment provided by engineer. */ +"Show percentage" = "Százalék megjelenítése"; + /* No comment provided by engineer. */ "Show preview" = "Előnézet megjelenítése"; @@ -3970,16 +3994,16 @@ "SimpleX links not allowed" = "A SimpleX hivatkozások küldése le van tiltva"; /* No comment provided by engineer. */ -"SimpleX Lock" = "SimpleX zárolás"; +"SimpleX Lock" = "SimpleX zár"; /* No comment provided by engineer. */ -"SimpleX Lock mode" = "SimpleX zárolási mód"; +"SimpleX Lock mode" = "Zárolási mód"; /* No comment provided by engineer. */ -"SimpleX Lock not enabled!" = "SimpleX zárolás nincs engedélyezve!"; +"SimpleX Lock not enabled!" = "A SimpleX zár nincs bekapcsolva!"; /* No comment provided by engineer. */ -"SimpleX Lock turned on" = "SimpleX zárolás bekapcsolva"; +"SimpleX Lock turned on" = "SimpleX zár bekapcsolva"; /* simplex link type */ "SimpleX one-time invitation" = "SimpleX egyszer használatos meghívó"; @@ -4096,7 +4120,7 @@ "System authentication" = "Rendszerhitelesítés"; /* No comment provided by engineer. */ -"Take picture" = "Fotó készítése"; +"Take picture" = "Kép készítése"; /* No comment provided by engineer. */ "Tap button " = "Koppintson a "; @@ -4228,7 +4252,7 @@ "They can be overridden in contact and group settings." = "Ezek felülbírálhatóak az ismerős- és csoportbeállításokban."; /* No comment provided by engineer. */ -"This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." = "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ú fotók viszont megmaradnak."; +"This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." = "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."; /* No comment provided by engineer. */ "This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes." = "Ez a művelet nem vonható vissza - a kiválasztottnál korábban küldött és fogadott üzenetek törlésre kerülnek. Ez több percet is igénybe vehet."; @@ -4291,7 +4315,7 @@ "To protect timezone, image/voice files use UTC." = "Az időzóna védelme érdekében a kép-/hangfájlok UTC-t használnak."; /* 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." = "Az adatavédelem érdekében kapcsolja be a SimpleX zárolás funkciót.\nA funkció engedélyezése előtt a rendszer felszólítja a hitelesítés befejezésére."; +"To protect your information, turn on SimpleX Lock.\nYou will be prompted to complete authentication before this feature is enabled." = "A biztonsága érdekében kapcsolja be a SimpleX zár funkciót.\nA funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beállítására az eszközén."; /* No comment provided by engineer. */ "To protect your IP address, private routing uses your SMP servers to deliver messages." = "Az IP-cím védelmének érdekében a privát útválasztás az SMP kiszolgálókat használja az üzenetek kézbesítéséhez."; @@ -4300,7 +4324,7 @@ "To record voice message please grant permission to use Microphone." = "Hangüzenet rögzítéséhez adjon engedélyt a mikrofon használathoz."; /* No comment provided by engineer. */ -"To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." = "Rejtett profilja feltárásához írja be a teljes jelszót a keresőmezőbe a **Csevegési profiljai** oldalon."; +"To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." = "Rejtett profilja megjelenítéséhez írja be a teljes jelszavát a keresőmezőbe a **Csevegési profilok** menüben."; /* No comment provided by engineer. */ "To support instant push notifications the chat database has to be migrated." = "Az azonnali push értesítések támogatásához a csevegési adatbázis migrálása szükséges."; @@ -4795,7 +4819,7 @@ "You can give another try." = "Megpróbálhatja még egyszer."; /* No comment provided by engineer. */ -"You can hide or mute a user profile - swipe it to the right." = "Elrejthet vagy némíthat egy felhasználói profilt – csúsztasson jobbra."; +"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."; @@ -4819,7 +4843,7 @@ "You can start chat via app Settings / Database or by restarting the app" = "A csevegést az alkalmazás Beállítások / Adatbázis menü segítségével vagy az alkalmazás újraindításával indíthatja el"; /* No comment provided by engineer. */ -"You can turn on SimpleX Lock via Settings." = "A SimpleX zárolás a Beállításokon keresztül kapcsolható be."; +"You can turn on SimpleX Lock via Settings." = "A SimpleX zár az „Adatvédelem és biztonság” menüben kapcsolható be."; /* No comment provided by engineer. */ "You can use markdown to format messages:" = "Üzenetek formázása a szövegbe szúrt speciális karakterekkel:"; @@ -4945,7 +4969,7 @@ "Your chat database is not encrypted - set passphrase to encrypt it." = "A csevegési adatbázis nincs titkosítva – adjon meg egy jelmondatot a titkosításhoz."; /* No comment provided by engineer. */ -"Your chat profiles" = "Csevegési profiljai"; +"Your chat profiles" = "Csevegési profilok"; /* No comment provided by engineer. */ "Your contact needs to be online for the connection to complete.\nYou can cancel this connection and remove the contact (and try later with a new link)." = "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)."; diff --git a/apps/ios/it.lproj/Localizable.strings b/apps/ios/it.lproj/Localizable.strings index 24b604ecc0..5854d28162 100644 --- a/apps/ios/it.lproj/Localizable.strings +++ b/apps/ios/it.lproj/Localizable.strings @@ -359,6 +359,9 @@ /* No comment provided by engineer. */ "Acknowledgement errors" = "Errori di riconoscimento"; +/* No comment provided by engineer. */ +"Active connections" = "Connessioni attive"; + /* No comment provided by engineer. */ "Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." = "Aggiungi l'indirizzo al tuo profilo, in modo che i tuoi contatti possano condividerlo con altre persone. L'aggiornamento del profilo verrà inviato ai tuoi contatti."; @@ -686,7 +689,7 @@ /* rcv group event chat item */ "blocked %@" = "ha bloccato %@"; -/* marked deleted chat item preview text */ +/* blocked chat item */ "blocked by admin" = "bloccato dall'amministratore"; /* No comment provided by engineer. */ @@ -909,6 +912,9 @@ /* No comment provided by engineer. */ "Configure ICE servers" = "Configura server ICE"; +/* No comment provided by engineer. */ +"Configured %@ servers" = "Configurati %@ server"; + /* No comment provided by engineer. */ "Confirm" = "Conferma"; @@ -2689,6 +2695,9 @@ /* notification */ "message received" = "messaggio ricevuto"; +/* No comment provided by engineer. */ +"Message reception" = "Ricezione messaggi"; + /* No comment provided by engineer. */ "Message routing fallback" = "Ripiego instradamento messaggio"; @@ -2940,7 +2949,7 @@ time to disappear */ "off" = "off"; -/* No comment provided by engineer. */ +/* blur media */ "Off" = "Off"; /* feature offered item */ @@ -3066,6 +3075,9 @@ /* No comment provided by engineer. */ "Other" = "Altro"; +/* No comment provided by engineer. */ +"Other %@ servers" = "Altri %@ server"; + /* No comment provided by engineer. */ "other errors" = "altri errori"; @@ -3219,6 +3231,9 @@ /* No comment provided by engineer. */ "Private routing" = "Instradamento privato"; +/* No comment provided by engineer. */ +"Private routing error" = "Errore di instradamento privato"; + /* No comment provided by engineer. */ "Profile and server connections" = "Profilo e connessioni al server"; @@ -3819,6 +3834,9 @@ /* No comment provided by engineer. */ "Server address" = "Indirizzo server"; +/* No comment provided by engineer. */ +"Server address is incompatible with network settings: %@." = "L'indirizzo del server è incompatibile con le impostazioni di rete: %@."; + /* srv error text. */ "Server address is incompatible with network settings." = "L'indirizzo del server non è compatibile con le impostazioni di rete."; @@ -3840,6 +3858,9 @@ /* srv error text */ "Server version is incompatible with network settings." = "La versione del server non è compatibile con le impostazioni di rete."; +/* No comment provided by engineer. */ +"Server version is incompatible with your app: %@." = "La versione del server è incompatibile con la tua app: %@."; + /* No comment provided by engineer. */ "Servers" = "Server"; @@ -3930,6 +3951,9 @@ /* No comment provided by engineer. */ "Show message status" = "Mostra stato del messaggio"; +/* No comment provided by engineer. */ +"Show percentage" = "Mostra percentuale"; + /* No comment provided by engineer. */ "Show preview" = "Mostra anteprima"; diff --git a/apps/ios/ja.lproj/Localizable.strings b/apps/ios/ja.lproj/Localizable.strings index 0f5ccc2b8c..7c8897e6cb 100644 --- a/apps/ios/ja.lproj/Localizable.strings +++ b/apps/ios/ja.lproj/Localizable.strings @@ -2238,7 +2238,7 @@ time to disappear */ "off" = "オフ"; -/* No comment provided by engineer. */ +/* blur media */ "Off" = "オフ"; /* feature offered item */ diff --git a/apps/ios/nl.lproj/Localizable.strings b/apps/ios/nl.lproj/Localizable.strings index 9cd3c078b3..23eccdcf3b 100644 --- a/apps/ios/nl.lproj/Localizable.strings +++ b/apps/ios/nl.lproj/Localizable.strings @@ -334,6 +334,9 @@ /* No comment provided by engineer. */ "above, then choose:" = "hier boven, kies dan:"; +/* No comment provided by engineer. */ +"Accent" = "Accent"; + /* accept contact request via notification accept incoming call via notification */ "Accept" = "Accepteer"; @@ -350,6 +353,15 @@ /* call status */ "accepted call" = "geaccepteerde oproep"; +/* No comment provided by engineer. */ +"Acknowledged" = "Erkend"; + +/* No comment provided by engineer. */ +"Acknowledgement errors" = "Bevestigingsfouten"; + +/* No comment provided by engineer. */ +"Active connections" = "Actieve verbindingen"; + /* No comment provided by engineer. */ "Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." = "Voeg een adres toe aan uw profiel, zodat uw contacten het met andere mensen kunnen delen. Profiel update wordt naar uw contacten verzonden."; @@ -372,7 +384,16 @@ "Add to another device" = "Toevoegen aan een ander apparaat"; /* No comment provided by engineer. */ -"Add welcome message" = "Welkomst bericht toevoegen"; +"Add welcome message" = "Welkom bericht toevoegen"; + +/* No comment provided by engineer. */ +"Additional accent" = "Extra accent"; + +/* No comment provided by engineer. */ +"Additional accent 2" = "Extra accent 2"; + +/* No comment provided by engineer. */ +"Additional secondary" = "Extra secundair"; /* No comment provided by engineer. */ "Address" = "Adres"; @@ -395,6 +416,9 @@ /* No comment provided by engineer. */ "Advanced network settings" = "Geavanceerde netwerk instellingen"; +/* No comment provided by engineer. */ +"Advanced settings" = "Geavanceerde instellingen"; + /* chat item text */ "agreeing encryption for %@…" = "versleuteling overeenkomen voor %@…"; @@ -410,6 +434,9 @@ /* No comment provided by engineer. */ "All data is erased when it is entered." = "Alle gegevens worden bij het invoeren gewist."; +/* No comment provided by engineer. */ +"All data is private to your device." = "Alle gegevens zijn privé op uw apparaat."; + /* No comment provided by engineer. */ "All group members will remain connected." = "Alle groepsleden blijven verbonden."; @@ -425,6 +452,9 @@ /* No comment provided by engineer. */ "All new messages from %@ will be hidden!" = "Alle nieuwe berichten van %@ worden verborgen!"; +/* No comment provided by engineer. */ +"All profiles" = "Alle profielen"; + /* No comment provided by engineer. */ "All your contacts will remain connected." = "Al uw contacten blijven verbonden."; @@ -450,10 +480,10 @@ "Allow irreversible message deletion only if your contact allows it to you. (24 hours)" = "Sta het onomkeerbaar verwijderen van berichten alleen toe als uw contact dit toestaat. (24 uur)"; /* No comment provided by engineer. */ -"Allow message reactions only if your contact allows them." = "Sta berichtreacties alleen toe als uw contact dit toestaat."; +"Allow message reactions only if your contact allows them." = "Sta bericht reacties alleen toe als uw contact dit toestaat."; /* No comment provided by engineer. */ -"Allow message reactions." = "Sta berichtreacties toe."; +"Allow message reactions." = "Sta bericht reacties toe."; /* No comment provided by engineer. */ "Allow sending direct messages to members." = "Sta het verzenden van directe berichten naar leden toe."; @@ -480,7 +510,7 @@ "Allow voice messages?" = "Spraak berichten toestaan?"; /* No comment provided by engineer. */ -"Allow your contacts adding message reactions." = "Sta uw contactpersonen toe om berichtreacties toe te voegen."; +"Allow your contacts adding message reactions." = "Sta uw contactpersonen toe om bericht reacties toe te voegen."; /* No comment provided by engineer. */ "Allow your contacts to call you." = "Sta toe dat uw contacten u bellen."; @@ -551,6 +581,9 @@ /* No comment provided by engineer. */ "Apply" = "Toepassen"; +/* No comment provided by engineer. */ +"Apply to" = "Toepassen op"; + /* No comment provided by engineer. */ "Archive and upload" = "Archiveren en uploaden"; @@ -560,6 +593,9 @@ /* No comment provided by engineer. */ "Attach" = "Bijvoegen"; +/* No comment provided by engineer. */ +"attempts" = "pogingen"; + /* No comment provided by engineer. */ "Audio & video calls" = "Audio en video gesprekken"; @@ -602,6 +638,9 @@ /* No comment provided by engineer. */ "Back" = "Terug"; +/* No comment provided by engineer. */ +"Background" = "Achtergrond"; + /* No comment provided by engineer. */ "Bad desktop address" = "Onjuist desktopadres"; @@ -623,6 +662,9 @@ /* No comment provided by engineer. */ "Better messages" = "Betere berichten"; +/* No comment provided by engineer. */ +"Black" = "Zwart"; + /* No comment provided by engineer. */ "Block" = "Blokkeren"; @@ -647,7 +689,7 @@ /* rcv group event chat item */ "blocked %@" = "geblokkeerd %@"; -/* marked deleted chat item preview text */ +/* blocked chat item */ "blocked by admin" = "geblokkeerd door beheerder"; /* No comment provided by engineer. */ @@ -657,7 +699,7 @@ "bold" = "vetgedrukt"; /* No comment provided by engineer. */ -"Both you and your contact can add message reactions." = "Zowel u als uw contact kunnen berichtreacties toevoegen."; +"Both you and your contact can add message reactions." = "Zowel u als uw contact kunnen bericht reacties toevoegen."; /* No comment provided by engineer. */ "Both you and your contact can irreversibly delete sent messages. (24 hours)" = "Zowel jij als je contact kunnen verzonden berichten onherroepelijk verwijderen. (24 uur)"; @@ -713,6 +755,9 @@ /* No comment provided by engineer. */ "Cannot access keychain to save database password" = "Geen toegang tot de keychain om database wachtwoord op te slaan"; +/* No comment provided by engineer. */ +"Cannot forward message" = "Kan bericht niet doorsturen"; + /* No comment provided by engineer. */ "Cannot receive file" = "Kan bestand niet ontvangen"; @@ -771,6 +816,9 @@ /* No comment provided by engineer. */ "Chat archive" = "Gesprek archief"; +/* No comment provided by engineer. */ +"Chat colors" = "Chat kleuren"; + /* No comment provided by engineer. */ "Chat console" = "Chat console"; @@ -798,6 +846,9 @@ /* No comment provided by engineer. */ "Chat preferences" = "Gesprek voorkeuren"; +/* No comment provided by engineer. */ +"Chat theme" = "Chat thema"; + /* No comment provided by engineer. */ "Chats" = "Gesprekken"; @@ -816,6 +867,15 @@ /* No comment provided by engineer. */ "Choose from library" = "Kies uit bibliotheek"; +/* No comment provided by engineer. */ +"Chunks deleted" = "Stukken verwijderd"; + +/* No comment provided by engineer. */ +"Chunks downloaded" = "Stukken gedownload"; + +/* No comment provided by engineer. */ +"Chunks uploaded" = "Stukken geüpload"; + /* No comment provided by engineer. */ "Clear" = "Wissen"; @@ -831,6 +891,9 @@ /* No comment provided by engineer. */ "Clear verification" = "Verwijderd verificatie"; +/* No comment provided by engineer. */ +"Color mode" = "Kleur mode"; + /* No comment provided by engineer. */ "colored" = "gekleurd"; @@ -843,9 +906,15 @@ /* No comment provided by engineer. */ "complete" = "compleet"; +/* No comment provided by engineer. */ +"Completed" = "voltooid"; + /* No comment provided by engineer. */ "Configure ICE servers" = "ICE servers configureren"; +/* No comment provided by engineer. */ +"Configured %@ servers" = "%@ servers geconfigureerd"; + /* No comment provided by engineer. */ "Confirm" = "Bevestigen"; @@ -912,18 +981,27 @@ /* No comment provided by engineer. */ "connected" = "verbonden"; +/* No comment provided by engineer. */ +"Connected" = "Verbonden"; + /* No comment provided by engineer. */ "Connected desktop" = "Verbonden desktop"; /* rcv group event chat item */ "connected directly" = "direct verbonden"; +/* No comment provided by engineer. */ +"Connected servers" = "Verbonden servers"; + /* No comment provided by engineer. */ "Connected to desktop" = "Verbonden met desktop"; /* No comment provided by engineer. */ "connecting" = "Verbinden"; +/* No comment provided by engineer. */ +"Connecting" = "Verbinden"; + /* No comment provided by engineer. */ "connecting (accepted)" = "verbinden (geaccepteerd)"; @@ -972,9 +1050,15 @@ /* No comment provided by engineer. */ "Connection timeout" = "Timeout verbinding"; +/* No comment provided by engineer. */ +"Connection with desktop stopped" = "Verbinding met desktop is gestopt"; + /* connection information */ "connection:%@" = "verbinding:%@"; +/* No comment provided by engineer. */ +"Connections" = "Verbindingen"; + /* profile update event chat item */ "contact %@ changed to %@" = "contactpersoon %1$@ gewijzigd in %2$@"; @@ -1017,6 +1101,9 @@ /* No comment provided by engineer. */ "Copy" = "Kopiëren"; +/* No comment provided by engineer. */ +"Copy error" = "Kopieerfout"; + /* No comment provided by engineer. */ "Core version: v%@" = "Core versie: v% @"; @@ -1062,6 +1149,9 @@ /* No comment provided by engineer. */ "Create your profile" = "Maak je profiel aan"; +/* No comment provided by engineer. */ +"Created" = "Gemaakt"; + /* No comment provided by engineer. */ "Created at" = "Gemaakt op"; @@ -1086,6 +1176,9 @@ /* No comment provided by engineer. */ "Current passphrase…" = "Huidige wachtwoord…"; +/* No comment provided by engineer. */ +"Current profile" = "Huidig profiel"; + /* No comment provided by engineer. */ "Currently maximum supported file size is %@." = "De momenteel maximaal ondersteunde bestandsgrootte is %@."; @@ -1095,9 +1188,15 @@ /* No comment provided by engineer. */ "Custom time" = "Aangepaste tijd"; +/* No comment provided by engineer. */ +"Customize theme" = "Thema aanpassen"; + /* No comment provided by engineer. */ "Dark" = "Donker"; +/* No comment provided by engineer. */ +"Dark mode colors" = "Kleuren in donkere modus"; + /* No comment provided by engineer. */ "Database downgrade" = "Database downgraden"; @@ -1167,6 +1266,9 @@ /* message decrypt error item */ "Decryption error" = "Decodering fout"; +/* No comment provided by engineer. */ +"decryption errors" = "decoderingsfouten"; + /* pref value */ "default (%@)" = "standaard (%@)"; @@ -1293,6 +1395,9 @@ /* deleted chat item */ "deleted" = "verwijderd"; +/* No comment provided by engineer. */ +"Deleted" = "Verwijderd"; + /* No comment provided by engineer. */ "Deleted at" = "Verwijderd om"; @@ -1305,6 +1410,9 @@ /* rcv group event chat item */ "deleted group" = "verwijderde groep"; +/* No comment provided by engineer. */ +"Deletion errors" = "Verwijderingsfouten"; + /* No comment provided by engineer. */ "Delivery" = "Bezorging"; @@ -1329,6 +1437,12 @@ /* snd error text */ "Destination server error: %@" = "Bestemmingsserverfout: %@"; +/* No comment provided by engineer. */ +"Detailed statistics" = "Gedetailleerde statistieken"; + +/* No comment provided by engineer. */ +"Details" = "Details"; + /* No comment provided by engineer. */ "Develop" = "Ontwikkelen"; @@ -1431,12 +1545,21 @@ /* chat item action */ "Download" = "Downloaden"; +/* No comment provided by engineer. */ +"Download errors" = "Downloadfouten"; + /* No comment provided by engineer. */ "Download failed" = "Download mislukt"; /* server test step */ "Download file" = "Download bestand"; +/* No comment provided by engineer. */ +"Downloaded" = "Gedownload"; + +/* No comment provided by engineer. */ +"Downloaded files" = "Gedownloade bestanden"; + /* No comment provided by engineer. */ "Downloading archive" = "Archief downloaden"; @@ -1449,6 +1572,9 @@ /* integrity error chat item */ "duplicate message" = "dubbel bericht"; +/* No comment provided by engineer. */ +"duplicates" = "duplicaten"; + /* No comment provided by engineer. */ "Duration" = "Duur"; @@ -1612,10 +1738,10 @@ "Enter this device name…" = "Voer deze apparaatnaam in…"; /* placeholder */ -"Enter welcome message…" = "Welkomst bericht invoeren…"; +"Enter welcome message…" = "Welkom bericht invoeren…"; /* placeholder */ -"Enter welcome message… (optional)" = "Voer welkomst bericht in... (optioneel)"; +"Enter welcome message… (optional)" = "Voer welkom bericht in... (optioneel)"; /* No comment provided by engineer. */ "Enter your name…" = "Vul uw naam in…"; @@ -1707,6 +1833,9 @@ /* No comment provided by engineer. */ "Error exporting chat database" = "Fout bij het exporteren van de chat database"; +/* No comment provided by engineer. */ +"Error exporting theme: %@" = "Fout bij exporteren van thema: %@"; + /* No comment provided by engineer. */ "Error importing chat database" = "Fout bij het importeren van de chat database"; @@ -1722,9 +1851,18 @@ /* No comment provided by engineer. */ "Error receiving file" = "Fout bij ontvangen van bestand"; +/* No comment provided by engineer. */ +"Error reconnecting server" = "Fout bij opnieuw verbinding maken met de server"; + +/* No comment provided by engineer. */ +"Error reconnecting servers" = "Fout bij opnieuw verbinden van servers"; + /* No comment provided by engineer. */ "Error removing member" = "Fout bij verwijderen van lid"; +/* No comment provided by engineer. */ +"Error resetting statistics" = "Fout bij het resetten van statistieken"; + /* No comment provided by engineer. */ "Error saving %@ servers" = "Fout bij opslaan van %@ servers"; @@ -1804,6 +1942,9 @@ /* No comment provided by engineer. */ "Error: URL is invalid" = "Fout: URL is ongeldig"; +/* No comment provided by engineer. */ +"Errors" = "Fouten"; + /* No comment provided by engineer. */ "Even when disabled in the conversation." = "Zelfs wanneer uitgeschakeld in het gesprek."; @@ -1816,12 +1957,18 @@ /* chat item action */ "Expand" = "Uitklappen"; +/* No comment provided by engineer. */ +"expired" = "verlopen"; + /* No comment provided by engineer. */ "Export database" = "Database exporteren"; /* No comment provided by engineer. */ "Export error:" = "Exportfout:"; +/* No comment provided by engineer. */ +"Export theme" = "Exporteer thema"; + /* No comment provided by engineer. */ "Exported database archive." = "Geëxporteerd database archief."; @@ -1843,6 +1990,21 @@ /* No comment provided by engineer. */ "Favorite" = "Favoriet"; +/* No comment provided by engineer. */ +"File error" = "Bestandsfout"; + +/* file error text */ +"File not found - most likely file was deleted or cancelled." = "Bestand niet gevonden - hoogstwaarschijnlijk is het bestand verwijderd of geannuleerd."; + +/* file error text */ +"File server error: %@" = "Bestandsserverfout: %@"; + +/* No comment provided by engineer. */ +"File status" = "Bestandsstatus"; + +/* copied message info */ +"File status: %@" = "Bestandsstatus: %@"; + /* No comment provided by engineer. */ "File will be deleted from servers." = "Het bestand wordt van de servers verwijderd."; @@ -1957,6 +2119,12 @@ /* No comment provided by engineer. */ "GIFs and stickers" = "GIF's en stickers"; +/* message preview */ +"Good afternoon!" = "Goedemiddag!"; + +/* message preview */ +"Good morning!" = "Goedemorgen!"; + /* No comment provided by engineer. */ "Group" = "Groep"; @@ -1994,7 +2162,7 @@ "Group links" = "Groep links"; /* No comment provided by engineer. */ -"Group members can add message reactions." = "Groepsleden kunnen berichtreacties toevoegen."; +"Group members can add message reactions." = "Groepsleden kunnen bericht reacties toevoegen."; /* No comment provided by engineer. */ "Group members can irreversibly delete sent messages. (24 hours)" = "Groepsleden kunnen verzonden berichten onherroepelijk verwijderen. (24 uur)"; @@ -2033,7 +2201,7 @@ "group profile updated" = "groep profiel bijgewerkt"; /* No comment provided by engineer. */ -"Group welcome message" = "Groep welkomst bericht"; +"Group welcome message" = "Groep welkom bericht"; /* No comment provided by engineer. */ "Group will be deleted for all members - this cannot be undone!" = "Groep wordt verwijderd voor alle leden, dit kan niet ongedaan worden gemaakt!"; @@ -2134,6 +2302,9 @@ /* No comment provided by engineer. */ "Import failed" = "Importeren is mislukt"; +/* No comment provided by engineer. */ +"Import theme" = "Thema importeren"; + /* No comment provided by engineer. */ "Importing archive" = "Archief importeren"; @@ -2155,6 +2326,9 @@ /* No comment provided by engineer. */ "In-call sounds" = "Geluiden tijdens het bellen"; +/* No comment provided by engineer. */ +"inactive" = "inactief"; + /* No comment provided by engineer. */ "Incognito" = "Incognito"; @@ -2218,6 +2392,9 @@ /* No comment provided by engineer. */ "Interface" = "Interface"; +/* No comment provided by engineer. */ +"Interface colors" = "Interface kleuren"; + /* invalid chat data */ "invalid chat" = "ongeldige gesprek"; @@ -2470,6 +2647,9 @@ /* rcv group event chat item */ "member connected" = "is toegetreden"; +/* item status text */ +"Member inactive" = "Lid inactief"; + /* No comment provided by engineer. */ "Member role will be changed to \"%@\". All group members will be notified." = "De rol van lid wordt gewijzigd in \"%@\". Alle groepsleden worden op de hoogte gebracht."; @@ -2479,6 +2659,9 @@ /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "Lid wordt uit de groep verwijderd, dit kan niet ongedaan worden gemaakt!"; +/* No comment provided by engineer. */ +"Menus" = "Menu's"; + /* item status text */ "Message delivery error" = "Fout bij bezorging van bericht"; @@ -2491,6 +2674,12 @@ /* No comment provided by engineer. */ "Message draft" = "Concept bericht"; +/* item status text */ +"Message forwarded" = "Bericht doorgestuurd"; + +/* item status description */ +"Message may be delivered later if member becomes active." = "Het bericht kan later worden bezorgd als het lid actief wordt."; + /* No comment provided by engineer. */ "Message queue info" = "Informatie over berichtenwachtrij"; @@ -2506,6 +2695,9 @@ /* notification */ "message received" = "bericht ontvangen"; +/* No comment provided by engineer. */ +"Message reception" = "Bericht ontvangst"; + /* No comment provided by engineer. */ "Message routing fallback" = "Terugval op berichtroutering"; @@ -2515,6 +2707,12 @@ /* No comment provided by engineer. */ "Message source remains private." = "Berichtbron blijft privé."; +/* No comment provided by engineer. */ +"Message status" = "Berichtstatus"; + +/* copied message info */ +"Message status: %@" = "Berichtstatus: %@"; + /* No comment provided by engineer. */ "Message text" = "Bericht tekst"; @@ -2530,6 +2728,12 @@ /* No comment provided by engineer. */ "Messages from %@ will be shown!" = "Berichten van %@ worden getoond!"; +/* No comment provided by engineer. */ +"Messages received" = "Berichten ontvangen"; + +/* No comment provided by engineer. */ +"Messages sent" = "Berichten verzonden"; + /* 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." = "Berichten, bestanden en oproepen worden beschermd door **end-to-end codering** met perfecte voorwaartse geheimhouding, afwijzing en inbraakherstel."; @@ -2695,6 +2899,9 @@ /* No comment provided by engineer. */ "No device token!" = "Geen apparaattoken!"; +/* item status description */ +"No direct connection yet, message is forwarded by admin." = "Nog geen directe verbinding, bericht wordt doorgestuurd door beheerder."; + /* No comment provided by engineer. */ "no e2e encryption" = "geen e2e versleuteling"; @@ -2707,6 +2914,9 @@ /* No comment provided by engineer. */ "No history" = "Geen geschiedenis"; +/* No comment provided by engineer. */ +"No info, try to reload" = "Geen info, probeer opnieuw te laden"; + /* No comment provided by engineer. */ "No network connection" = "Geen netwerkverbinding"; @@ -2739,7 +2949,7 @@ time to disappear */ "off" = "uit"; -/* No comment provided by engineer. */ +/* blur media */ "Off" = "Uit"; /* feature offered item */ @@ -2788,7 +2998,7 @@ "Only group owners can enable voice messages." = "Alleen groep eigenaren kunnen spraak berichten inschakelen."; /* No comment provided by engineer. */ -"Only you can add message reactions." = "Alleen jij kunt berichtreacties toevoegen."; +"Only you can add message reactions." = "Alleen jij kunt bericht reacties toevoegen."; /* No comment provided by engineer. */ "Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)" = "Alleen jij kunt berichten onomkeerbaar verwijderen (je contact kan ze markeren voor verwijdering). (24 uur)"; @@ -2803,7 +3013,7 @@ "Only you can send voice messages." = "Alleen jij kunt spraak berichten verzenden."; /* No comment provided by engineer. */ -"Only your contact can add message reactions." = "Alleen uw contact kan berichtreacties toevoegen."; +"Only your contact can add message reactions." = "Alleen uw contact kan bericht reacties toevoegen."; /* No comment provided by engineer. */ "Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)" = "Alleen uw contact kan berichten onherroepelijk verwijderen (u kunt ze markeren voor verwijdering). (24 uur)"; @@ -2832,6 +3042,9 @@ /* authentication reason */ "Open migration to another device" = "Open de migratie naar een ander apparaat"; +/* No comment provided by engineer. */ +"Open server settings" = "Server instellingen openen"; + /* No comment provided by engineer. */ "Open Settings" = "Open instellingen"; @@ -2856,9 +3069,18 @@ /* No comment provided by engineer. */ "Or show this code" = "Of laat deze code zien"; +/* No comment provided by engineer. */ +"other" = "overig"; + /* No comment provided by engineer. */ "Other" = "Ander"; +/* No comment provided by engineer. */ +"Other %@ servers" = "Andere %@ servers"; + +/* No comment provided by engineer. */ +"other errors" = "overige fouten"; + /* member role */ "owner" = "Eigenaar"; @@ -2901,6 +3123,9 @@ /* No comment provided by engineer. */ "peer-to-peer" = "peer-to-peer"; +/* No comment provided by engineer. */ +"Pending" = "in behandeling"; + /* No comment provided by engineer. */ "People can connect to you only via the links you share." = "Mensen kunnen alleen verbinding met u maken via de links die u deelt."; @@ -2922,6 +3147,9 @@ /* No comment provided by engineer. */ "Please ask your contact to enable sending voice messages." = "Vraag uw contact om het verzenden van spraak berichten in te schakelen."; +/* 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." = "Controleer of mobiel en desktop met hetzelfde lokale netwerk zijn verbonden en of de desktopfirewall de verbinding toestaat.\nDeel eventuele andere problemen met de ontwikkelaars."; + /* No comment provided by engineer. */ "Please check that you used the correct link or ask your contact to send you another one." = "Controleer of u de juiste link heeft gebruikt of vraag uw contact om u een andere te sturen."; @@ -2979,6 +3207,9 @@ /* No comment provided by engineer. */ "Preview" = "Voorbeeld"; +/* No comment provided by engineer. */ +"Previously connected servers" = "Eerder verbonden servers"; + /* No comment provided by engineer. */ "Privacy & security" = "Privacy en beveiliging"; @@ -3000,6 +3231,9 @@ /* No comment provided by engineer. */ "Private routing" = "Privéroutering"; +/* No comment provided by engineer. */ +"Private routing error" = "Fout in privéroutering"; + /* No comment provided by engineer. */ "Profile and server connections" = "Profiel- en serververbindingen"; @@ -3018,6 +3252,9 @@ /* No comment provided by engineer. */ "Profile password" = "Profiel wachtwoord"; +/* No comment provided by engineer. */ +"Profile theme" = "Profiel thema"; + /* No comment provided by engineer. */ "Profile update will be sent to your contacts." = "Profiel update wordt naar uw contacten verzonden."; @@ -3028,7 +3265,7 @@ "Prohibit irreversible message deletion." = "Verbied het onomkeerbaar verwijderen van berichten."; /* No comment provided by engineer. */ -"Prohibit message reactions." = "Berichtreacties verbieden."; +"Prohibit message reactions." = "Bericht reacties verbieden."; /* No comment provided by engineer. */ "Prohibit messages reactions." = "Berichten reacties verbieden."; @@ -3066,6 +3303,12 @@ /* No comment provided by engineer. */ "Protocol timeout per KB" = "Protocol timeout per KB"; +/* No comment provided by engineer. */ +"Proxied" = "Proxied"; + +/* No comment provided by engineer. */ +"Proxied servers" = "Proxied servers"; + /* No comment provided by engineer. */ "Push notifications" = "Push meldingen"; @@ -3108,6 +3351,9 @@ /* No comment provided by engineer. */ "Receipts are disabled" = "Bevestigingen zijn uitgeschakeld"; +/* No comment provided by engineer. */ +"Receive errors" = "Fouten ontvangen"; + /* No comment provided by engineer. */ "received answer…" = "antwoord gekregen…"; @@ -3126,6 +3372,15 @@ /* message info title */ "Received message" = "Ontvangen bericht"; +/* No comment provided by engineer. */ +"Received messages" = "Ontvangen berichten"; + +/* No comment provided by engineer. */ +"Received reply" = "Antwoord ontvangen"; + +/* No comment provided by engineer. */ +"Received total" = "Totaal ontvangen"; + /* No comment provided by engineer. */ "Receiving address will be changed to a different server. Address change will complete after sender comes online." = "Het ontvangstadres wordt gewijzigd naar een andere server. Adres wijziging wordt voltooid nadat de afzender online is."; @@ -3144,9 +3399,24 @@ /* No comment provided by engineer. */ "Recipients see updates as you type them." = "Ontvangers zien updates terwijl u ze typt."; +/* No comment provided by engineer. */ +"Reconnect" = "opnieuw verbinden"; + /* No comment provided by engineer. */ "Reconnect all connected servers to force message delivery. It uses additional traffic." = "Verbind alle verbonden servers opnieuw om de bezorging van berichten af te dwingen. Het maakt gebruik van extra data."; +/* No comment provided by engineer. */ +"Reconnect all servers" = "Maak opnieuw verbinding met alle servers"; + +/* No comment provided by engineer. */ +"Reconnect all servers?" = "Alle servers opnieuw verbinden?"; + +/* No comment provided by engineer. */ +"Reconnect server to force message delivery. It uses additional traffic." = "Maak opnieuw verbinding met de server om de bezorging van berichten te forceren. Er wordt gebruik gemaakt van extra verkeer.Maak opnieuw verbinding met de server om de bezorging van berichten te forceren. Er wordt gebruik gemaakt van extra data."; + +/* No comment provided by engineer. */ +"Reconnect server?" = "Server opnieuw verbinden?"; + /* No comment provided by engineer. */ "Reconnect servers?" = "Servers opnieuw verbinden?"; @@ -3180,6 +3450,9 @@ /* No comment provided by engineer. */ "Remove" = "Verwijderen"; +/* No comment provided by engineer. */ +"Remove image" = "Verwijder afbeelding"; + /* No comment provided by engineer. */ "Remove member" = "Lid verwijderen"; @@ -3237,12 +3510,24 @@ /* No comment provided by engineer. */ "Reset" = "Resetten"; +/* No comment provided by engineer. */ +"Reset all statistics" = "Reset alle statistieken"; + +/* No comment provided by engineer. */ +"Reset all statistics?" = "Alle statistieken resetten?"; + /* No comment provided by engineer. */ "Reset colors" = "Kleuren resetten"; +/* No comment provided by engineer. */ +"Reset to app theme" = "Terugzetten naar app thema"; + /* No comment provided by engineer. */ "Reset to defaults" = "Resetten naar standaardwaarden"; +/* No comment provided by engineer. */ +"Reset to user theme" = "Terugzetten naar gebruikersthema"; + /* No comment provided by engineer. */ "Restart the app to create a new chat profile" = "Start de app opnieuw om een nieuw chat profiel aan te maken"; @@ -3337,7 +3622,7 @@ "Save settings?" = "Instellingen opslaan?"; /* No comment provided by engineer. */ -"Save welcome message?" = "Welkomst bericht opslaan?"; +"Save welcome message?" = "Welkom bericht opslaan?"; /* No comment provided by engineer. */ "saved" = "opgeslagen"; @@ -3357,6 +3642,12 @@ /* No comment provided by engineer. */ "Saved WebRTC ICE servers will be removed" = "Opgeslagen WebRTC ICE servers worden verwijderd"; +/* No comment provided by engineer. */ +"Scale" = "Schaal"; + +/* No comment provided by engineer. */ +"Scan / Paste link" = "Link scannen/plakken"; + /* No comment provided by engineer. */ "Scan code" = "Code scannen"; @@ -3384,6 +3675,9 @@ /* network option */ "sec" = "sec"; +/* No comment provided by engineer. */ +"Secondary" = "Secundair"; + /* time unit */ "seconds" = "seconden"; @@ -3393,6 +3687,9 @@ /* server test step */ "Secure queue" = "Veilige wachtrij"; +/* No comment provided by engineer. */ +"Secured" = "Beveiligd"; + /* No comment provided by engineer. */ "Security assessment" = "Beveiligingsbeoordeling"; @@ -3405,6 +3702,9 @@ /* No comment provided by engineer. */ "Select" = "Selecteer"; +/* No comment provided by engineer. */ +"Selected chat preferences prohibit this message." = "Geselecteerde chat voorkeuren verbieden dit bericht."; + /* No comment provided by engineer. */ "Self-destruct" = "Zelfvernietiging"; @@ -3438,6 +3738,9 @@ /* No comment provided by engineer. */ "Send disappearing message" = "Stuur een verdwijnend bericht"; +/* No comment provided by engineer. */ +"Send errors" = "Verzend fouten"; + /* No comment provided by engineer. */ "Send link previews" = "Link voorbeelden verzenden"; @@ -3504,15 +3807,36 @@ /* copied message info */ "Sent at: %@" = "Verzonden op: %@"; +/* No comment provided by engineer. */ +"Sent directly" = "Direct verzonden"; + /* notification */ "Sent file event" = "Verzonden bestandsgebeurtenis"; /* message info title */ "Sent message" = "Verzonden bericht"; +/* No comment provided by engineer. */ +"Sent messages" = "Verzonden berichten"; + /* No comment provided by engineer. */ "Sent messages will be deleted after set time." = "Verzonden berichten worden na ingestelde tijd verwijderd."; +/* No comment provided by engineer. */ +"Sent reply" = "Antwoord verzonden"; + +/* No comment provided by engineer. */ +"Sent total" = "Totaal verzonden"; + +/* No comment provided by engineer. */ +"Sent via proxy" = "Verzonden via proxy"; + +/* No comment provided by engineer. */ +"Server address" = "Server adres"; + +/* No comment provided by engineer. */ +"Server address is incompatible with network settings: %@." = "Serveradres is incompatibel met netwerkinstellingen: %@."; + /* srv error text. */ "Server address is incompatible with network settings." = "Serveradres is niet compatibel met netwerkinstellingen."; @@ -3528,12 +3852,24 @@ /* No comment provided by engineer. */ "Server test failed!" = "Servertest mislukt!"; +/* No comment provided by engineer. */ +"Server type" = "Server type"; + /* srv error text */ "Server version is incompatible with network settings." = "Serverversie is incompatibel met netwerkinstellingen."; +/* No comment provided by engineer. */ +"Server version is incompatible with your app: %@." = "Serverversie is incompatibel met uw app: %@."; + /* No comment provided by engineer. */ "Servers" = "Servers"; +/* No comment provided by engineer. */ +"Servers info" = "Server informatie"; + +/* No comment provided by engineer. */ +"Servers statistics will be reset - this cannot be undone!" = "Serverstatistieken worden gereset - dit kan niet ongedaan worden gemaakt!"; + /* No comment provided by engineer. */ "Session code" = "Sessie code"; @@ -3543,6 +3879,9 @@ /* No comment provided by engineer. */ "Set contact name…" = "Contactnaam instellen…"; +/* No comment provided by engineer. */ +"Set default theme" = "Stel het standaard thema in"; + /* No comment provided by engineer. */ "Set group preferences" = "Groep voorkeuren instellen"; @@ -3612,6 +3951,9 @@ /* No comment provided by engineer. */ "Show message status" = "Toon berichtstatus"; +/* No comment provided by engineer. */ +"Show percentage" = "Percentage weergeven"; + /* No comment provided by engineer. */ "Show preview" = "Toon voorbeeld"; @@ -3621,6 +3963,9 @@ /* No comment provided by engineer. */ "Show:" = "Toon:"; +/* No comment provided by engineer. */ +"SimpleX" = "SimpleX"; + /* No comment provided by engineer. */ "SimpleX address" = "SimpleX adres"; @@ -3666,6 +4011,9 @@ /* No comment provided by engineer. */ "Simplified incognito mode" = "Vereenvoudigde incognitomodus"; +/* No comment provided by engineer. */ +"Size" = "Maat"; + /* No comment provided by engineer. */ "Skip" = "Overslaan"; @@ -3675,6 +4023,9 @@ /* No comment provided by engineer. */ "Small groups (max 20)" = "Kleine groepen (max 20)"; +/* No comment provided by engineer. */ +"SMP server" = "SMP server"; + /* No comment provided by engineer. */ "SMP servers" = "SMP servers"; @@ -3699,9 +4050,15 @@ /* No comment provided by engineer. */ "Start migration" = "Start migratie"; +/* No comment provided by engineer. */ +"Starting from %@." = "Beginnend vanaf %@."; + /* No comment provided by engineer. */ "starting…" = "beginnen…"; +/* No comment provided by engineer. */ +"Statistics" = "Statistieken"; + /* No comment provided by engineer. */ "Stop" = "Stop"; @@ -3744,6 +4101,15 @@ /* No comment provided by engineer. */ "Submit" = "Indienen"; +/* No comment provided by engineer. */ +"Subscribed" = "Ingeschreven"; + +/* No comment provided by engineer. */ +"Subscription errors" = "Inschrijving fouten"; + +/* No comment provided by engineer. */ +"Subscriptions ignored" = "Inschrijvingen genegeerd"; + /* No comment provided by engineer. */ "Support SimpleX Chat" = "Ondersteuning van SimpleX Chat"; @@ -3792,6 +4158,9 @@ /* No comment provided by engineer. */ "TCP_KEEPINTVL" = "TCP_KEEPINTVL"; +/* No comment provided by engineer. */ +"Temporary file error" = "Tijdelijke bestandsfout"; + /* server test failure */ "Test failed at step %@." = "Test mislukt bij stap %@."; @@ -3873,6 +4242,9 @@ /* No comment provided by engineer. */ "The text you pasted is not a SimpleX link." = "De tekst die u hebt geplakt is geen SimpleX link."; +/* No comment provided by engineer. */ +"Themes" = "Thema's"; + /* No comment provided by engineer. */ "These settings are for your current profile **%@**." = "Deze instellingen zijn voor uw huidige profiel **%@**."; @@ -3915,9 +4287,15 @@ /* No comment provided by engineer. */ "This is your own SimpleX address!" = "Dit is uw eigen SimpleX adres!"; +/* No comment provided by engineer. */ +"This link was used with another mobile device, please create a new link on the desktop." = "Deze link is gebruikt met een ander mobiel apparaat. Maak een nieuwe link op de desktop."; + /* No comment provided by engineer. */ "This setting applies to messages in your current chat profile **%@**." = "Deze instelling is van toepassing op berichten in je huidige chat profiel **%@**."; +/* No comment provided by engineer. */ +"Title" = "Titel"; + /* No comment provided by engineer. */ "To ask any questions and to receive updates:" = "Om vragen te stellen en updates te ontvangen:"; @@ -3957,9 +4335,15 @@ /* No comment provided by engineer. */ "Toggle incognito when connecting." = "Schakel incognito in tijdens het verbinden."; +/* No comment provided by engineer. */ +"Total" = "Totaal"; + /* No comment provided by engineer. */ "Transport isolation" = "Transport isolation"; +/* No comment provided by engineer. */ +"Transport sessions" = "Transportsessies"; + /* No comment provided by engineer. */ "Trying to connect to the server used to receive messages from this contact (error: %@)." = "Proberen verbinding te maken met de server die wordt gebruikt om berichten van dit contact te ontvangen (fout: %@)."; @@ -4095,12 +4479,21 @@ /* No comment provided by engineer. */ "Upgrade and open chat" = "Upgrade en open chat"; +/* No comment provided by engineer. */ +"Upload errors" = "Upload fouten"; + /* No comment provided by engineer. */ "Upload failed" = "Upload mislukt"; /* server test step */ "Upload file" = "Upload bestand"; +/* No comment provided by engineer. */ +"Uploaded" = "Geüpload"; + +/* No comment provided by engineer. */ +"Uploaded files" = "Geüploade bestanden"; + /* No comment provided by engineer. */ "Uploading archive" = "Archief uploaden"; @@ -4146,6 +4539,9 @@ /* No comment provided by engineer. */ "User profile" = "Gebruikers profiel"; +/* No comment provided by engineer. */ +"User selection" = "Gebruikersselectie"; + /* No comment provided by engineer. */ "Using .onion hosts requires compatible VPN provider." = "Het gebruik van .onion-hosts vereist een compatibele VPN-provider."; @@ -4254,6 +4650,12 @@ /* No comment provided by engineer. */ "Waiting for video" = "Wachten op video"; +/* No comment provided by engineer. */ +"Wallpaper accent" = "Achtergrond accent"; + +/* No comment provided by engineer. */ +"Wallpaper background" = "Wallpaper achtergrond"; + /* No comment provided by engineer. */ "wants to connect to you!" = "wil met je in contact komen!"; @@ -4273,10 +4675,10 @@ "Welcome %@!" = "Welkom %@!"; /* No comment provided by engineer. */ -"Welcome message" = "Welkomst bericht"; +"Welcome message" = "Welkom bericht"; /* No comment provided by engineer. */ -"Welcome message is too long" = "Welkomstbericht is te lang"; +"Welcome message is too long" = "Welkom bericht is te lang"; /* No comment provided by engineer. */ "What's new" = "Wat is er nieuw"; @@ -4309,7 +4711,7 @@ "With encrypted files and media." = "‐Met versleutelde bestanden en media."; /* No comment provided by engineer. */ -"With optional welcome message." = "Met optioneel welkomst bericht."; +"With optional welcome message." = "Met optioneel welkom bericht."; /* No comment provided by engineer. */ "With reduced battery usage." = "Met verminderd batterijgebruik."; @@ -4326,9 +4728,15 @@ /* snd error text */ "Wrong key or unknown connection - most likely this connection is deleted." = "Verkeerde sleutel of onbekende verbinding - hoogstwaarschijnlijk is deze verbinding verwijderd."; +/* file error text */ +"Wrong key or unknown file chunk address - most likely file is deleted." = "Verkeerde sleutel of onbekend bestanddeeladres - hoogstwaarschijnlijk is het bestand verwijderd."; + /* No comment provided by engineer. */ "Wrong passphrase!" = "Verkeerd wachtwoord!"; +/* No comment provided by engineer. */ +"XFTP server" = "XFTP server"; + /* No comment provided by engineer. */ "XFTP servers" = "XFTP servers"; @@ -4386,6 +4794,9 @@ /* No comment provided by engineer. */ "You are invited to group" = "Je bent uitgenodigd voor de groep"; +/* No comment provided by engineer. */ +"You are not connected to these servers. Private routing is used to deliver messages to them." = "U bent niet verbonden met deze servers. Privéroutering wordt gebruikt om berichten bij hen af te leveren."; + /* No comment provided by engineer. */ "you are observer" = "jij bent waarnemer"; diff --git a/apps/ios/pl.lproj/Localizable.strings b/apps/ios/pl.lproj/Localizable.strings index 6807e4f240..fd284d9537 100644 --- a/apps/ios/pl.lproj/Localizable.strings +++ b/apps/ios/pl.lproj/Localizable.strings @@ -334,6 +334,9 @@ /* No comment provided by engineer. */ "above, then choose:" = "powyżej, a następnie wybierz:"; +/* No comment provided by engineer. */ +"Accent" = "Akcent"; + /* accept contact request via notification accept incoming call via notification */ "Accept" = "Akceptuj"; @@ -350,6 +353,15 @@ /* call status */ "accepted call" = "zaakceptowane połączenie"; +/* No comment provided by engineer. */ +"Acknowledged" = "Potwierdzono"; + +/* No comment provided by engineer. */ +"Acknowledgement errors" = "Błędy potwierdzenia"; + +/* No comment provided by engineer. */ +"Active connections" = "Aktywne połączenia"; + /* No comment provided by engineer. */ "Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." = "Dodaj adres do swojego profilu, aby Twoje kontakty mogły go udostępnić innym osobom. Aktualizacja profilu zostanie wysłana do Twoich kontaktów."; @@ -374,6 +386,15 @@ /* No comment provided by engineer. */ "Add welcome message" = "Dodaj wiadomość powitalną"; +/* No comment provided by engineer. */ +"Additional accent" = "Dodatkowy akcent"; + +/* No comment provided by engineer. */ +"Additional accent 2" = "Dodatkowy akcent 2"; + +/* No comment provided by engineer. */ +"Additional secondary" = "Dodatkowy drugorzędny"; + /* No comment provided by engineer. */ "Address" = "Adres"; @@ -395,6 +416,9 @@ /* No comment provided by engineer. */ "Advanced network settings" = "Zaawansowane ustawienia sieci"; +/* No comment provided by engineer. */ +"Advanced settings" = "Zaawansowane ustawienia"; + /* chat item text */ "agreeing encryption for %@…" = "uzgadnianie szyfrowania dla %@…"; @@ -410,6 +434,9 @@ /* No comment provided by engineer. */ "All data is erased when it is entered." = "Wszystkie dane są usuwane po jego wprowadzeniu."; +/* No comment provided by engineer. */ +"All data is private to your device." = "Wszystkie dane są prywatne na Twoim urządzeniu."; + /* No comment provided by engineer. */ "All group members will remain connected." = "Wszyscy członkowie grupy pozostaną połączeni."; @@ -425,6 +452,9 @@ /* No comment provided by engineer. */ "All new messages from %@ will be hidden!" = "Wszystkie nowe wiadomości z %@ zostaną ukryte!"; +/* No comment provided by engineer. */ +"All profiles" = "Wszystkie profile"; + /* No comment provided by engineer. */ "All your contacts will remain connected." = "Wszystkie Twoje kontakty pozostaną połączone."; @@ -551,6 +581,9 @@ /* No comment provided by engineer. */ "Apply" = "Zastosuj"; +/* No comment provided by engineer. */ +"Apply to" = "Zastosuj dla"; + /* No comment provided by engineer. */ "Archive and upload" = "Archiwizuj i prześlij"; @@ -560,6 +593,9 @@ /* No comment provided by engineer. */ "Attach" = "Dołącz"; +/* No comment provided by engineer. */ +"attempts" = "próby"; + /* No comment provided by engineer. */ "Audio & video calls" = "Połączenia audio i wideo"; @@ -602,6 +638,9 @@ /* No comment provided by engineer. */ "Back" = "Wstecz"; +/* No comment provided by engineer. */ +"Background" = "Tło"; + /* No comment provided by engineer. */ "Bad desktop address" = "Zły adres komputera"; @@ -623,6 +662,9 @@ /* No comment provided by engineer. */ "Better messages" = "Lepsze wiadomości"; +/* No comment provided by engineer. */ +"Black" = "Czarny"; + /* No comment provided by engineer. */ "Block" = "Zablokuj"; @@ -647,7 +689,7 @@ /* rcv group event chat item */ "blocked %@" = "zablokowany %@"; -/* marked deleted chat item preview text */ +/* blocked chat item */ "blocked by admin" = "zablokowany przez admina"; /* No comment provided by engineer. */ @@ -713,6 +755,9 @@ /* No comment provided by engineer. */ "Cannot access keychain to save database password" = "Nie można uzyskać dostępu do pęku kluczy, aby zapisać hasło do bazy danych"; +/* No comment provided by engineer. */ +"Cannot forward message" = "Nie można przekazać wiadomości"; + /* No comment provided by engineer. */ "Cannot receive file" = "Nie można odebrać pliku"; @@ -771,6 +816,9 @@ /* No comment provided by engineer. */ "Chat archive" = "Archiwum czatu"; +/* No comment provided by engineer. */ +"Chat colors" = "Kolory czatu"; + /* No comment provided by engineer. */ "Chat console" = "Konsola czatu"; @@ -798,6 +846,9 @@ /* No comment provided by engineer. */ "Chat preferences" = "Preferencje czatu"; +/* No comment provided by engineer. */ +"Chat theme" = "Motyw czatu"; + /* No comment provided by engineer. */ "Chats" = "Czaty"; @@ -816,6 +867,15 @@ /* No comment provided by engineer. */ "Choose from library" = "Wybierz z biblioteki"; +/* No comment provided by engineer. */ +"Chunks deleted" = "Fragmenty usunięte"; + +/* No comment provided by engineer. */ +"Chunks downloaded" = "Fragmenty pobrane"; + +/* No comment provided by engineer. */ +"Chunks uploaded" = "Fragmenty przesłane"; + /* No comment provided by engineer. */ "Clear" = "Wyczyść"; @@ -831,6 +891,9 @@ /* No comment provided by engineer. */ "Clear verification" = "Wyczyść weryfikację"; +/* No comment provided by engineer. */ +"Color mode" = "Tryb koloru"; + /* No comment provided by engineer. */ "colored" = "kolorowy"; @@ -843,9 +906,15 @@ /* No comment provided by engineer. */ "complete" = "kompletny"; +/* No comment provided by engineer. */ +"Completed" = "Zakończono"; + /* No comment provided by engineer. */ "Configure ICE servers" = "Skonfiguruj serwery ICE"; +/* No comment provided by engineer. */ +"Configured %@ servers" = "Skonfigurowano %@ serwerów"; + /* No comment provided by engineer. */ "Confirm" = "Potwierdź"; @@ -912,18 +981,27 @@ /* No comment provided by engineer. */ "connected" = "połączony"; +/* No comment provided by engineer. */ +"Connected" = "Połączony"; + /* No comment provided by engineer. */ "Connected desktop" = "Połączony komputer"; /* rcv group event chat item */ "connected directly" = "połącz bezpośrednio"; +/* No comment provided by engineer. */ +"Connected servers" = "Połączone serwery"; + /* No comment provided by engineer. */ "Connected to desktop" = "Połączony do komputera"; /* No comment provided by engineer. */ "connecting" = "łączenie"; +/* No comment provided by engineer. */ +"Connecting" = "Łączenie"; + /* No comment provided by engineer. */ "connecting (accepted)" = "łączenie (zaakceptowane)"; @@ -972,9 +1050,15 @@ /* No comment provided by engineer. */ "Connection timeout" = "Czas połączenia minął"; +/* No comment provided by engineer. */ +"Connection with desktop stopped" = "Połączenie z komputerem zakończone"; + /* connection information */ "connection:%@" = "połączenie: %@"; +/* No comment provided by engineer. */ +"Connections" = "Połączenia"; + /* profile update event chat item */ "contact %@ changed to %@" = "kontakt %1$@ zmieniony na %2$@"; @@ -1017,6 +1101,9 @@ /* No comment provided by engineer. */ "Copy" = "Kopiuj"; +/* No comment provided by engineer. */ +"Copy error" = "Kopiuj błąd"; + /* No comment provided by engineer. */ "Core version: v%@" = "Wersja rdzenia: v%@"; @@ -1062,6 +1149,9 @@ /* No comment provided by engineer. */ "Create your profile" = "Utwórz swój profil"; +/* No comment provided by engineer. */ +"Created" = "Utworzono"; + /* No comment provided by engineer. */ "Created at" = "Utworzony o"; @@ -1086,6 +1176,9 @@ /* No comment provided by engineer. */ "Current passphrase…" = "Obecne hasło…"; +/* No comment provided by engineer. */ +"Current profile" = "Bieżący profil"; + /* No comment provided by engineer. */ "Currently maximum supported file size is %@." = "Obecnie maksymalna obsługiwana wielkość pliku wynosi %@."; @@ -1095,9 +1188,15 @@ /* No comment provided by engineer. */ "Custom time" = "Niestandardowy czas"; +/* No comment provided by engineer. */ +"Customize theme" = "Dostosuj motyw"; + /* No comment provided by engineer. */ "Dark" = "Ciemny"; +/* No comment provided by engineer. */ +"Dark mode colors" = "Kolory ciemnego trybu"; + /* No comment provided by engineer. */ "Database downgrade" = "Obniż wersję bazy danych"; @@ -1167,6 +1266,9 @@ /* message decrypt error item */ "Decryption error" = "Błąd odszyfrowania"; +/* No comment provided by engineer. */ +"decryption errors" = "błąd odszyfrowywania"; + /* pref value */ "default (%@)" = "domyślne (%@)"; @@ -1293,6 +1395,9 @@ /* deleted chat item */ "deleted" = "usunięty"; +/* No comment provided by engineer. */ +"Deleted" = "Usunięto"; + /* No comment provided by engineer. */ "Deleted at" = "Usunięto o"; @@ -1305,6 +1410,9 @@ /* rcv group event chat item */ "deleted group" = "usunięta grupa"; +/* No comment provided by engineer. */ +"Deletion errors" = "Błędy usuwania"; + /* No comment provided by engineer. */ "Delivery" = "Dostarczenie"; @@ -1329,6 +1437,12 @@ /* snd error text */ "Destination server error: %@" = "Błąd docelowego serwera: %@"; +/* No comment provided by engineer. */ +"Detailed statistics" = "Szczegółowe statystyki"; + +/* No comment provided by engineer. */ +"Details" = "Szczegóły"; + /* No comment provided by engineer. */ "Develop" = "Deweloperskie"; @@ -1431,12 +1545,21 @@ /* chat item action */ "Download" = "Pobierz"; +/* No comment provided by engineer. */ +"Download errors" = "Błędy pobierania"; + /* No comment provided by engineer. */ "Download failed" = "Pobieranie nie udane"; /* server test step */ "Download file" = "Pobierz plik"; +/* No comment provided by engineer. */ +"Downloaded" = "Pobrane"; + +/* No comment provided by engineer. */ +"Downloaded files" = "Pobrane pliki"; + /* No comment provided by engineer. */ "Downloading archive" = "Pobieranie archiwum"; @@ -1449,6 +1572,9 @@ /* integrity error chat item */ "duplicate message" = "zduplikowana wiadomość"; +/* No comment provided by engineer. */ +"duplicates" = "duplikaty"; + /* No comment provided by engineer. */ "Duration" = "Czas trwania"; @@ -1707,6 +1833,9 @@ /* No comment provided by engineer. */ "Error exporting chat database" = "Błąd eksportu bazy danych czatu"; +/* No comment provided by engineer. */ +"Error exporting theme: %@" = "Błąd eksportowania motywu: %@"; + /* No comment provided by engineer. */ "Error importing chat database" = "Błąd importu bazy danych czatu"; @@ -1722,9 +1851,18 @@ /* No comment provided by engineer. */ "Error receiving file" = "Błąd odbioru pliku"; +/* No comment provided by engineer. */ +"Error reconnecting server" = "Błąd ponownego łączenia z serwerem"; + +/* No comment provided by engineer. */ +"Error reconnecting servers" = "Błąd ponownego łączenia serwerów"; + /* No comment provided by engineer. */ "Error removing member" = "Błąd usuwania członka"; +/* No comment provided by engineer. */ +"Error resetting statistics" = "Błąd resetowania statystyk"; + /* No comment provided by engineer. */ "Error saving %@ servers" = "Błąd zapisu %@ serwerów"; @@ -1804,6 +1942,9 @@ /* No comment provided by engineer. */ "Error: URL is invalid" = "Błąd: URL jest nieprawidłowy"; +/* No comment provided by engineer. */ +"Errors" = "Błędy"; + /* No comment provided by engineer. */ "Even when disabled in the conversation." = "Nawet po wyłączeniu w rozmowie."; @@ -1816,12 +1957,18 @@ /* chat item action */ "Expand" = "Rozszerz"; +/* No comment provided by engineer. */ +"expired" = "wygasły"; + /* No comment provided by engineer. */ "Export database" = "Eksportuj bazę danych"; /* No comment provided by engineer. */ "Export error:" = "Błąd eksportu:"; +/* No comment provided by engineer. */ +"Export theme" = "Eksportuj motyw"; + /* No comment provided by engineer. */ "Exported database archive." = "Wyeksportowane archiwum bazy danych."; @@ -1843,6 +1990,21 @@ /* No comment provided by engineer. */ "Favorite" = "Ulubione"; +/* No comment provided by engineer. */ +"File error" = "Błąd pliku"; + +/* file error text */ +"File not found - most likely file was deleted or cancelled." = "Nie odnaleziono pliku - najprawdopodobniej plik został usunięty lub anulowany."; + +/* file error text */ +"File server error: %@" = "Błąd serwera plików: %@"; + +/* No comment provided by engineer. */ +"File status" = "Status pliku"; + +/* copied message info */ +"File status: %@" = "Status pliku: %@"; + /* No comment provided by engineer. */ "File will be deleted from servers." = "Plik zostanie usunięty z serwerów."; @@ -1957,6 +2119,12 @@ /* No comment provided by engineer. */ "GIFs and stickers" = "GIF-y i naklejki"; +/* message preview */ +"Good afternoon!" = "Dzień dobry!"; + +/* message preview */ +"Good morning!" = "Dzień dobry!"; + /* No comment provided by engineer. */ "Group" = "Grupa"; @@ -2134,6 +2302,9 @@ /* No comment provided by engineer. */ "Import failed" = "Import nie udał się"; +/* No comment provided by engineer. */ +"Import theme" = "Importuj motyw"; + /* No comment provided by engineer. */ "Importing archive" = "Importowanie archiwum"; @@ -2155,6 +2326,9 @@ /* No comment provided by engineer. */ "In-call sounds" = "Dźwięki w rozmowie"; +/* No comment provided by engineer. */ +"inactive" = "nieaktywny"; + /* No comment provided by engineer. */ "Incognito" = "Incognito"; @@ -2218,6 +2392,9 @@ /* No comment provided by engineer. */ "Interface" = "Interfejs"; +/* No comment provided by engineer. */ +"Interface colors" = "Kolory interfejsu"; + /* invalid chat data */ "invalid chat" = "nieprawidłowy czat"; @@ -2470,6 +2647,9 @@ /* rcv group event chat item */ "member connected" = "połączony"; +/* item status text */ +"Member inactive" = "Członek nieaktywny"; + /* No comment provided by engineer. */ "Member role will be changed to \"%@\". All group members will be notified." = "Rola członka grupy zostanie zmieniona na \"%@\". Wszyscy członkowie grupy zostaną powiadomieni."; @@ -2479,6 +2659,9 @@ /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "Członek zostanie usunięty z grupy - nie można tego cofnąć!"; +/* No comment provided by engineer. */ +"Menus" = "Menu"; + /* item status text */ "Message delivery error" = "Błąd dostarczenia wiadomości"; @@ -2491,6 +2674,12 @@ /* No comment provided by engineer. */ "Message draft" = "Wersja robocza wiadomości"; +/* item status text */ +"Message forwarded" = "Wiadomość przekazana"; + +/* item status description */ +"Message may be delivered later if member becomes active." = "Wiadomość może zostać dostarczona później jeśli członek stanie się aktywny."; + /* No comment provided by engineer. */ "Message queue info" = "Informacje kolejki wiadomości"; @@ -2506,6 +2695,9 @@ /* notification */ "message received" = "wiadomość otrzymana"; +/* No comment provided by engineer. */ +"Message reception" = "Odebranie wiadomości"; + /* No comment provided by engineer. */ "Message routing fallback" = "Rezerwowe trasowania wiadomości"; @@ -2515,6 +2707,12 @@ /* No comment provided by engineer. */ "Message source remains private." = "Źródło wiadomości pozostaje prywatne."; +/* No comment provided by engineer. */ +"Message status" = "Status wiadomości"; + +/* copied message info */ +"Message status: %@" = "Status wiadomości: %@"; + /* No comment provided by engineer. */ "Message text" = "Tekst wiadomości"; @@ -2530,6 +2728,12 @@ /* No comment provided by engineer. */ "Messages from %@ will be shown!" = "Wiadomości od %@ zostaną pokazane!"; +/* No comment provided by engineer. */ +"Messages received" = "Otrzymane wiadomości"; + +/* No comment provided by engineer. */ +"Messages sent" = "Wysłane wiadomości"; + /* 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." = "Wiadomości, pliki i połączenia są chronione przez **szyfrowanie end-to-end** z doskonałym utajnianiem z wyprzedzeniem i odzyskiem po złamaniu."; @@ -2695,6 +2899,9 @@ /* No comment provided by engineer. */ "No device token!" = "Brak tokenu urządzenia!"; +/* item status description */ +"No direct connection yet, message is forwarded by admin." = "Brak bezpośredniego połączenia, wiadomość została przekazana przez administratora."; + /* No comment provided by engineer. */ "no e2e encryption" = "brak szyfrowania e2e"; @@ -2707,6 +2914,9 @@ /* No comment provided by engineer. */ "No history" = "Brak historii"; +/* No comment provided by engineer. */ +"No info, try to reload" = "Brak informacji, spróbuj przeładować"; + /* No comment provided by engineer. */ "No network connection" = "Brak połączenia z siecią"; @@ -2739,7 +2949,7 @@ time to disappear */ "off" = "wyłączony"; -/* No comment provided by engineer. */ +/* blur media */ "Off" = "Wyłączony"; /* feature offered item */ @@ -2832,6 +3042,9 @@ /* authentication reason */ "Open migration to another device" = "Otwórz migrację na innym urządzeniu"; +/* No comment provided by engineer. */ +"Open server settings" = "Otwórz ustawienia serwera"; + /* No comment provided by engineer. */ "Open Settings" = "Otwórz Ustawienia"; @@ -2856,9 +3069,18 @@ /* No comment provided by engineer. */ "Or show this code" = "Lub pokaż ten kod"; +/* No comment provided by engineer. */ +"other" = "inne"; + /* No comment provided by engineer. */ "Other" = "Inne"; +/* No comment provided by engineer. */ +"Other %@ servers" = "Inne %@ serwery"; + +/* No comment provided by engineer. */ +"other errors" = "inne błędy"; + /* member role */ "owner" = "właściciel"; @@ -2901,6 +3123,9 @@ /* No comment provided by engineer. */ "peer-to-peer" = "peer-to-peer"; +/* No comment provided by engineer. */ +"Pending" = "Oczekujące"; + /* No comment provided by engineer. */ "People can connect to you only via the links you share." = "Ludzie mogą się z Tobą połączyć tylko poprzez linki, które udostępniasz."; @@ -2922,6 +3147,9 @@ /* No comment provided by engineer. */ "Please ask your contact to enable sending voice messages." = "Poproś Twój kontakt o włączenie wysyłania wiadomości głosowych."; +/* 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." = "Sprawdź, czy telefon i komputer są podłączone do tej samej sieci lokalnej i czy zapora sieciowa komputera umożliwia połączenie.\nProszę podzielić się innymi problemami z deweloperami."; + /* No comment provided by engineer. */ "Please check that you used the correct link or ask your contact to send you another one." = "Sprawdź, czy użyłeś prawidłowego linku lub poproś Twój kontakt o przesłanie innego."; @@ -2979,6 +3207,9 @@ /* No comment provided by engineer. */ "Preview" = "Podgląd"; +/* No comment provided by engineer. */ +"Previously connected servers" = "Wcześniej połączone serwery"; + /* No comment provided by engineer. */ "Privacy & security" = "Prywatność i bezpieczeństwo"; @@ -3000,6 +3231,9 @@ /* No comment provided by engineer. */ "Private routing" = "Prywatne trasowanie"; +/* No comment provided by engineer. */ +"Private routing error" = "Błąd prywatnego trasowania"; + /* No comment provided by engineer. */ "Profile and server connections" = "Profil i połączenia z serwerem"; @@ -3018,6 +3252,9 @@ /* No comment provided by engineer. */ "Profile password" = "Hasło profilu"; +/* No comment provided by engineer. */ +"Profile theme" = "Motyw profilu"; + /* No comment provided by engineer. */ "Profile update will be sent to your contacts." = "Aktualizacja profilu zostanie wysłana do Twoich kontaktów."; @@ -3066,6 +3303,12 @@ /* No comment provided by engineer. */ "Protocol timeout per KB" = "Limit czasu protokołu na KB"; +/* No comment provided by engineer. */ +"Proxied" = "Trasowane przez proxy"; + +/* No comment provided by engineer. */ +"Proxied servers" = "Serwery trasowane przez proxy"; + /* No comment provided by engineer. */ "Push notifications" = "Powiadomienia push"; @@ -3108,6 +3351,9 @@ /* No comment provided by engineer. */ "Receipts are disabled" = "Potwierdzenia są wyłączone"; +/* No comment provided by engineer. */ +"Receive errors" = "Błędy otrzymania"; + /* No comment provided by engineer. */ "received answer…" = "otrzymano odpowiedź…"; @@ -3126,6 +3372,15 @@ /* message info title */ "Received message" = "Otrzymano wiadomość"; +/* No comment provided by engineer. */ +"Received messages" = "Otrzymane wiadomości"; + +/* No comment provided by engineer. */ +"Received reply" = "Otrzymano odpowiedź"; + +/* No comment provided by engineer. */ +"Received total" = "Otrzymano łącznie"; + /* No comment provided by engineer. */ "Receiving address will be changed to a different server. Address change will complete after sender comes online." = "Adres odbiorczy zostanie zmieniony na inny serwer. Zmiana adresu zostanie zakończona gdy nadawca będzie online."; @@ -3144,9 +3399,24 @@ /* No comment provided by engineer. */ "Recipients see updates as you type them." = "Odbiorcy widzą aktualizacje podczas ich wpisywania."; +/* No comment provided by engineer. */ +"Reconnect" = "Połącz ponownie"; + /* No comment provided by engineer. */ "Reconnect all connected servers to force message delivery. It uses additional traffic." = "Połącz ponownie wszystkie połączone serwery, aby wymusić dostarczanie wiadomości. Wykorzystuje dodatkowy ruch."; +/* No comment provided by engineer. */ +"Reconnect all servers" = "Połącz ponownie wszystkie serwery"; + +/* No comment provided by engineer. */ +"Reconnect all servers?" = "Połączyć ponownie wszystkie serwery?"; + +/* No comment provided by engineer. */ +"Reconnect server to force message delivery. It uses additional traffic." = "Ponownie połącz z serwerem w celu wymuszenia dostarczenia wiadomości. Wykorzystuje to dodatkowy ruch."; + +/* No comment provided by engineer. */ +"Reconnect server?" = "Połączyć ponownie serwer?"; + /* No comment provided by engineer. */ "Reconnect servers?" = "Ponownie połączyć serwery?"; @@ -3180,6 +3450,9 @@ /* No comment provided by engineer. */ "Remove" = "Usuń"; +/* No comment provided by engineer. */ +"Remove image" = "Usuń obraz"; + /* No comment provided by engineer. */ "Remove member" = "Usuń członka"; @@ -3237,12 +3510,24 @@ /* No comment provided by engineer. */ "Reset" = "Resetuj"; +/* No comment provided by engineer. */ +"Reset all statistics" = "Resetuj wszystkie statystyki"; + +/* No comment provided by engineer. */ +"Reset all statistics?" = "Zresetować wszystkie statystyki?"; + /* No comment provided by engineer. */ "Reset colors" = "Resetuj kolory"; +/* No comment provided by engineer. */ +"Reset to app theme" = "Zresetuj do motywu aplikacji"; + /* No comment provided by engineer. */ "Reset to defaults" = "Przywróć wartości domyślne"; +/* No comment provided by engineer. */ +"Reset to user theme" = "Zresetuj do motywu użytkownika"; + /* No comment provided by engineer. */ "Restart the app to create a new chat profile" = "Uruchom ponownie aplikację, aby utworzyć nowy profil czatu"; @@ -3357,6 +3642,12 @@ /* No comment provided by engineer. */ "Saved WebRTC ICE servers will be removed" = "Zapisane serwery WebRTC ICE zostaną usunięte"; +/* No comment provided by engineer. */ +"Scale" = "Skaluj"; + +/* No comment provided by engineer. */ +"Scan / Paste link" = "Skanuj / Wklej link"; + /* No comment provided by engineer. */ "Scan code" = "Zeskanuj kod"; @@ -3384,6 +3675,9 @@ /* network option */ "sec" = "sek"; +/* No comment provided by engineer. */ +"Secondary" = "Drugorzędny"; + /* time unit */ "seconds" = "sekundy"; @@ -3393,6 +3687,9 @@ /* server test step */ "Secure queue" = "Bezpieczna kolejka"; +/* No comment provided by engineer. */ +"Secured" = "Zabezpieczone"; + /* No comment provided by engineer. */ "Security assessment" = "Ocena bezpieczeństwa"; @@ -3405,6 +3702,9 @@ /* No comment provided by engineer. */ "Select" = "Wybierz"; +/* No comment provided by engineer. */ +"Selected chat preferences prohibit this message." = "Wybrane preferencje czatu zabraniają tej wiadomości."; + /* No comment provided by engineer. */ "Self-destruct" = "Samozniszczenie"; @@ -3438,6 +3738,9 @@ /* No comment provided by engineer. */ "Send disappearing message" = "Wyślij znikającą wiadomość"; +/* No comment provided by engineer. */ +"Send errors" = "Wyślij błędy"; + /* No comment provided by engineer. */ "Send link previews" = "Wyślij podgląd linku"; @@ -3504,15 +3807,36 @@ /* copied message info */ "Sent at: %@" = "Wysłano o: %@"; +/* No comment provided by engineer. */ +"Sent directly" = "Wysłano bezpośrednio"; + /* notification */ "Sent file event" = "Wyślij zdarzenie pliku"; /* message info title */ "Sent message" = "Wyślij wiadomość"; +/* No comment provided by engineer. */ +"Sent messages" = "Wysłane wiadomości"; + /* No comment provided by engineer. */ "Sent messages will be deleted after set time." = "Wysłane wiadomości zostaną usunięte po ustawionym czasie."; +/* No comment provided by engineer. */ +"Sent reply" = "Wyślij odpowiedź"; + +/* No comment provided by engineer. */ +"Sent total" = "Wysłano łącznie"; + +/* No comment provided by engineer. */ +"Sent via proxy" = "Wysłano przez proxy"; + +/* No comment provided by engineer. */ +"Server address" = "Adres serwera"; + +/* No comment provided by engineer. */ +"Server address is incompatible with network settings: %@." = "Adres serwera jest niekompatybilny z ustawieniami sieci: %@."; + /* srv error text. */ "Server address is incompatible with network settings." = "Adres serwera jest niekompatybilny z ustawieniami sieciowymi."; @@ -3528,12 +3852,24 @@ /* No comment provided by engineer. */ "Server test failed!" = "Test serwera nie powiódł się!"; +/* No comment provided by engineer. */ +"Server type" = "Typ serwera"; + /* srv error text */ "Server version is incompatible with network settings." = "Wersja serwera jest niekompatybilna z ustawieniami sieciowymi."; +/* No comment provided by engineer. */ +"Server version is incompatible with your app: %@." = "Wersja serwera jest niekompatybilna z aplikacją: %@."; + /* No comment provided by engineer. */ "Servers" = "Serwery"; +/* No comment provided by engineer. */ +"Servers info" = "Informacje o serwerach"; + +/* No comment provided by engineer. */ +"Servers statistics will be reset - this cannot be undone!" = "Statystyki serwerów zostaną zresetowane - nie można tego cofnąć!"; + /* No comment provided by engineer. */ "Session code" = "Kod sesji"; @@ -3543,6 +3879,9 @@ /* No comment provided by engineer. */ "Set contact name…" = "Ustaw nazwę kontaktu…"; +/* No comment provided by engineer. */ +"Set default theme" = "Ustaw domyślny motyw"; + /* No comment provided by engineer. */ "Set group preferences" = "Ustaw preferencje grupy"; @@ -3612,6 +3951,9 @@ /* No comment provided by engineer. */ "Show message status" = "Pokaż status wiadomości"; +/* No comment provided by engineer. */ +"Show percentage" = "Pokaż procent"; + /* No comment provided by engineer. */ "Show preview" = "Pokaż podgląd"; @@ -3621,6 +3963,9 @@ /* No comment provided by engineer. */ "Show:" = "Pokaż:"; +/* No comment provided by engineer. */ +"SimpleX" = "SimpleX"; + /* No comment provided by engineer. */ "SimpleX address" = "Adres SimpleX"; @@ -3666,6 +4011,9 @@ /* No comment provided by engineer. */ "Simplified incognito mode" = "Uproszczony tryb incognito"; +/* No comment provided by engineer. */ +"Size" = "Rozmiar"; + /* No comment provided by engineer. */ "Skip" = "Pomiń"; @@ -3675,6 +4023,9 @@ /* No comment provided by engineer. */ "Small groups (max 20)" = "Małe grupy (maks. 20)"; +/* No comment provided by engineer. */ +"SMP server" = "Serwer SMP"; + /* No comment provided by engineer. */ "SMP servers" = "Serwery SMP"; @@ -3699,9 +4050,15 @@ /* No comment provided by engineer. */ "Start migration" = "Rozpocznij migrację"; +/* No comment provided by engineer. */ +"Starting from %@." = "Zaczynanie od %@."; + /* No comment provided by engineer. */ "starting…" = "uruchamianie…"; +/* No comment provided by engineer. */ +"Statistics" = "Statystyki"; + /* No comment provided by engineer. */ "Stop" = "Zatrzymaj"; @@ -3744,6 +4101,15 @@ /* No comment provided by engineer. */ "Submit" = "Zatwierdź"; +/* No comment provided by engineer. */ +"Subscribed" = "Zasubskrybowano"; + +/* No comment provided by engineer. */ +"Subscription errors" = "Błędy subskrypcji"; + +/* No comment provided by engineer. */ +"Subscriptions ignored" = "Subskrypcje zignorowane"; + /* No comment provided by engineer. */ "Support SimpleX Chat" = "Wspieraj SimpleX Chat"; @@ -3792,6 +4158,9 @@ /* No comment provided by engineer. */ "TCP_KEEPINTVL" = "TCP_KEEPINTVL"; +/* No comment provided by engineer. */ +"Temporary file error" = "Tymczasowy błąd pliku"; + /* server test failure */ "Test failed at step %@." = "Test nie powiódł się na etapie %@."; @@ -3873,6 +4242,9 @@ /* No comment provided by engineer. */ "The text you pasted is not a SimpleX link." = "Tekst, który wkleiłeś nie jest linkiem SimpleX."; +/* No comment provided by engineer. */ +"Themes" = "Motywy"; + /* No comment provided by engineer. */ "These settings are for your current profile **%@**." = "Te ustawienia dotyczą Twojego bieżącego profilu **%@**."; @@ -3915,9 +4287,15 @@ /* No comment provided by engineer. */ "This is your own SimpleX address!" = "To jest twój własny adres SimpleX!"; +/* No comment provided by engineer. */ +"This link was used with another mobile device, please create a new link on the desktop." = "Ten link dostał użyty z innym urządzeniem mobilnym, proszę stworzyć nowy link na komputerze."; + /* No comment provided by engineer. */ "This setting applies to messages in your current chat profile **%@**." = "To ustawienie dotyczy wiadomości Twojego bieżącego profilu czatu **%@**."; +/* No comment provided by engineer. */ +"Title" = "Tytuł"; + /* No comment provided by engineer. */ "To ask any questions and to receive updates:" = "Aby zadać wszelkie pytania i otrzymywać aktualizacje:"; @@ -3957,9 +4335,15 @@ /* No comment provided by engineer. */ "Toggle incognito when connecting." = "Przełącz incognito przy połączeniu."; +/* No comment provided by engineer. */ +"Total" = "Łącznie"; + /* No comment provided by engineer. */ "Transport isolation" = "Izolacja transportu"; +/* No comment provided by engineer. */ +"Transport sessions" = "Sesje transportowe"; + /* No comment provided by engineer. */ "Trying to connect to the server used to receive messages from this contact (error: %@)." = "Próbowanie połączenia z serwerem używanym do odbierania wiadomości od tego kontaktu (błąd: %@)."; @@ -4095,12 +4479,21 @@ /* No comment provided by engineer. */ "Upgrade and open chat" = "Zaktualizuj i otwórz czat"; +/* No comment provided by engineer. */ +"Upload errors" = "Błędy przesłania"; + /* No comment provided by engineer. */ "Upload failed" = "Wgrywanie nie udane"; /* server test step */ "Upload file" = "Prześlij plik"; +/* No comment provided by engineer. */ +"Uploaded" = "Przesłane"; + +/* No comment provided by engineer. */ +"Uploaded files" = "Przesłane pliki"; + /* No comment provided by engineer. */ "Uploading archive" = "Wgrywanie archiwum"; @@ -4146,6 +4539,9 @@ /* No comment provided by engineer. */ "User profile" = "Profil użytkownika"; +/* No comment provided by engineer. */ +"User selection" = "Wybór użytkownika"; + /* No comment provided by engineer. */ "Using .onion hosts requires compatible VPN provider." = "Używanie hostów .onion wymaga kompatybilnego dostawcy VPN."; @@ -4254,6 +4650,12 @@ /* No comment provided by engineer. */ "Waiting for video" = "Oczekiwanie na film"; +/* No comment provided by engineer. */ +"Wallpaper accent" = "Akcent tapety"; + +/* No comment provided by engineer. */ +"Wallpaper background" = "Tło tapety"; + /* No comment provided by engineer. */ "wants to connect to you!" = "chce się z Tobą połączyć!"; @@ -4326,9 +4728,15 @@ /* snd error text */ "Wrong key or unknown connection - most likely this connection is deleted." = "Zły klucz lub nieznane połączenie - najprawdopodobniej to połączenie jest usunięte."; +/* file error text */ +"Wrong key or unknown file chunk address - most likely file is deleted." = "Zły klucz lub nieznany adres fragmentu pliku - najprawdopodobniej plik został usunięty."; + /* No comment provided by engineer. */ "Wrong passphrase!" = "Nieprawidłowe hasło!"; +/* No comment provided by engineer. */ +"XFTP server" = "Serwer XFTP"; + /* No comment provided by engineer. */ "XFTP servers" = "Serwery XFTP"; @@ -4386,6 +4794,9 @@ /* No comment provided by engineer. */ "You are invited to group" = "Jesteś zaproszony do grupy"; +/* No comment provided by engineer. */ +"You are not connected to these servers. Private routing is used to deliver messages to them." = "Nie jesteś połączony z tymi serwerami. Prywatne trasowanie jest używane do dostarczania do nich wiadomości."; + /* No comment provided by engineer. */ "you are observer" = "jesteś obserwatorem"; diff --git a/apps/ios/ru.lproj/Localizable.strings b/apps/ios/ru.lproj/Localizable.strings index 255ce8a6b5..d842364f91 100644 --- a/apps/ios/ru.lproj/Localizable.strings +++ b/apps/ios/ru.lproj/Localizable.strings @@ -2733,7 +2733,7 @@ time to disappear */ "off" = "нет"; -/* No comment provided by engineer. */ +/* blur media */ "Off" = "Выключено"; /* feature offered item */ diff --git a/apps/ios/th.lproj/Localizable.strings b/apps/ios/th.lproj/Localizable.strings index 22b707f886..b1d328ce6e 100644 --- a/apps/ios/th.lproj/Localizable.strings +++ b/apps/ios/th.lproj/Localizable.strings @@ -2100,7 +2100,7 @@ time to disappear */ "off" = "ปิด"; -/* No comment provided by engineer. */ +/* blur media */ "Off" = "ปิด"; /* feature offered item */ diff --git a/apps/ios/tr.lproj/Localizable.strings b/apps/ios/tr.lproj/Localizable.strings index 644fa5fdbe..98645f8b42 100644 --- a/apps/ios/tr.lproj/Localizable.strings +++ b/apps/ios/tr.lproj/Localizable.strings @@ -647,7 +647,7 @@ /* rcv group event chat item */ "blocked %@" = "engellendi %@"; -/* marked deleted chat item preview text */ +/* blocked chat item */ "blocked by admin" = "yönetici tarafından engellendi"; /* No comment provided by engineer. */ @@ -2739,7 +2739,7 @@ time to disappear */ "off" = "kapalı"; -/* No comment provided by engineer. */ +/* blur media */ "Off" = "Kapalı"; /* feature offered item */ diff --git a/apps/ios/uk.lproj/Localizable.strings b/apps/ios/uk.lproj/Localizable.strings index d41e32efc9..1d2630dd1f 100644 --- a/apps/ios/uk.lproj/Localizable.strings +++ b/apps/ios/uk.lproj/Localizable.strings @@ -647,7 +647,7 @@ /* rcv group event chat item */ "blocked %@" = "заблоковано %@"; -/* marked deleted chat item preview text */ +/* blocked chat item */ "blocked by admin" = "заблоковано адміністратором"; /* No comment provided by engineer. */ @@ -2739,7 +2739,7 @@ time to disappear */ "off" = "вимкнено"; -/* No comment provided by engineer. */ +/* blur media */ "Off" = "Вимкнено"; /* feature offered item */ diff --git a/apps/ios/zh-Hans.lproj/Localizable.strings b/apps/ios/zh-Hans.lproj/Localizable.strings index 9d990ab609..07eecee246 100644 --- a/apps/ios/zh-Hans.lproj/Localizable.strings +++ b/apps/ios/zh-Hans.lproj/Localizable.strings @@ -599,7 +599,7 @@ /* rcv group event chat item */ "blocked %@" = "已封禁 %@"; -/* marked deleted chat item preview text */ +/* blocked chat item */ "blocked by admin" = "由管理员封禁"; /* No comment provided by engineer. */ @@ -2571,7 +2571,7 @@ time to disappear */ "off" = "关闭"; -/* No comment provided by engineer. */ +/* blur media */ "Off" = "关闭"; /* feature offered item */ 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 6ce582cad4..7875c80b15 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 @@ -13,6 +13,7 @@ import chat.simplex.app.model.NtfManager.getUserIdFromIntent import chat.simplex.common.* import chat.simplex.common.helpers.* import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chatlist.* import chat.simplex.common.views.helpers.* @@ -24,11 +25,13 @@ import java.lang.ref.WeakReference class MainActivity: FragmentActivity() { override fun onCreate(savedInstanceState: Bundle?) { + mainActivity = WeakReference(this) platform.androidSetNightModeIfSupported() + val c = CurrentColors.value.colors + platform.androidSetStatusAndNavBarColors(c.isLight, c.background, !appPrefs.oneHandUI.get(), appPrefs.oneHandUI.get()) applyAppLocale(ChatModel.controller.appPrefs.appLanguage) super.onCreate(savedInstanceState) // testJson() - mainActivity = WeakReference(this) // When call ended and orientation changes, it re-process old intent, it's unneeded. // Only needed to be processed on first creation of activity if (savedInstanceState == null) { 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 7d0312a43a..9c5f123f40 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,9 +7,13 @@ import chat.simplex.common.platform.Log import android.content.Intent import android.content.pm.ActivityInfo import android.os.* +import android.view.View import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext +import androidx.core.view.ViewCompat import androidx.lifecycle.* import androidx.work.* import chat.simplex.app.model.NtfManager @@ -18,16 +22,14 @@ import chat.simplex.app.views.call.CallActivity import chat.simplex.common.helpers.* import chat.simplex.common.model.* import chat.simplex.common.model.ChatController.appPrefs -import chat.simplex.common.model.ChatModel.updatingChatsMutex +import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.platform.* -import chat.simplex.common.ui.theme.CurrentColors -import chat.simplex.common.ui.theme.DefaultTheme +import chat.simplex.common.ui.theme.* import chat.simplex.common.views.call.* import chat.simplex.common.views.helpers.* import chat.simplex.common.views.onboarding.OnboardingStage import com.jakewharton.processphoenix.ProcessPhoenix import kotlinx.coroutines.* -import kotlinx.coroutines.sync.withLock import java.io.* import java.util.* import java.util.concurrent.TimeUnit @@ -86,7 +88,7 @@ class SimplexApp: Application(), LifecycleEventObserver { Lifecycle.Event.ON_START -> { isAppOnForeground = true if (chatModel.chatRunning.value == true) { - updatingChatsMutex.withLock { + withChats { kotlin.runCatching { val currentUserId = chatModel.currentUser.value?.userId val chats = ArrayList(chatController.apiGetChats(chatModel.remoteHostId())) @@ -99,7 +101,7 @@ class SimplexApp: Application(), LifecycleEventObserver { /** Pass old chatStats because unreadCounter can be changed already while [ChatController.apiGetChats] is executing */ if (indexOfCurrentChat >= 0) chats[indexOfCurrentChat] = chats[indexOfCurrentChat].copy(chatStats = oldStats) } - chatModel.updateChats(chats) + updateChats(chats) } }.onFailure { Log.e(TAG, it.stackTraceToString()) } } @@ -269,6 +271,35 @@ class SimplexApp: Application(), LifecycleEventObserver { uiModeManager.setApplicationNightMode(mode) } + override fun androidSetStatusAndNavBarColors(isLight: Boolean, backgroundColor: Color, hasTop: Boolean, hasBottom: Boolean) { + val window = mainActivity.get()?.window ?: return + @Suppress("DEPRECATION") + val windowInsetController = ViewCompat.getWindowInsetsController(window.decorView) + + val statusBar = (if (hasTop) { + backgroundColor.mixWith(CurrentColors.value.colors.onBackground, 0.97f) + } else { + backgroundColor + }).toArgb() + val navBar = (if (hasBottom) { + backgroundColor.mixWith(CurrentColors.value.colors.onBackground, 0.97f) + } else { + backgroundColor + }).toArgb() + if (window.statusBarColor != statusBar) { + window.statusBarColor = statusBar + } + if (windowInsetController?.isAppearanceLightStatusBars != isLight) { + windowInsetController?.isAppearanceLightStatusBars = isLight + } + if (window.navigationBarColor != navBar) { + window.navigationBarColor = navBar + } + if (windowInsetController?.isAppearanceLightNavigationBars != isLight) { + windowInsetController?.isAppearanceLightNavigationBars = isLight + } + } + override fun androidStartCallActivity(acceptCall: Boolean, remoteHostId: Long?, chatId: ChatId?) { val context = mainActivity.get() ?: return val intent = Intent(context, CallActivity::class.java) diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/PlatformTextField.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/PlatformTextField.android.kt index f49196c64a..070c96d6da 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/PlatformTextField.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/PlatformTextField.android.kt @@ -2,6 +2,7 @@ package chat.simplex.common.platform import android.annotation.SuppressLint import android.content.Context +import android.graphics.drawable.ColorDrawable import android.os.Build import android.text.InputType import android.util.Log @@ -16,6 +17,7 @@ import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.TextStyle @@ -56,7 +58,6 @@ actual fun PlatformTextField( ) { val cs = composeState.value val textColor = MaterialTheme.colors.onBackground - val tintColor = MaterialTheme.colors.secondaryVariant val padding = PaddingValues(12.dp, 7.dp, 45.dp, 0.dp) val paddingStart = with(LocalDensity.current) { 12.dp.roundToPx() } val paddingTop = with(LocalDensity.current) { 7.dp.roundToPx() } @@ -109,9 +110,7 @@ actual fun PlatformTextField( editText.inputType = InputType.TYPE_TEXT_FLAG_CAP_SENTENCES or editText.inputType editText.setTextColor(textColor.toArgb()) editText.textSize = textStyle.value.fontSize.value * appPrefs.fontScale.get() - val drawable = androidAppContext.getDrawable(R.drawable.send_msg_view_background)!! - DrawableCompat.setTint(drawable, tintColor.toArgb()) - editText.background = drawable + editText.background = ColorDrawable(Color.Transparent.toArgb()) editText.setPadding(paddingStart, paddingTop, paddingEnd, paddingBottom) editText.setText(cs.message) if (Build.VERSION.SDK_INT >= 29) { @@ -137,7 +136,6 @@ actual fun PlatformTextField( }) { it.setTextColor(textColor.toArgb()) it.textSize = textStyle.value.fontSize.value * appPrefs.fontScale.get() - DrawableCompat.setTint(it.background, tintColor.toArgb()) it.isFocusable = composeState.value.preview !is ComposePreview.VoicePreview it.isFocusableInTouchMode = it.isFocusable if (cs.message != it.text.toString()) { diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.android.kt index c606e9acb0..05a9430ff1 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.android.kt @@ -2,12 +2,15 @@ package chat.simplex.common.views.chat.item import android.os.Build.VERSION.SDK_INT import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.platform.LocalContext import chat.simplex.common.model.CIFile +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.CurrentColors import chat.simplex.common.views.helpers.ModalManager import coil.ImageLoader import coil.compose.rememberAsyncImagePainter @@ -21,6 +24,7 @@ actual fun SimpleAndAnimatedImageView( imageBitmap: ImageBitmap, file: CIFile?, imageProvider: () -> ImageGalleryProvider, + smallView: Boolean, ImageView: @Composable (painter: Painter, onClick: () -> Unit) -> Unit ) { val context = LocalContext.current @@ -35,6 +39,14 @@ actual fun SimpleAndAnimatedImageView( if (getLoadedFilePath(file) != null) { ModalManager.fullscreen.showCustomModal(animated = false) { close -> ImageFullScreenView(imageProvider, close) + if (smallView) { + DisposableEffect(Unit) { + onDispose { + val c = CurrentColors.value.colors + platform.androidSetStatusAndNavBarColors(c.isLight, c.background, !appPrefs.oneHandUI.get(), appPrefs.oneHandUI.get()) + } + } + } } } } diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.android.kt index f0f733111a..1b9e80b8a0 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.android.kt @@ -6,6 +6,7 @@ import androidx.compose.material.Divider import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.scale import androidx.compose.ui.unit.dp import chat.simplex.common.platform.onRightClick import chat.simplex.common.views.helpers.* @@ -19,8 +20,14 @@ actual fun ChatListNavLinkLayout( disabled: Boolean, selectedChat: State, nextChatSelected: State, + oneHandUI: State ) { var modifier = Modifier.fillMaxWidth() + + if (oneHandUI != null && oneHandUI.value) { + modifier = modifier.scale(scaleX = 1f, scaleY = -1f) + } + if (!disabled) modifier = modifier .combinedClickable(onClick = click, onLongClick = { showMenu.value = true }) .onRightClick { showMenu.value = true } diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.android.kt index 4a8b912cdd..db56eeb508 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.android.kt @@ -36,7 +36,7 @@ private val CALL_BOTTOM_ICON_OFFSET = (-15).dp private val CALL_BOTTOM_ICON_HEIGHT = CALL_INTERACTIVE_AREA_HEIGHT + CALL_BOTTOM_ICON_OFFSET @Composable -actual fun ActiveCallInteractiveArea(call: Call, newChatSheetState: MutableStateFlow) { +actual fun ActiveCallInteractiveArea(call: Call) { val onClick = { platform.androidStartCallActivity(false) } Box(Modifier.offset(y = CALL_TOP_OFFSET).height(CALL_INTERACTIVE_AREA_HEIGHT)) { val source = remember { MutableInteractionSource() } diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/Appearance.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/Appearance.android.kt index 9601152773..0c105a1da1 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/Appearance.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/Appearance.android.kt @@ -30,6 +30,7 @@ import chat.simplex.common.model.ChatModel import chat.simplex.common.platform.* import chat.simplex.common.helpers.APPLICATION_ID import chat.simplex.common.helpers.saveAppLocale +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.res.MR import dev.icerock.moko.resources.ImageResource import dev.icerock.moko.resources.compose.painterResource @@ -78,7 +79,7 @@ fun AppearanceScope.AppearanceLayout( Modifier.fillMaxWidth(), ) { AppBarTitle(stringResource(MR.strings.appearance_settings)) - SectionView(stringResource(MR.strings.settings_section_title_language), padding = PaddingValues()) { + SectionView(stringResource(MR.strings.settings_section_title_interface), padding = PaddingValues()) { val context = LocalContext.current // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { // SectionItemWithValue( @@ -104,6 +105,11 @@ fun AppearanceScope.AppearanceLayout( } } // } + + SettingsPreferenceItem(icon = null, stringResource(MR.strings.one_hand_ui), ChatModel.controller.appPrefs.oneHandUI) { + val c = CurrentColors.value.colors + platform.androidSetStatusAndNavBarColors(c.isLight, c.background, false, false) + } } SectionDividerSpaced(maxTopPadding = true) 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 59f5307a19..b24c05937d 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 @@ -235,7 +235,7 @@ fun AndroidScreen(settingsState: SettingsViewState) { BoxWithConstraints { val call = remember { chatModel.activeCall} .value val showCallArea = call != null && call.callState != CallState.WaitCapabilities && call.callState != CallState.InvitationAccepted - var currentChatId by rememberSaveable { mutableStateOf(chatModel.chatId.value) } + val currentChatId = remember { mutableStateOf(chatModel.chatId.value) } val offset = remember { Animatable(if (chatModel.chatId.value == null) 0f else maxWidth.value) } Box( Modifier @@ -264,21 +264,35 @@ fun AndroidScreen(settingsState: SettingsViewState) { snapshotFlow { chatModel.chatId.value } .distinctUntilChanged() .collect { - if (it == null) onComposed(null) - currentChatId = it + if (it == null) { + platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, CurrentColors.value.colors.background, !appPrefs.oneHandUI.get(), appPrefs.oneHandUI.get()) + onComposed(null) + } + currentChatId.value = it } } } + LaunchedEffect(Unit) { + snapshotFlow { ModalManager.center.modalCount.value > 0 } + .filter { chatModel.chatId.value == null } + .collect { modalBackground -> + if (modalBackground && !chatModel.newChatSheetVisible.value) { + platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, CurrentColors.value.colors.background, false, false) + } else { + platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, CurrentColors.value.colors.background, !appPrefs.oneHandUI.get(), appPrefs.oneHandUI.get()) + } + } + } Box(Modifier .graphicsLayer { translationX = maxWidth.toPx() - offset.value.dp.toPx() } .padding(top = if (showCallArea) ANDROID_CALL_TOP_PADDING else 0.dp) ) Box2@{ - currentChatId?.let { - ChatView(it, chatModel, onComposed) + currentChatId.value?.let { + ChatView(currentChatId, onComposed) } } if (call != null && showCallArea) { - ActiveCallInteractiveArea(call, remember { MutableStateFlow(AnimatedViewState.GONE) }) + ActiveCallInteractiveArea(call) } } } @@ -298,7 +312,7 @@ fun StartPartOfScreen(settingsState: SettingsViewState) { @Composable fun CenterPartOfScreen() { - val currentChatId by remember { ChatModel.chatId } + val currentChatId = remember { ChatModel.chatId } LaunchedEffect(Unit) { snapshotFlow { currentChatId } .distinctUntilChanged() @@ -308,7 +322,7 @@ fun CenterPartOfScreen() { } } } - when (val id = currentChatId) { + when (currentChatId.value) { null -> { if (!rememberUpdatedState(ModalManager.center.hasModalsOpen()).value) { Box( @@ -323,7 +337,7 @@ fun CenterPartOfScreen() { ModalManager.center.showInView() } } - else -> ChatView(id, chatModel) {} + else -> ChatView(currentChatId) {} } } 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 025f722734..e396ecad56 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 @@ -54,7 +54,9 @@ object ChatModel { val ctrlInitInProgress = mutableStateOf(false) val dbMigrationInProgress = mutableStateOf(false) val incompleteInitializedDbRemoved = mutableStateOf(false) - val chats = mutableStateListOf() + private val _chats = mutableStateOf(SnapshotStateList()) + val chats: State> = _chats + private val chatsContext = ChatsContext() // map of connections network statuses, key is agent connection id val networkStatuses = mutableStateMapOf() val switchingUsersAndHosts = mutableStateOf(false) @@ -81,6 +83,9 @@ object ChatModel { // set when app is opened via contact or invitation URI (rhId, uri) val appOpenUrl = mutableStateOf?>(null) + // Needed to check for bottom nav bar and to apply or not navigation bar color on Android + val newChatSheetVisible = mutableStateOf(false) + // preferences val notificationPreviewMode by lazy { mutableStateOf( @@ -126,7 +131,7 @@ object ChatModel { val updatingProgress = mutableStateOf(null as Float?) var updatingRequest: Closeable? = null - val updatingChatsMutex: Mutex = Mutex() + private val updatingChatsMutex: Mutex = Mutex() val changingActiveUserMutex: Mutex = Mutex() val desktopNoUserNoRemote: Boolean @Composable get() = appPlatform.isDesktop && currentUser.value == null && currentRemoteHost.value == null @@ -170,11 +175,11 @@ object ChatModel { } // toList() here is to prevent ConcurrentModificationException that is rarely happens but happens - fun hasChat(rhId: Long?, id: String): Boolean = chats.toList().firstOrNull { it.id == id && it.remoteHostId == rhId } != null + fun hasChat(rhId: Long?, id: String): Boolean = chats.value.firstOrNull { it.id == id && it.remoteHostId == rhId } != null // TODO pass rhId? - fun getChat(id: String): Chat? = chats.toList().firstOrNull { it.id == id } - fun getContactChat(contactId: Long): Chat? = chats.toList().firstOrNull { it.chatInfo is ChatInfo.Direct && it.chatInfo.apiId == contactId } - fun getGroupChat(groupId: Long): Chat? = chats.toList().firstOrNull { it.chatInfo is ChatInfo.Group && it.chatInfo.apiId == groupId } + fun getChat(id: String): Chat? = chats.value.firstOrNull { it.id == id } + fun getContactChat(contactId: Long): Chat? = chats.value.firstOrNull { it.chatInfo is ChatInfo.Direct && it.chatInfo.apiId == contactId } + fun getGroupChat(groupId: Long): Chat? = chats.value.firstOrNull { it.chatInfo is ChatInfo.Group && it.chatInfo.apiId == groupId } fun populateGroupMembersIndexes() { groupMembersIndexes.clear() @@ -192,97 +197,102 @@ object ChatModel { } } - private fun getChatIndex(rhId: Long?, id: String): Int = chats.toList().indexOfFirst { it.id == id && it.remoteHostId == rhId } - fun addChat(chat: Chat) = chats.add(index = 0, chat) + suspend fun withChats(action: suspend ChatsContext.() -> T): T = updatingChatsMutex.withLock { + chatsContext.action() + } - fun updateChatInfo(rhId: Long?, cInfo: ChatInfo) { - val i = getChatIndex(rhId, cInfo.id) - if (i >= 0) { - val currentCInfo = chats[i].chatInfo - var newCInfo = cInfo - if (currentCInfo is ChatInfo.Direct && newCInfo is ChatInfo.Direct) { - val currentStats = currentCInfo.contact.activeConn?.connectionStats - val newConn = newCInfo.contact.activeConn - val newStats = newConn?.connectionStats - if (currentStats != null && newConn != null && newStats == null) { - newCInfo = newCInfo.copy( - contact = newCInfo.contact.copy( - activeConn = newConn.copy( - connectionStats = currentStats + class ChatsContext { + val chats = _chats + + fun addChat(chat: Chat) = chats.add(index = 0, chat) + + fun updateChatInfo(rhId: Long?, cInfo: ChatInfo) { + val i = getChatIndex(rhId, cInfo.id) + if (i >= 0) { + val currentCInfo = chats[i].chatInfo + var newCInfo = cInfo + if (currentCInfo is ChatInfo.Direct && newCInfo is ChatInfo.Direct) { + val currentStats = currentCInfo.contact.activeConn?.connectionStats + val newConn = newCInfo.contact.activeConn + val newStats = newConn?.connectionStats + if (currentStats != null && newConn != null && newStats == null) { + newCInfo = newCInfo.copy( + contact = newCInfo.contact.copy( + activeConn = newConn.copy( + connectionStats = currentStats + ) ) ) - ) - } - } - chats[i] = chats[i].copy(chatInfo = newCInfo) - } - } - - fun updateContactConnection(rhId: Long?, contactConnection: PendingContactConnection) = updateChat(rhId, ChatInfo.ContactConnection(contactConnection)) - - fun updateContact(rhId: Long?, contact: Contact) = updateChat(rhId, ChatInfo.Direct(contact), addMissing = contact.directOrUsed) - - fun updateContactConnectionStats(rhId: Long?, contact: Contact, connectionStats: ConnectionStats) { - val updatedConn = contact.activeConn?.copy(connectionStats = connectionStats) - val updatedContact = contact.copy(activeConn = updatedConn) - updateContact(rhId, updatedContact) - } - - fun updateGroup(rhId: Long?, groupInfo: GroupInfo) = updateChat(rhId, ChatInfo.Group(groupInfo)) - - private fun updateChat(rhId: Long?, cInfo: ChatInfo, addMissing: Boolean = true) { - if (hasChat(rhId, cInfo.id)) { - updateChatInfo(rhId, cInfo) - } else if (addMissing) { - addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf())) - } - } - - fun updateChats(newChats: List) { - chats.clear() - chats.addAll(newChats) - - val cId = chatId.value - // If chat is null, it was deleted in background after apiGetChats call - if (cId != null && getChat(cId) == null) { - chatId.value = null - } - } - - fun replaceChat(rhId: Long?, id: String, chat: Chat) { - val i = getChatIndex(rhId, id) - if (i >= 0) { - chats[i] = chat - } else { - // invalid state, correcting - chats.add(index = 0, chat) - } - } - - suspend fun addChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem) = updatingChatsMutex.withLock { - // update previews - val i = getChatIndex(rhId, cInfo.id) - val chat: Chat - if (i >= 0) { - chat = chats[i] - val newPreviewItem = when (cInfo) { - is ChatInfo.Group -> { - val currentPreviewItem = chat.chatItems.firstOrNull() - if (currentPreviewItem != null) { - if (cItem.meta.itemTs >= currentPreviewItem.meta.itemTs) { - cItem - } else { - currentPreviewItem - } - } else { - cItem } } - else -> cItem + chats[i] = chats[i].copy(chatInfo = newCInfo) } - chats[i] = chat.copy( - chatItems = arrayListOf(newPreviewItem), - chatStats = + } + + fun updateContactConnection(rhId: Long?, contactConnection: PendingContactConnection) = updateChat(rhId, ChatInfo.ContactConnection(contactConnection)) + + fun updateContact(rhId: Long?, contact: Contact) = updateChat(rhId, ChatInfo.Direct(contact), addMissing = contact.directOrUsed) + + fun updateContactConnectionStats(rhId: Long?, contact: Contact, connectionStats: ConnectionStats) { + val updatedConn = contact.activeConn?.copy(connectionStats = connectionStats) + val updatedContact = contact.copy(activeConn = updatedConn) + updateContact(rhId, updatedContact) + } + + fun updateGroup(rhId: Long?, groupInfo: GroupInfo) = updateChat(rhId, ChatInfo.Group(groupInfo)) + + private fun updateChat(rhId: Long?, cInfo: ChatInfo, addMissing: Boolean = true) { + if (hasChat(rhId, cInfo.id)) { + updateChatInfo(rhId, cInfo) + } else if (addMissing) { + addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf())) + } + } + + fun updateChats(newChats: List) { + chats.clear() + chats.addAll(newChats) + + val cId = chatId.value + // If chat is null, it was deleted in background after apiGetChats call + if (cId != null && getChat(cId) == null) { + chatId.value = null + } + } + + fun replaceChat(rhId: Long?, id: String, chat: Chat) { + val i = getChatIndex(rhId, id) + if (i >= 0) { + chats[i] = chat + } else { + // invalid state, correcting + chats.add(index = 0, chat) + } + } + suspend fun addChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem) { + // update previews + val i = getChatIndex(rhId, cInfo.id) + val chat: Chat + if (i >= 0) { + chat = chats[i] + val newPreviewItem = when (cInfo) { + is ChatInfo.Group -> { + val currentPreviewItem = chat.chatItems.firstOrNull() + if (currentPreviewItem != null) { + if (cItem.meta.itemTs >= currentPreviewItem.meta.itemTs) { + cItem + } else { + currentPreviewItem + } + } else { + cItem + } + } + else -> cItem + } + chats[i] = chat.copy( + chatItems = arrayListOf(newPreviewItem), + chatStats = if (cItem.meta.itemStatus is CIStatus.RcvNew) { val minUnreadId = if(chat.chatStats.minUnreadItemId == 0L) cItem.id else chat.chatStats.minUnreadItemId increaseUnreadCounter(rhId, currentUser.value!!) @@ -290,123 +300,197 @@ object ChatModel { } else chat.chatStats - ) - if (i > 0) { - popChat_(i) + ) + if (i > 0) { + chats.add(index = 0, chats.removeAt(i)) + } + } else { + addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem))) } - } else { - addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem))) - } - withContext(Dispatchers.Main) { - // add to current chat - if (chatId.value == cInfo.id) { - // Prevent situation when chat item already in the list received from backend - if (chatItems.value.none { it.id == cItem.id }) { - if (chatItems.value.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) { - chatItems.add(kotlin.math.max(0, chatItems.value.lastIndex), cItem) - } else { - chatItems.add(cItem) + withContext(Dispatchers.Main) { + // add to current chat + if (chatId.value == cInfo.id) { + // Prevent situation when chat item already in the list received from backend + if (chatItems.value.none { it.id == cItem.id }) { + if (chatItems.value.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) { + chatItems.add(kotlin.math.max(0, chatItems.value.lastIndex), cItem) + } else { + chatItems.add(cItem) + } } } } } - } - suspend fun upsertChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem): Boolean = updatingChatsMutex.withLock { - // update previews - val i = getChatIndex(rhId, cInfo.id) - val chat: Chat - val res: Boolean - if (i >= 0) { - chat = chats[i] - val pItem = chat.chatItems.lastOrNull() - if (pItem?.id == cItem.id) { - chats[i] = chat.copy(chatItems = arrayListOf(cItem)) - if (pItem.isRcvNew && !cItem.isRcvNew) { - // status changed from New to Read, update counter - decreaseCounterInChat(rhId, cInfo.id) + suspend fun upsertChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem): Boolean { + // update previews + val i = getChatIndex(rhId, cInfo.id) + val chat: Chat + val res: Boolean + if (i >= 0) { + chat = chats[i] + val pItem = chat.chatItems.lastOrNull() + if (pItem?.id == cItem.id) { + chats[i] = chat.copy(chatItems = arrayListOf(cItem)) + if (pItem.isRcvNew && !cItem.isRcvNew) { + // status changed from New to Read, update counter + decreaseCounterInChat(rhId, cInfo.id) + } + } + res = false + } else { + addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem))) + res = true + } + return withContext(Dispatchers.Main) { + // update current chat + if (chatId.value == cInfo.id) { + val items = chatItems.value + val itemIndex = items.indexOfFirst { it.id == cItem.id } + if (itemIndex >= 0) { + items[itemIndex] = cItem + false + } else { + val status = chatItemStatuses.remove(cItem.id) + val ci = if (status != null && cItem.meta.itemStatus is CIStatus.SndNew) { + cItem.copy(meta = cItem.meta.copy(itemStatus = status)) + } else { + cItem + } + chatItems.add(ci) + true + } + } else { + res } } - res = false - } else { - addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem))) - res = true } - return withContext(Dispatchers.Main) { - // update current chat + + suspend fun updateChatItem(cInfo: ChatInfo, cItem: ChatItem, status: CIStatus? = null) { + withContext(Dispatchers.Main) { + if (chatId.value == cInfo.id) { + val items = chatItems.value + val itemIndex = items.indexOfFirst { it.id == cItem.id } + if (itemIndex >= 0) { + items[itemIndex] = cItem + } + } else if (status != null) { + chatItemStatuses[cItem.id] = status + } + } + } + + fun removeChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem) { + if (cItem.isRcvNew) { + decreaseCounterInChat(rhId, cInfo.id) + } + // update previews + val i = getChatIndex(rhId, cInfo.id) + val chat: Chat + if (i >= 0) { + chat = chats[i] + val pItem = chat.chatItems.lastOrNull() + if (pItem?.id == cItem.id) { + chats[i] = chat.copy(chatItems = arrayListOf(ChatItem.deletedItemDummy)) + } + } + // remove from current chat if (chatId.value == cInfo.id) { - val items = chatItems.value - val itemIndex = items.indexOfFirst { it.id == cItem.id } - if (itemIndex >= 0) { - items[itemIndex] = cItem + chatItems.removeAll { + val remove = it.id == cItem.id + if (remove) { AudioPlayer.stop(it) } + remove + } + } + } + + fun clearChat(rhId: Long?, cInfo: ChatInfo) { + // clear preview + val i = getChatIndex(rhId, cInfo.id) + if (i >= 0) { + decreaseUnreadCounter(rhId, currentUser.value!!, chats[i].chatStats.unreadCount) + chats[i] = chats[i].copy(chatItems = arrayListOf(), chatStats = Chat.ChatStats(), chatInfo = cInfo) + } + // clear current chat + if (chatId.value == cInfo.id) { + chatItemStatuses.clear() + chatItems.clear() + } + } + + fun markChatItemsRead(remoteHostId: Long?, chatInfo: ChatInfo, range: CC.ItemRange? = null, unreadCountAfter: Int? = null) { + val cInfo = chatInfo + val markedRead = markItemsReadInCurrentChat(chatInfo, range) + // update preview + val chatIdx = getChatIndex(remoteHostId, cInfo.id) + if (chatIdx >= 0) { + val chat = chats[chatIdx] + val lastId = chat.chatItems.lastOrNull()?.id + if (lastId != null) { + val unreadCount = unreadCountAfter ?: if (range != null) chat.chatStats.unreadCount - markedRead else 0 + decreaseUnreadCounter(remoteHostId, currentUser.value!!, chat.chatStats.unreadCount - unreadCount) + chats[chatIdx] = chat.copy( + chatStats = chat.chatStats.copy( + unreadCount = unreadCount, + // Can't use minUnreadItemId currently since chat items can have unread items between read items + //minUnreadItemId = if (range != null) kotlin.math.max(chat.chatStats.minUnreadItemId, range.to + 1) else lastId + 1 + ) + ) + } + } + } + + private fun decreaseCounterInChat(rhId: Long?, chatId: ChatId) { + val chatIndex = getChatIndex(rhId, chatId) + if (chatIndex == -1) return + + val chat = chats[chatIndex] + val unreadCount = kotlin.math.max(chat.chatStats.unreadCount - 1, 0) + decreaseUnreadCounter(rhId, currentUser.value!!, chat.chatStats.unreadCount - unreadCount) + chats[chatIndex] = chat.copy( + chatStats = chat.chatStats.copy( + unreadCount = unreadCount, + ) + ) + } + + fun removeChat(rhId: Long?, id: String) { + chats.removeAll { it.id == id && it.remoteHostId == rhId } + } + + fun upsertGroupMember(rhId: Long?, groupInfo: GroupInfo, member: GroupMember): Boolean { + // user member was updated + if (groupInfo.membership.groupMemberId == member.groupMemberId) { + updateGroup(rhId, groupInfo) + return false + } + // update current chat + return if (chatId.value == groupInfo.id) { + val memberIndex = groupMembersIndexes[member.groupMemberId] + if (memberIndex != null) { + groupMembers[memberIndex] = member false } else { - val status = chatItemStatuses.remove(cItem.id) - val ci = if (status != null && cItem.meta.itemStatus is CIStatus.SndNew) { - cItem.copy(meta = cItem.meta.copy(itemStatus = status)) - } else { - cItem - } - chatItems.add(ci) + groupMembers.add(member) + groupMembersIndexes[member.groupMemberId] = groupMembers.size - 1 true } } else { - res + false + } + } + + fun updateGroupMemberConnectionStats(rhId: Long?, groupInfo: GroupInfo, member: GroupMember, connectionStats: ConnectionStats) { + val memberConn = member.activeConn + if (memberConn != null) { + val updatedConn = memberConn.copy(connectionStats = connectionStats) + val updatedMember = member.copy(activeConn = updatedConn) + upsertGroupMember(rhId, groupInfo, updatedMember) } } } - suspend fun updateChatItem(cInfo: ChatInfo, cItem: ChatItem, status: CIStatus? = null) { - withContext(Dispatchers.Main) { - if (chatId.value == cInfo.id) { - val items = chatItems.value - val itemIndex = items.indexOfFirst { it.id == cItem.id } - if (itemIndex >= 0) { - items[itemIndex] = cItem - } - } else if (status != null) { - chatItemStatuses[cItem.id] = status - } - } - } - - fun removeChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem) { - if (cItem.isRcvNew) { - decreaseCounterInChat(rhId, cInfo.id) - } - // update previews - val i = getChatIndex(rhId, cInfo.id) - val chat: Chat - if (i >= 0) { - chat = chats[i] - val pItem = chat.chatItems.lastOrNull() - if (pItem?.id == cItem.id) { - chats[i] = chat.copy(chatItems = arrayListOf(ChatItem.deletedItemDummy)) - } - } - // remove from current chat - if (chatId.value == cInfo.id) { - chatItems.removeAll { - val remove = it.id == cItem.id - if (remove) { AudioPlayer.stop(it) } - remove - } - } - } - - fun clearChat(rhId: Long?, cInfo: ChatInfo) { - // clear preview - val i = getChatIndex(rhId, cInfo.id) - if (i >= 0) { - decreaseUnreadCounter(rhId, currentUser.value!!, chats[i].chatStats.unreadCount) - chats[i] = chats[i].copy(chatItems = arrayListOf(), chatStats = Chat.ChatStats(), chatInfo = cInfo) - } - // clear current chat - if (chatId.value == cInfo.id) { - chatItemStatuses.clear() - chatItems.clear() - } - } + private fun getChatIndex(rhId: Long?, id: String): Int = chats.value.indexOfFirst { it.id == id && it.remoteHostId == rhId } fun updateCurrentUser(rhId: Long?, newProfile: Profile, preferences: FullChatPreferences? = null) { val current = currentUser.value ?: return @@ -447,30 +531,8 @@ object ChatModel { } } - fun markChatItemsRead(chat: Chat, range: CC.ItemRange? = null, unreadCountAfter: Int? = null) { - val cInfo = chat.chatInfo - val markedRead = markItemsReadInCurrentChat(chat, range) - // update preview - val chatIdx = getChatIndex(chat.remoteHostId, cInfo.id) - if (chatIdx >= 0) { - val chat = chats[chatIdx] - val lastId = chat.chatItems.lastOrNull()?.id - if (lastId != null) { - val unreadCount = unreadCountAfter ?: if (range != null) chat.chatStats.unreadCount - markedRead else 0 - decreaseUnreadCounter(chat.remoteHostId, currentUser.value!!, chat.chatStats.unreadCount - unreadCount) - chats[chatIdx] = chat.copy( - chatStats = chat.chatStats.copy( - unreadCount = unreadCount, - // Can't use minUnreadItemId currently since chat items can have unread items between read items - //minUnreadItemId = if (range != null) kotlin.math.max(chat.chatStats.minUnreadItemId, range.to + 1) else lastId + 1 - ) - ) - } - } - } - - private fun markItemsReadInCurrentChat(chat: Chat, range: CC.ItemRange? = null): Int { - val cInfo = chat.chatInfo + private fun markItemsReadInCurrentChat(chatInfo: ChatInfo, range: CC.ItemRange? = null): Int { + val cInfo = chatInfo var markedRead = 0 if (chatId.value == cInfo.id) { var i = 0 @@ -493,20 +555,6 @@ object ChatModel { return markedRead } - private fun decreaseCounterInChat(rhId: Long?, chatId: ChatId) { - val chatIndex = getChatIndex(rhId, chatId) - if (chatIndex == -1) return - - val chat = chats[chatIndex] - val unreadCount = kotlin.math.max(chat.chatStats.unreadCount - 1, 0) - decreaseUnreadCounter(rhId, currentUser.value!!, chat.chatStats.unreadCount - unreadCount) - chats[chatIndex] = chat.copy( - chatStats = chat.chatStats.copy( - unreadCount = unreadCount, - ) - ) - } - fun increaseUnreadCounter(rhId: Long?, user: UserLike) { changeUnreadCounter(rhId, user, 1) } @@ -600,11 +648,6 @@ object ChatModel { // } // } - private fun popChat_(i: Int) { - val chat = chats.removeAt(i) - chats.add(index = 0, chat) - } - fun replaceConnReqView(id: String, withId: String) { if (id == showingInvitation.value?.connId) { showingInvitation.value = null @@ -629,41 +672,6 @@ object ChatModel { showingInvitation.value = showingInvitation.value?.copy(connChatUsed = true) } - fun removeChat(rhId: Long?, id: String) { - chats.removeAll { it.id == id && it.remoteHostId == rhId } - } - - fun upsertGroupMember(rhId: Long?, groupInfo: GroupInfo, member: GroupMember): Boolean { - // user member was updated - if (groupInfo.membership.groupMemberId == member.groupMemberId) { - updateGroup(rhId, groupInfo) - return false - } - // update current chat - return if (chatId.value == groupInfo.id) { - val memberIndex = groupMembersIndexes[member.groupMemberId] - if (memberIndex != null) { - groupMembers[memberIndex] = member - false - } else { - groupMembers.add(member) - groupMembersIndexes[member.groupMemberId] = groupMembers.size - 1 - true - } - } else { - false - } - } - - fun updateGroupMemberConnectionStats(rhId: Long?, groupInfo: GroupInfo, member: GroupMember, connectionStats: ConnectionStats) { - val memberConn = member.activeConn - if (memberConn != null) { - val updatedConn = memberConn.copy(connectionStats = connectionStats) - val updatedMember = member.copy(activeConn = updatedConn) - upsertGroupMember(rhId, groupInfo, updatedMember) - } - } - fun setContactNetworkStatus(contact: Contact, status: NetworkStatus) { val conn = contact.activeConn if (conn != null) { @@ -802,6 +810,7 @@ interface SomeChat { val id: ChatId val apiId: Long val ready: Boolean + val chatDeleted: Boolean val sendMsgEnabled: Boolean val ntfsEnabled: Boolean val incognito: Boolean @@ -818,14 +827,6 @@ data class Chat( val chatItems: List, val chatStats: ChatStats = ChatStats() ) { - val userCanSend: Boolean - get() = when (chatInfo) { - is ChatInfo.Direct -> true - is ChatInfo.Group -> chatInfo.groupInfo.membership.memberRole >= GroupMemberRole.Member - is ChatInfo.Local -> true - else -> false - } - val nextSendGrpInv: Boolean get() = when (chatInfo) { is ChatInfo.Direct -> chatInfo.contact.nextSendGrpInv @@ -882,6 +883,7 @@ sealed class ChatInfo: SomeChat, NamedChat { override val id get() = contact.id override val apiId get() = contact.apiId override val ready get() = contact.ready + override val chatDeleted get() = contact.chatDeleted override val sendMsgEnabled get() = contact.sendMsgEnabled override val ntfsEnabled get() = contact.ntfsEnabled override val incognito get() = contact.incognito @@ -906,6 +908,7 @@ sealed class ChatInfo: SomeChat, NamedChat { override val id get() = groupInfo.id override val apiId get() = groupInfo.apiId override val ready get() = groupInfo.ready + override val chatDeleted get() = groupInfo.chatDeleted override val sendMsgEnabled get() = groupInfo.sendMsgEnabled override val ntfsEnabled get() = groupInfo.ntfsEnabled override val incognito get() = groupInfo.incognito @@ -930,6 +933,7 @@ sealed class ChatInfo: SomeChat, NamedChat { override val id get() = noteFolder.id override val apiId get() = noteFolder.apiId override val ready get() = noteFolder.ready + override val chatDeleted get() = noteFolder.chatDeleted override val sendMsgEnabled get() = noteFolder.sendMsgEnabled override val ntfsEnabled get() = noteFolder.ntfsEnabled override val incognito get() = noteFolder.incognito @@ -954,6 +958,7 @@ sealed class ChatInfo: SomeChat, NamedChat { override val id get() = contactRequest.id override val apiId get() = contactRequest.apiId override val ready get() = contactRequest.ready + override val chatDeleted get() = contactRequest.chatDeleted override val sendMsgEnabled get() = contactRequest.sendMsgEnabled override val ntfsEnabled get() = contactRequest.ntfsEnabled override val incognito get() = contactRequest.incognito @@ -978,6 +983,7 @@ sealed class ChatInfo: SomeChat, NamedChat { override val id get() = contactConnection.id override val apiId get() = contactConnection.apiId override val ready get() = contactConnection.ready + override val chatDeleted get() = contactConnection.chatDeleted override val sendMsgEnabled get() = contactConnection.sendMsgEnabled override val ntfsEnabled get() = false override val incognito get() = contactConnection.incognito @@ -1003,6 +1009,7 @@ sealed class ChatInfo: SomeChat, NamedChat { override val id get() = "" override val apiId get() = 0L override val ready get() = false + override val chatDeleted get() = false override val sendMsgEnabled get() = false override val ntfsEnabled get() = false override val incognito get() = false @@ -1036,6 +1043,15 @@ sealed class ChatInfo: SomeChat, NamedChat { is ContactConnection -> contactConnection.updatedAt is InvalidJSON -> updatedAt } + + val userCanSend: Boolean + get() = when (this) { + is ChatInfo.Direct -> true + is ChatInfo.Group -> groupInfo.membership.memberRole >= GroupMemberRole.Member + is ChatInfo.Local -> true + else -> false + } + } @Serializable @@ -1079,6 +1095,7 @@ data class Contact( val chatTs: Instant?, val contactGroupMemberId: Long? = null, val contactGrpInvSent: Boolean, + override val chatDeleted: Boolean, val uiThemes: ThemeModeOverrides? = null, ): SomeChat, NamedChat { override val chatType get() = ChatType.Direct @@ -1153,6 +1170,7 @@ data class Contact( updatedAt = Clock.System.now(), chatTs = Clock.System.now(), contactGrpInvSent = false, + chatDeleted = false, uiThemes = null, ) } @@ -1161,7 +1179,8 @@ data class Contact( @Serializable enum class ContactStatus { @SerialName("active") Active, - @SerialName("deleted") Deleted; + @SerialName("deleted") Deleted, + @SerialName("deletedByUser") DeletedByUser; } @Serializable @@ -1304,6 +1323,7 @@ data class GroupInfo ( override val id get() = "#$groupId" override val apiId get() = groupId override val ready get() = membership.memberActive + override val chatDeleted get() = false override val sendMsgEnabled get() = membership.memberActive override val ntfsEnabled get() = chatSettings.enableNtfs == MsgFilter.All override val incognito get() = membership.memberIncognito @@ -1609,6 +1629,7 @@ class NoteFolder( override val chatType get() = ChatType.Local override val id get() = "*$noteFolderId" override val apiId get() = noteFolderId + override val chatDeleted get() = false override val ready get() = true override val sendMsgEnabled get() = true override val ntfsEnabled get() = false @@ -1645,6 +1666,7 @@ class UserContactRequest ( override val chatType get() = ChatType.ContactRequest override val id get() = "<@$contactRequestId" override val apiId get() = contactRequestId + override val chatDeleted get() = false override val ready get() = true override val sendMsgEnabled get() = false override val ntfsEnabled get() = false @@ -1684,6 +1706,7 @@ class PendingContactConnection( override val chatType get() = ChatType.ContactConnection override val id get () = ":$pccConnId" override val apiId get() = pccConnId + override val chatDeleted get() = false override val ready get() = false override val sendMsgEnabled get() = false override val ntfsEnabled get() = false @@ -1760,6 +1783,12 @@ enum class ConnStatus { } } +@Serializable +data class ChatItemDeletion ( + val deletedChatItem: AChatItem, + val toChatItem: AChatItem? = null +) + @Serializable class AChatItem ( val chatInfo: ChatInfo, @@ -2102,45 +2131,55 @@ data class ChatItem ( } } -fun MutableState>.add(index: Int, chatItem: ChatItem) { - value = SnapshotStateList().apply { addAll(value); add(index, chatItem) } +fun MutableState>.add(index: Int, elem: T) { + value = SnapshotStateList().apply { addAll(value); add(index, elem) } } -fun MutableState>.add(chatItem: ChatItem) { - value = SnapshotStateList().apply { addAll(value); add(chatItem) } +fun MutableState>.add(elem: T) { + value = SnapshotStateList().apply { addAll(value); add(elem) } } -fun MutableState>.addAll(index: Int, chatItems: List) { - value = SnapshotStateList().apply { addAll(value); addAll(index, chatItems) } +fun MutableState>.addAll(index: Int, elems: List) { + value = SnapshotStateList().apply { addAll(value); addAll(index, elems) } } -fun MutableState>.addAll(chatItems: List) { - value = SnapshotStateList().apply { addAll(value); addAll(chatItems) } +fun MutableState>.addAll(elems: List) { + value = SnapshotStateList().apply { addAll(value); addAll(elems) } } -fun MutableState>.removeAll(block: (ChatItem) -> Boolean) { - value = SnapshotStateList().apply { addAll(value); removeAll(block) } +fun MutableState>.removeAll(block: (T) -> Boolean) { + value = SnapshotStateList().apply { addAll(value); removeAll(block) } } -fun MutableState>.removeAt(index: Int) { - value = SnapshotStateList().apply { addAll(value); removeAt(index) } +fun MutableState>.removeAt(index: Int): T { + val new = SnapshotStateList() + new.addAll(value) + val res = new.removeAt(index) + value = new + return res } -fun MutableState>.removeLast() { - value = SnapshotStateList().apply { addAll(value); removeLast() } +fun MutableState>.removeLast() { + value = SnapshotStateList().apply { addAll(value); removeLast() } } -fun MutableState>.replaceAll(chatItems: List) { - value = SnapshotStateList().apply { addAll(chatItems) } +fun MutableState>.replaceAll(elems: List) { + value = SnapshotStateList().apply { addAll(elems) } } -fun MutableState>.clear() { - value = SnapshotStateList() +fun MutableState>.clear() { + value = SnapshotStateList() } -fun State>.asReversed(): MutableList = value.asReversed() +fun State>.asReversed(): MutableList = value.asReversed() -val State>.size: Int get() = value.size +fun State>.toList(): List = value.toList() + +operator fun State>.get(i: Int): T = value[i] + +operator fun State>.set(index: Int, elem: T) { value[index] = elem } + +val State>.size: Int get() = value.size enum class CIMergeCategory { MemberConnected, 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 37945729dd..2ac6886eb1 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 @@ -15,8 +15,8 @@ import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextAlign import chat.simplex.common.model.ChatController.getNetCfg import chat.simplex.common.model.ChatController.setNetCfg -import chat.simplex.common.model.ChatModel.updatingChatsMutex import chat.simplex.common.model.ChatModel.changingActiveUserMutex +import chat.simplex.common.model.ChatModel.withChats import dev.icerock.moko.resources.compose.painterResource import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* @@ -217,13 +217,16 @@ class AppPreferences { val desktopWindowState = mkStrPreference(SHARED_PREFS_DESKTOP_WINDOW_STATE, null) + val showDeleteConversationNotice = mkBoolPreference(SHARED_PREFS_SHOW_DELETE_CONVERSATION_NOTICE, true) + val showDeleteContactNotice = mkBoolPreference(SHARED_PREFS_SHOW_DELETE_CONTACT_NOTICE, true) val showSentViaProxy = mkBoolPreference(SHARED_PREFS_SHOW_SENT_VIA_RPOXY, false) val iosCallKitEnabled = mkBoolPreference(SHARED_PREFS_IOS_CALL_KIT_ENABLED, true) val iosCallKitCallsInRecents = mkBoolPreference(SHARED_PREFS_IOS_CALL_KIT_CALLS_IN_RECENTS, false) - + val oneHandUI = mkBoolPreference(SHARED_PREFS_ONE_HAND_UI, false) + private fun mkIntPreference(prefName: String, default: Int) = SharedPreference( get = fun() = settings.getInt(prefName, default), @@ -381,6 +384,7 @@ class AppPreferences { private const val SHARED_PREFS_ENCRYPTION_STARTED_AT = "EncryptionStartedAt" private const val SHARED_PREFS_NEW_DATABASE_INITIALIZED = "NewDatabaseInitialized" private const val SHARED_PREFS_CONFIRM_DB_UPGRADES = "ConfirmDBUpgrades" + private const val SHARED_PREFS_ONE_HAND_UI = "OneHandUI" private const val SHARED_PREFS_SELF_DESTRUCT = "LocalAuthenticationSelfDestruct" private const val SHARED_PREFS_SELF_DESTRUCT_DISPLAY_NAME = "LocalAuthenticationSelfDestructDisplayName" private const val SHARED_PREFS_PQ_EXPERIMENTAL_ENABLED = "PQExperimentalEnabled" // no longer used @@ -401,6 +405,8 @@ class AppPreferences { private const val SHARED_PREFS_CONNECT_REMOTE_VIA_MULTICAST_AUTO = "ConnectRemoteViaMulticastAuto" private const val SHARED_PREFS_OFFER_REMOTE_MULTICAST = "OfferRemoteMulticast" private const val SHARED_PREFS_DESKTOP_WINDOW_STATE = "DesktopWindowState" + private const val SHARED_PREFS_SHOW_DELETE_CONVERSATION_NOTICE = "showDeleteConversationNotice" + private const val SHARED_PREFS_SHOW_DELETE_CONTACT_NOTICE = "showDeleteContactNotice" private const val SHARED_PREFS_SHOW_SENT_VIA_RPOXY = "showSentViaProxy" private const val SHARED_PREFS_IOS_CALL_KIT_ENABLED = "iOSCallKitEnabled" @@ -478,9 +484,9 @@ object ChatController { } Log.d(TAG, "startChat: started") } else { - updatingChatsMutex.withLock { + withChats { val chats = apiGetChats(null) - chatModel.updateChats(chats) + updateChats(chats) } Log.d(TAG, "startChat: running") } @@ -558,9 +564,9 @@ object ChatController { val hasUser = chatModel.currentUser.value != null chatModel.userAddress.value = if (hasUser) apiGetUserAddress(rhId) else null chatModel.chatItemTTL.value = if (hasUser) getChatItemTTL(rhId) else ChatItemTTL.None - updatingChatsMutex.withLock { + withChats { val chats = apiGetChats(rhId) - chatModel.updateChats(chats) + updateChats(chats) } } @@ -776,9 +782,9 @@ object ChatController { throw Exception("failed to get app settings: ${r.responseType} ${r.details}") } - suspend fun apiExportArchive(config: ArchiveConfig) { + suspend fun apiExportArchive(config: ArchiveConfig): List { val r = sendCmd(null, CC.ApiExportArchive(config)) - if (r is CR.CmdOk) return + if (r is CR.ArchiveExported) return r.archiveErrors throw Exception("failed to export archive: ${r.responseType} ${r.details}") } @@ -885,16 +891,16 @@ object ChatController { return null } - suspend fun apiDeleteChatItem(rh: Long?, type: ChatType, id: Long, itemId: Long, mode: CIDeleteMode): CR.ChatItemDeleted? { - val r = sendCmd(rh, CC.ApiDeleteChatItem(type, id, itemId, mode)) - if (r is CR.ChatItemDeleted) return r + suspend fun apiDeleteChatItems(rh: Long?, type: ChatType, id: Long, itemIds: List, mode: CIDeleteMode): List? { + val r = sendCmd(rh, CC.ApiDeleteChatItem(type, id, itemIds, mode)) + if (r is CR.ChatItemsDeleted) return r.chatItemDeletions Log.e(TAG, "apiDeleteChatItem bad response: ${r.responseType} ${r.details}") return null } - suspend fun apiDeleteMemberChatItem(rh: Long?, groupId: Long, groupMemberId: Long, itemId: Long): Pair? { - val r = sendCmd(rh, CC.ApiDeleteMemberChatItem(groupId, groupMemberId, itemId)) - if (r is CR.ChatItemDeleted) return r.deletedChatItem.chatItem to r.toChatItem?.chatItem + suspend fun apiDeleteMemberChatItems(rh: Long?, groupId: Long, itemIds: List): List? { + val r = sendCmd(rh, CC.ApiDeleteMemberChatItem(groupId, itemIds)) + if (r is CR.ChatItemsDeleted) return r.chatItemDeletions Log.e(TAG, "apiDeleteMemberChatItem bad response: ${r.responseType} ${r.details}") return null } @@ -1177,16 +1183,18 @@ object ChatController { } } - suspend fun deleteChat(chat: Chat, notify: Boolean? = null) { + suspend fun deleteChat(chat: Chat, chatDeleteMode: ChatDeleteMode = ChatDeleteMode.Full(notify = true)) { val cInfo = chat.chatInfo - if (apiDeleteChat(rh = chat.remoteHostId, type = cInfo.chatType, id = cInfo.apiId, notify = notify)) { - chatModel.removeChat(chat.remoteHostId, cInfo.id) + if (apiDeleteChat(rh = chat.remoteHostId, type = cInfo.chatType, id = cInfo.apiId, chatDeleteMode = chatDeleteMode)) { + withChats { + removeChat(chat.remoteHostId, cInfo.id) + } } } - suspend fun apiDeleteChat(rh: Long?, type: ChatType, id: Long, notify: Boolean? = null): Boolean { + suspend fun apiDeleteChat(rh: Long?, type: ChatType, id: Long, chatDeleteMode: ChatDeleteMode = ChatDeleteMode.Full(notify = true)): Boolean { chatModel.deletedChats.value += rh to type.type + id - val r = sendCmd(rh, CC.ApiDeleteChat(type, id, notify)) + val r = sendCmd(rh, CC.ApiDeleteChat(type, id, chatDeleteMode)) val success = when { r is CR.ContactDeleted && type == ChatType.Direct -> true r is CR.ContactConnectionDeleted && type == ChatType.ContactConnection -> true @@ -1207,11 +1215,29 @@ object ChatController { return success } + suspend fun apiDeleteContact(rh: Long?, id: Long, chatDeleteMode: ChatDeleteMode = ChatDeleteMode.Full(notify = true)): Contact? { + val type = ChatType.Direct + chatModel.deletedChats.value += rh to type.type + id + val r = sendCmd(rh, CC.ApiDeleteChat(type, id, chatDeleteMode)) + val contact = when { + r is CR.ContactDeleted -> r.contact + else -> { + val titleId = MR.strings.error_deleting_contact + apiErrorAlert("apiDeleteChat", generalGetString(titleId), r) + null + } + } + chatModel.deletedChats.value -= rh to type.type + id + return contact + } + fun clearChat(chat: Chat, close: (() -> Unit)? = null) { withBGApi { val updatedChatInfo = apiClearChat(chat.remoteHostId, chat.chatInfo.chatType, chat.chatInfo.apiId) if (updatedChatInfo != null) { - chatModel.clearChat(chat.remoteHostId, updatedChatInfo) + withChats { + clearChat(chat.remoteHostId, updatedChatInfo) + } ntfManager.cancelNotificationsForChat(chat.chatInfo.id) close?.invoke() } @@ -1546,10 +1572,12 @@ object ChatController { val r = sendCmd(rh, CC.ApiJoinGroup(groupId)) when (r) { is CR.UserAcceptedGroupSent -> - chatModel.updateGroup(rh, r.groupInfo) + withChats { + updateGroup(rh, r.groupInfo) + } is CR.ChatCmdError -> { val e = r.chatError - suspend fun deleteGroup() { if (apiDeleteChat(rh, ChatType.Group, groupId)) { chatModel.removeChat(rh, "#$groupId") } } + suspend fun deleteGroup() { if (apiDeleteChat(rh, ChatType.Group, groupId)) { withChats { removeChat(rh, "#$groupId") } } } if (e is ChatError.ChatErrorAgent && e.agentError is AgentErrorType.SMP && e.agentError.smpErr is SMPErrorType.AUTH) { deleteGroup() AlertManager.shared.showAlertMsg(generalGetString(MR.strings.alert_title_group_invitation_expired), generalGetString(MR.strings.alert_message_group_invitation_expired)) @@ -1703,7 +1731,9 @@ object ChatController { val prefs = contact.mergedPreferences.toPreferences().setAllowed(feature, param = param) val toContact = apiSetContactPrefs(rh, contact.contactId, prefs) if (toContact != null) { - chatModel.updateContact(rh, toContact) + withChats { + updateContact(rh, toContact) + } } } @@ -1973,16 +2003,20 @@ object ChatController { when (r) { is CR.ContactDeletedByContact -> { if (active(r.user) && r.contact.directOrUsed) { - chatModel.updateContact(rhId, r.contact) + withChats { + updateContact(rhId, r.contact) + } } } is CR.ContactConnected -> { if (active(r.user) && r.contact.directOrUsed) { - chatModel.updateContact(rhId, r.contact) - val conn = r.contact.activeConn - if (conn != null) { - chatModel.replaceConnReqView(conn.id, "@${r.contact.contactId}") - chatModel.removeChat(rhId, conn.id) + withChats { + updateContact(rhId, r.contact) + val conn = r.contact.activeConn + if (conn != null) { + chatModel.replaceConnReqView(conn.id, "@${r.contact.contactId}") + removeChat(rhId, conn.id) + } } } if (r.contact.directOrUsed) { @@ -1992,21 +2026,25 @@ object ChatController { } is CR.ContactConnecting -> { if (active(r.user) && r.contact.directOrUsed) { - chatModel.updateContact(rhId, r.contact) - val conn = r.contact.activeConn - if (conn != null) { - chatModel.replaceConnReqView(conn.id, "@${r.contact.contactId}") - chatModel.removeChat(rhId, conn.id) + withChats { + updateContact(rhId, r.contact) + val conn = r.contact.activeConn + if (conn != null) { + chatModel.replaceConnReqView(conn.id, "@${r.contact.contactId}") + removeChat(rhId, conn.id) + } } } } is CR.ContactSndReady -> { if (active(r.user) && r.contact.directOrUsed) { - chatModel.updateContact(rhId, r.contact) - val conn = r.contact.activeConn - if (conn != null) { - chatModel.replaceConnReqView(conn.id, "@${r.contact.contactId}") - chatModel.removeChat(rhId, conn.id) + withChats { + updateContact(rhId, r.contact) + val conn = r.contact.activeConn + if (conn != null) { + chatModel.replaceConnReqView(conn.id, "@${r.contact.contactId}") + removeChat(rhId, conn.id) + } } } chatModel.setContactNetworkStatus(r.contact, NetworkStatus.Connected()) @@ -2015,10 +2053,12 @@ object ChatController { val contactRequest = r.contactRequest val cInfo = ChatInfo.ContactRequest(contactRequest) if (active(r.user)) { - if (chatModel.hasChat(rhId, contactRequest.id)) { - chatModel.updateChatInfo(rhId, cInfo) - } else { - chatModel.addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = listOf())) + withChats { + if (chatModel.hasChat(rhId, contactRequest.id)) { + updateChatInfo(rhId, cInfo) + } else { + addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = listOf())) + } } } ntfManager.notifyContactRequestReceived(r.user, cInfo) @@ -2026,12 +2066,16 @@ object ChatController { is CR.ContactUpdated -> { if (active(r.user) && chatModel.hasChat(rhId, r.toContact.id)) { val cInfo = ChatInfo.Direct(r.toContact) - chatModel.updateChatInfo(rhId, cInfo) + withChats { + updateChatInfo(rhId, cInfo) + } } } is CR.GroupMemberUpdated -> { if (active(r.user)) { - chatModel.upsertGroupMember(rhId, r.groupInfo, r.toMember) + withChats { + upsertGroupMember(rhId, r.groupInfo, r.toMember) + } } } is CR.ContactsMerged -> { @@ -2039,7 +2083,9 @@ object ChatController { if (chatModel.chatId.value == r.mergedContact.id) { chatModel.chatId.value = r.intoContact.id } - chatModel.removeChat(rhId, r.mergedContact.id) + withChats { + removeChat(rhId, r.mergedContact.id) + } } } // ContactsSubscribed, ContactsDisconnected and ContactSubSummary are only used in CLI, @@ -2049,7 +2095,9 @@ object ChatController { is CR.ContactSubSummary -> { for (sub in r.contactSubscriptions) { if (active(r.user)) { - chatModel.updateContact(rhId, sub.contact) + withChats { + updateContact(rhId, sub.contact) + } } val err = sub.contactError if (err == null) { @@ -2073,7 +2121,15 @@ object ChatController { val cInfo = r.chatItem.chatInfo val cItem = r.chatItem.chatItem if (active(r.user)) { - chatModel.addChatItem(rhId, cInfo, cItem) + if (cInfo is ChatInfo.Direct && cInfo.chatDeleted) { + val updatedContact = cInfo.contact.copy(chatDeleted = false) + withChats { + updateContact(rhId, updatedContact) + } + } + withChats { + addChatItem(rhId, cInfo, cItem) + } } else if (cItem.isRcvNew && cInfo.ntfsEnabled) { chatModel.increaseUnreadCounter(rhId, r.user) } @@ -2094,112 +2150,150 @@ object ChatController { val cInfo = r.chatItem.chatInfo val cItem = r.chatItem.chatItem if (!cItem.isDeletedContent && active(r.user)) { - chatModel.updateChatItem(cInfo, cItem, status = cItem.meta.itemStatus) + withChats { + updateChatItem(cInfo, cItem, status = cItem.meta.itemStatus) + } } } is CR.ChatItemUpdated -> chatItemSimpleUpdate(rhId, r.user, r.chatItem) is CR.ChatItemReaction -> { if (active(r.user)) { - chatModel.updateChatItem(r.reaction.chatInfo, r.reaction.chatReaction.chatItem) + withChats { + updateChatItem(r.reaction.chatInfo, r.reaction.chatReaction.chatItem) + } } } - is CR.ChatItemDeleted -> { + is CR.ChatItemsDeleted -> { if (!active(r.user)) { - if (r.toChatItem == null && r.deletedChatItem.chatItem.isRcvNew && r.deletedChatItem.chatInfo.ntfsEnabled) { - chatModel.decreaseUnreadCounter(rhId, r.user) + r.chatItemDeletions.forEach { (deletedChatItem, toChatItem) -> + if (toChatItem == null && deletedChatItem.chatItem.isRcvNew && deletedChatItem.chatInfo.ntfsEnabled) { + chatModel.decreaseUnreadCounter(rhId, r.user) + } } return } - - val cInfo = r.deletedChatItem.chatInfo - val cItem = r.deletedChatItem.chatItem - AudioPlayer.stop(cItem) - val isLastChatItem = chatModel.getChat(cInfo.id)?.chatItems?.lastOrNull()?.id == cItem.id - if (isLastChatItem && ntfManager.hasNotificationsForChat(cInfo.id)) { - ntfManager.cancelNotificationsForChat(cInfo.id) - ntfManager.displayNotification( - r.user, - cInfo.id, - cInfo.displayName, - generalGetString(if (r.toChatItem != null) MR.strings.marked_deleted_description else MR.strings.deleted_description) - ) - } - if (r.toChatItem == null) { - chatModel.removeChatItem(rhId, cInfo, cItem) - } else { - chatModel.upsertChatItem(rhId, cInfo, r.toChatItem.chatItem) + r.chatItemDeletions.forEach { (deletedChatItem, toChatItem) -> + val cInfo = deletedChatItem.chatInfo + val cItem = deletedChatItem.chatItem + AudioPlayer.stop(cItem) + val isLastChatItem = chatModel.getChat(cInfo.id)?.chatItems?.lastOrNull()?.id == cItem.id + if (isLastChatItem && ntfManager.hasNotificationsForChat(cInfo.id)) { + ntfManager.cancelNotificationsForChat(cInfo.id) + ntfManager.displayNotification( + r.user, + cInfo.id, + cInfo.displayName, + generalGetString(if (toChatItem != null) MR.strings.marked_deleted_description else MR.strings.deleted_description) + ) + } + withChats { + if (toChatItem == null) { + removeChatItem(rhId, cInfo, cItem) + } else { + upsertChatItem(rhId, cInfo, toChatItem.chatItem) + } + } } } is CR.ReceivedGroupInvitation -> { if (active(r.user)) { - chatModel.updateGroup(rhId, r.groupInfo) // update so that repeat group invitations are not duplicated + withChats { + // update so that repeat group invitations are not duplicated + updateGroup(rhId, r.groupInfo) + } // TODO NtfManager.shared.notifyGroupInvitation } } is CR.UserAcceptedGroupSent -> { if (!active(r.user)) return - chatModel.updateGroup(rhId, r.groupInfo) - val conn = r.hostContact?.activeConn - if (conn != null) { - chatModel.replaceConnReqView(conn.id, "#${r.groupInfo.groupId}") - chatModel.removeChat(rhId, conn.id) + withChats { + updateGroup(rhId, r.groupInfo) + val conn = r.hostContact?.activeConn + if (conn != null) { + chatModel.replaceConnReqView(conn.id, "#${r.groupInfo.groupId}") + removeChat(rhId, conn.id) + } } } is CR.GroupLinkConnecting -> { if (!active(r.user)) return - chatModel.updateGroup(rhId, r.groupInfo) - val hostConn = r.hostMember.activeConn - if (hostConn != null) { - chatModel.replaceConnReqView(hostConn.id, "#${r.groupInfo.groupId}") - chatModel.removeChat(rhId, hostConn.id) + withChats { + updateGroup(rhId, r.groupInfo) + val hostConn = r.hostMember.activeConn + if (hostConn != null) { + chatModel.replaceConnReqView(hostConn.id, "#${r.groupInfo.groupId}") + removeChat(rhId, hostConn.id) + } } } is CR.JoinedGroupMemberConnecting -> if (active(r.user)) { - chatModel.upsertGroupMember(rhId, r.groupInfo, r.member) + withChats { + upsertGroupMember(rhId, r.groupInfo, r.member) + } } is CR.DeletedMemberUser -> // TODO update user member if (active(r.user)) { - chatModel.updateGroup(rhId, r.groupInfo) + withChats { + updateGroup(rhId, r.groupInfo) + } } is CR.DeletedMember -> if (active(r.user)) { - chatModel.upsertGroupMember(rhId, r.groupInfo, r.deletedMember) + withChats { + upsertGroupMember(rhId, r.groupInfo, r.deletedMember) + } } is CR.LeftMember -> if (active(r.user)) { - chatModel.upsertGroupMember(rhId, r.groupInfo, r.member) + withChats { + upsertGroupMember(rhId, r.groupInfo, r.member) + } } is CR.MemberRole -> if (active(r.user)) { - chatModel.upsertGroupMember(rhId, r.groupInfo, r.member) + withChats { + upsertGroupMember(rhId, r.groupInfo, r.member) + } } is CR.MemberRoleUser -> if (active(r.user)) { - chatModel.upsertGroupMember(rhId, r.groupInfo, r.member) + withChats { + upsertGroupMember(rhId, r.groupInfo, r.member) + } } is CR.MemberBlockedForAll -> if (active(r.user)) { - chatModel.upsertGroupMember(rhId, r.groupInfo, r.member) + withChats { + upsertGroupMember(rhId, r.groupInfo, r.member) + } } is CR.GroupDeleted -> // TODO update user member if (active(r.user)) { - chatModel.updateGroup(rhId, r.groupInfo) + withChats { + updateGroup(rhId, r.groupInfo) + } } is CR.UserJoinedGroup -> if (active(r.user)) { - chatModel.updateGroup(rhId, r.groupInfo) + withChats { + updateGroup(rhId, r.groupInfo) + } } is CR.JoinedGroupMember -> if (active(r.user)) { - chatModel.upsertGroupMember(rhId, r.groupInfo, r.member) + withChats { + upsertGroupMember(rhId, r.groupInfo, r.member) + } } is CR.ConnectedToGroupMember -> { if (active(r.user)) { - chatModel.upsertGroupMember(rhId, r.groupInfo, r.member) + withChats { + upsertGroupMember(rhId, r.groupInfo, r.member) + } } if (r.memberContact != null) { chatModel.setContactNetworkStatus(r.memberContact, NetworkStatus.Connected()) @@ -2207,11 +2301,15 @@ object ChatController { } is CR.GroupUpdated -> if (active(r.user)) { - chatModel.updateGroup(rhId, r.toGroup) + withChats { + updateGroup(rhId, r.toGroup) + } } is CR.NewMemberContactReceivedInv -> if (active(r.user)) { - chatModel.updateContact(rhId, r.contact) + withChats { + updateContact(rhId, r.contact) + } } is CR.RcvFileStart -> chatItemSimpleUpdate(rhId, r.user, r.chatItem) @@ -2311,19 +2409,27 @@ object ChatController { } is CR.ContactSwitch -> if (active(r.user)) { - chatModel.updateContactConnectionStats(rhId, r.contact, r.switchProgress.connectionStats) + withChats { + updateContactConnectionStats(rhId, r.contact, r.switchProgress.connectionStats) + } } is CR.GroupMemberSwitch -> if (active(r.user)) { - chatModel.updateGroupMemberConnectionStats(rhId, r.groupInfo, r.member, r.switchProgress.connectionStats) + withChats { + updateGroupMemberConnectionStats(rhId, r.groupInfo, r.member, r.switchProgress.connectionStats) + } } is CR.ContactRatchetSync -> if (active(r.user)) { - chatModel.updateContactConnectionStats(rhId, r.contact, r.ratchetSyncProgress.connectionStats) + withChats { + updateContactConnectionStats(rhId, r.contact, r.ratchetSyncProgress.connectionStats) + } } is CR.GroupMemberRatchetSync -> if (active(r.user)) { - chatModel.updateGroupMemberConnectionStats(rhId, r.groupInfo, r.member, r.ratchetSyncProgress.connectionStats) + withChats { + updateGroupMemberConnectionStats(rhId, r.groupInfo, r.member, r.ratchetSyncProgress.connectionStats) + } } is CR.RemoteHostSessionCode -> { chatModel.remoteHostPairing.value = r.remoteHost_ to RemoteHostSessionState.PendingConfirmation(r.sessionCode) @@ -2335,7 +2441,9 @@ object ChatController { } is CR.ContactDisabled -> { if (active(r.user)) { - chatModel.updateContact(rhId, r.contact) + withChats { + updateContact(rhId, r.contact) + } } } is CR.RemoteHostStopped -> { @@ -2456,7 +2564,9 @@ object ChatController { } is CR.ContactPQEnabled -> if (active(r.user)) { - chatModel.updateContact(rhId, r.contact) + withChats { + updateContact(rhId, r.contact) + } } is CR.ChatRespError -> when { r.chatError is ChatError.ChatErrorAgent && r.chatError.agentError is AgentErrorType.CRITICAL -> { @@ -2532,7 +2642,9 @@ object ChatController { suspend fun leaveGroup(rh: Long?, groupId: Long) { val groupInfo = apiLeaveGroup(rh, groupId) if (groupInfo != null) { - chatModel.updateGroup(rh, groupInfo) + withChats { + updateGroup(rh, groupInfo) + } } } @@ -2542,7 +2654,7 @@ object ChatController { val notify = { ntfManager.notifyMessageReceived(user, cInfo, cItem) } if (!activeUser(rh, user)) { notify() - } else if (chatModel.upsertChatItem(rh, cInfo, cItem)) { + } else if (withChats { upsertChatItem(rh, cInfo, cItem) }) { notify() } else if (cItem.content is CIContent.RcvCall && cItem.content.status == CICallStatus.Missed) { notify() @@ -2586,7 +2698,9 @@ object ChatController { chatModel.currentUser.value = user if (user == null) { chatModel.chatItems.clear() - chatModel.chats.clear() + withChats { + chats.clear() + } } val statuses = apiGetNetworkStatuses(rhId) if (statuses != null) { @@ -2737,8 +2851,8 @@ sealed class 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 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 itemId: Long, val mode: CIDeleteMode): CC() - class ApiDeleteMemberChatItem(val groupId: Long, val groupMemberId: Long, val itemId: Long): 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 ApiNewGroup(val userId: Long, val incognito: Boolean, val groupProfile: GroupProfile): CC() @@ -2787,7 +2901,7 @@ sealed class 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() - class ApiDeleteChat(val type: ChatType, val id: Long, val notify: Boolean?): CC() + class ApiDeleteChat(val type: ChatType, val id: Long, val chatDeleteMode: ChatDeleteMode): CC() class ApiClearChat(val type: ChatType, val id: Long): CC() class ApiListContacts(val userId: Long): CC() class ApiUpdateProfile(val userId: Long, val profile: Profile): CC() @@ -2887,8 +3001,8 @@ sealed class CC { "/_create *$noteFolderId json ${json.encodeToString(ComposedMessage(file, null, mc))}" } is ApiUpdateChatItem -> "/_update item ${chatRef(type, id)} $itemId live=${onOff(live)} ${mc.cmdString}" - is ApiDeleteChatItem -> "/_delete item ${chatRef(type, id)} $itemId ${mode.deleteMode}" - is ApiDeleteMemberChatItem -> "/_delete member item #$groupId $groupMemberId $itemId" + 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 -> { val ttlStr = if (ttl != null) "$ttl" else "default" @@ -2940,11 +3054,7 @@ sealed class CC { is APIConnectPlan -> "/_connect plan $userId $connReq" is APIConnect -> "/_connect $userId incognito=${onOff(incognito)} $connReq" is ApiConnectContactViaAddress -> "/_connect contact $userId incognito=${onOff(incognito)} $contactId" - is ApiDeleteChat -> if (notify != null) { - "/_delete ${chatRef(type, id)} notify=${onOff(notify)}" - } else { - "/_delete ${chatRef(type, id)}" - } + is ApiDeleteChat -> "/_delete ${chatRef(type, id)} ${chatDeleteMode.cmdString}" is ApiClearChat -> "/_clear chat ${chatRef(type, id)}" is ApiListContacts -> "/_contacts $userId" is ApiUpdateProfile -> "/_profile $userId ${json.encodeToString(profile)}" @@ -3166,8 +3276,6 @@ sealed class CC { null } - private fun onOff(b: Boolean): String = if (b) "on" else "off" - private fun maybePwd(pwd: String?): String = if (pwd == "" || pwd == null) "" else " " + json.encodeToString(pwd) companion object { @@ -3177,6 +3285,8 @@ sealed class CC { } } +fun onOff(b: Boolean): String = if (b) "on" else "off" + @Serializable data class NewUser( val profile: Profile?, @@ -4679,7 +4789,7 @@ sealed class CR { @Serializable @SerialName("chatItemUpdated") class ChatItemUpdated(val user: UserRef, val chatItem: AChatItem): CR() @Serializable @SerialName("chatItemNotChanged") class ChatItemNotChanged(val user: UserRef, val chatItem: AChatItem): CR() @Serializable @SerialName("chatItemReaction") class ChatItemReaction(val user: UserRef, val added: Boolean, val reaction: ACIReaction): CR() - @Serializable @SerialName("chatItemDeleted") class ChatItemDeleted(val user: UserRef, val deletedChatItem: AChatItem, val toChatItem: AChatItem? = null, val byUser: Boolean): CR() + @Serializable @SerialName("chatItemsDeleted") class ChatItemsDeleted(val user: UserRef, val chatItemDeletions: List, val byUser: Boolean): CR() // group events @Serializable @SerialName("groupCreated") class GroupCreated(val user: UserRef, val groupInfo: GroupInfo): CR() @Serializable @SerialName("sentGroupInvitation") class SentGroupInvitation(val user: UserRef, val groupInfo: GroupInfo, val contact: Contact, val member: GroupMember): CR() @@ -4772,6 +4882,7 @@ sealed class CR { @Serializable @SerialName("cmdOk") class CmdOk(val user: UserRef?): CR() @Serializable @SerialName("chatCmdError") class ChatCmdError(val user_: UserRef?, val chatError: ChatError): CR() @Serializable @SerialName("chatError") class ChatRespError(val user_: UserRef?, val chatError: ChatError): CR() + @Serializable @SerialName("archiveExported") class ArchiveExported(val archiveErrors: List): CR() @Serializable @SerialName("archiveImported") class ArchiveImported(val archiveErrors: List): CR() @Serializable @SerialName("appSettings") class AppSettingsR(val appSettings: AppSettings): CR() @Serializable @SerialName("agentSubsTotal") class AgentSubsTotal(val user: UserRef, val subsTotal: SMPServerSubs, val hasSession: Boolean): CR() @@ -4854,7 +4965,7 @@ sealed class CR { is ChatItemUpdated -> "chatItemUpdated" is ChatItemNotChanged -> "chatItemNotChanged" is ChatItemReaction -> "chatItemReaction" - is ChatItemDeleted -> "chatItemDeleted" + is ChatItemsDeleted -> "chatItemsDeleted" is GroupCreated -> "groupCreated" is SentGroupInvitation -> "sentGroupInvitation" is UserAcceptedGroupSent -> "userAcceptedGroupSent" @@ -4941,6 +5052,7 @@ sealed class CR { is CmdOk -> "cmdOk" is ChatCmdError -> "chatCmdError" is ChatRespError -> "chatError" + is ArchiveExported -> "archiveExported" is ArchiveImported -> "archiveImported" is AppSettingsR -> "appSettings" is Response -> "* $type" @@ -5021,7 +5133,7 @@ sealed class CR { is ChatItemUpdated -> withUser(user, json.encodeToString(chatItem)) is ChatItemNotChanged -> withUser(user, json.encodeToString(chatItem)) is ChatItemReaction -> withUser(user, "added: $added\n${json.encodeToString(reaction)}") - is ChatItemDeleted -> withUser(user, "deletedChatItem:\n${json.encodeToString(deletedChatItem)}\ntoChatItem:\n${json.encodeToString(toChatItem)}\nbyUser: $byUser") + is ChatItemsDeleted -> withUser(user, "${chatItemDeletions.map { (deletedChatItem, toChatItem) -> "deletedChatItem: ${json.encodeToString(deletedChatItem)}\ntoChatItem: ${json.encodeToString(toChatItem)}" }} \nbyUser: $byUser") is GroupCreated -> withUser(user, json.encodeToString(groupInfo)) is SentGroupInvitation -> withUser(user, "groupInfo: $groupInfo\ncontact: $contact\nmember: $member") is UserAcceptedGroupSent -> json.encodeToString(groupInfo) @@ -5125,6 +5237,7 @@ sealed class CR { is CmdOk -> withUser(user, noDetails()) is ChatCmdError -> withUser(user_, chatError.string) is ChatRespError -> withUser(user_, chatError.string) + is ArchiveExported -> "${archiveErrors.map { it.string } }" is ArchiveImported -> "${archiveErrors.map { it.string } }" is AppSettingsR -> json.encodeToString(appSettings) is Response -> json @@ -5144,6 +5257,19 @@ fun chatError(r: CR): ChatErrorType? { ) } +@Serializable +sealed class ChatDeleteMode { + @Serializable @SerialName("full") class Full(val notify: Boolean): ChatDeleteMode() + @Serializable @SerialName("entity") class Entity(val notify: Boolean): ChatDeleteMode() + @Serializable @SerialName("messages") class Messages: ChatDeleteMode() + + val cmdString: String get() = when (this) { + is ChatDeleteMode.Full -> "full notify=${onOff(notify)}" + is ChatDeleteMode.Entity -> "entity notify=${onOff(notify)}" + is ChatDeleteMode.Messages -> "messages" + } +} + @Serializable sealed class ConnectionPlan { @Serializable @SerialName("invitationLink") class InvitationLink(val invitationLinkPlan: InvitationLinkPlan): ConnectionPlan() @@ -5917,11 +6043,11 @@ sealed class RCErrorType { @Serializable sealed class ArchiveError { val string: String get() = when (this) { - is ArchiveErrorImport -> "import ${chatError.string}" - is ArchiveErrorImportFile -> "importFile $file ${chatError.string}" + is ArchiveErrorImport -> "import ${importError}" + is ArchiveErrorFile -> "importFile $file ${fileError}" } - @Serializable @SerialName("import") class ArchiveErrorImport(val chatError: ChatError): ArchiveError() - @Serializable @SerialName("importFile") class ArchiveErrorImportFile(val file: String, val chatError: ChatError): ArchiveError() + @Serializable @SerialName("import") class ArchiveErrorImport(val importError: String): ArchiveError() + @Serializable @SerialName("fileError") class ArchiveErrorFile(val file: String, val fileError: String): ArchiveError() } @Serializable @@ -6031,6 +6157,7 @@ data class AppSettings( var uiDarkColorScheme: String? = null, var uiCurrentThemeIds: Map? = null, var uiThemes: List? = null, + var oneHandUI: Boolean? = null ) { fun prepareForExport(): AppSettings { val empty = AppSettings() @@ -6061,6 +6188,7 @@ data class AppSettings( if (uiDarkColorScheme != def.uiDarkColorScheme) { empty.uiDarkColorScheme = uiDarkColorScheme } if (uiCurrentThemeIds != def.uiCurrentThemeIds) { empty.uiCurrentThemeIds = uiCurrentThemeIds } if (uiThemes != def.uiThemes) { empty.uiThemes = uiThemes } + if (oneHandUI != def.oneHandUI) { empty.oneHandUI = oneHandUI } return empty } @@ -6099,6 +6227,7 @@ data class AppSettings( uiDarkColorScheme?.let { def.systemDarkTheme.set(it) } uiCurrentThemeIds?.let { def.currentThemeIds.set(it) } uiThemes?.let { def.themeOverrides.set(it.skipDuplicates()) } + oneHandUI?.let { def.oneHandUI.set(it) } } companion object { @@ -6130,6 +6259,7 @@ data class AppSettings( uiDarkColorScheme = DefaultTheme.SIMPLEX.themeName, uiCurrentThemeIds = null, uiThemes = null, + oneHandUI = false ) val current: AppSettings @@ -6162,6 +6292,7 @@ data class AppSettings( uiDarkColorScheme = def.systemDarkTheme.get() ?: DefaultTheme.SIMPLEX.themeName, uiCurrentThemeIds = def.currentThemeIds.get(), uiThemes = def.themeOverrides.get(), + oneHandUI = def.oneHandUI.get() ) } } 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 7020a42c1e..b48f7cebec 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 @@ -6,6 +6,7 @@ import androidx.compose.foundation.ScrollState import androidx.compose.foundation.lazy.LazyListState import androidx.compose.runtime.* import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import chat.simplex.common.model.ChatId import chat.simplex.common.model.NotificationsMode import kotlinx.coroutines.Job @@ -20,6 +21,7 @@ interface PlatformInterface { fun androidChatInitializedAndStarted() {} fun androidIsBackgroundCallAllowed(): Boolean = true fun androidSetNightModeIfSupported() {} + fun androidSetStatusAndNavBarColors(isLight: Boolean, backgroundColor: Color, hasTop: Boolean, hasBottom: Boolean) {} 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/ui/theme/ThemeManager.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt index e047f82837..a86e92efba 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt @@ -1,6 +1,7 @@ package chat.simplex.common.ui.theme import androidx.compose.material.Colors +import androidx.compose.material.MaterialTheme import androidx.compose.runtime.MutableState import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb @@ -103,6 +104,8 @@ object ThemeManager { appPrefs.currentTheme.set(theme) CurrentColors.value = currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) platform.androidSetNightModeIfSupported() + val c = CurrentColors.value.colors + platform.androidSetStatusAndNavBarColors(c.isLight, c.background, !ChatController.appPrefs.oneHandUI.get(), ChatController.appPrefs.oneHandUI.get()) } fun changeDarkTheme(theme: String) { @@ -120,6 +123,10 @@ object ThemeManager { themeIds[nonSystemThemeName] = prevValue.themeId appPrefs.currentThemeIds.set(themeIds) CurrentColors.value = currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) + if (name == ThemeColor.BACKGROUND) { + val c = CurrentColors.value.colors + platform.androidSetStatusAndNavBarColors(c.isLight, c.background, false, false) + } } fun applyThemeColor(name: ThemeColor, color: Color? = null, pref: MutableState) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt index 838225afb8..7c694e00b1 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt @@ -12,18 +12,23 @@ import SectionView import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.* import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.text.* import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.platform.* +import chat.simplex.common.views.call.CallMediaType +import chat.simplex.common.views.chatlist.* import androidx.compose.ui.text.* import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -31,6 +36,7 @@ import androidx.compose.ui.unit.sp import chat.simplex.common.model.* import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatModel.controller +import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.common.views.usersettings.* @@ -54,10 +60,11 @@ fun ChatInfoView( localAlias: String, connectionCode: String?, close: () -> Unit, + onSearchClicked: () -> Unit ) { BackHandler(onBack = close) val contact = rememberUpdatedState(contact).value - val chat = remember(contact.id) { chatModel.chats.firstOrNull { it.id == contact.id } } + val chat = remember(contact.id) { chatModel.chats.value.firstOrNull { it.id == contact.id } } val currentUser = remember { chatModel.currentUser }.value val connStats = remember(contact.id, connectionStats) { mutableStateOf(connectionStats) } val developerTools = chatModel.controller.appPrefs.developerTools.get() @@ -74,7 +81,7 @@ fun ChatInfoView( sendReceipts = sendReceipts, setSendReceipts = { sendRcpts -> val chatSettings = (chat.chatInfo.chatSettings ?: ChatSettings.defaults).copy(sendRcpts = sendRcpts.bool) - updateChatSettings(chat, chatSettings, chatModel) + updateChatSettings(chat.remoteHostId, chat.chatInfo, chatSettings, chatModel) sendReceipts.value = sendRcpts }, connStats = connStats, @@ -102,7 +109,9 @@ fun ChatInfoView( val cStats = chatModel.controller.apiSwitchContact(chatRh, contact.contactId) connStats.value = cStats if (cStats != null) { - chatModel.updateContactConnectionStats(chatRh, contact, cStats) + withChats { + updateContactConnectionStats(chatRh, contact, cStats) + } } close.invoke() } @@ -114,7 +123,9 @@ fun ChatInfoView( val cStats = chatModel.controller.apiAbortSwitchContact(chatRh, contact.contactId) connStats.value = cStats if (cStats != null) { - chatModel.updateContactConnectionStats(chatRh, contact, cStats) + withChats { + updateContactConnectionStats(chatRh, contact, cStats) + } } } }) @@ -124,7 +135,9 @@ fun ChatInfoView( val cStats = chatModel.controller.apiSyncContactRatchet(chatRh, contact.contactId, force = false) connStats.value = cStats if (cStats != null) { - chatModel.updateContactConnectionStats(chatRh, contact, cStats) + withChats { + updateContactConnectionStats(chatRh, contact, cStats) + } } close.invoke() } @@ -135,7 +148,9 @@ fun ChatInfoView( val cStats = chatModel.controller.apiSyncContactRatchet(chatRh, contact.contactId, force = true) connStats.value = cStats if (cStats != null) { - chatModel.updateContactConnectionStats(chatRh, contact, cStats) + withChats { + updateContactConnectionStats(chatRh, contact, cStats) + } } close.invoke() } @@ -151,14 +166,16 @@ fun ChatInfoView( verify = { code -> chatModel.controller.apiVerifyContact(chatRh, ct.contactId, code)?.let { r -> val (verified, existingCode) = r - chatModel.updateContact( - chatRh, - ct.copy( - activeConn = ct.activeConn?.copy( - connectionCode = if (verified) SecurityCode(existingCode, Clock.System.now()) else null + withChats { + updateContact( + chatRh, + ct.copy( + activeConn = ct.activeConn?.copy( + connectionCode = if (verified) SecurityCode(existingCode, Clock.System.now()) else null + ) ) ) - ) + } r } }, @@ -166,7 +183,9 @@ fun ChatInfoView( ) } } - } + }, + close = close, + onSearchClicked = onSearchClicked ) } } @@ -201,34 +220,42 @@ sealed class SendReceipts { fun deleteContactDialog(chat: Chat, chatModel: ChatModel, close: (() -> Unit)? = null) { val chatInfo = chat.chatInfo + if (chatInfo is ChatInfo.Direct) { + val contact = chatInfo.contact + when { + contact.sndReady && contact.active && !chatInfo.chatDeleted -> + deleteContactOrConversationDialog(chat, contact, chatModel, close) + + contact.sndReady && contact.active && chatInfo.chatDeleted -> + deleteContactWithoutConversation(chat, chatModel, close) + + else -> // !(contact.sndReady && contact.active) + deleteNotReadyContact(chat, chatModel, close) + } + } +} + +private fun deleteContactOrConversationDialog(chat: Chat, contact: Contact, chatModel: ChatModel, close: (() -> Unit)?) { AlertManager.shared.showAlertDialogButtonsColumn( title = generalGetString(MR.strings.delete_contact_question), - text = AnnotatedString(generalGetString(MR.strings.delete_contact_all_messages_deleted_cannot_undo_warning)), buttons = { Column { - if (chatInfo is ChatInfo.Direct && chatInfo.contact.sndReady && chatInfo.contact.active) { - // Delete and notify contact - SectionItemView({ - AlertManager.shared.hideAlert() - deleteContact(chat, chatModel, close, notify = true) - }) { - Text(generalGetString(MR.strings.delete_and_notify_contact), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) - } - // Delete - SectionItemView({ - AlertManager.shared.hideAlert() - deleteContact(chat, chatModel, close, notify = false) - }) { - Text(generalGetString(MR.strings.delete_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) - } - } else { - // Delete - SectionItemView({ - AlertManager.shared.hideAlert() - deleteContact(chat, chatModel, close) - }) { - Text(generalGetString(MR.strings.delete_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) + // Only delete conversation + SectionItemView({ + AlertManager.shared.hideAlert() + deleteContact(chat, chatModel, close, chatDeleteMode = ChatDeleteMode.Messages()) + if (chatModel.controller.appPrefs.showDeleteConversationNotice.get()) { + showDeleteConversationNotice(contact) } + }) { + Text(generalGetString(MR.strings.only_delete_conversation), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) + } + // Delete contact + SectionItemView({ + AlertManager.shared.hideAlert() + deleteActiveContactDialog(chat, contact, chatModel, close) + }) { + Text(generalGetString(MR.strings.button_delete_contact), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) } // Cancel SectionItemView({ @@ -241,13 +268,207 @@ fun deleteContactDialog(chat: Chat, chatModel: ChatModel, close: (() -> Unit)? = ) } -fun deleteContact(chat: Chat, chatModel: ChatModel, close: (() -> Unit)?, notify: Boolean? = null) { +private fun showDeleteConversationNotice(contact: Contact) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.conversation_deleted), + text = String.format(generalGetString(MR.strings.you_can_still_send_messages_to_contact), contact.displayName), + confirmText = generalGetString(MR.strings.ok), + dismissText = generalGetString(MR.strings.dont_show_again), + onDismiss = { + chatModel.controller.appPrefs.showDeleteConversationNotice.set(false) + }, + ) +} + +sealed class ContactDeleteMode { + class Full: ContactDeleteMode() + class Entity: ContactDeleteMode() + + fun toChatDeleteMode(notify: Boolean): ChatDeleteMode = + when (this) { + is Full -> ChatDeleteMode.Full(notify) + is Entity -> ChatDeleteMode.Entity(notify) + } +} + +private fun deleteActiveContactDialog(chat: Chat, contact: Contact, chatModel: ChatModel, close: (() -> Unit)? = null) { + val contactDeleteMode = mutableStateOf(ContactDeleteMode.Full()) + + AlertManager.shared.showAlertDialogButtonsColumn( + title = generalGetString(MR.strings.delete_contact_question), + text = generalGetString(MR.strings.delete_contact_cannot_undo_warning), + buttons = { + Column { + // Keep conversation toggle + SectionItemView { + Row( + Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Text(stringResource(MR.strings.keep_conversation)) + Spacer(Modifier.width(DEFAULT_PADDING)) + DefaultSwitch( + checked = contactDeleteMode.value is ContactDeleteMode.Entity, + onCheckedChange = { + contactDeleteMode.value = + if (it) ContactDeleteMode.Entity() else ContactDeleteMode.Full() + }, + ) + } + } + // Delete without notification + SectionItemView({ + AlertManager.shared.hideAlert() + deleteContact(chat, chatModel, close, chatDeleteMode = contactDeleteMode.value.toChatDeleteMode(notify = false)) + if (contactDeleteMode.value is ContactDeleteMode.Entity && chatModel.controller.appPrefs.showDeleteContactNotice.get()) { + showDeleteContactNotice(contact) + } + }) { + Text(generalGetString(MR.strings.delete_without_notification), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) + } + // Delete contact and notify + SectionItemView({ + AlertManager.shared.hideAlert() + deleteContact(chat, chatModel, close, chatDeleteMode = contactDeleteMode.value.toChatDeleteMode(notify = true)) + if (contactDeleteMode.value is ContactDeleteMode.Entity && chatModel.controller.appPrefs.showDeleteContactNotice.get()) { + showDeleteContactNotice(contact) + } + }) { + Text(generalGetString(MR.strings.delete_and_notify_contact), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) + } + // Cancel + SectionItemView({ + AlertManager.shared.hideAlert() + }) { + Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + } + } + ) +} + +private fun deleteContactWithoutConversation(chat: Chat, chatModel: ChatModel, close: (() -> Unit)?) { + AlertManager.shared.showAlertDialogButtonsColumn( + title = generalGetString(MR.strings.confirm_delete_contact_question), + text = generalGetString(MR.strings.delete_contact_cannot_undo_warning), + buttons = { + Column { + // Delete and notify contact + SectionItemView({ + AlertManager.shared.hideAlert() + deleteContact( + chat, + chatModel, + close, + chatDeleteMode = ContactDeleteMode.Full().toChatDeleteMode(notify = true) + ) + }) { + Text( + generalGetString(MR.strings.delete_and_notify_contact), + Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + color = MaterialTheme.colors.error + ) + } + // Delete without notification + SectionItemView({ + AlertManager.shared.hideAlert() + deleteContact( + chat, + chatModel, + close, + chatDeleteMode = ContactDeleteMode.Full().toChatDeleteMode(notify = false) + ) + }) { + Text( + generalGetString(MR.strings.delete_without_notification), + Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + color = MaterialTheme.colors.error + ) + } + } + // Cancel + SectionItemView({ + AlertManager.shared.hideAlert() + }) { + Text( + stringResource(MR.strings.cancel_verb), + Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + color = MaterialTheme.colors.primary + ) + } + } + ) +} + +private fun deleteNotReadyContact(chat: Chat, chatModel: ChatModel, close: (() -> Unit)?) { + AlertManager.shared.showAlertDialogButtonsColumn( + title = generalGetString(MR.strings.confirm_delete_contact_question), + text = generalGetString(MR.strings.delete_contact_cannot_undo_warning), + buttons = { + // Confirm + SectionItemView({ + AlertManager.shared.hideAlert() + deleteContact( + chat, + chatModel, + close, + chatDeleteMode = ContactDeleteMode.Full().toChatDeleteMode(notify = false) + ) + }) { + Text( + generalGetString(MR.strings.confirm_verb), + Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + color = MaterialTheme.colors.error + ) + } + // Cancel + SectionItemView({ + AlertManager.shared.hideAlert() + }) { + Text( + stringResource(MR.strings.cancel_verb), + Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + color = MaterialTheme.colors.primary + ) + } + } + ) +} + +private fun showDeleteContactNotice(contact: Contact) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.contact_deleted), + text = String.format(generalGetString(MR.strings.you_can_still_view_conversation_with_contact), contact.displayName), + confirmText = generalGetString(MR.strings.ok), + dismissText = generalGetString(MR.strings.dont_show_again), + onDismiss = { + chatModel.controller.appPrefs.showDeleteContactNotice.set(false) + }, + ) +} + +fun deleteContact(chat: Chat, chatModel: ChatModel, close: (() -> Unit)?, chatDeleteMode: ChatDeleteMode = ChatDeleteMode.Full(notify = true)) { val chatInfo = chat.chatInfo withBGApi { val chatRh = chat.remoteHostId - val r = chatModel.controller.apiDeleteChat(chatRh, chatInfo.chatType, chatInfo.apiId, notify) - if (r) { - chatModel.removeChat(chatRh, chatInfo.id) + val ct = chatModel.controller.apiDeleteContact(chatRh, chatInfo.apiId, chatDeleteMode) + if (ct != null) { + withChats { + when (chatDeleteMode) { + is ChatDeleteMode.Full -> + removeChat(chatRh, chatInfo.id) + is ChatDeleteMode.Entity -> + updateContact(chatRh, ct) + is ChatDeleteMode.Messages -> + clearChat(chatRh, ChatInfo.Direct(ct)) + } + } if (chatModel.chatId.value == chatInfo.id) { chatModel.chatId.value = null ModalManager.end.closeModals() @@ -300,6 +521,8 @@ fun ChatInfoLayout( syncContactConnection: () -> Unit, syncContactConnectionForce: () -> Unit, verifyClicked: () -> Unit, + close: () -> Unit, + onSearchClicked: () -> Unit ) { val cStats = connStats.value val scrollState = rememberScrollState() @@ -319,7 +542,27 @@ fun ChatInfoLayout( } LocalAliasEditor(chat.id, localAlias, updateValue = onLocalAliasChanged) + SectionSpacer() + + Row( + Modifier + .fillMaxWidth() + .padding(horizontal = DEFAULT_PADDING), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + SearchButton(chat, contact, close, onSearchClicked) + Spacer(Modifier.weight(1f)) + AudioCallButton(chat, contact) + Spacer(Modifier.weight(1f)) + VideoButton(chat, contact) + Spacer(Modifier.weight(1f)) + MuteButton(chat, contact) + } + + SectionSpacer() + if (customUserProfile != null) { SectionView(generalGetString(MR.strings.incognito).uppercase()) { SectionItemViewSpaceBetween { @@ -347,7 +590,7 @@ fun ChatInfoLayout( WallpaperButton { ModalManager.end.showModal { - val chat = remember { derivedStateOf { chatModel.chats.firstOrNull { it.id == chat.id } } } + val chat = remember { derivedStateOf { chatModel.chats.value.firstOrNull { it.id == chat.id } } } val c = chat.value if (c != null) { ChatWallpaperEditorModal(c) @@ -535,6 +778,174 @@ fun LocalAliasEditor( } } +@Composable +fun SearchButton(chat: Chat, contact: Contact, close: () -> Unit, onSearchClicked: () -> Unit) { + val disabled = !contact.ready || chat.chatItems.isEmpty() + InfoViewActionButton( + icon = painterResource(MR.images.ic_search), + title = generalGetString(MR.strings.info_view_search_button), + disabled = disabled, + disabledLook = disabled, + onClick = { + if (appPlatform.isAndroid) { + close.invoke() + } + onSearchClicked() + } + ) +} + +@Composable +fun MuteButton(chat: Chat, contact: Contact) { + val ntfsEnabled = remember { mutableStateOf(chat.chatInfo.ntfsEnabled) } + val disabled = !contact.ready || !contact.active + + InfoViewActionButton( + icon = if (ntfsEnabled.value) painterResource(MR.images.ic_notifications_off) else painterResource(MR.images.ic_notifications), + title = if (ntfsEnabled.value) stringResource(MR.strings.mute_chat) else stringResource(MR.strings.unmute_chat), + disabled = disabled, + disabledLook = disabled, + onClick = { + toggleNotifications(chat.remoteHostId, chat.chatInfo, !ntfsEnabled.value, chatModel, ntfsEnabled) + } + ) +} + +@Composable +fun AudioCallButton(chat: Chat, contact: Contact) { + CallButton( + chat, + contact, + icon = painterResource(MR.images.ic_call), + title = generalGetString(MR.strings.info_view_call_button), + mediaType = CallMediaType.Audio + ) +} + +@Composable +fun VideoButton(chat: Chat, contact: Contact) { + CallButton( + chat, + contact, + icon = painterResource(MR.images.ic_videocam), + title = generalGetString(MR.strings.info_view_video_button), + mediaType = CallMediaType.Video + ) +} + +@Composable +fun CallButton(chat: Chat, contact: Contact, icon: Painter, title: String, mediaType: CallMediaType) { + val canCall = contact.ready && contact.active && contact.mergedPreferences.calls.enabled.forUser && chatModel.activeCall.value == null + val needToAllowCallsToContact = remember(chat.chatInfo) { + chat.chatInfo is ChatInfo.Direct && with(chat.chatInfo.contact.mergedPreferences.calls) { + ((userPreference as? ContactUserPref.User)?.preference?.allow == FeatureAllowed.NO || (userPreference as? ContactUserPref.Contact)?.preference?.allow == FeatureAllowed.NO) && + contactPreference.allow == FeatureAllowed.YES + } + } + val allowedCallsByPrefs = remember(chat.chatInfo) { chat.chatInfo.featureEnabled(ChatFeature.Calls) } + + InfoViewActionButton( + icon = icon, + title = title, + disabled = chatModel.activeCall.value != null, + disabledLook = !canCall, + onClick = + when { + canCall -> { { startChatCall(chat.remoteHostId, chat.chatInfo, mediaType) } } + contact.nextSendGrpInv -> { { showCantCallContactSendMessageAlert() } } + !contact.active -> { { showCantCallContactDeletedAlert() } } + !contact.ready -> { { showCantCallContactConnectingAlert() } } + needToAllowCallsToContact -> { { showNeedToAllowCallsAlert(onConfirm = { allowCallsToContact(chat) }) } } + !allowedCallsByPrefs -> { { showCallsProhibitedAlert() }} + else -> { { AlertManager.shared.showAlertMsg(title = generalGetString(MR.strings.cant_call_contact_alert_title)) } } + } + ) +} + +private fun showCantCallContactSendMessageAlert() { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.cant_call_contact_alert_title), + text = generalGetString(MR.strings.cant_call_member_send_message_alert_text) + ) +} + +private fun showCantCallContactConnectingAlert() { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.cant_call_contact_alert_title), + text = generalGetString(MR.strings.cant_call_contact_connecting_wait_alert_text) + ) +} + +private fun showCantCallContactDeletedAlert() { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.cant_call_contact_alert_title), + text = generalGetString(MR.strings.cant_call_contact_deleted_alert_text) + ) +} + +private fun showNeedToAllowCallsAlert(onConfirm: () -> Unit) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.allow_calls_question), + text = generalGetString(MR.strings.you_need_to_allow_calls), + confirmText = generalGetString(MR.strings.allow_verb), + dismissText = generalGetString(MR.strings.cancel_verb), + onConfirm = onConfirm, + ) +} + +private fun allowCallsToContact(chat: Chat) { + val contact = (chat.chatInfo as ChatInfo.Direct?)?.contact ?: return + withBGApi { + chatModel.controller.allowFeatureToContact(chat.remoteHostId, contact, ChatFeature.Calls) + } +} + +private fun showCallsProhibitedAlert() { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.calls_prohibited_alert_title), + text = generalGetString(MR.strings.calls_prohibited_ask_to_enable_calls_alert_text) + ) +} + +// for ChatInfoView (it has most buttons - 4) we use Spacer(Modifier.weight(1f)) to fit, +// for GroupChat And GroupMemberInfoViews (2 to 3 buttons) we use this as approximately equal to spacing in ChatInfoView +val INFO_VIEW_BUTTONS_PADDING = 36.dp + +@Composable +fun InfoViewActionButton(icon: Painter, title: String, disabled: Boolean, disabledLook: Boolean, onClick: () -> Unit) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + IconButton( + onClick = onClick, + enabled = !disabled + ) { + Box( + modifier = Modifier + .background( + if (disabledLook) MaterialTheme.colors.secondaryVariant else MaterialTheme.colors.primary, + shape = CircleShape + ) + .padding(16.dp) + ) { + Icon( + icon, + contentDescription = null, + Modifier.size(24.dp * fontSizeSqrtMultiplier), + tint = if (disabledLook) MaterialTheme.colors.secondary else MaterialTheme.colors.onPrimary + ) + } + } + Text( + title.capitalize(Locale.current), + style = MaterialTheme.typography.subtitle2.copy(fontWeight = FontWeight.Normal), + color = MaterialTheme.colors.secondary, + modifier = Modifier.padding(top = DEFAULT_SPACE_AFTER_ICON) + ) + } +} + @Composable private fun NetworkStatusRow(networkStatus: NetworkStatus) { Row( @@ -768,10 +1179,12 @@ suspend fun save(applyToMode: DefaultThemeMode?, newTheme: ThemeModeOverride?, c wallpaperFilesToDelete.forEach(::removeWallpaperFile) if (controller.apiSetChatUIThemes(chat.remoteHostId, chat.id, changedThemes)) { - if (chat.chatInfo is ChatInfo.Direct) { - chatModel.updateChatInfo(chat.remoteHostId, chat.chatInfo.copy(contact = chat.chatInfo.contact.copy(uiThemes = changedThemes))) - } else if (chat.chatInfo is ChatInfo.Group) { - chatModel.updateChatInfo(chat.remoteHostId, chat.chatInfo.copy(groupInfo = chat.chatInfo.groupInfo.copy(uiThemes = changedThemes))) + withChats { + if (chat.chatInfo is ChatInfo.Direct) { + updateChatInfo(chat.remoteHostId, chat.chatInfo.copy(contact = chat.chatInfo.contact.copy(uiThemes = changedThemes))) + } else if (chat.chatInfo is ChatInfo.Group) { + updateChatInfo(chat.remoteHostId, chat.chatInfo.copy(groupInfo = chat.chatInfo.groupInfo.copy(uiThemes = changedThemes))) + } } } } @@ -779,7 +1192,9 @@ suspend fun save(applyToMode: DefaultThemeMode?, newTheme: ThemeModeOverride?, c private fun setContactAlias(chat: Chat, localAlias: String, chatModel: ChatModel) = withBGApi { val chatRh = chat.remoteHostId chatModel.controller.apiSetContactAlias(chatRh, chat.chatInfo.apiId, localAlias)?.let { - chatModel.updateContact(chatRh, it) + withChats { + updateContact(chatRh, it) + } } } @@ -847,6 +1262,8 @@ fun PreviewChatInfoLayout() { syncContactConnection = {}, syncContactConnectionForce = {}, verifyClicked = {}, + close = {}, + onSearchClicked = {} ) } } 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 610d8d95e9..6b879d1d02 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 @@ -11,7 +11,6 @@ import androidx.compose.runtime.* import androidx.compose.runtime.saveable.mapSaver import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.* -import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.draw.drawWithCache import androidx.compose.ui.graphics.* import androidx.compose.ui.platform.* @@ -25,6 +24,7 @@ import androidx.compose.ui.unit.* import chat.simplex.common.model.* import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatModel.controller +import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.ui.theme.* import chat.simplex.common.views.call.* import chat.simplex.common.views.chat.group.* @@ -44,85 +44,76 @@ import java.net.URI import kotlin.math.sign @Composable -fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId: String) -> Unit) { - val activeChat = remember { mutableStateOf(chatModel.chats.firstOrNull { chat -> chat.chatInfo.id == chatId }) } - val searchText = rememberSaveable { mutableStateOf("") } +// staleChatId means the id that was before chatModel.chatId becomes null. It's needed for Android only to make transition from chat +// to chat list smooth. Otherwise, chat view will become blank right before the transition starts +fun ChatView(staleChatId: State, onComposed: suspend (chatId: String) -> Unit) { + val shouldReturn = remember { mutableStateOf(false) } + val remoteHostId = remember { derivedStateOf { chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.remoteHostId } } + val showSearch = rememberSaveable { mutableStateOf(false) } + val activeChatInfo = remember { derivedStateOf { + val info = chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.chatInfo + if (info == null) { + shouldReturn.value = true + } + return@derivedStateOf info ?: ChatInfo.Direct.sampleData + } + } val user = chatModel.currentUser.value - val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get() - val composeState = rememberSaveable(saver = ComposeState.saver()) { - val draft = chatModel.draft.value - val sharedContent = chatModel.sharedContent.value - mutableStateOf( - if (chatModel.draftChatId.value == chatId && draft != null && (sharedContent !is SharedContent.Forward || sharedContent.fromChatInfo.id == chatId)) { - draft - } else { - ComposeState(useLinkPreviews = useLinkPreviews) - } - ) - } - val attachmentOption = rememberSaveable { mutableStateOf(null) } - val attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden) - val scope = rememberCoroutineScope() - LaunchedEffect(Unit) { - // snapshotFlow here is because it reacts much faster on changes in chatModel.chatId.value. - // With LaunchedEffect(chatModel.chatId.value) there is a noticeable delay before reconstruction of the view - launch { - snapshotFlow { chatModel.chatId.value } - .distinctUntilChanged() - .filterNotNull() - .collect { chatId -> - if (activeChat.value?.id != chatId) { - // Redisplay the whole hierarchy if the chat is different to make going from groups to direct chat working correctly - // Also for situation when chatId changes after clicking in notification, etc - activeChat.value = chatModel.getChat(chatId) - } - markUnreadChatAsRead(activeChat, chatModel) - } + if (shouldReturn.value || user == null) { + LaunchedEffect(Unit) { + chatModel.chatId.value = null + ModalManager.end.closeModals() } - launch { - snapshotFlow { - /** - * It's possible that in some cases concurrent modification can happen on [ChatModel.chats] list. - * In this case only error log will be printed here (no crash). - * TODO: Re-write [ChatModel.chats] logic to a new list assignment instead of changing content of mutableList to prevent that - * */ - try { - chatModel.chats.firstOrNull { chat -> chat.chatInfo.id == chatModel.chatId.value } - } catch (e: ConcurrentModificationException) { - Log.e(TAG, e.stackTraceToString()) - null - } - } - .distinctUntilChanged() - // Only changed chatInfo is important thing. Other properties can be skipped for reducing recompositions - .filter { it != null && it.chatInfo != activeChat.value?.chatInfo } - .collect { - activeChat.value = it - } - } - } - val view = LocalMultiplatformView() - val chat = activeChat.value - if (chat == null || user == null) { - chatModel.chatId.value = null - ModalManager.end.closeModals() } else { - val chatRh = chat.remoteHostId + val chatInfo = activeChatInfo.value + val searchText = rememberSaveable { mutableStateOf("") } + val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get() + val composeState = rememberSaveable(saver = ComposeState.saver()) { + val draft = chatModel.draft.value + val sharedContent = chatModel.sharedContent.value + mutableStateOf( + if (chatModel.draftChatId.value == staleChatId.value && draft != null && (sharedContent !is SharedContent.Forward || sharedContent.fromChatInfo.id == staleChatId.value)) { + draft + } else { + ComposeState(useLinkPreviews = useLinkPreviews) + } + ) + } + val attachmentOption = rememberSaveable { mutableStateOf(null) } + val attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden) + val scope = rememberCoroutineScope() + LaunchedEffect(Unit) { + // snapshotFlow here is because it reacts much faster on changes in chatModel.chatId.value. + // With LaunchedEffect(chatModel.chatId.value) there is a noticeable delay before reconstruction of the view + launch { + snapshotFlow { chatModel.chatId.value } + .distinctUntilChanged() + .filterNotNull() + .collect { chatId -> + markUnreadChatAsRead(chatId) + showSearch.value = false + } + } + } + val view = LocalMultiplatformView() + + val chatRh = remoteHostId.value // We need to have real unreadCount value for displaying it inside top right button // Having activeChat reloaded on every change in it is inefficient (UI lags) val unreadCount = remember { derivedStateOf { - chatModel.chats.firstOrNull { chat -> chat.chatInfo.id == chatModel.chatId.value }?.chatStats?.unreadCount ?: 0 + chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == chatModel.chatId.value }?.chatStats?.unreadCount ?: 0 } } val clipboard = LocalClipboardManager.current - when (chat.chatInfo) { + when (chatInfo) { is ChatInfo.Direct, is ChatInfo.Group, is ChatInfo.Local -> { - val perChatTheme = remember(chat.chatInfo, CurrentColors.value.base) { if (chat.chatInfo is ChatInfo.Direct) chat.chatInfo.contact.uiThemes?.preferredMode(!CurrentColors.value.colors.isLight) else if (chat.chatInfo is ChatInfo.Group) chat.chatInfo.groupInfo.uiThemes?.preferredMode(!CurrentColors.value.colors.isLight) else null } + val perChatTheme = remember(chatInfo, CurrentColors.value.base) { if (chatInfo is ChatInfo.Direct) chatInfo.contact.uiThemes?.preferredMode(!CurrentColors.value.colors.isLight) else if (chatInfo is ChatInfo.Group) chatInfo.groupInfo.uiThemes?.preferredMode(!CurrentColors.value.colors.isLight) else null } val overrides = if (perChatTheme != null) ThemeManager.currentColors(null, perChatTheme, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) else null SimpleXThemeOverride(overrides ?: CurrentColors.collectAsState().value) { ChatLayout( - chat, + remoteHostId = remoteHostId, + chatInfo = activeChatInfo, unreadCount, composeState, composeView = { @@ -131,10 +122,10 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId: horizontalAlignment = Alignment.CenterHorizontally ) { if ( - chat.chatInfo is ChatInfo.Direct - && !chat.chatInfo.contact.sndReady - && chat.chatInfo.contact.active - && !chat.chatInfo.contact.nextSendGrpInv + chatInfo is ChatInfo.Direct + && !chatInfo.contact.sndReady + && chatInfo.contact.active + && !chatInfo.contact.nextSendGrpInv ) { Text( generalGetString(MR.strings.contact_connection_pending), @@ -144,7 +135,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId: ) } ComposeView( - chatModel, chat, composeState, attachmentOption, + chatModel, Chat(remoteHostId = chatRh, chatInfo = chatInfo, chatItems = emptyList()), composeState, attachmentOption, showChooseAttachment = { scope.launch { attachmentBottomSheetState.show() } } ) } @@ -172,36 +163,38 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId: var preloadedContactInfo: Pair? = null var preloadedCode: String? = null var preloadedLink: Pair? = null - if (chat.chatInfo is ChatInfo.Direct) { - preloadedContactInfo = chatModel.controller.apiContactInfo(chatRh, chat.chatInfo.apiId) - preloadedCode = chatModel.controller.apiGetContactCode(chatRh, chat.chatInfo.apiId)?.second - } else if (chat.chatInfo is ChatInfo.Group) { - setGroupMembers(chatRh, chat.chatInfo.groupInfo, chatModel) - preloadedLink = chatModel.controller.apiGetGroupLink(chatRh, chat.chatInfo.groupInfo.groupId) + if (chatInfo is ChatInfo.Direct) { + preloadedContactInfo = chatModel.controller.apiContactInfo(chatRh, chatInfo.apiId) + preloadedCode = chatModel.controller.apiGetContactCode(chatRh, chatInfo.apiId)?.second + } else if (chatInfo is ChatInfo.Group) { + setGroupMembers(chatRh, chatInfo.groupInfo, chatModel) + preloadedLink = chatModel.controller.apiGetGroupLink(chatRh, chatInfo.groupInfo.groupId) } ModalManager.end.showModalCloseable(true) { close -> - val chat = remember { activeChat }.value - if (chat?.chatInfo is ChatInfo.Direct) { + val chatInfo = remember { activeChatInfo }.value + if (chatInfo is ChatInfo.Direct) { var contactInfo: Pair? by remember { mutableStateOf(preloadedContactInfo) } var code: String? by remember { mutableStateOf(preloadedCode) } - KeyChangeEffect(chat.id, ChatModel.networkStatuses.toMap()) { - contactInfo = chatModel.controller.apiContactInfo(chatRh, chat.chatInfo.apiId) + KeyChangeEffect(chatInfo.id, ChatModel.networkStatuses.toMap()) { + contactInfo = chatModel.controller.apiContactInfo(chatRh, chatInfo.apiId) preloadedContactInfo = contactInfo - code = chatModel.controller.apiGetContactCode(chatRh, chat.chatInfo.apiId)?.second + code = chatModel.controller.apiGetContactCode(chatRh, chatInfo.apiId)?.second preloadedCode = code } - ChatInfoView(chatModel, (chat.chatInfo as ChatInfo.Direct).contact, contactInfo?.first, contactInfo?.second, chat.chatInfo.localAlias, code, close) - } else if (chat?.chatInfo is ChatInfo.Group) { - var link: Pair? by remember(chat.id) { mutableStateOf(preloadedLink) } - KeyChangeEffect(chat.id) { - setGroupMembers(chatRh, (chat.chatInfo as ChatInfo.Group).groupInfo, chatModel) - link = chatModel.controller.apiGetGroupLink(chatRh, chat.chatInfo.groupInfo.groupId) + ChatInfoView(chatModel, chatInfo.contact, contactInfo?.first, contactInfo?.second, chatInfo.localAlias, code, close) { + showSearch.value = true + } + } else if (chatInfo is ChatInfo.Group) { + var link: Pair? by remember(chatInfo.id) { mutableStateOf(preloadedLink) } + KeyChangeEffect(chatInfo.id) { + setGroupMembers(chatRh, chatInfo.groupInfo, chatModel) + link = chatModel.controller.apiGetGroupLink(chatRh, chatInfo.groupInfo.groupId) preloadedLink = link } - GroupChatInfoView(chatModel, chatRh, chat.id, link?.first, link?.second, { + GroupChatInfoView(chatModel, chatRh, chatInfo.id, link?.first, link?.second, { link = it preloadedLink = it - }, close) + }, close, { showSearch.value = true }) } else { LaunchedEffect(Unit) { close() @@ -231,7 +224,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId: } }, loadPrevMessages = { - if (chatModel.chatId.value != activeChat.value?.id) return@ChatLayout + if (chatModel.chatId.value != activeChatInfo.value.id) return@ChatLayout val c = chatModel.getChat(chatModel.chatId.value ?: return@ChatLayout) val firstId = chatModel.chatItems.value.firstOrNull()?.id if (c != null && firstId != null) { @@ -242,56 +235,55 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId: }, deleteMessage = { itemId, mode -> withBGApi { - val cInfo = chat.chatInfo + val cInfo = chatInfo val toDeleteItem = chatModel.chatItems.value.firstOrNull { it.id == itemId } - val toModerate = toDeleteItem?.memberToModerate(chat.chatInfo) + val toModerate = toDeleteItem?.memberToModerate(chatInfo) val groupInfo = toModerate?.first val groupMember = toModerate?.second val deletedChatItem: ChatItem? val toChatItem: ChatItem? - if (mode == CIDeleteMode.cidmBroadcast && groupInfo != null && groupMember != null) { - val r = chatModel.controller.apiDeleteMemberChatItem( + val r = if (mode == CIDeleteMode.cidmBroadcast && groupInfo != null && groupMember != null) { + chatModel.controller.apiDeleteMemberChatItems( chatRh, groupId = groupInfo.groupId, - groupMemberId = groupMember.groupMemberId, - itemId = itemId + itemIds = listOf(itemId) ) - deletedChatItem = r?.first - toChatItem = r?.second } else { - val r = chatModel.controller.apiDeleteChatItem( + chatModel.controller.apiDeleteChatItems( chatRh, type = cInfo.chatType, id = cInfo.apiId, - itemId = itemId, + itemIds = listOf(itemId), mode = mode ) - deletedChatItem = r?.deletedChatItem?.chatItem - toChatItem = r?.toChatItem?.chatItem } - if (toChatItem == null && deletedChatItem != null) { - chatModel.removeChatItem(chatRh, cInfo, deletedChatItem) - } else if (toChatItem != null) { - chatModel.upsertChatItem(chatRh, cInfo, toChatItem) + val deleted = r?.firstOrNull() + if (deleted != null) { + deletedChatItem = deleted.deletedChatItem.chatItem + toChatItem = deleted.toChatItem?.chatItem + withChats { + if (toChatItem != null) { + upsertChatItem(chatRh, cInfo, toChatItem) + } else { + removeChatItem(chatRh, cInfo, deletedChatItem) + } + } } } }, deleteMessages = { itemIds -> if (itemIds.isNotEmpty()) { - val chatInfo = chat.chatInfo withBGApi { - val deletedItems: ArrayList = arrayListOf() - for (itemId in itemIds) { - val di = chatModel.controller.apiDeleteChatItem( - chatRh, chatInfo.chatType, chatInfo.apiId, itemId, CIDeleteMode.cidmInternal - )?.deletedChatItem?.chatItem - if (di != null) { - deletedItems.add(di) + val deleted = chatModel.controller.apiDeleteChatItems( + chatRh, chatInfo.chatType, chatInfo.apiId, itemIds, CIDeleteMode.cidmInternal + ) + if (deleted != null) { + withChats { + for (di in deleted) { + removeChatItem(chatRh, chatInfo, di.deletedChatItem.chatItem) + } } } - for (di in deletedItems) { - chatModel.removeChatItem(chatRh, chatInfo, di) - } } } }, @@ -307,18 +299,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId: onComplete.invoke() } }, - startCall = out@{ media -> - withBGApi { - val cInfo = chat.chatInfo - if (cInfo is ChatInfo.Direct) { - val contactInfo = chatModel.controller.apiContactInfo(chat.remoteHostId, cInfo.contact.contactId) - val profile = contactInfo?.second ?: chatModel.currentUser.value?.profile?.toProfile() ?: return@withBGApi - chatModel.activeCall.value = Call(remoteHostId = chatRh, contact = cInfo.contact, callState = CallState.WaitCapabilities, localMedia = media, userProfile = profile) - chatModel.showCallView.value = true - chatModel.callCommand.add(WCallCommand.Capabilities(media)) - } - } - }, + startCall = out@{ media -> startChatCall(chatRh, chatInfo, media) }, endCall = { val call = chatModel.activeCall.value if (call != null) withBGApi { chatModel.callManager.endCall(call) } @@ -341,7 +322,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId: } }, openDirectChat = { contactId -> - withBGApi { + scope.launch { openDirectChat(chatRh, contactId, chatModel) } }, @@ -351,11 +332,13 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId: }, updateContactStats = { contact -> withBGApi { - val r = chatModel.controller.apiContactInfo(chatRh, chat.chatInfo.apiId) + val r = chatModel.controller.apiContactInfo(chatRh, chatInfo.apiId) if (r != null) { val contactStats = r.first if (contactStats != null) - chatModel.updateContactConnectionStats(chatRh, contact, contactStats) + withChats { + updateContactConnectionStats(chatRh, contact, contactStats) + } } } }, @@ -365,7 +348,9 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId: if (r != null) { val memStats = r.second if (memStats != null) { - chatModel.updateGroupMemberConnectionStats(chatRh, groupInfo, r.first, memStats) + withChats { + updateGroupMemberConnectionStats(chatRh, groupInfo, r.first, memStats) + } } } } @@ -374,7 +359,9 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId: withBGApi { val cStats = chatModel.controller.apiSyncContactRatchet(chatRh, contact.contactId, force = false) if (cStats != null) { - chatModel.updateContactConnectionStats(chatRh, contact, cStats) + withChats { + updateContactConnectionStats(chatRh, contact, cStats) + } } } }, @@ -382,7 +369,9 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId: withBGApi { val r = chatModel.controller.apiSyncGroupMemberRatchet(chatRh, groupInfo.apiId, member.groupMemberId, force = false) if (r != null) { - chatModel.updateGroupMemberConnectionStats(chatRh, groupInfo, r.first, r.second) + withChats { + updateGroupMemberConnectionStats(chatRh, groupInfo, r.first, r.second) + } } } }, @@ -403,7 +392,9 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId: reaction = reaction ) if (updatedCI != null) { - chatModel.updateChatItem(cInfo, updatedCI) + withChats { + updateChatItem(cInfo, updatedCI) + } } } }, @@ -411,8 +402,8 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId: suspend fun loadChatItemInfo(): ChatItemInfo? { val ciInfo = chatModel.controller.apiGetChatItemInfo(chatRh, cInfo.chatType, cInfo.apiId, cItem.id) if (ciInfo != null) { - if (chat.chatInfo is ChatInfo.Group) { - setGroupMembers(chatRh, chat.chatInfo.groupInfo, chatModel) + if (chatInfo is ChatInfo.Group) { + setGroupMembers(chatRh, chatInfo.groupInfo, chatModel) } } return ciInfo @@ -447,42 +438,26 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId: } } }, - addMembers = { groupInfo -> - hideKeyboard(view) - withBGApi { - setGroupMembers(chatRh, groupInfo, chatModel) - ModalManager.end.closeModals() - ModalManager.end.showModalCloseable(true) { close -> - AddGroupMembersView(chatRh, groupInfo, false, chatModel, close) - } - } - }, - openGroupLink = { groupInfo -> - hideKeyboard(view) - withBGApi { - val link = chatModel.controller.apiGetGroupLink(chatRh, groupInfo.groupId) - ModalManager.end.closeModals() - ModalManager.end.showModalCloseable(true) { - GroupLinkView(chatModel, chatRh, groupInfo, link?.first, link?.second, onGroupLinkUpdated = null) - } - } - }, + addMembers = { groupInfo -> addGroupMembers(view = view, groupInfo = groupInfo, rhId = chatRh, close = { ModalManager.end.closeModals() }) }, + openGroupLink = { groupInfo -> openGroupLink(view = view, groupInfo = groupInfo, rhId = chatRh, close = { ModalManager.end.closeModals() }) }, markRead = { range, unreadCountAfter -> - chatModel.markChatItemsRead(chat, range, unreadCountAfter) - ntfManager.cancelNotificationsForChat(chat.id) withBGApi { - chatModel.controller.apiChatRead( - chatRh, - chat.chatInfo.chatType, - chat.chatInfo.apiId, - range - ) + withChats { + markChatItemsRead(chatRh, chatInfo, range, unreadCountAfter) + ntfManager.cancelNotificationsForChat(chatInfo.id) + chatModel.controller.apiChatRead( + chatRh, + chatInfo.chatType, + chatInfo.apiId, + range + ) + } } }, - changeNtfsState = { enabled, currentValue -> toggleNotifications(chat, enabled, chatModel, currentValue) }, + changeNtfsState = { enabled, currentValue -> toggleNotifications(chatRh, chatInfo, enabled, chatModel, currentValue) }, onSearchValueChanged = { value -> if (searchText.value == value) return@ChatLayout - if (chatModel.chatId.value != activeChat.value?.id) return@ChatLayout + if (chatModel.chatId.value != activeChatInfo.value.id) return@ChatLayout val c = chatModel.getChat(chatModel.chatId.value ?: return@ChatLayout) ?: return@ChatLayout withBGApi { apiFindMessages(c, chatModel, value) @@ -492,27 +467,42 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId: onComposed, developerTools = chatModel.controller.appPrefs.developerTools.get(), showViaProxy = chatModel.controller.appPrefs.showSentViaProxy.get(), + showSearch = showSearch ) + if (appPlatform.isAndroid) { + val backgroundColor = MaterialTheme.colors.background + val backgroundColorState = rememberUpdatedState(backgroundColor) + LaunchedEffect(Unit) { + snapshotFlow { ModalManager.center.modalCount.value > 0 } + .collect { modalBackground -> + if (modalBackground) { + platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, CurrentColors.value.colors.background, false, false) + } else { + platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, backgroundColorState.value, true, true) + } + } + } + } } } is ChatInfo.ContactConnection -> { val close = { chatModel.chatId.value = null } ModalView(close, showClose = appPlatform.isAndroid, content = { - ContactConnectionInfoView(chatModel, chat.remoteHostId, chat.chatInfo.contactConnection.connReqInv, chat.chatInfo.contactConnection, false, close) + ContactConnectionInfoView(chatModel, chatRh, chatInfo.contactConnection.connReqInv, chatInfo.contactConnection, false, close) }) - LaunchedEffect(chat.id) { - onComposed(chat.id) + LaunchedEffect(chatInfo.id) { + onComposed(chatInfo.id) ModalManager.end.closeModals() chatModel.chatItems.clear() } } is ChatInfo.InvalidJSON -> { val close = { chatModel.chatId.value = null } - ModalView(close, showClose = appPlatform.isAndroid, endButtons = { ShareButton { clipboard.shareText(chat.chatInfo.json) } }, content = { - InvalidJSONView(chat.chatInfo.json) + ModalView(close, showClose = appPlatform.isAndroid, endButtons = { ShareButton { clipboard.shareText(chatInfo.json) } }, content = { + InvalidJSONView(chatInfo.json) }) - LaunchedEffect(chat.id) { - onComposed(chat.id) + LaunchedEffect(chatInfo.id) { + onComposed(chatInfo.id) ModalManager.end.closeModals() chatModel.chatItems.clear() } @@ -522,9 +512,22 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId: } } +fun startChatCall(remoteHostId: Long?, chatInfo: ChatInfo, media: CallMediaType) { + withBGApi { + if (chatInfo is ChatInfo.Direct) { + val contactInfo = chatModel.controller.apiContactInfo(remoteHostId, chatInfo.contact.contactId) + val profile = contactInfo?.second ?: chatModel.currentUser.value?.profile?.toProfile() ?: return@withBGApi + chatModel.activeCall.value = Call(remoteHostId = remoteHostId, contact = chatInfo.contact, callState = CallState.WaitCapabilities, localMedia = media, userProfile = profile) + chatModel.showCallView.value = true + chatModel.callCommand.add(WCallCommand.Capabilities(media)) + } + } +} + @Composable fun ChatLayout( - chat: Chat, + remoteHostId: State, + chatInfo: State, unreadCount: State, composeState: MutableState, composeView: (@Composable () -> Unit), @@ -563,15 +566,17 @@ fun ChatLayout( onSearchValueChanged: (String) -> Unit, onComposed: suspend (chatId: String) -> Unit, developerTools: Boolean, - showViaProxy: Boolean + showViaProxy: Boolean, + showSearch: MutableState ) { val scope = rememberCoroutineScope() val attachmentDisabled = remember { derivedStateOf { composeState.value.attachmentDisabled } } + Box( Modifier .fillMaxWidth() .desktopOnExternalDrag( - enabled = !attachmentDisabled.value && rememberUpdatedState(chat.userCanSend).value, + enabled = !attachmentDisabled.value && rememberUpdatedState(chatInfo.value).value.userCanSend, onFiles = { paths -> composeState.onFilesAttached(paths.map { it.toURI() }) }, onImage = { // TODO: file is not saved anywhere?! @@ -606,7 +611,7 @@ fun ChatLayout( } Scaffold( - topBar = { ChatInfoToolbar(chat, back, info, startCall, endCall, addMembers, openGroupLink, changeNtfsState, onSearchValueChanged) }, + topBar = { ChatInfoToolbar(chatInfo, back, info, startCall, endCall, addMembers, openGroupLink, changeNtfsState, onSearchValueChanged, showSearch) }, bottomBar = composeView, modifier = Modifier.navigationBarsWithImePadding(), floatingActionButton = { floatingButton.value() }, @@ -628,7 +633,7 @@ fun ChatLayout( .padding(contentPadding) ) { ChatItemsList( - chat, unreadCount, composeState, searchValue, + remoteHostId, chatInfo, unreadCount, composeState, searchValue, useLinkPreviews, linkMode, showMemberInfo, loadPrevMessages, deleteMessage, deleteMessages, receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, forwardItem, updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember, @@ -643,7 +648,7 @@ fun ChatLayout( @Composable fun ChatInfoToolbar( - chat: Chat, + chatInfo: State, back: () -> Unit, info: () -> Unit, startCall: (CallMediaType) -> Unit, @@ -652,35 +657,38 @@ fun ChatInfoToolbar( openGroupLink: (GroupInfo) -> Unit, changeNtfsState: (Boolean, currentValue: MutableState) -> Unit, onSearchValueChanged: (String) -> Unit, + showSearch: MutableState ) { val scope = rememberCoroutineScope() val showMenu = rememberSaveable { mutableStateOf(false) } - var showSearch by rememberSaveable { mutableStateOf(false) } + val onBackClicked = { - if (!showSearch) { + if (!showSearch.value) { back() } else { onSearchValueChanged("") - showSearch = false + showSearch.value = false } } if (appPlatform.isAndroid) { BackHandler(onBack = onBackClicked) } + val chatInfo = chatInfo.value val barButtons = arrayListOf<@Composable RowScope.() -> Unit>() val menuItems = arrayListOf<@Composable () -> Unit>() val activeCall by remember { chatModel.activeCall } - if (chat.chatInfo is ChatInfo.Local) { + if (chatInfo is ChatInfo.Local) { barButtons.add { - IconButton({ - showMenu.value = false - showSearch = true - }, enabled = chat.chatInfo.noteFolder.ready + IconButton( + { + showMenu.value = false + showSearch.value = true + }, enabled = chatInfo.noteFolder.ready ) { Icon( painterResource(MR.images.ic_search), stringResource(MR.strings.search_verb).capitalize(Locale.current), - tint = if (chat.chatInfo.noteFolder.ready) MaterialTheme.colors.primary else MaterialTheme.colors.secondary + tint = if (chatInfo.noteFolder.ready) MaterialTheme.colors.primary else MaterialTheme.colors.secondary ) } } @@ -688,41 +696,41 @@ fun ChatInfoToolbar( menuItems.add { ItemAction(stringResource(MR.strings.search_verb), painterResource(MR.images.ic_search), onClick = { showMenu.value = false - showSearch = true + showSearch.value = true }) } } - if (chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.mergedPreferences.calls.enabled.forUser) { + if (chatInfo is ChatInfo.Direct && chatInfo.contact.mergedPreferences.calls.enabled.forUser) { if (activeCall == null) { barButtons.add { if (appPlatform.isAndroid) { IconButton({ showMenu.value = false startCall(CallMediaType.Audio) - }, enabled = chat.chatInfo.contact.ready && chat.chatInfo.contact.active + }, enabled = chatInfo.contact.ready && chatInfo.contact.active ) { Icon( painterResource(MR.images.ic_call_500), stringResource(MR.strings.icon_descr_audio_call).capitalize(Locale.current), - tint = if (chat.chatInfo.contact.ready && chat.chatInfo.contact.active) MaterialTheme.colors.primary else MaterialTheme.colors.secondary + tint = if (chatInfo.contact.ready && chatInfo.contact.active) MaterialTheme.colors.primary else MaterialTheme.colors.secondary ) } } else { IconButton({ showMenu.value = false startCall(CallMediaType.Video) - }, enabled = chat.chatInfo.contact.ready && chat.chatInfo.contact.active + }, enabled = chatInfo.contact.ready && chatInfo.contact.active ) { Icon( painterResource(MR.images.ic_videocam), stringResource(MR.strings.icon_descr_video_call).capitalize(Locale.current), - tint = if (chat.chatInfo.contact.ready && chat.chatInfo.contact.active) MaterialTheme.colors.primary else MaterialTheme.colors.secondary + tint = if (chatInfo.contact.ready && chatInfo.contact.active) MaterialTheme.colors.primary else MaterialTheme.colors.secondary ) } } } - } else if (activeCall?.contact?.id == chat.id && appPlatform.isDesktop) { + } else if (activeCall?.contact?.id == chatInfo.id && appPlatform.isDesktop) { barButtons.add { val call = remember { chatModel.activeCall }.value val connectedAt = call?.connectedAt @@ -751,7 +759,7 @@ fun ChatInfoToolbar( } } } - if (chat.chatInfo.contact.ready && chat.chatInfo.contact.active && activeCall == null) { + if (chatInfo.contact.ready && chatInfo.contact.active && activeCall == null) { menuItems.add { if (appPlatform.isAndroid) { ItemAction(stringResource(MR.strings.icon_descr_video_call).capitalize(Locale.current), painterResource(MR.images.ic_videocam), onClick = { @@ -766,12 +774,12 @@ fun ChatInfoToolbar( } } } - } else if (chat.chatInfo is ChatInfo.Group && chat.chatInfo.groupInfo.canAddMembers) { - if (!chat.chatInfo.incognito) { + } else if (chatInfo is ChatInfo.Group && chatInfo.groupInfo.canAddMembers) { + if (!chatInfo.incognito) { barButtons.add { IconButton({ showMenu.value = false - addMembers(chat.chatInfo.groupInfo) + addMembers(chatInfo.groupInfo) }) { Icon(painterResource(MR.images.ic_person_add_500), stringResource(MR.strings.icon_descr_add_members), tint = MaterialTheme.colors.primary) } @@ -780,15 +788,16 @@ fun ChatInfoToolbar( barButtons.add { IconButton({ showMenu.value = false - openGroupLink(chat.chatInfo.groupInfo) + openGroupLink(chatInfo.groupInfo) }) { Icon(painterResource(MR.images.ic_add_link), stringResource(MR.strings.group_link), tint = MaterialTheme.colors.primary) } } } } - if ((chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.ready && chat.chatInfo.contact.active) || chat.chatInfo is ChatInfo.Group) { - val ntfsEnabled = remember { mutableStateOf(chat.chatInfo.ntfsEnabled) } + + if ((chatInfo is ChatInfo.Direct && chatInfo.contact.ready && chatInfo.contact.active) || chatInfo is ChatInfo.Group) { + val ntfsEnabled = remember { mutableStateOf(chatInfo.ntfsEnabled) } menuItems.add { ItemAction( if (ntfsEnabled.value) stringResource(MR.strings.mute_chat) else stringResource(MR.strings.unmute_chat), @@ -814,10 +823,10 @@ fun ChatInfoToolbar( } DefaultTopAppBar( - navigationButton = { if (appPlatform.isAndroid || showSearch) { NavigationButtonBack(onBackClicked) } }, - title = { ChatInfoToolbarTitle(chat.chatInfo) }, - onTitleClick = if (chat.chatInfo is ChatInfo.Local) null else info, - showSearch = showSearch, + navigationButton = { if (appPlatform.isAndroid || showSearch.value) { NavigationButtonBack(onBackClicked) } }, + title = { ChatInfoToolbarTitle(chatInfo) }, + onTitleClick = if (chatInfo is ChatInfo.Local) null else info, + showSearch = showSearch.value, onSearchValueChanged = onSearchValueChanged, buttons = barButtons ) @@ -883,7 +892,8 @@ val CIListStateSaver = run { @Composable fun BoxWithConstraintsScope.ChatItemsList( - chat: Chat, + remoteHostId: State, + chatInfo: State, unreadCount: State, composeState: MutableState, searchValue: State, @@ -916,7 +926,9 @@ fun BoxWithConstraintsScope.ChatItemsList( ) { val listState = rememberLazyListState() val scope = rememberCoroutineScope() - ScrollToBottom(chat.id, listState, chatModel.chatItems) + val remoteHostId = remember { remoteHostId }.value + val chatInfo = remember { chatInfo }.value + ScrollToBottom(chatInfo.id, listState, chatModel.chatItems) var prevSearchEmptiness by rememberSaveable { mutableStateOf(searchValue.value.isEmpty()) } // Scroll to bottom when search value changes from something to nothing and back LaunchedEffect(searchValue.value.isEmpty()) { @@ -941,13 +953,13 @@ fun BoxWithConstraintsScope.ChatItemsList( scope.launch { listState.animateScrollToItem(kotlin.math.min(reversedChatItems.lastIndex, index + 1), -maxHeightRounded) } } } - LaunchedEffect(chat.id) { + LaunchedEffect(chatInfo.id) { var stopListening = false snapshotFlow { listState.layoutInfo.visibleItemsInfo.lastIndex } .distinctUntilChanged() .filter { !stopListening } .collect { - onComposed(chat.id) + onComposed(chatInfo.id) stopListening = true } } @@ -966,7 +978,7 @@ fun BoxWithConstraintsScope.ChatItemsList( val dismissState = rememberDismissState(initialValue = DismissValue.Default) { if (it == DismissValue.DismissedToStart) { scope.launch { - if ((cItem.content is CIContent.SndMsgContent || cItem.content is CIContent.RcvMsgContent) && chat.chatInfo !is ChatInfo.Local) { + if ((cItem.content is CIContent.SndMsgContent || cItem.content is CIContent.RcvMsgContent) && chatInfo !is ChatInfo.Local) { if (composeState.value.editing) { composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews) } else if (cItem.id != ChatItem.TEMP_LIVE_CHAT_ITEM_ID) { @@ -1000,14 +1012,14 @@ fun BoxWithConstraintsScope.ChatItemsList( tryOrShowError("${cItem.id}ChatItem", error = { CIBrokenComposableView(if (cItem.chatDir.sent) Alignment.CenterEnd else Alignment.CenterStart) }) { - ChatItemView(chat.remoteHostId, chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, range = range, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools, showViaProxy = showViaProxy) + ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, range = range, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools, showViaProxy = showViaProxy) } } @Composable fun ChatItemView(cItem: ChatItem, range: IntRange?, prevItem: ChatItem?) { val voiceWithTransparentBack = cItem.content.msgContent is MsgContent.MCVoice && cItem.content.text.isEmpty() && cItem.quotedItem == null && cItem.meta.itemForwarded == null - if (chat.chatInfo is ChatInfo.Group) { + if (chatInfo is ChatInfo.Group) { if (cItem.chatDir is CIDirection.GroupRcv) { val member = cItem.chatDir.groupMember val (prevMember, memCount) = @@ -1047,7 +1059,7 @@ fun BoxWithConstraintsScope.ChatItemsList( swipeableModifier, horizontalArrangement = Arrangement.spacedBy(4.dp) ) { - Box(Modifier.clickable { showMemberInfo(chat.chatInfo.groupInfo, member) }) { + Box(Modifier.clickable { showMemberInfo(chatInfo.groupInfo, member) }) { MemberImage(member) } ChatItemViewShortHand(cItem, range) @@ -1101,7 +1113,7 @@ fun BoxWithConstraintsScope.ChatItemsList( } } - if (cItem.isRcvNew && chat.id == ChatModel.chatId.value) { + if (cItem.isRcvNew && chatInfo.id == ChatModel.chatId.value) { LaunchedEffect(cItem.id) { scope.launch { delay(600) @@ -1112,7 +1124,7 @@ fun BoxWithConstraintsScope.ChatItemsList( } } } - FloatingButtons(chatModel.chatItems, unreadCount, chat.chatStats.minUnreadItemId, searchValue, markRead, setFloatingButton, listState) + FloatingButtons(chatModel.chatItems, unreadCount, remoteHostId, chatInfo, searchValue, markRead, setFloatingButton, listState) LaunchedEffect(Unit) { snapshotFlow { listState.isScrollInProgress } .collect { @@ -1145,7 +1157,7 @@ private fun ScrollToBottom(chatId: ChatId, listState: LazyListState, chatItems: .filter { listState.layoutInfo.visibleItemsInfo.firstOrNull()?.key != it } .collect { try { - if (listState.firstVisibleItemIndex == 0 || (listState.firstVisibleItemIndex == 1 && listState.layoutInfo.totalItemsCount == chatItems.size)) { + if (listState.firstVisibleItemIndex == 0 || (listState.firstVisibleItemIndex == 1 && listState.layoutInfo.totalItemsCount == chatItems.value.size)) { if (appPlatform.isAndroid) listState.animateScrollToItem(0) else listState.scrollToItem(0) } else { if (appPlatform.isAndroid) listState.animateScrollBy(scrollDistance) else listState.scrollBy(scrollDistance) @@ -1156,6 +1168,8 @@ private fun ScrollToBottom(chatId: ChatId, listState: LazyListState, chatItems: * this coroutine will be canceled with the message "Current mutation had a higher priority" because of animatedScroll. * Which breaks auto-scrolling to bottom. So just ignoring the exception * */ + } catch (e: IllegalArgumentException) { + Log.e(TAG, "Failed to scroll: ${e.stackTraceToString()}") } } } @@ -1165,7 +1179,8 @@ private fun ScrollToBottom(chatId: ChatId, listState: LazyListState, chatItems: fun BoxWithConstraintsScope.FloatingButtons( chatItems: State>, unreadCount: State, - minUnreadItemId: Long, + remoteHostId: Long?, + chatInfo: ChatInfo, searchValue: State, markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit, setFloatingButton: (@Composable () -> Unit) -> Unit, @@ -1246,8 +1261,9 @@ fun BoxWithConstraintsScope.FloatingButtons( generalGetString(MR.strings.mark_read), painterResource(MR.images.ic_check), onClick = { + val minUnreadItemId = chatModel.chats.value.firstOrNull { it.remoteHostId == remoteHostId && it.id == chatInfo.id }?.chatStats?.minUnreadItemId ?: return@ItemAction markRead( - CC.ItemRange(minUnreadItemId, chatItems.value[chatItems.size - listState.layoutInfo.visibleItemsInfo.lastIndex - 1].id - 1), + CC.ItemRange(minUnreadItemId, chatItems.value[chatItems.value.size - listState.layoutInfo.visibleItemsInfo.lastIndex - 1].id - 1), bottomUnreadCount ) showDropDown.value = false @@ -1316,7 +1332,7 @@ private fun TopEndFloatingButton( val interactionSource = interactionSourceWithDetection(onClick, onLongClick) FloatingActionButton( {}, // no action here - modifier.size(48.dp), + modifier.size(48.dp).onRightClick(onLongClick), backgroundColor = MaterialTheme.colors.secondaryVariant, elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp), interactionSource = interactionSource, @@ -1334,6 +1350,28 @@ private fun TopEndFloatingButton( val chatViewScrollState = MutableStateFlow(false) +fun addGroupMembers(groupInfo: GroupInfo, rhId: Long?, view: Any? = null, close: (() -> Unit)? = null) { + hideKeyboard(view) + withBGApi { + setGroupMembers(rhId, groupInfo, chatModel) + close?.invoke() + ModalManager.end.showModalCloseable(true) { close -> + AddGroupMembersView(rhId, groupInfo, false, chatModel, close) + } + } +} + +fun openGroupLink(groupInfo: GroupInfo, rhId: Long?, view: Any? = null, close: (() -> Unit)? = null) { + hideKeyboard(view) + withBGApi { + val link = chatModel.controller.apiGetGroupLink(rhId, groupInfo.groupId) + close?.invoke() + ModalManager.end.showModalCloseable(true) { + GroupLinkView(chatModel, rhId, groupInfo, link?.first, link?.second, onGroupLinkUpdated = null) + } + } +} + private fun bottomEndFloatingButton( unreadCount: Int, showButtonWithCounter: Boolean, @@ -1378,8 +1416,8 @@ private fun bottomEndFloatingButton( } } -private fun markUnreadChatAsRead(activeChat: MutableState, chatModel: ChatModel) { - val chat = activeChat.value +private fun markUnreadChatAsRead(chatId: String) { + val chat = chatModel.chats.value.firstOrNull { it.id == chatId } if (chat?.chatStats?.unreadChat != true) return withApi { val chatRh = chat.remoteHostId @@ -1389,9 +1427,10 @@ private fun markUnreadChatAsRead(activeChat: MutableState, chatModel: Cha chat.chatInfo.apiId, false ) - if (success && chat.id == activeChat.value?.id) { - activeChat.value = chat.copy(chatStats = chat.chatStats.copy(unreadChat = false)) - chatModel.replaceChat(chatRh, chat.id, activeChat.value!!) + if (success) { + withChats { + replaceChat(chatRh, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = false))) + } } } } @@ -1544,12 +1583,8 @@ fun PreviewChatLayout() { val unreadCount = remember { mutableStateOf(chatItems.count { it.isRcvNew }) } val searchValue = remember { mutableStateOf("") } ChatLayout( - chat = Chat( - remoteHostId = null, - chatInfo = ChatInfo.Direct.sampleData, - chatItems = chatItems, - chatStats = Chat.ChatStats() - ), + remoteHostId = remember { mutableStateOf(null) }, + chatInfo = remember { mutableStateOf(ChatInfo.Direct.sampleData) }, unreadCount = unreadCount, composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) }, composeView = {}, @@ -1589,6 +1624,7 @@ fun PreviewChatLayout() { onComposed = {}, developerTools = false, showViaProxy = false, + showSearch = remember { mutableStateOf(false) } ) } } @@ -1618,12 +1654,8 @@ fun PreviewGroupChatLayout() { val unreadCount = remember { mutableStateOf(chatItems.count { it.isRcvNew }) } val searchValue = remember { mutableStateOf("") } ChatLayout( - chat = Chat( - remoteHostId = null, - chatInfo = ChatInfo.Group.sampleData, - chatItems = chatItems, - chatStats = Chat.ChatStats() - ), + remoteHostId = remember { mutableStateOf(null) }, + chatInfo = remember { mutableStateOf(ChatInfo.Direct.sampleData) }, unreadCount = unreadCount, composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) }, composeView = {}, @@ -1663,6 +1695,7 @@ fun PreviewGroupChatLayout() { onComposed = {}, developerTools = false, showViaProxy = false, + showSearch = remember { mutableStateOf(false) } ) } } 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 a0e1bf8107..f7e4b708e6 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 @@ -22,6 +22,7 @@ import androidx.compose.ui.unit.sp import chat.simplex.common.model.* import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.model.ChatModel.filesToDelete +import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chat.item.* @@ -393,7 +394,9 @@ fun ComposeView( ttl = ttl ) if (aChatItem != null) { - chatModel.addChatItem(chat.remoteHostId, cInfo, aChatItem.chatItem) + withChats { + addChatItem(chat.remoteHostId, cInfo, aChatItem.chatItem) + } return aChatItem.chatItem } if (file != null) removeFile(file.filePath) @@ -421,7 +424,9 @@ fun ComposeView( ttl = ttl ) if (chatItem != null) { - chatModel.addChatItem(rhId, chat.chatInfo, chatItem) + withChats { + addChatItem(rhId, chat.chatInfo, chatItem) + } } return chatItem } @@ -458,7 +463,9 @@ fun ComposeView( val mc = checkLinkPreview() val contact = chatModel.controller.apiSendMemberContactInvitation(chat.remoteHostId, chat.chatInfo.apiId, mc) if (contact != null) { - chatModel.updateContact(chat.remoteHostId, contact) + withChats { + updateContact(chat.remoteHostId, contact) + } } } @@ -474,7 +481,9 @@ fun ComposeView( mc = updateMsgContent(oldMsgContent), live = live ) - if (updatedItem != null) chatModel.upsertChatItem(chat.remoteHostId, cInfo, updatedItem.chatItem) + if (updatedItem != null) withChats { + upsertChatItem(chat.remoteHostId, cInfo, updatedItem.chatItem) + } return updatedItem?.chatItem } return null @@ -827,7 +836,7 @@ fun ComposeView( chatModel.sharedContent.value = null } - val userCanSend = rememberUpdatedState(chat.userCanSend) + val userCanSend = rememberUpdatedState(chat.chatInfo.userCanSend) val sendMsgEnabled = rememberUpdatedState(chat.chatInfo.sendMsgEnabled) val userIsObserver = rememberUpdatedState(chat.userIsObserver) val nextSendGrpInv = rememberUpdatedState(chat.nextSendGrpInv) @@ -863,6 +872,7 @@ fun ComposeView( } } } + Divider() Row( modifier = Modifier.background(MaterialTheme.colors.background).padding(end = 8.dp), verticalAlignment = Alignment.Bottom, @@ -886,7 +896,7 @@ fun ComposeView( && !nextSendGrpInv.value IconButton( attachmentClicked, - Modifier.padding(bottom = if (appPlatform.isAndroid) 0.dp else with(LocalDensity.current) { 7.sp.toDp() }), + Modifier.padding(bottom = if (appPlatform.isAndroid) 2.dp else with(LocalDensity.current) { 7.sp.toDp() }), enabled = attachmentEnabled ) { Icon( @@ -927,8 +937,8 @@ fun ComposeView( } } - LaunchedEffect(rememberUpdatedState(chat.userCanSend).value) { - if (!chat.userCanSend) { + LaunchedEffect(rememberUpdatedState(chat.chatInfo.userCanSend).value) { + if (!chat.chatInfo.userCanSend) { clearCurrentDraft() clearState() } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContactPreferences.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContactPreferences.kt index 502074d629..f7f0a55372 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContactPreferences.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContactPreferences.kt @@ -19,6 +19,7 @@ import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.common.views.usersettings.PreferenceToggle import chat.simplex.common.model.* +import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.platform.ColumnWithScrollBar import chat.simplex.res.MR @@ -40,8 +41,10 @@ fun ContactPreferencesView( val prefs = contactFeaturesAllowedToPrefs(featuresAllowed) val toContact = m.controller.apiSetContactPrefs(rhId, ct.contactId, prefs) if (toContact != null) { - m.updateContact(rhId, toContact) - currentFeaturesAllowed = featuresAllowed + withChats { + updateContact(rhId, toContact) + currentFeaturesAllowed = featuresAllowed + } } afterSave() } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt index 931cc17872..42fbec35cd 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt @@ -24,6 +24,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.model.* +import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chat.ChatInfoToolbarTitle import chat.simplex.common.views.helpers.* @@ -58,7 +59,9 @@ fun AddGroupMembersView(rhId: Long?, groupInfo: GroupInfo, creatingGroup: Boolea for (contactId in selectedContacts) { val member = chatModel.controller.apiAddMember(rhId, groupInfo.groupId, contactId, selectedRole.value) if (member != null) { - chatModel.upsertGroupMember(rhId, groupInfo, member) + withChats { + upsertGroupMember(rhId, groupInfo, member) + } } else { break } @@ -81,7 +84,7 @@ fun getContactsToAdd(chatModel: ChatModel, search: String): List { val memberContactIds = chatModel.groupMembers .filter { it.memberCurrent } .mapNotNull { it.memberContactId } - return chatModel.chats + return chatModel.chats.value .asSequence() .map { it.chatInfo } .filterIsInstance() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt index c91bc3bcfc..1f63e61a02 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt @@ -26,6 +26,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.model.* +import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.common.views.usersettings.* @@ -40,10 +41,10 @@ import kotlinx.coroutines.launch const val SMALL_GROUPS_RCPS_MEM_LIMIT: Int = 20 @Composable -fun GroupChatInfoView(chatModel: ChatModel, rhId: Long?, chatId: String, groupLink: String?, groupLinkMemberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair?) -> Unit, close: () -> Unit) { +fun GroupChatInfoView(chatModel: ChatModel, rhId: Long?, chatId: String, groupLink: String?, groupLinkMemberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair?) -> Unit, close: () -> Unit, onSearchClicked: () -> Unit) { BackHandler(onBack = close) // TODO derivedStateOf? - val chat = chatModel.chats.firstOrNull { ch -> ch.id == chatId && ch.remoteHostId == rhId } + val chat = chatModel.chats.value.firstOrNull { ch -> ch.id == chatId && ch.remoteHostId == rhId } val currentUser = chatModel.currentUser.value val developerTools = chatModel.controller.appPrefs.developerTools.get() if (chat != null && chat.chatInfo is ChatInfo.Group && currentUser != null) { @@ -56,7 +57,7 @@ fun GroupChatInfoView(chatModel: ChatModel, rhId: Long?, chatId: String, groupLi sendReceipts = sendReceipts, setSendReceipts = { sendRcpts -> val chatSettings = (chat.chatInfo.chatSettings ?: ChatSettings.defaults).copy(sendRcpts = sendRcpts.bool) - updateChatSettings(chat, chatSettings, chatModel) + updateChatSettings(chat.remoteHostId, chat.chatInfo, chatSettings, chatModel) sendReceipts.value = sendRcpts }, members = chatModel.groupMembers @@ -113,7 +114,8 @@ fun GroupChatInfoView(chatModel: ChatModel, rhId: Long?, chatId: String, groupLi leaveGroup = { leaveGroupDialog(rhId, groupInfo, chatModel, close) }, manageGroupLink = { ModalManager.end.showModal { GroupLinkView(chatModel, rhId, groupInfo, groupLink, groupLinkMemberRole, onGroupLinkUpdated) } - } + }, + onSearchClicked = onSearchClicked ) } } @@ -131,13 +133,15 @@ fun deleteGroupDialog(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, cl withBGApi { val r = chatModel.controller.apiDeleteChat(chat.remoteHostId, chatInfo.chatType, chatInfo.apiId) if (r) { - chatModel.removeChat(chat.remoteHostId, chatInfo.id) - if (chatModel.chatId.value == chatInfo.id) { - chatModel.chatId.value = null - ModalManager.end.closeModals() + withChats { + removeChat(chat.remoteHostId, chatInfo.id) + if (chatModel.chatId.value == chatInfo.id) { + chatModel.chatId.value = null + ModalManager.end.closeModals() + } + ntfManager.cancelNotificationsForChat(chatInfo.id) + close?.invoke() } - ntfManager.cancelNotificationsForChat(chatInfo.id) - close?.invoke() } } }, @@ -169,7 +173,9 @@ private fun removeMemberAlert(rhId: Long?, groupInfo: GroupInfo, mem: GroupMembe withBGApi { val updatedMember = chatModel.controller.apiRemoveMember(rhId, groupInfo.groupId, mem.groupMemberId) if (updatedMember != null) { - chatModel.upsertGroupMember(rhId, groupInfo, updatedMember) + withChats { + upsertGroupMember(rhId, groupInfo, updatedMember) + } } } }, @@ -177,6 +183,57 @@ private fun removeMemberAlert(rhId: Long?, groupInfo: GroupInfo, mem: GroupMembe ) } +@Composable +fun SearchButton(chat: Chat, group: GroupInfo, close: () -> Unit, onSearchClicked: () -> Unit) { + val disabled = !group.ready || chat.chatItems.isEmpty() + + InfoViewActionButton( + icon = painterResource(MR.images.ic_search), + title = generalGetString(MR.strings.info_view_search_button), + disabled = disabled, + disabledLook = disabled, + onClick = { + if (appPlatform.isAndroid) { + close.invoke() + } + onSearchClicked() + } + ) +} + +@Composable +fun MuteButton(chat: Chat, groupInfo: GroupInfo) { + val ntfsEnabled = remember { mutableStateOf(chat.chatInfo.ntfsEnabled) } + + InfoViewActionButton( + icon = if (ntfsEnabled.value) painterResource(MR.images.ic_notifications_off) else painterResource(MR.images.ic_notifications), + title = if (ntfsEnabled.value) stringResource(MR.strings.mute_chat) else stringResource(MR.strings.unmute_chat), + disabled = !groupInfo.ready, + disabledLook = !groupInfo.ready, + onClick = { + toggleNotifications(chat.remoteHostId, chat.chatInfo, !ntfsEnabled.value, chatModel, ntfsEnabled) + } + ) +} + +@Composable +fun AddGroupMembersButton(chat: Chat, groupInfo: GroupInfo) { + InfoViewActionButton( + icon = if (groupInfo.incognito) painterResource(MR.images.ic_add_link) else painterResource(MR.images.ic_person_add_500), + title = stringResource(MR.strings.action_button_add_members), + disabled = !groupInfo.ready, + disabledLook = !groupInfo.ready, + onClick = { + if (groupInfo.incognito) { + openGroupLink(groupInfo = groupInfo, rhId = chat.remoteHostId) + } else { + addGroupMembers(groupInfo = groupInfo, rhId = chat.remoteHostId) + } + } + ) +} + + @Composable fun GroupChatInfoLayout( chat: Chat, @@ -196,6 +253,8 @@ fun GroupChatInfoLayout( clearChat: () -> Unit, leaveGroup: () -> Unit, manageGroupLink: () -> Unit, + close: () -> Unit = { ModalManager.closeAllModalsEverywhere()}, + onSearchClicked: () -> Unit ) { val listState = rememberLazyListState() val scope = rememberCoroutineScope() @@ -219,6 +278,24 @@ fun GroupChatInfoLayout( } SectionSpacer() + Row( + Modifier + .fillMaxWidth() + .padding(horizontal = DEFAULT_PADDING), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + SearchButton(chat, groupInfo, close, onSearchClicked) + if (groupInfo.canAddMembers) { + Spacer(Modifier.width(INFO_VIEW_BUTTONS_PADDING)) + AddGroupMembersButton(chat, groupInfo) + } + Spacer(Modifier.width(INFO_VIEW_BUTTONS_PADDING)) + MuteButton(chat, groupInfo) + } + + SectionSpacer() + SectionView { if (groupInfo.canEdit) { EditGroupProfileButton(editGroupProfile) @@ -235,7 +312,7 @@ fun GroupChatInfoLayout( WallpaperButton { ModalManager.end.showModal { - val chat = remember { derivedStateOf { chatModel.chats.firstOrNull { it.id == chat.id } } } + val chat = remember { derivedStateOf { chatModel.chats.value.firstOrNull { it.id == chat.id } } } val c = chat.value if (c != null) { ChatWallpaperEditorModal(c) @@ -584,7 +661,7 @@ fun PreviewGroupChatInfoLayout() { members = listOf(GroupMember.sampleData, GroupMember.sampleData, GroupMember.sampleData), developerTools = false, groupLink = null, - addMembers = {}, showMemberInfo = {}, editGroupProfile = {}, addOrEditWelcomeMessage = {}, openPreferences = {}, deleteGroup = {}, clearChat = {}, leaveGroup = {}, manageGroupLink = {}, + addMembers = {}, showMemberInfo = {}, editGroupProfile = {}, addOrEditWelcomeMessage = {}, openPreferences = {}, deleteGroup = {}, clearChat = {}, leaveGroup = {}, manageGroupLink = {}, onSearchClicked = {}, ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt index 98b9ca729f..7a087a9263 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.model.* import chat.simplex.common.model.ChatModel.controller +import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chat.* import chat.simplex.common.views.helpers.* @@ -52,7 +53,7 @@ fun GroupMemberInfoView( closeAll: () -> Unit, // Close all open windows up to ChatView ) { BackHandler(onBack = close) - val chat = chatModel.chats.firstOrNull { ch -> ch.id == chatModel.chatId.value && ch.remoteHostId == rhId } + val chat = chatModel.chats.value.firstOrNull { ch -> ch.id == chatModel.chatId.value && ch.remoteHostId == rhId } val connStats = remember { mutableStateOf(connectionStats) } val developerTools = chatModel.controller.appPrefs.developerTools.get() var progressIndicator by remember { mutableStateOf(false) } @@ -72,13 +73,15 @@ fun GroupMemberInfoView( withBGApi { val c = chatModel.controller.apiGetChat(rhId, ChatType.Direct, it) if (c != null) { - if (chatModel.getContactChat(it) == null) { - chatModel.addChat(c) + withChats { + if (chatModel.getContactChat(it) == null) { + addChat(c) + } + chatModel.chatItemStatuses.clear() + chatModel.chatItems.replaceAll(c.chatItems) + chatModel.chatId.value = c.id + closeAll() } - chatModel.chatItemStatuses.clear() - chatModel.chatItems.replaceAll(c.chatItems) - chatModel.chatId.value = c.id - closeAll() } } }, @@ -88,8 +91,10 @@ fun GroupMemberInfoView( val memberContact = chatModel.controller.apiCreateMemberContact(rhId, groupInfo.apiId, member.groupMemberId) if (memberContact != null) { val memberChat = Chat(remoteHostId = rhId, ChatInfo.Direct(memberContact), chatItems = arrayListOf()) - chatModel.addChat(memberChat) - openLoadedChat(memberChat, chatModel) + withChats { + addChat(memberChat) + openLoadedChat(memberChat, chatModel) + } closeAll() chatModel.setContactNetworkStatus(memberContact, NetworkStatus.Connected()) } @@ -114,7 +119,9 @@ fun GroupMemberInfoView( withBGApi { kotlin.runCatching { val mem = chatModel.controller.apiMemberRole(rhId, groupInfo.groupId, member.groupMemberId, it) - chatModel.upsertGroupMember(rhId, groupInfo, mem) + withChats { + upsertGroupMember(rhId, groupInfo, mem) + } }.onFailure { newRole.value = prevValue } @@ -127,7 +134,9 @@ fun GroupMemberInfoView( val r = chatModel.controller.apiSwitchGroupMember(rhId, groupInfo.apiId, member.groupMemberId) if (r != null) { connStats.value = r.second - chatModel.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) + withChats { + updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) + } close.invoke() } } @@ -139,7 +148,9 @@ fun GroupMemberInfoView( val r = chatModel.controller.apiAbortSwitchGroupMember(rhId, groupInfo.apiId, member.groupMemberId) if (r != null) { connStats.value = r.second - chatModel.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) + withChats { + updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) + } close.invoke() } } @@ -150,7 +161,9 @@ fun GroupMemberInfoView( val r = chatModel.controller.apiSyncGroupMemberRatchet(rhId, groupInfo.apiId, member.groupMemberId, force = false) if (r != null) { connStats.value = r.second - chatModel.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) + withChats { + updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) + } close.invoke() } } @@ -161,7 +174,9 @@ fun GroupMemberInfoView( val r = chatModel.controller.apiSyncGroupMemberRatchet(rhId, groupInfo.apiId, member.groupMemberId, force = true) if (r != null) { connStats.value = r.second - chatModel.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) + withChats { + updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) + } close.invoke() } } @@ -177,15 +192,17 @@ fun GroupMemberInfoView( verify = { code -> chatModel.controller.apiVerifyGroupMember(rhId, mem.groupId, mem.groupMemberId, code)?.let { r -> val (verified, existingCode) = r - chatModel.upsertGroupMember( - rhId, - groupInfo, - mem.copy( - activeConn = mem.activeConn?.copy( - connectionCode = if (verified) SecurityCode(existingCode, Clock.System.now()) else null + withChats { + upsertGroupMember( + rhId, + groupInfo, + mem.copy( + activeConn = mem.activeConn?.copy( + connectionCode = if (verified) SecurityCode(existingCode, Clock.System.now()) else null + ) ) ) - ) + } r } }, @@ -211,7 +228,9 @@ fun removeMemberDialog(rhId: Long?, groupInfo: GroupInfo, member: GroupMember, c withBGApi { val removedMember = chatModel.controller.apiRemoveMember(rhId, member.groupId, member.groupMemberId) if (removedMember != null) { - chatModel.upsertGroupMember(rhId, groupInfo, removedMember) + withChats { + upsertGroupMember(rhId, groupInfo, removedMember) + } } close?.invoke() } @@ -246,10 +265,10 @@ fun GroupMemberInfoLayout( verifyClicked: () -> Unit, ) { val cStats = connStats.value - fun knownDirectChat(contactId: Long): Chat? { + fun knownDirectChat(contactId: Long): Pair? { val chat = getContactChat(contactId) return if (chat != null && chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.directOrUsed) { - chat + chat to chat.chatInfo.contact } else { null } @@ -309,17 +328,53 @@ fun GroupMemberInfoLayout( val contactId = member.memberContactId + Row( + Modifier + .fillMaxWidth() + .padding(horizontal = DEFAULT_PADDING), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + val knownChat = if (contactId != null) knownDirectChat(contactId) else null + if (knownChat != null) { + val (chat, contact) = knownChat + OpenChatButton(onClick = { openDirectChat(contact.contactId) }) + Spacer(Modifier.width(INFO_VIEW_BUTTONS_PADDING)) + AudioCallButton(chat, contact) + Spacer(Modifier.width(INFO_VIEW_BUTTONS_PADDING)) + VideoButton(chat, contact) + } else if (groupInfo.fullGroupPreferences.directMessages.on(groupInfo.membership)) { + if (contactId != null) { + OpenChatButton(onClick = { openDirectChat(contactId) }) // legacy - only relevant for direct contacts created when joining group + } else { + OpenChatButton(onClick = { createMemberContact() }) + } + Spacer(Modifier.width(INFO_VIEW_BUTTONS_PADDING)) + InfoViewActionButton(painterResource(MR.images.ic_call), generalGetString(MR.strings.info_view_call_button), disabled = false, disabledLook = true, onClick = { + showSendMessageToEnableCallsAlert() + }) + Spacer(Modifier.width(INFO_VIEW_BUTTONS_PADDING)) + InfoViewActionButton(painterResource(MR.images.ic_videocam), generalGetString(MR.strings.info_view_video_button), disabled = false, disabledLook = true, onClick = { + showSendMessageToEnableCallsAlert() + }) + } else { // no known contact chat && directMessages are off + InfoViewActionButton(painterResource(MR.images.ic_chat_bubble), generalGetString(MR.strings.info_view_message_button), disabled = false, disabledLook = true, onClick = { + showDirectMessagesProhibitedAlert(generalGetString(MR.strings.cant_send_message_to_member_alert_title)) + }) + Spacer(Modifier.width(INFO_VIEW_BUTTONS_PADDING)) + InfoViewActionButton(painterResource(MR.images.ic_call), generalGetString(MR.strings.info_view_call_button), disabled = false, disabledLook = true, onClick = { + showDirectMessagesProhibitedAlert(generalGetString(MR.strings.cant_call_member_alert_title)) + }) + Spacer(Modifier.width(INFO_VIEW_BUTTONS_PADDING)) + InfoViewActionButton(painterResource(MR.images.ic_videocam), generalGetString(MR.strings.info_view_video_button), disabled = false, disabledLook = true, onClick = { + showDirectMessagesProhibitedAlert(generalGetString(MR.strings.cant_call_member_alert_title)) + }) + } + } + SectionSpacer() + if (member.memberActive) { SectionView { - if (contactId != null && knownDirectChat(contactId) != null) { - OpenChatButton(onClick = { openDirectChat(contactId) }) - } else if (groupInfo.fullGroupPreferences.directMessages.on(groupInfo.membership)) { - if (contactId != null) { - OpenChatButton(onClick = { openDirectChat(contactId) }) - } else if (member.activeConn?.peerChatVRange?.isCompatibleRange(CREATE_MEMBER_CONTACT_VRANGE) == true) { - OpenChatButton(onClick = { createMemberContact() }) - } - } if (connectionCode != null) { VerifyCodeButton(member.verified, verifyClicked) } @@ -420,6 +475,20 @@ fun GroupMemberInfoLayout( } } +private fun showSendMessageToEnableCallsAlert() { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.cant_call_member_alert_title), + text = generalGetString(MR.strings.cant_call_member_send_message_alert_text) + ) +} + +private fun showDirectMessagesProhibitedAlert(title: String) { + AlertManager.shared.showAlertMsg( + title = title, + text = generalGetString(MR.strings.direct_messages_are_prohibited_in_chat) + ) +} + @Composable fun GroupMemberInfoHeader(member: GroupMember) { Column( @@ -513,12 +582,12 @@ fun RemoveMemberButton(onClick: () -> Unit) { @Composable fun OpenChatButton(onClick: () -> Unit) { - SettingsActionItem( - painterResource(MR.images.ic_chat), - stringResource(MR.strings.button_send_direct_message), - click = onClick, - textColor = MaterialTheme.colors.primary, - iconColor = MaterialTheme.colors.primary, + InfoViewActionButton( + icon = painterResource(MR.images.ic_chat_bubble), + title = generalGetString(MR.strings.info_view_message_button), + disabled = false, + disabledLook = false, + onClick = onClick ) } @@ -621,7 +690,9 @@ fun updateMemberSettings(rhId: Long?, gInfo: GroupInfo, member: GroupMember, mem withBGApi { val success = ChatController.apiSetMemberSettings(rhId, gInfo.groupId, member.groupMemberId, memberSettings) if (success) { - ChatModel.upsertGroupMember(rhId, gInfo, member.copy(memberSettings = memberSettings)) + withChats { + upsertGroupMember(rhId, gInfo, member.copy(memberSettings = memberSettings)) + } } } } @@ -652,7 +723,9 @@ fun unblockForAllAlert(rhId: Long?, gInfo: GroupInfo, mem: GroupMember) { fun blockMemberForAll(rhId: Long?, gInfo: GroupInfo, member: GroupMember, blocked: Boolean) { withBGApi { val updatedMember = ChatController.apiBlockMemberForAll(rhId, gInfo.groupId, member.groupMemberId, blocked) - chatModel.upsertGroupMember(rhId, gInfo, updatedMember) + withChats { + upsertGroupMember(rhId, gInfo, updatedMember) + } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt index 82646a99c5..b7d66dd4f6 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt @@ -17,6 +17,7 @@ import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.common.views.usersettings.PreferenceToggleWithIcon import chat.simplex.common.model.* +import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.platform.ColumnWithScrollBar import chat.simplex.res.MR import dev.icerock.moko.resources.compose.painterResource @@ -43,8 +44,10 @@ fun GroupPreferencesView(m: ChatModel, rhId: Long?, chatId: String, close: () -> val gp = gInfo.groupProfile.copy(groupPreferences = preferences.toGroupPreferences()) val g = m.controller.apiUpdateGroup(rhId, gInfo.groupId, gp) if (g != null) { - m.updateGroup(rhId, g) - currentPreferences = preferences + withChats { + updateGroup(rhId, g) + currentPreferences = preferences + } } afterSave() } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupProfileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupProfileView.kt index 7975a298d1..6375ef1a20 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupProfileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupProfileView.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.model.* +import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.* @@ -38,7 +39,9 @@ fun GroupProfileView(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, cl withBGApi { val gInfo = chatModel.controller.apiUpdateGroup(rhId, groupInfo.groupId, p) if (gInfo != null) { - chatModel.updateGroup(rhId, gInfo) + withChats { + updateGroup(rhId, gInfo) + } close.invoke() } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/WelcomeMessageView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/WelcomeMessageView.kt index 3bbeceb03c..794d6d4a20 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/WelcomeMessageView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/WelcomeMessageView.kt @@ -28,6 +28,7 @@ import chat.simplex.common.ui.theme.DEFAULT_PADDING import chat.simplex.common.views.chat.item.MarkdownText import chat.simplex.common.views.helpers.* import chat.simplex.common.model.ChatModel +import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.model.GroupInfo import chat.simplex.common.platform.ColumnWithScrollBar import chat.simplex.common.platform.chatJsonLength @@ -52,7 +53,9 @@ fun GroupWelcomeView(m: ChatModel, rhId: Long?, groupInfo: GroupInfo, close: () val res = m.controller.apiUpdateGroup(rhId, gInfo.groupId, groupProfileUpdated) if (res != null) { gInfo = res - m.updateGroup(rhId, res) + withChats { + updateGroup(rhId, res) + } welcomeText.value = welcome ?: "" } afterSave() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt index e234a73136..fc6befad20 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt @@ -209,7 +209,7 @@ fun CIImageView( val loaded = res.value if (loaded != null && file != null) { val (imageBitmap, data, _) = loaded - SimpleAndAnimatedImageView(data, imageBitmap, file, imageProvider, @Composable { painter, onClick -> ImageView(painter, image, file.fileSource, onClick) }) + SimpleAndAnimatedImageView(data, imageBitmap, file, imageProvider, smallView, @Composable { painter, onClick -> ImageView(painter, image, file.fileSource, onClick) }) } else { imageView(base64ToBitmap(image), onClick = { if (file != null) { @@ -285,5 +285,6 @@ expect fun SimpleAndAnimatedImageView( imageBitmap: ImageBitmap, file: CIFile?, imageProvider: () -> ImageGalleryProvider, + smallView: Boolean, ImageView: @Composable (painter: Painter, onClick: () -> Unit) -> Unit ) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.kt index 09838796c5..ab3918549d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.kt @@ -12,8 +12,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.* import androidx.compose.ui.input.pointer.* import androidx.compose.ui.layout.onGloballyPositioned +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.CryptoFile import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.CurrentColors import chat.simplex.common.views.chat.ProviderMedia import chat.simplex.common.views.helpers.* import kotlinx.coroutines.flow.distinctUntilChanged @@ -55,6 +57,9 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () -> val scope = rememberCoroutineScope() val playersToRelease = rememberSaveable { mutableSetOf() } DisposableEffectOnGone( + always = { + platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, Color.Black, false, false) + }, whenGone = { playersToRelease.forEach { VideoPlayerHolder.release(it, true, true) } } ) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt index dc32bb1318..1f8155d3dd 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt @@ -1,6 +1,7 @@ package chat.simplex.common.views.chatlist import SectionItemView +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.* @@ -19,6 +20,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.model.* +import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chat.* @@ -28,11 +30,11 @@ import chat.simplex.common.views.chat.item.ItemAction import chat.simplex.common.views.helpers.* import chat.simplex.common.views.newchat.* import chat.simplex.res.MR -import kotlinx.coroutines.delay +import kotlinx.coroutines.* import kotlinx.datetime.Clock @Composable -fun ChatListNavLinkView(chat: Chat, nextChatSelected: State) { +fun ChatListNavLinkView(chat: Chat, nextChatSelected: State, oneHandUI: State) { val showMenu = remember { mutableStateOf(false) } val showMarkRead = remember(chat.chatStats.unreadCount, chat.chatStats.unreadChat) { chat.chatStats.unreadCount > 0 || chat.chatStats.unreadChat @@ -47,6 +49,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State) { val showChatPreviews = chatModel.showChatPreviews.value val inProgress = remember { mutableStateOf(false) } var progressByTimeout by rememberSaveable { mutableStateOf(false) } + LaunchedEffect(inProgress.value) { progressByTimeout = if (inProgress.value) { delay(1000) @@ -56,6 +59,8 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State) { } } + val scope = rememberCoroutineScope() + when (chat.chatInfo) { is ChatInfo.Direct -> { val contactNetworkStatus = chatModel.contactNetworkStatus(chat.chatInfo.contact) @@ -65,7 +70,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State) { ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, contactNetworkStatus, disabled, linkMode, inProgress = false, progressByTimeout = false) } }, - click = { directChatAction(chat.remoteHostId, chat.chatInfo.contact, chatModel) }, + click = { scope.launch { directChatAction(chat.remoteHostId, chat.chatInfo.contact, chatModel) } }, dropdownMenuItems = { tryOrShowError("${chat.id}ChatListNavLinkDropdown", error = {}) { ContactMenuItems(chat, chat.chatInfo.contact, chatModel, showMenu, showMarkRead) @@ -75,6 +80,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State) { disabled, selectedChat, nextChatSelected, + oneHandUI ) } is ChatInfo.Group -> @@ -84,7 +90,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State) { ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, null, disabled, linkMode, inProgress.value, progressByTimeout) } }, - click = { if (!inProgress.value) groupChatAction(chat.remoteHostId, chat.chatInfo.groupInfo, chatModel, inProgress) }, + click = { if (!inProgress.value) scope.launch { groupChatAction(chat.remoteHostId, chat.chatInfo.groupInfo, chatModel, inProgress) } }, dropdownMenuItems = { tryOrShowError("${chat.id}ChatListNavLinkDropdown", error = {}) { GroupMenuItems(chat, chat.chatInfo.groupInfo, chatModel, showMenu, inProgress, showMarkRead) @@ -94,6 +100,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State) { disabled, selectedChat, nextChatSelected, + oneHandUI ) is ChatInfo.Local -> { ChatListNavLinkLayout( @@ -102,7 +109,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State) { ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, null, disabled, linkMode, inProgress = false, progressByTimeout = false) } }, - click = { noteFolderChatAction(chat.remoteHostId, chat.chatInfo.noteFolder) }, + click = { scope.launch { noteFolderChatAction(chat.remoteHostId, chat.chatInfo.noteFolder) } }, dropdownMenuItems = { tryOrShowError("${chat.id}ChatListNavLinkDropdown", error = {}) { NoteFolderMenuItems(chat, showMenu, showMarkRead) @@ -112,6 +119,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State) { disabled, selectedChat, nextChatSelected, + oneHandUI ) } is ChatInfo.ContactRequest -> @@ -131,6 +139,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State) { disabled, selectedChat, nextChatSelected, + oneHandUI ) is ChatInfo.ContactConnection -> ChatListNavLinkLayout( @@ -151,6 +160,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State) { disabled, selectedChat, nextChatSelected, + oneHandUI ) is ChatInfo.InvalidJSON -> ChatListNavLinkLayout( @@ -167,53 +177,54 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State) { disabled, selectedChat, nextChatSelected, + oneHandUI ) } } @Composable -private fun ErrorChatListItem() { +fun ErrorChatListItem() { Box(Modifier.fillMaxWidth().padding(horizontal = 10.dp, vertical = 6.dp)) { Text(stringResource(MR.strings.error_showing_content), color = MaterialTheme.colors.error, fontStyle = FontStyle.Italic) } } -fun directChatAction(rhId: Long?, contact: Contact, chatModel: ChatModel) { +suspend fun directChatAction(rhId: Long?, contact: Contact, chatModel: ChatModel) { when { contact.activeConn == null && contact.profile.contactLink != null -> askCurrentOrIncognitoProfileConnectContactViaAddress(chatModel, rhId, contact, close = null, openChat = true) - else -> withBGApi { openChat(rhId, ChatInfo.Direct(contact), chatModel) } + else -> openChat(rhId, ChatInfo.Direct(contact), chatModel) } } -fun groupChatAction(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, inProgress: MutableState? = null) { +suspend fun groupChatAction(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, inProgress: MutableState? = null) { when (groupInfo.membership.memberStatus) { GroupMemberStatus.MemInvited -> acceptGroupInvitationAlertDialog(rhId, groupInfo, chatModel, inProgress) GroupMemberStatus.MemAccepted -> groupInvitationAcceptedAlert(rhId) - else -> withBGApi { openChat(rhId, ChatInfo.Group(groupInfo), chatModel) } + else -> openChat(rhId, ChatInfo.Group(groupInfo), chatModel) } } -fun noteFolderChatAction(rhId: Long?, noteFolder: NoteFolder) { - withBGApi { openChat(rhId, ChatInfo.Local(noteFolder), chatModel) } +suspend fun noteFolderChatAction(rhId: Long?, noteFolder: NoteFolder) { + openChat(rhId, ChatInfo.Local(noteFolder), chatModel) } -suspend fun openDirectChat(rhId: Long?, contactId: Long, chatModel: ChatModel) { +suspend fun openDirectChat(rhId: Long?, contactId: Long, chatModel: ChatModel) = coroutineScope { val chat = chatModel.controller.apiGetChat(rhId, ChatType.Direct, contactId) - if (chat != null) { + if (chat != null && isActive) { openLoadedChat(chat, chatModel) } } -suspend fun openGroupChat(rhId: Long?, groupId: Long, chatModel: ChatModel) { +suspend fun openGroupChat(rhId: Long?, groupId: Long, chatModel: ChatModel) = coroutineScope { val chat = chatModel.controller.apiGetChat(rhId, ChatType.Group, groupId) - if (chat != null) { + if (chat != null && isActive) { openLoadedChat(chat, chatModel) } } -suspend fun openChat(rhId: Long?, chatInfo: ChatInfo, chatModel: ChatModel) { +suspend fun openChat(rhId: Long?, chatInfo: ChatInfo, chatModel: ChatModel) = coroutineScope { val chat = chatModel.controller.apiGetChat(rhId, chatInfo.chatType, chatInfo.apiId) - if (chat != null) { + if (chat != null && isActive) { openLoadedChat(chat, chatModel) } } @@ -359,7 +370,7 @@ fun ToggleFavoritesChatAction(chat: Chat, chatModel: ChatModel, favorite: Boolea if (favorite) stringResource(MR.strings.unfavorite_chat) else stringResource(MR.strings.favorite_chat), if (favorite) painterResource(MR.images.ic_star_off) else painterResource(MR.images.ic_star), onClick = { - toggleChatFavorite(chat, !favorite, chatModel) + toggleChatFavorite(chat.remoteHostId, chat.chatInfo, !favorite, chatModel) showMenu.value = false } ) @@ -371,7 +382,7 @@ fun ToggleNotificationsChatAction(chat: Chat, chatModel: ChatModel, ntfsEnabled: if (ntfsEnabled) stringResource(MR.strings.mute_chat) else stringResource(MR.strings.unmute_chat), if (ntfsEnabled) painterResource(MR.images.ic_notifications_off) else painterResource(MR.images.ic_notifications), onClick = { - toggleNotifications(chat, !ntfsEnabled, chatModel) + toggleNotifications(chat.remoteHostId, chat.chatInfo, !ntfsEnabled, chatModel) showMenu.value = false } ) @@ -469,13 +480,13 @@ fun LeaveGroupAction(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, sh } @Composable -fun ContactRequestMenuItems(rhId: Long?, chatInfo: ChatInfo.ContactRequest, chatModel: ChatModel, showMenu: MutableState) { +fun ContactRequestMenuItems(rhId: Long?, chatInfo: ChatInfo.ContactRequest, chatModel: ChatModel, showMenu: MutableState, onSuccess: ((chat: Chat) -> Unit)? = null) { ItemAction( stringResource(MR.strings.accept_contact_button), painterResource(MR.images.ic_check), color = MaterialTheme.colors.onBackground, onClick = { - acceptContactRequest(rhId, incognito = false, chatInfo.apiId, chatInfo, true, chatModel) + acceptContactRequest(rhId, incognito = false, chatInfo.apiId, chatInfo, true, chatModel, onSuccess) showMenu.value = false } ) @@ -484,7 +495,7 @@ fun ContactRequestMenuItems(rhId: Long?, chatInfo: ChatInfo.ContactRequest, chat painterResource(MR.images.ic_theater_comedy), color = MaterialTheme.colors.onBackground, onClick = { - acceptContactRequest(rhId, incognito = true, chatInfo.apiId, chatInfo, true, chatModel) + acceptContactRequest(rhId, incognito = true, chatInfo.apiId, chatInfo, true, chatModel, onSuccess) showMenu.value = false } ) @@ -554,7 +565,9 @@ fun markChatRead(c: Chat, chatModel: ChatModel) { withApi { if (chat.chatStats.unreadCount > 0) { val minUnreadItemId = chat.chatStats.minUnreadItemId - chatModel.markChatItemsRead(chat) + withChats { + markChatItemsRead(chat.remoteHostId, chat.chatInfo) + } chatModel.controller.apiChatRead( chat.remoteHostId, chat.chatInfo.chatType, @@ -571,7 +584,9 @@ fun markChatRead(c: Chat, chatModel: ChatModel) { false ) if (success) { - chatModel.replaceChat(chat.remoteHostId, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = false))) + withChats { + replaceChat(chat.remoteHostId, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = false))) + } } } } @@ -589,12 +604,14 @@ fun markChatUnread(chat: Chat, chatModel: ChatModel) { true ) if (success) { - chatModel.replaceChat(chat.remoteHostId, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = true))) + withChats { + replaceChat(chat.remoteHostId, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = true))) + } } } } -fun contactRequestAlertDialog(rhId: Long?, contactRequest: ChatInfo.ContactRequest, chatModel: ChatModel) { +fun contactRequestAlertDialog(rhId: Long?, contactRequest: ChatInfo.ContactRequest, chatModel: ChatModel, onSucess: ((chat: Chat) -> Unit)? = null) { AlertManager.shared.showAlertDialogButtonsColumn( title = generalGetString(MR.strings.accept_connection_request__question), text = AnnotatedString(generalGetString(MR.strings.if_you_choose_to_reject_the_sender_will_not_be_notified)), @@ -602,13 +619,13 @@ fun contactRequestAlertDialog(rhId: Long?, contactRequest: ChatInfo.ContactReque Column { SectionItemView({ AlertManager.shared.hideAlert() - acceptContactRequest(rhId, incognito = false, contactRequest.apiId, contactRequest, true, chatModel) + acceptContactRequest(rhId, incognito = false, contactRequest.apiId, contactRequest, true, chatModel, onSucess) }) { Text(generalGetString(MR.strings.accept_contact_button), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) } SectionItemView({ AlertManager.shared.hideAlert() - acceptContactRequest(rhId, incognito = true, contactRequest.apiId, contactRequest, true, chatModel) + acceptContactRequest(rhId, incognito = true, contactRequest.apiId, contactRequest, true, chatModel, onSucess) }) { Text(generalGetString(MR.strings.accept_contact_incognito_button), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) } @@ -624,13 +641,16 @@ fun contactRequestAlertDialog(rhId: Long?, contactRequest: ChatInfo.ContactReque ) } -fun acceptContactRequest(rhId: Long?, incognito: Boolean, apiId: Long, contactRequest: ChatInfo.ContactRequest?, isCurrentUser: Boolean, chatModel: ChatModel) { +fun acceptContactRequest(rhId: Long?, incognito: Boolean, apiId: Long, contactRequest: ChatInfo.ContactRequest?, isCurrentUser: Boolean, chatModel: ChatModel, close: ((chat: Chat) -> Unit)? = null ) { withBGApi { val contact = chatModel.controller.apiAcceptContactRequest(rhId, incognito, apiId) if (contact != null && isCurrentUser && contactRequest != null) { val chat = Chat(remoteHostId = rhId, ChatInfo.Direct(contact), listOf()) - chatModel.replaceChat(rhId, contactRequest.id, chat) + withChats { + replaceChat(rhId, contactRequest.id, chat) + } chatModel.setContactNetworkStatus(contact, NetworkStatus.Connected()) + close?.invoke(chat) } } } @@ -638,7 +658,9 @@ fun acceptContactRequest(rhId: Long?, incognito: Boolean, apiId: Long, contactRe fun rejectContactRequest(rhId: Long?, contactRequest: ChatInfo.ContactRequest, chatModel: ChatModel) { withBGApi { chatModel.controller.apiRejectContactRequest(rhId, contactRequest.apiId) - chatModel.removeChat(rhId, contactRequest.id) + withChats { + removeChat(rhId, contactRequest.id) + } } } @@ -654,7 +676,9 @@ fun deleteContactConnectionAlert(rhId: Long?, connection: PendingContactConnecti withBGApi { AlertManager.shared.hideAlert() if (chatModel.controller.apiDeleteChat(rhId, ChatType.ContactConnection, connection.apiId)) { - chatModel.removeChat(rhId, connection.id) + withChats { + removeChat(rhId, connection.id) + } onSuccess() } } @@ -673,7 +697,9 @@ fun pendingContactAlertDialog(rhId: Long?, chatInfo: ChatInfo, chatModel: ChatMo withBGApi { val r = chatModel.controller.apiDeleteChat(rhId, chatInfo.chatType, chatInfo.apiId) if (r) { - chatModel.removeChat(rhId, chatInfo.id) + withChats { + removeChat(rhId, chatInfo.id) + } if (chatModel.chatId.value == chatInfo.id) { chatModel.chatId.value = null ModalManager.end.closeModals() @@ -735,7 +761,9 @@ fun askCurrentOrIncognitoProfileConnectContactViaAddress( suspend fun connectContactViaAddress(chatModel: ChatModel, rhId: Long?, contactId: Long, incognito: Boolean): Boolean { val contact = chatModel.controller.apiConnectContactViaAddress(rhId, incognito, contactId) if (contact != null) { - chatModel.updateContact(rhId, contact) + withChats { + updateContact(rhId, contact) + } AlertManager.privacySensitive.showAlertMsg( title = generalGetString(MR.strings.connection_request_sent), text = generalGetString(MR.strings.you_will_be_connected_when_your_connection_request_is_accepted), @@ -776,7 +804,9 @@ fun deleteGroup(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel) { withBGApi { val r = chatModel.controller.apiDeleteChat(rhId, ChatType.Group, groupInfo.apiId) if (r) { - chatModel.removeChat(rhId, groupInfo.id) + withChats { + removeChat(rhId, groupInfo.id) + } if (chatModel.chatId.value == groupInfo.id) { chatModel.chatId.value = null ModalManager.end.closeModals() @@ -794,22 +824,22 @@ fun groupInvitationAcceptedAlert(rhId: Long?) { ) } -fun toggleNotifications(chat: Chat, enableAllNtfs: Boolean, chatModel: ChatModel, currentState: MutableState? = null) { - val chatSettings = (chat.chatInfo.chatSettings ?: ChatSettings.defaults).copy(enableNtfs = if (enableAllNtfs) MsgFilter.All else MsgFilter.None) - updateChatSettings(chat, chatSettings, chatModel, currentState) +fun toggleNotifications(remoteHostId: Long?, chatInfo: ChatInfo, enableAllNtfs: Boolean, chatModel: ChatModel, currentState: MutableState? = null) { + val chatSettings = (chatInfo.chatSettings ?: ChatSettings.defaults).copy(enableNtfs = if (enableAllNtfs) MsgFilter.All else MsgFilter.None) + updateChatSettings(remoteHostId, chatInfo, chatSettings, chatModel, currentState) } -fun toggleChatFavorite(chat: Chat, favorite: Boolean, chatModel: ChatModel) { - val chatSettings = (chat.chatInfo.chatSettings ?: ChatSettings.defaults).copy(favorite = favorite) - updateChatSettings(chat, chatSettings, chatModel) +fun toggleChatFavorite(remoteHostId: Long?, chatInfo: ChatInfo, favorite: Boolean, chatModel: ChatModel) { + val chatSettings = (chatInfo.chatSettings ?: ChatSettings.defaults).copy(favorite = favorite) + updateChatSettings(remoteHostId, chatInfo, chatSettings, chatModel) } -fun updateChatSettings(chat: Chat, chatSettings: ChatSettings, chatModel: ChatModel, currentState: MutableState? = null) { - val newChatInfo = when(chat.chatInfo) { - is ChatInfo.Direct -> with (chat.chatInfo) { +fun updateChatSettings(remoteHostId: Long?, chatInfo: ChatInfo, chatSettings: ChatSettings, chatModel: ChatModel, currentState: MutableState? = null) { + val newChatInfo = when(chatInfo) { + is ChatInfo.Direct -> with (chatInfo) { ChatInfo.Direct(contact.copy(chatSettings = chatSettings)) } - is ChatInfo.Group -> with(chat.chatInfo) { + is ChatInfo.Group -> with(chatInfo) { ChatInfo.Group(groupInfo.copy(chatSettings = chatSettings)) } else -> null @@ -817,17 +847,19 @@ fun updateChatSettings(chat: Chat, chatSettings: ChatSettings, chatModel: ChatMo withBGApi { val res = when (newChatInfo) { is ChatInfo.Direct -> with(newChatInfo) { - chatModel.controller.apiSetSettings(chat.remoteHostId, chatType, apiId, contact.chatSettings) + chatModel.controller.apiSetSettings(remoteHostId, chatType, apiId, contact.chatSettings) } is ChatInfo.Group -> with(newChatInfo) { - chatModel.controller.apiSetSettings(chat.remoteHostId, chatType, apiId, groupInfo.chatSettings) + chatModel.controller.apiSetSettings(remoteHostId, chatType, apiId, groupInfo.chatSettings) } else -> false } if (res && newChatInfo != null) { - chatModel.updateChatInfo(chat.remoteHostId, newChatInfo) + withChats { + updateChatInfo(remoteHostId, newChatInfo) + } if (chatSettings.enableNtfs != MsgFilter.All) { - ntfManager.cancelNotificationsForChat(chat.id) + ntfManager.cancelNotificationsForChat(chatInfo.id) } val current = currentState?.value if (current != null) { @@ -846,6 +878,7 @@ expect fun ChatListNavLinkLayout( disabled: Boolean, selectedChat: State, nextChatSelected: State, + oneHandUI: State ) @Preview/*( @@ -888,7 +921,8 @@ fun PreviewChatListNavLinkDirect() { showMenu = remember { mutableStateOf(false) }, disabled = false, selectedChat = remember { mutableStateOf(false) }, - nextChatSelected = remember { mutableStateOf(false) } + nextChatSelected = remember { mutableStateOf(false) }, + oneHandUI = remember { mutableStateOf(false) } ) } } @@ -933,7 +967,8 @@ fun PreviewChatListNavLinkGroup() { showMenu = remember { mutableStateOf(false) }, disabled = false, selectedChat = remember { mutableStateOf(false) }, - nextChatSelected = remember { mutableStateOf(false) } + nextChatSelected = remember { mutableStateOf(false) }, + oneHandUI = remember { mutableStateOf(false) } ) } } @@ -955,7 +990,8 @@ fun PreviewChatListNavLinkContactRequest() { showMenu = remember { mutableStateOf(false) }, disabled = false, selectedChat = remember { mutableStateOf(false) }, - nextChatSelected = remember { mutableStateOf(false) } + nextChatSelected = remember { mutableStateOf(false) }, + oneHandUI = remember { mutableStateOf(false) } ) } } 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 43f0b7ef9b..32f8a2f481 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 @@ -12,7 +12,7 @@ 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.shadow +import androidx.compose.ui.draw.scale import androidx.compose.ui.focus.* import androidx.compose.ui.graphics.* import androidx.compose.ui.text.font.FontStyle @@ -33,7 +33,6 @@ import chat.simplex.common.views.onboarding.shouldShowWhatsNew import chat.simplex.common.views.usersettings.SettingsView import chat.simplex.common.platform.* import chat.simplex.common.views.call.Call -import chat.simplex.common.views.chat.group.ProgressIndicator import chat.simplex.common.views.chat.item.CIFileViewScope import chat.simplex.common.views.newchat.* import chat.simplex.res.MR @@ -42,28 +41,37 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.serialization.json.Json import java.net.URI -import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds +private fun showNewChatSheet(oneHandUI: State, barTitle: String) { + ModalManager.start.closeModals() + ModalManager.end.closeModals() + chatModel.newChatSheetVisible.value = true + ModalManager.start.showModalCloseable( + closeOnTop = !oneHandUI.value, + closeBarTitle = if (oneHandUI.value) barTitle else null, + endButtons = { Spacer(Modifier.minimumInteractiveComponentSize()) } + ) { close -> + NewChatSheet(rh = chatModel.currentRemoteHost.value, close) + DisposableEffect(Unit) { + onDispose { + chatModel.newChatSheetVisible.value = false + } + } + } +} + @Composable fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerformLA: (Boolean) -> Unit, stopped: Boolean) { - val newChatSheetState by rememberSaveable(stateSaver = AnimatedViewState.saver()) { mutableStateOf(MutableStateFlow(AnimatedViewState.GONE)) } - val showNewChatSheet = { - newChatSheetState.value = AnimatedViewState.VISIBLE - } - val hideNewChatSheet: (animated: Boolean) -> Unit = { animated -> - if (animated) newChatSheetState.value = AnimatedViewState.HIDING - else newChatSheetState.value = AnimatedViewState.GONE - } + val oneHandUI = remember { chatModel.controller.appPrefs.oneHandUI } + LaunchedEffect(Unit) { if (shouldShowWhatsNew(chatModel)) { delay(1000L) ModalManager.center.showCustomModal { close -> WhatsNewView(close = close) } } } - LaunchedEffect(chatModel.clearOverlays.value) { - if (chatModel.clearOverlays.value && newChatSheetState.value.isVisible()) hideNewChatSheet(false) - } + if (appPlatform.isDesktop) { KeyChangeEffect(chatModel.chatId.value) { if (chatModel.chatId.value != null) { @@ -77,7 +85,31 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) } val scope = rememberCoroutineScope() val (userPickerState, scaffoldState ) = settingsState - Scaffold(topBar = { Box(Modifier.padding(end = endPadding)) { ChatListToolbar(scaffoldState.drawerState, userPickerState, stopped)} }, + Scaffold( + topBar = { + if (!oneHandUI.state.value) { + Box(Modifier.padding(end = endPadding)) { + ChatListToolbar( + scaffoldState.drawerState, + userPickerState, + stopped, + oneHandUI + ) + } + } + }, + bottomBar = { + if (oneHandUI.state.value) { + Box(Modifier.padding(end = endPadding)) { + ChatListToolbar( + scaffoldState.drawerState, + userPickerState, + stopped, + oneHandUI + ) + } + } + }, scaffoldState = scaffoldState, drawerContent = { tryOrShowError("Settings", error = { ErrorSettingsView() }) { @@ -89,11 +121,11 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf drawerScrimColor = MaterialTheme.colors.onSurface.copy(alpha = if (isInDarkTheme()) 0.16f else 0.32f), drawerGesturesEnabled = appPlatform.isAndroid, floatingActionButton = { - if (searchText.value.text.isEmpty() && !chatModel.desktopNoUserNoRemote && chatModel.chatRunning.value == true) { + if (!oneHandUI.state.value && searchText.value.text.isEmpty() && !chatModel.desktopNoUserNoRemote && chatModel.chatRunning.value == true) { FloatingActionButton( onClick = { if (!stopped) { - if (newChatSheetState.value.isVisible()) hideNewChatSheet(true) else showNewChatSheet() + showNewChatSheet(oneHandUI.state, generalGetString(MR.strings.new_chat)) } }, Modifier @@ -108,25 +140,33 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf backgroundColor = if (!stopped) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, contentColor = Color.White ) { - Icon(if (!newChatSheetState.collectAsState().value.isVisible()) painterResource(MR.images.ic_edit_filled) else painterResource(MR.images.ic_close), stringResource(MR.strings.add_contact_or_create_group), Modifier.size(24.dp * fontSizeSqrtMultiplier)) + Icon(painterResource(MR.images.ic_edit_filled), stringResource(MR.strings.add_contact_or_create_group), Modifier.size(24.dp * fontSizeSqrtMultiplier)) } } } ) { - Box(Modifier.padding(it).padding(end = endPadding)) { + var modifier = Modifier.padding(it).padding(end = endPadding) + if (oneHandUI.state.value) { + modifier = modifier.scale(scaleX = 1f, scaleY = -1f) + } + + Box(modifier) { Box( modifier = Modifier .fillMaxSize() ) { if (!chatModel.desktopNoUserNoRemote) { - ChatList(chatModel, searchText = searchText) + ChatList(chatModel, searchText = searchText, oneHandUI = oneHandUI) } - if (chatModel.chats.isEmpty() && !chatModel.switchingUsersAndHosts.value && !chatModel.desktopNoUserNoRemote) { - Text(stringResource( - if (chatModel.chatRunning.value == null) MR.strings.loading_chats else MR.strings.you_have_no_chats), Modifier.align(Alignment.Center), color = MaterialTheme.colors.secondary) - if (!stopped && !newChatSheetState.collectAsState().value.isVisible() && chatModel.chatRunning.value == true && searchText.value.text.isEmpty()) { - OnboardingButtons(showNewChatSheet) + if (chatModel.chats.value.isEmpty() && !chatModel.switchingUsersAndHosts.value && !chatModel.desktopNoUserNoRemote) { + var textModifier = Modifier.align(Alignment.Center) + + if (oneHandUI.state.value) { + textModifier = textModifier.scale(scaleX = 1f, scaleY = -1f) } + + Text(stringResource( + if (chatModel.chatRunning.value == null) MR.strings.loading_chats else MR.strings.you_have_no_chats), textModifier, color = MaterialTheme.colors.secondary) } } } @@ -135,17 +175,17 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf if (appPlatform.isDesktop) { val call = remember { chatModel.activeCall }.value if (call != null) { - ActiveCallInteractiveArea(call, newChatSheetState) + ActiveCallInteractiveArea(call) } } - // TODO disable this button and sheet for the duration of the switch - tryOrShowError("NewChatSheet", error = {}) { - NewChatSheet(chatModel, newChatSheetState, stopped, hideNewChatSheet) - } } if (appPlatform.isAndroid) { tryOrShowError("UserPicker", error = {}) { - UserPicker(chatModel, userPickerState) { + UserPicker( + chatModel = chatModel, + userPickerState = userPickerState, + contentAlignment = if (oneHandUI.state.value) Alignment.BottomStart else Alignment.TopStart + ) { scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() } userPickerState.value = AnimatedViewState.GONE } @@ -153,27 +193,6 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf } } -@Composable -private fun OnboardingButtons(openNewChatSheet: () -> Unit) { - Column(Modifier.fillMaxSize().padding(DEFAULT_PADDING), horizontalAlignment = Alignment.End, verticalArrangement = Arrangement.Bottom) { - ConnectButton(generalGetString(MR.strings.tap_to_start_new_chat), openNewChatSheet) - val color = MaterialTheme.colors.primaryVariant - Canvas(modifier = Modifier.width(40.dp).height(10.dp), onDraw = { - val trianglePath = Path().apply { - moveTo(0.dp.toPx(), 0f) - lineTo(16.dp.toPx(), 0.dp.toPx()) - lineTo(8.dp.toPx(), 10.dp.toPx()) - lineTo(0.dp.toPx(), 0.dp.toPx()) - } - drawPath( - color = color, - path = trianglePath - ) - }) - Spacer(Modifier.height(62.dp)) - } -} - @Composable private fun ConnectButton(text: String, onClick: () -> Unit) { Button( @@ -191,10 +210,37 @@ private fun ConnectButton(text: String, onClick: () -> Unit) { } @Composable -private fun ChatListToolbar(drawerState: DrawerState, userPickerState: MutableStateFlow, stopped: Boolean) { +private fun ChatListToolbar(drawerState: DrawerState, userPickerState: MutableStateFlow, stopped: Boolean, oneHandUI: SharedPreference) { val serversSummary: MutableState = remember { mutableStateOf(null) } val barButtons = arrayListOf<@Composable RowScope.() -> Unit>() val updatingProgress = remember { chatModel.updatingProgress }.value + + if (oneHandUI.state.value) { + val sp16 = with(LocalDensity.current) { 16.sp.toDp() } + + barButtons.add { + IconButton( + onClick = { + if (!stopped) { + showNewChatSheet(oneHandUI.state, generalGetString(MR.strings.new_chat)) + } + }, + ) { + Box( + modifier = Modifier + .background(if (!stopped) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, shape = CircleShape) + .padding(DEFAULT_PADDING_HALF) + ){ + Icon( + painterResource(MR.images.ic_edit_filled), + stringResource(MR.strings.add_contact_or_create_group), + Modifier.size(sp16), + tint = if (!stopped) MaterialTheme.colors.onPrimary else MaterialTheme.colors.onSecondary) + } + } + } + } + if (updatingProgress != null) { barButtons.add { val interactionSource = remember { MutableInteractionSource() } @@ -291,10 +337,12 @@ fun SubscriptionStatusIndicator(click: (() -> Unit)) { val scope = rememberCoroutineScope() suspend fun setSubsTotal() { - val r = chatModel.controller.getAgentSubsTotal(chatModel.remoteHostId()) - if (r != null) { - subs = r.first - hasSess = r.second + if (chatModel.currentUser.value != null) { + val r = chatModel.controller.getAgentSubsTotal(chatModel.remoteHostId()) + if (r != null) { + subs = r.first + hasSess = r.second + } } } @@ -342,6 +390,7 @@ fun UserProfileButton(image: String?, allRead: Boolean, onButtonClicked: () -> U } } + @Composable private fun BoxScope.unreadBadge(text: String? = "") { Text( @@ -377,7 +426,7 @@ private fun ToggleFilterEnabledButton() { } @Composable -expect fun ActiveCallInteractiveArea(call: Call, newChatSheetState: MutableStateFlow) +expect fun ActiveCallInteractiveArea(call: Call) fun connectIfOpenedViaUri(rhId: Long?, uri: URI, chatModel: ChatModel) { Log.d(TAG, "connectIfOpenedViaUri: opened via link") @@ -391,11 +440,22 @@ fun connectIfOpenedViaUri(rhId: Long?, uri: URI, chatModel: ChatModel) { } @Composable -private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState, searchShowingSimplexLink: MutableState, searchChatFilteredBySimplexLink: MutableState) { - Row(verticalAlignment = Alignment.CenterVertically) { +private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState, searchShowingSimplexLink: MutableState, searchChatFilteredBySimplexLink: MutableState, oneHandUI: SharedPreference) { + var modifier = Modifier.fillMaxWidth(); + + if (oneHandUI.state.value) { + modifier = modifier.scale(scaleX = 1f, scaleY = -1f) + } + + Row(verticalAlignment = Alignment.CenterVertically, modifier = modifier) { val focusRequester = remember { FocusRequester() } var focused by remember { mutableStateOf(false) } - Icon(painterResource(MR.images.ic_search), null, Modifier.padding(horizontal = DEFAULT_PADDING_HALF).size(24.dp * fontSizeSqrtMultiplier), tint = MaterialTheme.colors.secondary) + Icon( + painterResource(MR.images.ic_search), + contentDescription = null, + Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING_HALF).size(24.dp * fontSizeSqrtMultiplier), + tint = MaterialTheme.colors.secondary + ) SearchTextField( Modifier.weight(1f).onFocusChanged { focused = it.hasFocus }.focusRequester(focusRequester), placeholder = stringResource(MR.strings.search_or_paste_simplex_link), @@ -415,7 +475,7 @@ private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState } } else { val padding = if (appPlatform.isDesktop) 0.dp else 7.dp - if (chatModel.chats.size > 0) { + if (chatModel.chats.value.isNotEmpty()) { ToggleFilterEnabledButton() } Spacer(Modifier.width(padding)) @@ -481,9 +541,35 @@ private fun ErrorSettingsView() { private var lazyListState = 0 to 0 +enum class ScrollDirection { + Up, Down, Idle +} + @Composable -private fun ChatList(chatModel: ChatModel, searchText: MutableState) { +private fun ChatList(chatModel: ChatModel, searchText: MutableState, oneHandUI: SharedPreference) { val listState = rememberLazyListState(lazyListState.first, lazyListState.second) + var scrollDirection by remember { mutableStateOf(ScrollDirection.Idle) } + var previousIndex by remember { mutableStateOf(0) } + var previousScrollOffset by remember { mutableStateOf(0) } + + LaunchedEffect(listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset) { + val currentIndex = listState.firstVisibleItemIndex + val currentScrollOffset = listState.firstVisibleItemScrollOffset + val threshold = 25 + + scrollDirection = when { + currentIndex > previousIndex -> ScrollDirection.Down + currentIndex < previousIndex -> ScrollDirection.Up + currentScrollOffset > previousScrollOffset + threshold -> ScrollDirection.Down + currentScrollOffset < previousScrollOffset - threshold -> ScrollDirection.Up + currentScrollOffset == previousScrollOffset -> ScrollDirection.Idle + else -> scrollDirection + } + + previousIndex = currentIndex + previousScrollOffset = currentScrollOffset + } + DisposableEffect(Unit) { onDispose { lazyListState = listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset } } @@ -494,7 +580,7 @@ private fun ChatList(chatModel: ChatModel, searchText: MutableState(null) } - val chats = filteredChats(showUnreadAndFavorites, searchShowingSimplexLink, searchChatFilteredBySimplexLink, searchText.value.text, allChats.toList()) + val chats = filteredChats(showUnreadAndFavorites, searchShowingSimplexLink, searchChatFilteredBySimplexLink, searchText.value.text, allChats.value.toList()) LazyColumnWithScrollBar( Modifier.fillMaxWidth(), listState @@ -504,7 +590,9 @@ private fun ChatList(chatModel: ChatModel, searchText: MutableState !chat.chatInfo.chatDeleted && chatContactType(chat) != ContactType.CARD } else { chats.filter { chat -> when (val cInfo = chat.chatInfo) { - is ChatInfo.Direct -> if (s.isEmpty()) { - chat.id == chatModel.chatId.value || filtered(chat) - } else { - (viewNameContains(cInfo, s) || - cInfo.contact.profile.displayName.lowercase().contains(s) || - cInfo.contact.fullName.lowercase().contains(s)) - } + is ChatInfo.Direct -> chatContactType(chat) != ContactType.CARD && !chat.chatInfo.chatDeleted && ( + if (s.isEmpty()) { + chat.id == chatModel.chatId.value || filtered(chat) + } else { + (viewNameContains(cInfo, s) || + cInfo.contact.profile.displayName.lowercase().contains(s) || + cInfo.contact.fullName.lowercase().contains(s)) + }) is ChatInfo.Group -> if (s.isEmpty()) { chat.id == chatModel.chatId.value || filtered(chat) || cInfo.groupInfo.membership.memberStatus == GroupMemberStatus.MemInvited } else { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt index 2cf9008fc3..cb22bd8f60 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt @@ -207,7 +207,7 @@ fun ChatPreviewView( } else { when (cInfo) { is ChatInfo.Direct -> - if (cInfo.contact.activeConn == null && cInfo.contact.profile.contactLink != null) { + if (cInfo.contact.activeConn == null && cInfo.contact.profile.contactLink != null && cInfo.contact.active) { Text(stringResource(MR.strings.contact_tap_to_connect), color = MaterialTheme.colors.primary) } else if (!cInfo.contact.sndReady && cInfo.contact.activeConn != null) { if (cInfo.contact.nextSendGrpInv) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ServersSummaryView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ServersSummaryView.kt index 8621c73ef3..4bb5474fa6 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ServersSummaryView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ServersSummaryView.kt @@ -719,7 +719,9 @@ fun ModalData.ServersSummaryView(rh: RemoteHostInfo?, serversSummary: MutableSta val scope = rememberCoroutineScope() suspend fun setServersSummary() { - serversSummary.value = chatModel.controller.getAgentServersSummary(chatModel.remoteHostId()) + if (chatModel.currentUser.value != null) { + serversSummary.value = chatModel.controller.getAgentServersSummary(chatModel.remoteHostId()) + } } LaunchedEffect(Unit) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListNavLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListNavLinkView.kt index 91dffeb7c8..8035cddc6d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListNavLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListNavLinkView.kt @@ -6,6 +6,7 @@ import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -13,6 +14,7 @@ import chat.simplex.common.model.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.res.MR +import kotlinx.coroutines.launch @Composable fun ShareListNavLinkView( @@ -20,19 +22,21 @@ fun ShareListNavLinkView( chatModel: ChatModel, isMediaOrFileAttachment: Boolean, isVoice: Boolean, - hasSimplexLink: Boolean + hasSimplexLink: Boolean, + oneHandUI: State ) { val stopped = chatModel.chatRunning.value == false + val scope = rememberCoroutineScope() when (chat.chatInfo) { is ChatInfo.Direct -> { val voiceProhibited = isVoice && !chat.chatInfo.featureEnabled(ChatFeature.Voice) ShareListNavLinkLayout( - chatLinkPreview = { SharePreviewView(chat, disabled = voiceProhibited) }, + chatLinkPreview = { SharePreviewView(chat, disabled = voiceProhibited, oneHandUI = oneHandUI) }, click = { if (voiceProhibited) { showForwardProhibitedByPrefAlert() } else { - directChatAction(chat.remoteHostId, chat.chatInfo.contact, chatModel) + scope.launch { directChatAction(chat.remoteHostId, chat.chatInfo.contact, chatModel) } } }, stopped @@ -44,12 +48,12 @@ fun ShareListNavLinkView( val voiceProhibited = isVoice && !chat.chatInfo.featureEnabled(ChatFeature.Voice) val prohibitedByPref = simplexLinkProhibited || fileProhibited || voiceProhibited ShareListNavLinkLayout( - chatLinkPreview = { SharePreviewView(chat, disabled = prohibitedByPref) }, + chatLinkPreview = { SharePreviewView(chat, disabled = prohibitedByPref, oneHandUI = oneHandUI) }, click = { if (prohibitedByPref) { showForwardProhibitedByPrefAlert() } else { - groupChatAction(chat.remoteHostId, chat.chatInfo.groupInfo, chatModel) + scope.launch { groupChatAction(chat.remoteHostId, chat.chatInfo.groupInfo, chatModel) } } }, stopped @@ -57,8 +61,8 @@ fun ShareListNavLinkView( } is ChatInfo.Local -> ShareListNavLinkLayout( - chatLinkPreview = { SharePreviewView(chat, disabled = false) }, - click = { noteFolderChatAction(chat.remoteHostId, chat.chatInfo.noteFolder) }, + chatLinkPreview = { SharePreviewView(chat, disabled = false, oneHandUI = oneHandUI) }, + click = { scope.launch { noteFolderChatAction(chat.remoteHostId, chat.chatInfo.noteFolder) } }, stopped ) is ChatInfo.ContactRequest, is ChatInfo.ContactConnection, is ChatInfo.InvalidJSON -> {} @@ -76,7 +80,7 @@ private fun showForwardProhibitedByPrefAlert() { private fun ShareListNavLinkLayout( chatLinkPreview: @Composable () -> Unit, click: () -> Unit, - stopped: Boolean + stopped: Boolean, ) { SectionItemView(minHeight = 50.dp, click = click, disabled = stopped) { chatLinkPreview() @@ -85,9 +89,15 @@ private fun ShareListNavLinkLayout( } @Composable -private fun SharePreviewView(chat: Chat, disabled: Boolean) { +private fun SharePreviewView(chat: Chat, disabled: Boolean, oneHandUI: State) { + var modifier = Modifier.fillMaxSize() + + if (oneHandUI.value) { + modifier = modifier.scale(scaleX = 1f, scaleY = -1f) + } + Row( - Modifier.fillMaxSize(), + modifier, horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { 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 69382d9fde..b86bc16dbc 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 @@ -7,6 +7,7 @@ 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.scale import androidx.compose.ui.graphics.Color import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource @@ -24,13 +25,16 @@ fun ShareListView(chatModel: ChatModel, settingsState: SettingsViewState, stoppe var searchInList by rememberSaveable { mutableStateOf("") } val (userPickerState, scaffoldState) = settingsState val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp + val oneHandUI = remember { chatModel.controller.appPrefs.oneHandUI } + Scaffold( Modifier.padding(end = endPadding), contentColor = LocalContentColor.current, drawerContentColor = LocalContentColor.current, scaffoldState = scaffoldState, - topBar = { Column { ShareListToolbar(chatModel, userPickerState, stopped) { searchInList = it.trim() } } }, - ) { + topBar = { if (!oneHandUI.state.value) Column { ShareListToolbar(chatModel, userPickerState, stopped) { searchInList = it.trim() } } }, + bottomBar = { if (oneHandUI.state.value) Column { ShareListToolbar(chatModel, userPickerState, stopped) { searchInList = it.trim() } } }, + ) { val sharedContent = chatModel.sharedContent.value var isMediaOrFileAttachment = false var isVoice = false @@ -56,21 +60,27 @@ fun ShareListView(chatModel: ChatModel, settingsState: SettingsViewState, stoppe } null -> {} } + var modifier = Modifier.fillMaxSize() + + if (oneHandUI.state.value) { + modifier = modifier.scale(scaleX = 1f, scaleY = -1f) + } + Box(Modifier.padding(it)) { Column( - modifier = Modifier - .fillMaxSize() + modifier = modifier ) { - if (chatModel.chats.isNotEmpty()) { + if (chatModel.chats.value.isNotEmpty()) { ShareList( chatModel, search = searchInList, isMediaOrFileAttachment = isMediaOrFileAttachment, isVoice = isVoice, - hasSimplexLink = hasSimplexLink + hasSimplexLink = hasSimplexLink, + oneHandUI = oneHandUI.state ) } else { - EmptyList() + EmptyList(oneHandUI = oneHandUI.state) } } } @@ -91,8 +101,14 @@ private fun hasSimplexLink(msg: String): Boolean { } @Composable -private fun EmptyList() { - Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { +private fun EmptyList(oneHandUI: State) { + var modifier = Modifier.fillMaxSize() + + if (oneHandUI.value) { + modifier = modifier.scale(scaleX = 1f, scaleY = -1f) + } + + Box(modifier, contentAlignment = Alignment.Center) { Text(stringResource(MR.strings.you_have_no_chats), color = MaterialTheme.colors.secondary) } } @@ -127,7 +143,7 @@ private fun ShareListToolbar(chatModel: ChatModel, userPickerState: MutableState }) } } - if (chatModel.chats.size >= 8) { + if (chatModel.chats.value.size >= 8) { barButtons.add { IconButton({ showSearch = true }) { Icon(painterResource(MR.images.ic_search_500), stringResource(MR.strings.search_verb), tint = MaterialTheme.colors.primary) @@ -182,11 +198,12 @@ private fun ShareList( search: String, isMediaOrFileAttachment: Boolean, isVoice: Boolean, - hasSimplexLink: Boolean + hasSimplexLink: Boolean, + oneHandUI: State ) { val chats by remember(search) { derivedStateOf { - val sorted = chatModel.chats.toList().sortedByDescending { it.chatInfo is ChatInfo.Local } + val sorted = chatModel.chats.value.toList().sortedByDescending { it.chatInfo is ChatInfo.Local } if (search.isEmpty()) { sorted.filter { it.chatInfo.ready } } else { @@ -203,7 +220,8 @@ private fun ShareList( chatModel, isMediaOrFileAttachment = isMediaOrFileAttachment, isVoice = isVoice, - hasSimplexLink = hasSimplexLink + hasSimplexLink = hasSimplexLink, + oneHandUI = oneHandUI ) } } 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 f79ff441d3..de77d13cbf 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 @@ -40,6 +40,7 @@ fun UserPicker( chatModel: ChatModel, userPickerState: MutableStateFlow, showSettings: Boolean = true, + contentAlignment: Alignment = Alignment.TopStart, showCancel: Boolean = false, cancelClicked: () -> Unit = {}, useFromDesktopClicked: () -> Unit = {}, @@ -149,7 +150,8 @@ fun UserPicker( .graphicsLayer { alpha = animatedFloat.value translationY = (animatedFloat.value - 1) * xOffset - } + }, + contentAlignment = contentAlignment ) { Column( Modifier diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactListNavView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactListNavView.kt new file mode 100644 index 0000000000..4622bff03b --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactListNavView.kt @@ -0,0 +1,143 @@ +package chat.simplex.common.views.contacts + +import androidx.compose.runtime.* +import androidx.compose.ui.graphics.Color +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource +import chat.simplex.common.model.* +import chat.simplex.common.model.ChatModel.withChats +import chat.simplex.common.platform.* +import chat.simplex.common.views.chat.* +import chat.simplex.common.views.chat.item.ItemAction +import chat.simplex.common.views.chatlist.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.newchat.ContactType +import chat.simplex.common.views.newchat.chatContactType +import chat.simplex.res.MR +import kotlinx.coroutines.delay + +private fun onRequestAccepted(chat: Chat) { + val chatInfo = chat.chatInfo + if (chatInfo is ChatInfo.Direct) { + ModalManager.start.closeModals() + if (chatInfo.contact.sndReady) { + openLoadedChat(chat, chatModel) + } + } +} + +@Composable +fun ContactListNavLinkView(chat: Chat, nextChatSelected: State, oneHandUI: State) { + val showMenu = remember { mutableStateOf(false) } + val rhId = chat.remoteHostId + val disabled = chatModel.chatRunning.value == false || chatModel.deletedChats.value.contains(rhId to chat.chatInfo.id) + val contactType = chatContactType(chat) + + LaunchedEffect(chat.id) { + showMenu.value = false + delay(500L) + } + + val selectedChat = remember(chat.id) { derivedStateOf { chat.id == chatModel.chatId.value } } + val view = LocalMultiplatformView() + + when (chat.chatInfo) { + is ChatInfo.Direct -> { + ChatListNavLinkLayout( + chatLinkPreview = { + tryOrShowError("${chat.id}ContactListNavLink", error = { ErrorChatListItem() }) { + ContactPreviewView(chat, disabled) + } + }, + click = { + hideKeyboard(view) + when (contactType) { + ContactType.RECENT -> { + withApi { + openChat(rhId, chat.chatInfo, chatModel) + ModalManager.start.closeModals() + } + } + ContactType.CHAT_DELETED -> { + withApi { + openChat(rhId, chat.chatInfo, chatModel) + withChats { + updateContact(rhId, chat.chatInfo.contact.copy(chatDeleted = false)) + } + ModalManager.start.closeModals() + } + } + ContactType.CARD -> { + askCurrentOrIncognitoProfileConnectContactViaAddress( + chatModel, + rhId, + chat.chatInfo.contact, + close = { ModalManager.start.closeModals() }, + openChat = true + ) + } + else -> {} + } + }, + dropdownMenuItems = { + tryOrShowError("${chat.id}ContactListNavLinkDropdown", error = {}) { + DeleteContactAction(chat, chatModel, showMenu) + } + }, + showMenu, + disabled, + selectedChat, + nextChatSelected, + oneHandUI + ) + } + is ChatInfo.ContactRequest -> { + ChatListNavLinkLayout( + chatLinkPreview = { + tryOrShowError("${chat.id}ContactListNavLink", error = { ErrorChatListItem() }) { + ContactPreviewView(chat, disabled) + } + }, + click = { + hideKeyboard(view) + contactRequestAlertDialog( + rhId, + chat.chatInfo, + chatModel, + onSucess = { onRequestAccepted(it) } + ) + }, + dropdownMenuItems = { + tryOrShowError("${chat.id}ContactListNavLinkDropdown", error = {}) { + ContactRequestMenuItems( + rhId = chat.remoteHostId, + chatInfo = chat.chatInfo, + chatModel = chatModel, + showMenu = showMenu, + onSuccess = { onRequestAccepted(it) } + ) + } + }, + showMenu, + disabled, + selectedChat, + nextChatSelected, + oneHandUI + ) + } + else -> {} + } +} + +@Composable +fun DeleteContactAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState) { + ItemAction( + stringResource(MR.strings.delete_contact_menu_action), + painterResource(MR.images.ic_delete), + onClick = { + deleteContactDialog(chat, chatModel) + showMenu.value = false + }, + color = Color.Red + ) +} \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactPreviewView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactPreviewView.kt new file mode 100644 index 0000000000..d47853f03c --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactPreviewView.kt @@ -0,0 +1,131 @@ +package chat.simplex.common.views.contacts + +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import dev.icerock.moko.resources.compose.painterResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.model.* +import chat.simplex.common.platform.chatModel +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.newchat.ContactType +import chat.simplex.common.views.newchat.chatContactType +import chat.simplex.res.MR + +@Composable +fun ContactPreviewView( + chat: Chat, + disabled: Boolean, +) { + val cInfo = chat.chatInfo + val contactType = chatContactType(chat) + + @Composable + fun VerifiedIcon() { + Icon(painterResource(MR.images.ic_verified_user), null, Modifier.size(19.dp).padding(end = 3.dp, top = 1.dp), tint = MaterialTheme.colors.secondary) + } + + @Composable + fun chatPreviewTitle() { + val deleting by remember(disabled, chat.id) { mutableStateOf(chatModel.deletedChats.value.contains(chat.remoteHostId to chat.chatInfo.id)) } + + val textColor = when { + deleting -> MaterialTheme.colors.secondary + contactType == ContactType.CARD -> MaterialTheme.colors.primary + contactType == ContactType.REQUEST -> MaterialTheme.colors.primary + contactType == ContactType.RECENT && chat.chatInfo.incognito -> Indigo + else -> Color.Unspecified + } + + when (cInfo) { + is ChatInfo.Direct -> + Row(verticalAlignment = Alignment.CenterVertically) { + if (cInfo.contact.verified) { + VerifiedIcon() + } + Text( + cInfo.chatViewName, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = textColor + ) + } + is ChatInfo.ContactRequest -> + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + cInfo.chatViewName, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = textColor + ) + } + else -> {} + } + } + + Row( + modifier = Modifier.padding(PaddingValues(horizontal = DEFAULT_PADDING_HALF)), + verticalAlignment = Alignment.CenterVertically, + ) { + Box(contentAlignment = Alignment.BottomEnd) { + ChatInfoImage(cInfo, size = 42.dp) + } + + Spacer(Modifier.width(DEFAULT_SPACE_AFTER_ICON)) + + Box(modifier = Modifier.weight(10f, fill = true)) { + chatPreviewTitle() + } + + Spacer(Modifier.fillMaxWidth().weight(1f)) + + if (chat.chatInfo is ChatInfo.ContactRequest) { + Icon( + painterResource(MR.images.ic_check), + contentDescription = null, + tint = MaterialTheme.colors.primary, + modifier = Modifier + .size(23.dp) + ) + } + + if (contactType == ContactType.CARD) { + Icon( + painterResource(MR.images.ic_mail), + contentDescription = null, + tint = MaterialTheme.colors.primary, + modifier = Modifier + .size(21.dp) + ) + } + + if (chat.chatInfo.chatSettings?.favorite == true) { + Icon( + painterResource(MR.images.ic_star_filled), + contentDescription = generalGetString(MR.strings.favorite_chat), + tint = MaterialTheme.colors.secondary, + modifier = Modifier + .size(17.dp) + ) + if (chat.chatInfo.incognito) { + Spacer(Modifier.width(DEFAULT_SPACE_AFTER_ICON)) + } + } + + + if (chat.chatInfo.incognito) { + Icon( + painterResource(MR.images.ic_theater_comedy), + contentDescription = null, + tint = MaterialTheme.colors.secondary, + modifier = Modifier + .size(21.dp) + ) + } + } +} \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt index fa43048e9e..3ade14bdce 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt @@ -20,7 +20,7 @@ import androidx.compose.ui.unit.dp import chat.simplex.common.model.* import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatModel.controller -import chat.simplex.common.model.ChatModel.updatingChatsMutex +import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.common.views.usersettings.* @@ -502,7 +502,11 @@ fun deleteChatDatabaseFilesAndState() { // Clear sensitive data on screen just in case ModalManager will fail to prevent hiding its modals while database encrypts itself chatModel.chatId.value = null chatModel.chatItems.clear() - chatModel.chats.clear() + withLongRunningApi { + withChats { + chats.clear() + } + } chatModel.users.clear() ntfManager.cancelAllNotifications() } @@ -714,10 +718,10 @@ private fun afterSetCiTTL( appFilesCountAndSize.value = directoryFileCountAndSize(appFilesDir.absolutePath) withApi { try { - updatingChatsMutex.withLock { + withChats { // this is using current remote host on purpose - if it changes during update, it will load correct chats val chats = m.controller.apiGetChats(m.remoteHostId()) - m.updateChats(chats) + updateChats(chats) } } catch (e: Exception) { Log.e(TAG, "apiGetChats error: ${e.message}") 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 d92dccddc2..8cdac67bf4 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 @@ -11,6 +11,8 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.background +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import chat.simplex.common.ui.theme.* @@ -18,17 +20,26 @@ import chat.simplex.res.MR import dev.icerock.moko.resources.compose.painterResource @Composable -fun CloseSheetBar(close: (() -> Unit)?, showClose: Boolean = true, tintColor: Color = if (close != null) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, endButtons: @Composable RowScope.() -> Unit = {}) { +fun CloseSheetBar(close: (() -> Unit)?, showClose: Boolean = true, tintColor: Color = if (close != null) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, arrangement: Arrangement.Vertical = Arrangement.Top, closeBarTitle: String? = null, endButtons: @Composable RowScope.() -> Unit = {}) { + var rowModifier = Modifier + .fillMaxWidth() + .height(AppBarHeight * fontSizeSqrtMultiplier) + + if (!closeBarTitle.isNullOrEmpty()) { + rowModifier = rowModifier.background(MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.97f)) + } + Column( - Modifier + verticalArrangement = arrangement, + modifier = Modifier .fillMaxWidth() .heightIn(min = AppBarHeight * fontSizeSqrtMultiplier) - .padding(horizontal = AppBarHorizontalPadding) ) { Row( + modifier = Modifier.padding(horizontal = AppBarHorizontalPadding), content = { Row( - Modifier.fillMaxWidth().height(AppBarHeight * fontSizeSqrtMultiplier), + rowModifier, horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { @@ -37,6 +48,18 @@ fun CloseSheetBar(close: (() -> Unit)?, showClose: Boolean = true, tintColor: Co } else { Spacer(Modifier) } + if (!closeBarTitle.isNullOrEmpty()) { + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + closeBarTitle, + color = MaterialTheme.colors.onBackground, + fontWeight = FontWeight.SemiBold, + ) + } + } Row { endButtons() } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultTopAppBar.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultTopAppBar.kt index 3d6d242832..c7ae522684 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultTopAppBar.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultTopAppBar.kt @@ -16,7 +16,7 @@ import chat.simplex.res.MR @Composable fun DefaultTopAppBar( - navigationButton: @Composable RowScope.() -> Unit, + navigationButton: (@Composable RowScope.() -> Unit)? = null, title: (@Composable () -> Unit)?, onTitleClick: (() -> Unit)? = null, showSearch: Boolean, @@ -126,5 +126,6 @@ private fun TopAppBar( val AppBarHeight = 56.dp val AppBarHorizontalPadding = 4.dp +val BottomAppBarHeight = 60.dp private val TitleInsetWithoutIcon = DEFAULT_PADDING - AppBarHorizontalPadding val TitleInsetWithIcon = 72.dp diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt index 4acb18561a..a1d8574d9f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.* +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import chat.simplex.common.model.ChatController.appPrefs @@ -24,6 +25,8 @@ fun ModalView( enableClose: Boolean = true, background: Color = MaterialTheme.colors.background, modifier: Modifier = Modifier, + closeOnTop: Boolean = true, + closeBarTitle: String? = null, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable () -> Unit, ) { @@ -32,8 +35,16 @@ fun ModalView( } Surface(Modifier.fillMaxSize(), contentColor = LocalContentColor.current) { Column(if (background != MaterialTheme.colors.background) Modifier.background(background) else Modifier.themedBackground()) { - CloseSheetBar(if (enableClose) close else null, showClose, endButtons = endButtons) - Box(modifier) { content() } + if (closeOnTop) { + CloseSheetBar(if (enableClose) close else null, showClose, endButtons = endButtons) + } + Box(if (closeOnTop) modifier else modifier.padding(bottom = AppBarHeight * fontSizeSqrtMultiplier)) { + content() + } + } + + if (!closeOnTop) { + CloseSheetBar(if (enableClose) close else null, showClose, endButtons = endButtons, arrangement = Arrangement.Bottom, closeBarTitle = closeBarTitle) } } } @@ -53,24 +64,25 @@ class ModalData { class ModalManager(private val placement: ModalPlacement? = null) { private val modalViews = arrayListOf Unit) -> Unit)>>() - private val modalCount = mutableStateOf(0) + private val _modalCount = mutableStateOf(0) + val modalCount: State = _modalCount private val toRemove = mutableSetOf() private var oldViewChanging = AtomicBoolean(false) // Don't use mutableStateOf() here, because it produces this if showing from SimpleXAPI.startChat(): // java.lang.IllegalStateException: Reading a state that was created after the snapshot was taken or in a snapshot that has not yet been applied private var passcodeView: MutableStateFlow<(@Composable (close: () -> Unit) -> Unit)?> = MutableStateFlow(null) - fun showModal(settings: Boolean = false, showClose: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.() -> Unit) { + fun showModal(settings: Boolean = false, showClose: Boolean = true, closeOnTop: Boolean = true, closeBarTitle: String? = null,endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.() -> Unit) { val data = ModalData() showCustomModal { close -> - ModalView(close, showClose = showClose, endButtons = endButtons, content = { data.content() }) + ModalView(close, showClose = showClose, closeOnTop = closeOnTop, closeBarTitle = closeBarTitle, endButtons = endButtons, content = { data.content() }) } } - fun showModalCloseable(settings: Boolean = false, showClose: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.(close: () -> Unit) -> Unit) { + fun showModalCloseable(settings: Boolean = false, showClose: Boolean = true, closeOnTop: Boolean = true, closeBarTitle: String? = null, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.(close: () -> Unit) -> Unit) { val data = ModalData() showCustomModal { close -> - ModalView(close, showClose = showClose, endButtons = endButtons, content = { data.content(close) }) + ModalView(close, showClose = showClose, endButtons = endButtons, closeOnTop = closeOnTop, closeBarTitle = closeBarTitle, content = { data.content(close) }) } } @@ -86,7 +98,7 @@ class ModalManager(private val placement: ModalPlacement? = null) { // to prevent unneeded animation on different situations val anim = if (appPlatform.isAndroid) animated else animated && (modalCount.value > 0 || placement == ModalPlacement.START) modalViews.add(Triple(anim, data, modal)) - modalCount.value = modalViews.size - toRemove.size + _modalCount.value = modalViews.size - toRemove.size if (placement == ModalPlacement.CENTER) { ChatModel.chatId.value = null @@ -105,18 +117,20 @@ class ModalManager(private val placement: ModalPlacement? = null) { val hasModalsOpen: Boolean @Composable get () = remember { modalCount }.value > 0 + fun openModalCount() = modalCount.value + fun closeModal() { if (modalViews.isNotEmpty()) { if (modalViews.lastOrNull()?.first == false) modalViews.removeAt(modalViews.lastIndex) else runAtomically { toRemove.add(modalViews.lastIndex - min(toRemove.size, modalViews.lastIndex)) } } - modalCount.value = modalViews.size - toRemove.size + _modalCount.value = modalViews.size - toRemove.size } fun closeModals() { modalViews.clear() toRemove.clear() - modalCount.value = 0 + _modalCount.value = 0 } fun closeModalsExceptFirst() { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt index 807b3a09c0..a5458859db 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt @@ -18,6 +18,7 @@ import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.model.* +import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chat.group.AddGroupMembersView import chat.simplex.common.views.chatlist.setGroupMembers @@ -32,19 +33,22 @@ import kotlinx.coroutines.launch import java.net.URI @Composable -fun AddGroupView(chatModel: ChatModel, rh: RemoteHostInfo?, close: () -> Unit) { +fun AddGroupView(chatModel: ChatModel, rh: RemoteHostInfo?, close: () -> Unit, closeAll: () -> Unit) { val rhId = rh?.remoteHostId AddGroupLayout( createGroup = { incognito, groupProfile -> withBGApi { val groupInfo = chatModel.controller.apiNewGroup(rhId, incognito, groupProfile) if (groupInfo != null) { - chatModel.updateGroup(rhId = rhId, groupInfo) - chatModel.chatItems.clear() - chatModel.chatItemStatuses.clear() - chatModel.chatId.value = groupInfo.id + withChats { + updateGroup(rhId = rhId, groupInfo) + chatModel.chatItems.clear() + chatModel.chatItemStatuses.clear() + chatModel.chatId.value = groupInfo.id + } setGroupMembers(rhId, groupInfo, chatModel) - close.invoke() + closeAll.invoke() + if (!groupInfo.incognito) { ModalManager.end.showModalCloseable(true) { close -> AddGroupMembersView(rhId, groupInfo, creatingGroup = true, chatModel, close) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt index 5b56fe5e39..e49fbcf1e6 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt @@ -7,6 +7,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import dev.icerock.moko.resources.compose.stringResource import chat.simplex.common.model.* +import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.platform.* import chat.simplex.common.views.chatlist.* import chat.simplex.common.views.helpers.* @@ -331,7 +332,9 @@ suspend fun connectViaUri( val pcc = chatModel.controller.apiConnect(rhId, incognito, uri.toString()) val connLinkType = if (connectionPlan != null) planToConnectionLinkType(connectionPlan) else ConnectionLinkType.INVITATION if (pcc != null) { - chatModel.updateContactConnection(rhId, pcc) + withChats { + updateContactConnection(rhId, pcc) + } close?.invoke() AlertManager.privacySensitive.showAlertMsg( title = generalGetString(MR.strings.connection_request_sent), 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 31623e4a61..09a6c447d5 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 @@ -22,6 +22,7 @@ import chat.simplex.common.views.chat.LocalAliasEditor import chat.simplex.common.views.chatlist.deleteContactConnectionAlert import chat.simplex.common.views.helpers.* import chat.simplex.common.model.ChatModel +import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.model.PendingContactConnection import chat.simplex.common.platform.* import chat.simplex.common.views.usersettings.* @@ -186,7 +187,9 @@ fun DeleteButton(onClick: () -> Unit) { private fun setContactAlias(rhId: Long?, contactConnection: PendingContactConnection, localAlias: String, chatModel: ChatModel) = withBGApi { chatModel.controller.apiSetConnectionAlias(rhId, contactConnection.pccConnId, localAlias)?.let { - chatModel.updateContactConnection(rhId, it) + withChats { + updateContactConnection(rhId, it) + } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt index 9faee4532a..6714e3aff6 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt @@ -1,177 +1,597 @@ package chat.simplex.common.views.newchat -import androidx.compose.animation.* -import androidx.compose.animation.core.* +import SectionDividerSpaced +import SectionItemView +import SectionView +import TextIconSpaced import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.* -import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.draw.scale +import androidx.compose.ui.focus.* import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.platform.* +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.text.TextRange import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.IntOffset -import androidx.compose.ui.unit.dp -import chat.simplex.common.model.ChatModel +import androidx.compose.ui.unit.* +import chat.simplex.common.model.* import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.chatlist.ScrollDirection +import chat.simplex.common.views.contacts.* import chat.simplex.common.views.helpers.* import chat.simplex.res.MR -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.launch -import kotlin.math.roundToInt +import kotlinx.coroutines.flow.distinctUntilChanged +import java.net.URI @Composable -fun NewChatSheet(chatModel: ChatModel, newChatSheetState: StateFlow, stopped: Boolean, closeNewChatSheet: (animated: Boolean) -> Unit) { - // TODO close new chat if remote host changes in model - if (newChatSheetState.collectAsState().value.isVisible()) BackHandler { closeNewChatSheet(true) } - NewChatSheetLayout( - newChatSheetState, - stopped, - addContact = { - closeNewChatSheet(false) - ModalManager.center.closeModals() - ModalManager.center.showModalCloseable { close -> NewChatView(chatModel.currentRemoteHost.value, NewChatOption.INVITE, close = close) } - }, - scanPaste = { - closeNewChatSheet(false) - ModalManager.center.closeModals() - ModalManager.center.showModalCloseable { close -> NewChatView(chatModel.currentRemoteHost.value, NewChatOption.CONNECT, showQRCodeScanner = true, close = close) } - }, - createGroup = { - closeNewChatSheet(false) - ModalManager.center.closeModals() - ModalManager.center.showCustomModal { close -> AddGroupView(chatModel, chatModel.currentRemoteHost.value, close) } - }, - closeNewChatSheet, - ) +fun NewChatSheet(rh: RemoteHostInfo?, close: () -> Unit) { + val oneHandUI = remember { chatModel.controller.appPrefs.oneHandUI } + + Column( + modifier = Modifier.fillMaxSize() + ) { + if (!oneHandUI.state.value) { + Box(contentAlignment = Alignment.Center) { + val bottomPadding = DEFAULT_PADDING + AppBarTitle( + stringResource(MR.strings.new_chat), + hostDevice(rh?.remoteHostId), + bottomPadding = bottomPadding + ) + } + } + + val closeAll = { ModalManager.start.closeModals() } + + var modifier = Modifier.fillMaxSize() + + if (oneHandUI.state.value) { + modifier = modifier.scale(scaleX = 1f, scaleY = -1f) + } + + Column(modifier = modifier) { + NewChatSheetLayout( + addContact = { + ModalManager.start.showModalCloseable { _ -> NewChatView(chatModel.currentRemoteHost.value, NewChatOption.INVITE, close = closeAll ) } + }, + scanPaste = { + ModalManager.start.showModalCloseable { _ -> NewChatView(chatModel.currentRemoteHost.value, NewChatOption.CONNECT, showQRCodeScanner = appPlatform.isAndroid, close = closeAll) } + }, + createGroup = { + ModalManager.start.showCustomModal { close -> AddGroupView(chatModel, chatModel.currentRemoteHost.value, close, closeAll) } + }, + rh = rh, + close = close, + oneHandUI = oneHandUI + ) + } + } } -private val titles = listOf( - MR.strings.add_contact_tab, - MR.strings.scan_paste_link, - MR.strings.create_group_button -) -private val icons = listOf(MR.images.ic_add_link, MR.images.ic_qr_code, MR.images.ic_group) +enum class ContactType { + CARD, REQUEST, RECENT, CHAT_DELETED, UNLISTED +} + +fun chatContactType(chat: Chat): ContactType { + return when (val cInfo = chat.chatInfo) { + is ChatInfo.ContactRequest -> ContactType.REQUEST + is ChatInfo.Direct -> { + val contact = cInfo.contact; + + when { + contact.activeConn == null && contact.profile.contactLink != null -> ContactType.CARD + contact.chatDeleted -> ContactType.CHAT_DELETED + contact.contactStatus == ContactStatus.Active -> ContactType.RECENT + else -> ContactType.UNLISTED + } + } + else -> ContactType.UNLISTED + } +} + +private fun filterContactTypes(c: List, contactTypes: List): List { + return c.filter { chat -> contactTypes.contains(chatContactType(chat)) } +} + +private var lazyListState = 0 to 0 @Composable private fun NewChatSheetLayout( - newChatSheetState: StateFlow, - stopped: Boolean, + rh: RemoteHostInfo?, addContact: () -> Unit, scanPaste: () -> Unit, createGroup: () -> Unit, - closeNewChatSheet: (animated: Boolean) -> Unit, + close: () -> Unit, + oneHandUI: SharedPreference ) { - var newChat by remember { mutableStateOf(newChatSheetState.value) } - val resultingColor = if (isInDarkTheme()) Color.Black.copy(0.64f) else DrawerDefaults.scrimColor - val animatedColor = remember { - Animatable( - if (newChat.isVisible()) Color.Transparent else resultingColor, - Color.VectorConverter(resultingColor.colorSpace) - ) + val listState = rememberLazyListState(lazyListState.first, lazyListState.second) + val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) } + val searchShowingSimplexLink = remember { mutableStateOf(false) } + val searchChatFilteredBySimplexLink = remember { mutableStateOf(null) } + val showUnreadAndFavorites = remember { ChatController.appPrefs.showUnreadAndFavorites.state }.value + val baseContactTypes = listOf(ContactType.CARD, ContactType.RECENT, ContactType.REQUEST) + val contactTypes by remember(baseContactTypes, searchText.value.text.isEmpty()) { + derivedStateOf { contactTypesSearchTargets(baseContactTypes, searchText.value.text.isEmpty()) } } - val animatedFloat = remember { Animatable(if (newChat.isVisible()) 0f else 1f) } - LaunchedEffect(Unit) { - launch { - newChatSheetState.collect { - newChat = it - launch { - animatedColor.animateTo(if (newChat.isVisible()) resultingColor else Color.Transparent, newChatSheetAnimSpec()) - } - launch { - animatedFloat.animateTo(if (newChat.isVisible()) 1f else 0f, newChatSheetAnimSpec()) - if (newChat.isHiding()) closeNewChatSheet(false) + val allChats by remember(chatModel.chats.value, contactTypes) { + derivedStateOf { filterContactTypes(chatModel.chats.value, contactTypes) } + } + var scrollDirection by remember { mutableStateOf(ScrollDirection.Idle) } + var previousIndex by remember { mutableStateOf(0) } + var previousScrollOffset by remember { mutableStateOf(0) } + + LaunchedEffect(listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset) { + val currentIndex = listState.firstVisibleItemIndex + val currentScrollOffset = listState.firstVisibleItemScrollOffset + val threshold = 25 + + scrollDirection = when { + currentIndex > previousIndex -> ScrollDirection.Down + currentIndex < previousIndex -> ScrollDirection.Up + currentScrollOffset > previousScrollOffset + threshold -> ScrollDirection.Down + currentScrollOffset < previousScrollOffset - threshold -> ScrollDirection.Up + currentScrollOffset == previousScrollOffset -> ScrollDirection.Idle + else -> scrollDirection + } + + previousIndex = currentIndex + previousScrollOffset = currentScrollOffset + } + + val filteredContactChats = filteredContactChats( + showUnreadAndFavorites = showUnreadAndFavorites, + searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink, + searchShowingSimplexLink = searchShowingSimplexLink, + searchText = searchText.value.text, + contactChats = allChats + ) + + var sectionModifier = Modifier.fillMaxWidth() + + if (oneHandUI.state.value) { + sectionModifier = sectionModifier.scale(scaleX = 1f, scaleY = -1f) + } + + LazyColumnWithScrollBar( + Modifier.fillMaxWidth(), + listState + ) { + stickyHeader { + Column( + Modifier + .offset { + val y = if (searchText.value.text.isEmpty()) { + if (oneHandUI.state.value && scrollDirection == ScrollDirection.Up) { + 0 + } else if (listState.firstVisibleItemIndex == 0) -listState.firstVisibleItemScrollOffset else -1000 + } else { + 0 + } + IntOffset(0, y) + } + .background(MaterialTheme.colors.background) + ) { + if (!oneHandUI.state.value) { + Divider() } + ContactsSearchBar( + listState = listState, + searchText = searchText, + searchShowingSimplexLink = searchShowingSimplexLink, + searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink, + close = close, + oneHandUI = oneHandUI + ) + Divider() } } - } - val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp - val maxWidth = with(LocalDensity.current) { windowWidth() * density } - Column( - Modifier - .fillMaxSize() - .padding(end = endPadding) - .offset { IntOffset(if (newChat.isGone()) -maxWidth.value.roundToInt() else 0, 0) } - .clickable(interactionSource = remember { MutableInteractionSource() }, indication = null) { closeNewChatSheet(true) } - .drawBehind { drawRect(animatedColor.value) }, - verticalArrangement = Arrangement.Bottom, - horizontalAlignment = Alignment.End - ) { - val actions = remember { listOf(addContact, scanPaste, createGroup) } - val backgroundColor = if (isInDarkTheme()) - blendARGB(MaterialTheme.colors.primary, Color.Black, 0.7F) - else - MaterialTheme.colors.background - LazyColumn(Modifier - .graphicsLayer { - alpha = animatedFloat.value - translationY = (1 - animatedFloat.value) * 20.dp.toPx() - }) { - items(actions.size) { index -> + item { + Spacer(Modifier.padding(bottom = DEFAULT_PADDING)) + + if (searchText.value.text.isEmpty()) { Row { - Spacer(Modifier.weight(1f)) - Box(contentAlignment = Alignment.CenterEnd) { - Button( - actions[index], - shape = RoundedCornerShape(21.dp * fontSizeSqrtMultiplier), - colors = ButtonDefaults.textButtonColors(backgroundColor = backgroundColor), - elevation = null, - contentPadding = PaddingValues(horizontal = DEFAULT_PADDING_HALF, vertical = DEFAULT_PADDING_HALF), - modifier = Modifier.height(42.dp * fontSizeSqrtMultiplier) - ) { - Text( - stringResource(titles[index]), - Modifier.padding(start = DEFAULT_PADDING_HALF), - color = if (isInDarkTheme()) MaterialTheme.colors.primary else MaterialTheme.colors.primary, - fontWeight = FontWeight.Medium, - ) - Icon( - painterResource(icons[index]), - stringResource(titles[index]), - Modifier.size(42.dp * fontSizeSqrtMultiplier), - tint = if (isInDarkTheme()) MaterialTheme.colors.primary else MaterialTheme.colors.primary - ) + SectionView { + NewChatButton( + icon = painterResource(MR.images.ic_add_link), + text = stringResource(MR.strings.add_contact_tab), + click = addContact, + extraPadding = true, + oneHandUI = oneHandUI.state + ) + NewChatButton( + icon = painterResource(MR.images.ic_qr_code), + text = if (appPlatform.isAndroid) stringResource(MR.strings.scan_paste_link) else stringResource(MR.strings.paste_link), + click = scanPaste, + extraPadding = true, + oneHandUI = oneHandUI.state + ) + NewChatButton( + icon = painterResource(MR.images.ic_group), + text = stringResource(MR.strings.create_group_button), + click = createGroup, + extraPadding = true, + oneHandUI = oneHandUI.state + ) + } + } + SectionDividerSpaced(maxBottomPadding = false) + + val deletedContactTypes = listOf(ContactType.CHAT_DELETED) + val deletedChats by remember(chatModel.chats.value, deletedContactTypes) { + derivedStateOf { filterContactTypes(chatModel.chats.value, deletedContactTypes) } + } + if (deletedChats.isNotEmpty()) { + Row(modifier = sectionModifier) { + SectionView { + SectionItemView( + click = { + ModalManager.start.showCustomModal { closeDeletedChats -> + ModalView( + close = closeDeletedChats, + closeOnTop = !oneHandUI.state.value, + closeBarTitle = if (oneHandUI.state.value) generalGetString(MR.strings.deleted_chats) else null, + endButtons = { Spacer(Modifier.minimumInteractiveComponentSize()) } + ) { + DeletedContactsView(rh = rh, close = { + ModalManager.start.closeModals() + }) + } + } + } + ) { + Icon( + painterResource(MR.images.ic_folder_open), + contentDescription = stringResource(MR.strings.deleted_chats), + tint = MaterialTheme.colors.secondary, + ) + TextIconSpaced(extraPadding = true) + Text(text = stringResource(MR.strings.deleted_chats), color = MaterialTheme.colors.onBackground) + } } } - Spacer(Modifier.width(DEFAULT_PADDING)) + SectionDividerSpaced() } - Spacer(Modifier.height(DEFAULT_PADDING)) } } - FloatingActionButton( - onClick = { if (!stopped) closeNewChatSheet(true) }, - Modifier.padding(end = DEFAULT_PADDING, bottom = DEFAULT_PADDING).size(AppBarHeight * fontSizeSqrtMultiplier), - elevation = FloatingActionButtonDefaults.elevation( - defaultElevation = 0.dp, - pressedElevation = 0.dp, - hoveredElevation = 0.dp, - focusedElevation = 0.dp, - ), - backgroundColor = if (!stopped) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, - contentColor = Color.White + + item { + if (filteredContactChats.isNotEmpty() && !oneHandUI.state.value) { + Text( + stringResource(MR.strings.contact_list_header_title).uppercase(), color = MaterialTheme.colors.secondary, style = MaterialTheme.typography.body2, + modifier = sectionModifier.padding(start = DEFAULT_PADDING, bottom = 5.dp), fontSize = 12.sp + ) + } + } + + itemsIndexed(filteredContactChats) { index, chat -> + val nextChatSelected = remember(chat.id, filteredContactChats) { + derivedStateOf { + chatModel.chatId.value != null && filteredContactChats.getOrNull(index + 1)?.id == chatModel.chatId.value + } + } + ContactListNavLinkView(chat, nextChatSelected, oneHandUI.state) + } + } + + if (filteredContactChats.isEmpty() && allChats.isNotEmpty()) { + Column(sectionModifier.fillMaxSize().padding(DEFAULT_PADDING)) { + Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { + Text( + generalGetString(MR.strings.no_filtered_contacts), + color = MaterialTheme.colors.secondary + ) + } + } + } +} + +@Composable +private fun NewChatButton( + icon: Painter, + text: String, + click: () -> Unit, + textColor: Color = Color.Unspecified, + iconColor: Color = MaterialTheme.colors.secondary, + disabled: Boolean = false, + extraPadding: Boolean = false, + oneHandUI: State +) { + SectionItemView(click, disabled = disabled) { + Row(modifier = if (oneHandUI.value) Modifier.scale(scaleX = 1f, scaleY = -1f) else Modifier) { + Icon(icon, text, tint = if (disabled) MaterialTheme.colors.secondary else iconColor) + TextIconSpaced(extraPadding) + Text(text, color = if (disabled) MaterialTheme.colors.secondary else textColor) + } + } +} + +@Composable +private fun ContactsSearchBar( + listState: LazyListState, + searchText: MutableState, + searchShowingSimplexLink: MutableState, + searchChatFilteredBySimplexLink: MutableState, + close: () -> Unit, + oneHandUI: SharedPreference +) { + var modifier = Modifier.fillMaxWidth(); + + if (oneHandUI.state.value) { + modifier = modifier.scale(scaleX = 1f, scaleY = -1f) + } + + var focused by remember { mutableStateOf(false) } + + Row(verticalAlignment = Alignment.CenterVertically, modifier = modifier) { + val focusRequester = remember { FocusRequester() } + Icon( + painterResource(MR.images.ic_search), + contentDescription = null, + Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING_HALF).size(24.dp * fontSizeSqrtMultiplier), + tint = MaterialTheme.colors.secondary + ) + SearchTextField( + Modifier.weight(1f).onFocusChanged { focused = it.hasFocus }.focusRequester(focusRequester), + placeholder = stringResource(MR.strings.search_or_paste_simplex_link), + alwaysVisible = true, + searchText = searchText, + trailingContent = null, ) { - Icon( - painterResource(MR.images.ic_edit_filled), stringResource(MR.strings.add_contact_or_create_group), - Modifier.graphicsLayer { alpha = 1 - animatedFloat.value }.size(24.dp * fontSizeSqrtMultiplier) - ) - Icon( - painterResource(MR.images.ic_close), stringResource(MR.strings.add_contact_or_create_group), - Modifier.graphicsLayer { alpha = animatedFloat.value }.size(24.dp * fontSizeSqrtMultiplier) + searchText.value = searchText.value.copy(it) + } + val hasText = remember { derivedStateOf { searchText.value.text.isNotEmpty() } } + if (hasText.value) { + val hideSearchOnBack: () -> Unit = { searchText.value = TextFieldValue() } + BackHandler(onBack = hideSearchOnBack) + KeyChangeEffect(chatModel.currentRemoteHost.value) { + hideSearchOnBack() + } + } else { + Row { + val padding = if (appPlatform.isDesktop) 0.dp else 7.dp + if (chatModel.chats.size > 0) { + ToggleFilterButton() + } + Spacer(Modifier.width(padding)) + } + } + val focusManager = LocalFocusManager.current + val keyboardState = getKeyboardState() + LaunchedEffect(keyboardState.value) { + if (keyboardState.value == KeyboardState.Closed && focused) { + focusManager.clearFocus() + } + } + val view = LocalMultiplatformView() + LaunchedEffect(Unit) { + snapshotFlow { searchText.value.text } + .distinctUntilChanged() + .collect { + val link = strHasSingleSimplexLink(it.trim()) + if (link != null) { + // if SimpleX link is pasted, show connection dialogue + hideKeyboard(view) + if (link.format is Format.SimplexLink) { + val linkText = + link.simplexLinkText(link.format.linkType, link.format.smpHosts) + searchText.value = + searchText.value.copy(linkText, selection = TextRange.Zero) + } + searchShowingSimplexLink.value = true + searchChatFilteredBySimplexLink.value = null + connect( + link = link.text, + searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink, + close = close, + cleanup = { searchText.value = TextFieldValue() } + ) + } else if (!searchShowingSimplexLink.value || it.isEmpty()) { + if (it.isNotEmpty()) { + // if some other text is pasted, enter search mode + focusRequester.requestFocus() + } else if (listState.layoutInfo.totalItemsCount > 0) { + listState.scrollToItem(0) + } + searchShowingSimplexLink.value = false + searchChatFilteredBySimplexLink.value = null + } + } + } + } +} + +@Composable +private fun ToggleFilterButton() { + val pref = remember { ChatController.appPrefs.showUnreadAndFavorites } + IconButton(onClick = { pref.set(!pref.get()) }) { + val sp16 = with(LocalDensity.current) { 16.sp.toDp() } + Icon( + painterResource(MR.images.ic_filter_list), + null, + tint = if (pref.state.value) MaterialTheme.colors.background else MaterialTheme.colors.secondary, + modifier = Modifier + .padding(3.dp) + .background(color = if (pref.state.value) MaterialTheme.colors.primary else Color.Unspecified, shape = RoundedCornerShape(50)) + .border(width = 1.dp, color = if (pref.state.value) MaterialTheme.colors.primary else Color.Unspecified, shape = RoundedCornerShape(50)) + .padding(3.dp) + .size(sp16) + ) + } +} + +private fun connect(link: String, searchChatFilteredBySimplexLink: MutableState, close: () -> Unit, cleanup: (() -> Unit)?) { + withBGApi { + planAndConnect( + chatModel.remoteHostId(), + URI.create(link), + incognito = null, + filterKnownContact = { searchChatFilteredBySimplexLink.value = it.id }, + close = close, + cleanup = cleanup, + ) + } +} + +private fun filteredContactChats( + showUnreadAndFavorites: Boolean, + searchShowingSimplexLink: State, + searchChatFilteredBySimplexLink: State, + searchText: String, + contactChats: List +): List { + val linkChatId = searchChatFilteredBySimplexLink.value + val s = if (searchShowingSimplexLink.value) "" else searchText.trim().lowercase() + + return if (linkChatId != null) { + contactChats.filter { it.id == linkChatId } + } else { + contactChats.filter { chat -> + filterChat( + chat = chat, + searchText = s, + showUnreadAndFavorites = showUnreadAndFavorites ) } } + .sortedWith(chatsByTypeComparator) +} + +private fun filterChat(chat: Chat, searchText: String, showUnreadAndFavorites: Boolean): Boolean { + var meetsPredicate = true; + val s = searchText.trim().lowercase() + val cInfo = chat.chatInfo + + if (searchText.isNotEmpty()) { + meetsPredicate = viewNameContains(cInfo, s) || + if (cInfo is ChatInfo.Direct) (cInfo.contact.profile.displayName.lowercase().contains(s) || + cInfo.contact.fullName.lowercase().contains(s)) else false + } + + if (showUnreadAndFavorites) { + meetsPredicate = meetsPredicate && (cInfo.chatSettings?.favorite ?: false) + } + + return meetsPredicate; +} + +private fun viewNameContains(cInfo: ChatInfo, s: String): Boolean = + cInfo.chatViewName.lowercase().contains(s.lowercase()) + +private val chatsByTypeComparator = Comparator { chat1, chat2 -> + val chat1Type = chatContactType(chat1) + val chat2Type = chatContactType(chat2) + + when { + chat1Type.ordinal < chat2Type.ordinal -> -1 + chat1Type.ordinal > chat2Type.ordinal -> 1 + + else -> chat2.chatInfo.chatTs.compareTo(chat1.chatInfo.chatTs) + } +} + +private fun contactTypesSearchTargets(baseContactTypes: List, searchEmpty: Boolean): List { + return if (baseContactTypes.contains(ContactType.CHAT_DELETED) || searchEmpty) { + baseContactTypes + } else { + baseContactTypes + ContactType.CHAT_DELETED + } +} + +@Composable +private fun DeletedContactsView(rh: RemoteHostInfo?, close: () -> Unit) { + val oneHandUI = remember { chatModel.controller.appPrefs.oneHandUI } + + var modifier = Modifier.fillMaxSize() + + if (oneHandUI.state.value) { + modifier = modifier.scale(scaleX = 1f, scaleY = -1f) + } + + Column( + modifier, + ) { + if (!oneHandUI.state.value) { + Box(contentAlignment = Alignment.Center) { + val bottomPadding = DEFAULT_PADDING + AppBarTitle( + stringResource(MR.strings.deleted_chats), + hostDevice(rh?.remoteHostId), + bottomPadding = bottomPadding + ) + } + } + + val listState = rememberLazyListState(lazyListState.first, lazyListState.second) + val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) } + val searchShowingSimplexLink = remember { mutableStateOf(false) } + val searchChatFilteredBySimplexLink = remember { mutableStateOf(null) } + val showUnreadAndFavorites = remember { ChatController.appPrefs.showUnreadAndFavorites.state }.value + val contactTypes = listOf(ContactType.CHAT_DELETED) + val allChats by remember(chatModel.chats.value, contactTypes) { + derivedStateOf { filterContactTypes(chatModel.chats.value, contactTypes) } + } + val filteredContactChats = filteredContactChats( + showUnreadAndFavorites = showUnreadAndFavorites, + searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink, + searchShowingSimplexLink = searchShowingSimplexLink, + searchText = searchText.value.text, + contactChats = allChats + ) + + LazyColumnWithScrollBar( + Modifier.fillMaxWidth(), + listState + ) { + item { + Divider() + ContactsSearchBar( + listState = listState, + searchText = searchText, + searchShowingSimplexLink = searchShowingSimplexLink, + searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink, + close = close, + oneHandUI = oneHandUI + ) + Divider() + + Spacer(Modifier.padding(bottom = DEFAULT_PADDING)) + } + + itemsIndexed(filteredContactChats) { index, chat -> + val nextChatSelected = remember(chat.id, filteredContactChats) { + derivedStateOf { + chatModel.chatId.value != null && filteredContactChats.getOrNull(index + 1)?.id == chatModel.chatId.value + } + } + ContactListNavLinkView(chat, nextChatSelected, oneHandUI.state) + } + } + if (filteredContactChats.isEmpty() && allChats.isNotEmpty()) { + Column(Modifier.fillMaxSize().padding(DEFAULT_PADDING)) { + Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { + Text( + generalGetString(MR.strings.no_filtered_contacts), + color = MaterialTheme.colors.secondary, + modifier = if (oneHandUI.state.value) Modifier.scale(scaleX = 1f, scaleY = -1f) else Modifier + ) + } + } + } + } } @Composable @@ -267,13 +687,6 @@ fun ActionButton( @Composable private fun PreviewNewChatSheet() { SimpleXTheme { - NewChatSheetLayout( - MutableStateFlow(AnimatedViewState.VISIBLE), - stopped = false, - addContact = {}, - scanPaste = {}, - createGroup = {}, - closeNewChatSheet = {}, - ) + NewChatSheet(rh = null, close = {}) } } 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 a877e123b3..77e5662b21 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 @@ -25,6 +25,7 @@ import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.unit.sp import chat.simplex.common.model.* import chat.simplex.common.model.ChatModel.controller +import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* @@ -62,7 +63,7 @@ fun ModalData.NewChatView(rh: RemoteHostInfo?, selection: NewChatOption, showQRC * It will be dropped automatically when connection established or when user goes away from this screen. * It applies only to Android because on Desktop center space will not be overlapped by [AddContactLearnMore] **/ - if (chatModel.showingInvitation.value != null && (!ModalManager.center.hasModalsOpen() || appPlatform.isDesktop)) { + if (chatModel.showingInvitation.value != null && (ModalManager.start.openModalCount() == 1 || appPlatform.isDesktop)) { val conn = contactConnection.value if (chatModel.showingInvitation.value?.connChatUsed == false && conn != null) { AlertManager.shared.showAlertDialog( @@ -218,8 +219,10 @@ private fun InviteView(rhId: Long?, connReqInvitation: String, contactConnection withBGApi { val contactConn = contactConnection.value ?: return@withBGApi val conn = controller.apiSetConnectionIncognito(rhId, contactConn.pccConnId, incognito.value) ?: return@withBGApi - contactConnection.value = conn - chatModel.updateContactConnection(rhId, conn) + withChats { + contactConnection.value = conn + updateContactConnection(rhId, conn) + } } chatModel.markShowingInvitationUsed() } @@ -234,7 +237,8 @@ private fun AddContactLearnMoreButton() { ModalManager.end.showModalCloseable { close -> AddContactLearnMore(close) } - } + }, + Modifier.size(18.dp * fontSizeSqrtMultiplier) ) { Icon( painterResource(MR.images.ic_info), @@ -367,9 +371,11 @@ private fun createInvitation( withBGApi { val (r, alert) = controller.apiAddContact(rhId, incognito = controller.appPrefs.incognito.get()) if (r != null) { - chatModel.updateContactConnection(rhId, r.second) - chatModel.showingInvitation.value = ShowingInvitation(connId = r.second.id, connReq = simplexChatLink(r.first), connChatUsed = false) - contactConnection.value = r.second + withChats { + updateContactConnection(rhId, r.second) + chatModel.showingInvitation.value = ShowingInvitation(connId = r.second.id, connReq = simplexChatLink(r.first), connChatUsed = false) + contactConnection.value = r.second + } } else { creatingConnReq.value = false if (alert != null) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Preferences.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Preferences.kt index cd0e40f5d0..96a0bdcda3 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Preferences.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Preferences.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.Modifier import dev.icerock.moko.resources.compose.stringResource import chat.simplex.common.views.helpers.* import chat.simplex.common.model.* +import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.platform.ColumnWithScrollBar import chat.simplex.res.MR @@ -33,7 +34,9 @@ fun PreferencesView(m: ChatModel, user: User, close: () -> Unit,) { if (updated != null) { val (updatedProfile, updatedContacts) = updated m.updateCurrentUser(user.remoteHostId, updatedProfile, preferences) - updatedContacts.forEach { m.updateContact(user.remoteHostId, it) } + withChats { + updatedContacts.forEach { updateContact(user.remoteHostId, it) } + } currentPreferences = preferences } afterSave() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt index 88b14b6a66..578a505e0c 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt @@ -32,6 +32,7 @@ import chat.simplex.common.views.isValidDisplayName import chat.simplex.common.views.localauth.SetAppPasscodeView import chat.simplex.common.views.onboarding.ReadableText import chat.simplex.common.model.ChatModel +import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.platform.* import kotlin.math.min import kotlin.math.roundToInt @@ -121,14 +122,16 @@ fun PrivacySettingsView( chatModel.currentUser.value = currentUser.copy(sendRcptsContacts = enable) if (clearOverrides) { // For loop here is to prevent ConcurrentModificationException that happens with forEach - for (i in 0 until chatModel.chats.size) { - val chat = chatModel.chats[i] - if (chat.chatInfo is ChatInfo.Direct) { - var contact = chat.chatInfo.contact - val sendRcpts = contact.chatSettings.sendRcpts - if (sendRcpts != null && sendRcpts != enable) { - contact = contact.copy(chatSettings = contact.chatSettings.copy(sendRcpts = null)) - chatModel.updateContact(currentUser.remoteHostId, contact) + withChats { + for (i in 0 until chats.size) { + val chat = chats[i] + if (chat.chatInfo is ChatInfo.Direct) { + var contact = chat.chatInfo.contact + val sendRcpts = contact.chatSettings.sendRcpts + if (sendRcpts != null && sendRcpts != enable) { + contact = contact.copy(chatSettings = contact.chatSettings.copy(sendRcpts = null)) + updateContact(currentUser.remoteHostId, contact) + } } } } @@ -143,15 +146,17 @@ fun PrivacySettingsView( chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true) chatModel.currentUser.value = currentUser.copy(sendRcptsSmallGroups = enable) if (clearOverrides) { - // For loop here is to prevent ConcurrentModificationException that happens with forEach - for (i in 0 until chatModel.chats.size) { - val chat = chatModel.chats[i] - if (chat.chatInfo is ChatInfo.Group) { - var groupInfo = chat.chatInfo.groupInfo - val sendRcpts = groupInfo.chatSettings.sendRcpts - if (sendRcpts != null && sendRcpts != enable) { - groupInfo = groupInfo.copy(chatSettings = groupInfo.chatSettings.copy(sendRcpts = null)) - chatModel.updateGroup(currentUser.remoteHostId, groupInfo) + withChats { + // For loop here is to prevent ConcurrentModificationException that happens with forEach + for (i in 0 until chats.size) { + val chat = chats[i] + if (chat.chatInfo is ChatInfo.Group) { + var groupInfo = chat.chatInfo.groupInfo + val sendRcpts = groupInfo.chatSettings.sendRcpts + if (sendRcpts != null && sendRcpts != enable) { + groupInfo = groupInfo.copy(chatSettings = groupInfo.chatSettings.copy(sendRcpts = null)) + updateGroup(currentUser.remoteHostId, groupInfo) + } } } } @@ -164,7 +169,7 @@ fun PrivacySettingsView( DeliveryReceiptsSection( currentUser = currentUser, setOrAskSendReceiptsContacts = { enable -> - val contactReceiptsOverrides = chatModel.chats.fold(0) { count, chat -> + val contactReceiptsOverrides = chatModel.chats.value.fold(0) { count, chat -> if (chat.chatInfo is ChatInfo.Direct) { val sendRcpts = chat.chatInfo.contact.chatSettings.sendRcpts count + (if (sendRcpts == null || sendRcpts == enable) 0 else 1) @@ -179,7 +184,7 @@ fun PrivacySettingsView( } }, setOrAskSendReceiptsGroups = { enable -> - val groupReceiptsOverrides = chatModel.chats.fold(0) { count, chat -> + val groupReceiptsOverrides = chatModel.chats.value.fold(0) { count, chat -> if (chat.chatInfo is ChatInfo.Group) { val sendRcpts = chat.chatInfo.groupInfo.chatSettings.sendRcpts count + (if (sendRcpts == null || sendRcpts == enable) 0 else 1) diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml index 2b87fdd91c..cd50902ef7 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml @@ -318,7 +318,7 @@ ملون لدى جهة الاتصال التعمية بين الطريفين إنشاء - إنشاء حسابك الشخصي + إنشاء ملف تعريف مكالمة جارية... تفعيل التدمير الذاتي الموافقة على التعمية… @@ -354,7 +354,7 @@ حذف المجموعة؟ حذف الرابط حُذِفت في: %s - المجموعة المحذوفة + المجموعة حُذِفت حذف الصورة تخصيص السمات حذف قاعدة البيانات @@ -1403,7 +1403,7 @@ سيتم إخفاء كافة الرسائل الجديدة من %s! محظور حظر أعضاء المجموعة - جهة الاتصال المحذوفة + جهة الاتصال حُذِفت أنشِئ مجموعة باستخدام ملف تعريف عشوائي. أنشِئ مجموعة أنشِئ ملف تعريف @@ -1889,7 +1889,7 @@ خوادم SMP أخرى خوادم XFTP المهيأة خوادم XFTP أخرى - نسبة الاشتراك + أظهِر النسبة المئوية مُعطّل مستقرّ يتوفر تحديث: %s @@ -1947,15 +1947,15 @@ التمس التحديثات أخطاء معترف بها نُزّل تحديث التطبيق - جميع المستخدمين + جميع ملفات التعريف المحاولات تجريبي رُفع القطع متصل الخوادم المتصلة جارِ الاتصال - الاتصالات المشتركة - المستخدم الحالي + الاتصالات النسطة + ملف التعريف الحالي أخطاء الحذف إحصائيات مفصلة عطّل @@ -1969,7 +1969,7 @@ ثبّت التحديث قد يتم تسليم الرسالة لاحقًا إذا أصبح العضو نشطًا. الرسائل المُستلمة - اشتراكات الرسائل + استقبال الرسائل لا توجد معلومات، حاول إعادة التحميل أخطاء أخرى قيد الانتظار @@ -1987,4 +1987,15 @@ لكي يتم إعلامك بالإصدارات الجديدة، شغّل الفحص الدوري للإصدارات المستقرة أو التجريبية. أنت غير متصل بهذه الخوادم. يتم استخدام التوجيه الخاص لتسليم الرسائل إليهم. قرّب + حدث خطأ أثناء الاتصال بخادم التحويل %1$s. يُرجى المحاولة لاحقا. + عنوان خادم التحويل غير متوافق مع إعدادات الشبكة: %1$s. + عنوان خادم الوجهة %1$s غير متوافق مع إعدادات خادم التحويل %2$s. + إصدار الخادم الوجهة %1$s غير متوافق مع خادم التحويل %2$s. + فشل خادم التحويل %1$s في الاتصال بالخادم الوجهة %2$s. يُرجى المحاولة لاحقا. + إصدار خادم التحويل غير متوافق مع إعدادات الشبكة: %1$s. + مطفي + قوي + تمويه الوسائط + متوسط + ناعم \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 0564d87cd3..9812e7b10a 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -351,6 +351,7 @@ Welcome! This text is available in settings Chats + Settings connecting… send direct message you are invited to group @@ -442,10 +443,25 @@ Notifications + connect + open + message + call + search + video Delete contact? Contact and all messages will be deleted - this cannot be undone! + Contact will be deleted - this cannot be undone! + Keep conversation + Only delete conversation + Confirm contact deletion? Delete and notify contact + Delete without notification Delete contact + Conversation deleted! + You can still send messages to %1$s from the Deleted chats. + Contact deleted! + You can still view conversation with %1$s in the list of chats. Set contact name… Connected Disconnected @@ -633,6 +649,7 @@ New chat Add contact Scan / Paste link + Paste link One-time invitation link 1-time link SimpleX address @@ -651,6 +668,10 @@ Invalid QR code The code you scanned is not a SimpleX link QR code. + Deleted chats + No filtered contacts + Your contacts + Scan code Incorrect security code! @@ -1123,6 +1144,7 @@ Developer tools Experimental features SOCKS PROXY + INTERFACE LANGUAGE APP ICON THEMES @@ -1258,6 +1280,7 @@ Database downgrade Incompatible database version Confirm database upgrades + One-hand UI Show console in new window Show chat list in new window Invalid migration confirmation @@ -1450,6 +1473,7 @@ disabled Receipts are disabled This group has over %1$d members, delivery receipts are not sent. + Invite FOR CONSOLE @@ -1527,6 +1551,17 @@ none server queue info: %1$s\n\nlast received msg: %2$s + Can\'t call contact + Connecting to contact, please wait or check later! + Contact is deleted. + Allow calls? + You need to allow your contact to call to be able to call them. + Calls prohibited! + Please ask your contact to enable calls. + Can\'t call group member + Send message to enable calls. + Can\'t message group member + Welcome message Save welcome message? 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 2029650eb8..b4fcc3867a 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml @@ -218,7 +218,7 @@ Benachrichtigungen Kontakt löschen? - Der Kontakt und alle Nachrichten werden gelöscht. Dies kann nicht rückgängig gemacht werden! + Es wird der Kontakt und alle Nachrichten gelöscht. Dies kann nicht rückgängig gemacht werden! Kontakt löschen Kontaktname festlegen… Verbunden @@ -278,7 +278,7 @@ Ablehnen Chatinhalte löschen? - Alle Nachrichten werden gelöscht. Dies kann nicht rückgängig gemacht werden! Die Nachrichten werden NUR bei Ihnen gelöscht. + 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 @@ -593,20 +593,20 @@ Fehler beim Exportieren der Chat-Datenbank Chat-Datenbank importieren? Ihre aktuelle Chat-Datenbank wird GELÖSCHT und durch die importierte ERSETZT. -\nDiese Aktion kann nicht rückgängig gemacht werden – Ihr Profil, Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren. +\nDiese Aktion kann nicht rückgängig gemacht werden! Ihr Profil, Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren. Importieren Fehler beim Löschen der Chat-Datenbank Fehler beim Importieren der Chat-Datenbank Chat-Datenbank importiert Starten Sie die App neu, um die importierte Chat-Datenbank zu verwenden. Chat-Profil löschen? - Diese Aktion kann nicht rückgängig gemacht werden – Ihr Profil, Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren. + Diese Aktion kann nicht rückgängig gemacht werden! Ihr Profil, Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren. Chat-Datenbank gelöscht Starten Sie die App neu, um ein neues Chat-Profil zu erstellen. Sie dürfen die neueste Version Ihrer Chat-Datenbank NUR auf einem Gerät verwenden, andernfalls erhalten Sie möglicherweise keine Nachrichten mehr von einigen Ihrer Kontakte. Chat beenden, um Datenbankaktionen zu erlauben. Dateien und Medien löschen? - Diese Aktion kann nicht rückgängig gemacht werden – alle empfangenen und gesendeten Dateien und Medien werden gelöscht. Bilder mit niedriger Auflösung bleiben erhalten. + Diese Aktion kann nicht rückgängig gemacht werden! Es werden alle empfangenen und gesendeten Dateien und Medien gelöscht. Bilder mit niedriger Auflösung bleiben erhalten. Keine empfangenen oder gesendeten Dateien %d Datei(en) mit einem Gesamtspeicherverbrauch von %s nie @@ -616,7 +616,7 @@ %s Sekunde(n) Löschen der Nachrichten Automatisches Löschen von Nachrichten aktivieren? - Diese Aktion kann nicht rückgängig gemacht werden – alle empfangenen und gesendeten Nachrichten, die über den ausgewählten Zeitraum hinaus gehen, werden gelöscht. Dieser Vorgang kann mehrere Minuten dauern. + Diese Aktion kann nicht rückgängig gemacht werden! Es werden alle empfangenen und gesendeten Nachrichten, die über den ausgewählten Zeitraum hinaus gehen, gelöscht. Dieser Vorgang kann mehrere Minuten dauern. Nachrichten löschen Fehler beim Ändern der Einstellung @@ -667,7 +667,7 @@ Der Versuch, das Passwort der Datenbank zu ändern, konnte nicht abgeschlossen werden. Datenbanksicherung wiederherstellen Datenbanksicherung wiederherstellen? - Bitte geben Sie das vorherige Passwort ein, nachdem Sie die Datenbanksicherung wiederhergestellt haben. Diese Aktion kann nicht rückgängig gemacht werden. + Bitte geben Sie das vorherige Passwort ein, nachdem Sie die Datenbanksicherung wiederhergestellt haben. Diese Aktion kann nicht rückgängig gemacht werden! Wiederherstellen Fehler bei der Wiederherstellung der Datenbank Das Passwort wurde nicht im Schlüsselbund gefunden. Bitte geben Sie es manuell ein. Das kann passieren, wenn Sie die App-Daten mit einem Backup-Programm wieder hergestellt haben. Bitte nehmen Sie Kontakt mit den Entwicklern auf, wenn das nicht der Fall ist. @@ -989,7 +989,7 @@ App-Version: v%s Core Version: v%s Profil hinzufügen - Alle Chats und Nachrichten werden gelöscht. Dies kann nicht rückgängig gemacht werden! + Es werden alle Chats und Nachrichten gelöscht. Dies kann nicht rückgängig gemacht werden! Chat-Profil löschen für PING-Zähler Transport-Isolations-Modus aktualisieren\? @@ -1088,7 +1088,7 @@ Datenbank-Aktualisierung Unterschiedlicher Migrationsstand in der App/Datenbank: %s / %s Datenbank herabstufen und den Chat öffnen - Inkompatible Datenbank-Version + Datenbank-Version nicht kompatibel Warnung: Sie könnten einige Daten verlieren! Datenbank auf alte Version herabstufen Datenbank-IDs und Transport-Isolationsoption. @@ -1555,14 +1555,14 @@ Falsche Desktop-Adresse Geräte Desktop-Verbindung trennen? - Desktop-App-Version %s ist mit dieser App nicht kompatibel. + Die Desktop-App-Version %s ist nicht mit dieser App kompatibel. Neues Mobiltelefon-Gerät Nur ein Gerät kann gleichzeitig genutzt werden Verknüpfe Mobiltelefon- und Desktop-Apps! 🔗 Über ein sicheres quantenbeständiges Protokoll Vom Desktop aus nutzen und scannen Sie den QR-Code.]]> Um unerwünschte Nachrichten zu verbergen. - Inkompatible Version + Version nicht kompatibel (Neu)]]> Desktop entkoppeln? Verknüpfte Desktop-Optionen @@ -1854,14 +1854,14 @@ Fehler: %1$s Warnung bei der Nachrichtenzustellung Falscher Schlüssel oder unbekannte Verbindung - höchstwahrscheinlich ist diese Verbindung gelöscht. - Die Server-Version ist nicht mit den Netzwerk-Einstellungen kompatibel. + Die Server-Version ist nicht mit den Netzwerkeinstellungen kompatibel. Kapazität überschritten - der Empfänger hat die zuvor gesendeten Nachrichten nicht empfangen. Weiterleitungsserver: %1$s \nFehler auf dem Zielserver: %2$s Weiterleitungsserver: %1$s \nFehler: %2$s Netzwerk-Fehler - die Nachricht ist nach vielen Sende-Versuchen abgelaufen. - Die Server-Adresse ist nicht mit den Netzwerk-Einstellungen kompatibel. + Die Server-Adresse ist nicht mit den Netzwerkeinstellungen kompatibel. Immer Privates Routing Nie @@ -1967,8 +1967,8 @@ Inaktiv Verbunden Verbinden - Abonnierte Verbindungen - Aktueller Nutzer + Aktive Verbindungen + Aktuelles Profil Detaillierte Statistiken Details Heruntergeladen @@ -1977,7 +1977,7 @@ Fehler beim Zurücksetzen der Statistiken Fehler Empfangene Nachrichten - Nachrichten-Abonnements + Nachrichtenempfang Ausstehend Bisher verbundene Server Proxy-Server @@ -2010,7 +2010,7 @@ Aktualisierung installieren Bitte starten Sie die App neu. Bestätigt - Alle Nutzer + Alle Profile App-Aktualisierung wurde heruntergeladen Nach Aktualisierungen suchen Daten-Pakete heruntergeladen @@ -2026,7 +2026,7 @@ Keine Information - es wird versucht neu zu laden Dateispeicherort öffnen andere - Die Server-Adresse ist nicht mit den Netzwerk-Einstellungen kompatibel: %1$s. + Die Server-Adresse ist nicht mit den Netzwerkeinstellungen kompatibel: %1$s. Link scannen / einfügen Alle Server neu verbinden Server neu verbinden? @@ -2045,13 +2045,13 @@ Server-Adresse Später erinnern Server-Informationen - Ihre App ist nicht kompatibel mit der Server-Version: %1$s. - Prozentualer Anteil der Abonnements + Ihre App ist nicht mit der Server-Version kompatibel: %1$s. + Prozentualen Anteil anzeigen Zoom SMP-Server Informationen zeigen für Beginnend mit %s. -\nAlle Daten sind auf Ihrem Gerät geschützt. +\nAlle Daten werden nur auf Ihrem Gerät gespeichert. Statistiken Transport-Sitzungen Hochgeladen @@ -2071,4 +2071,15 @@ Aktualisierung verfügbar: %s Aktivieren Sie die periodische Überprüfung auf stabile oder Beta-Versionen der App, um über neue Versionen benachrichtigt zu werden. Herunterladen der Aktualisierung abgebrochen + Die Weiterleitungsserver-Adresse ist nicht kompatibel mit den Netzwerkeinstellungen: %1$s. + Die Verbindung des Weiterleitungsservers %1$s zum Zielserver %2$s schlug fehl. Bitte versuchen Sie es später erneut. + Die Zielserver-Version von %1$s ist nicht mit dem Weiterleitungsserver %2$s kompatibel. + Die Weiterleitungsserver-Version ist nicht kompatibel mit den Netzwerkeinstellungen: %1$s. + Die Zielserver-Adresse von %1$s ist nicht mit den Einstellungen des Weiterleitungsservers %2$s kompatibel. + Fehler beim Verbinden zum Weiterleitungsserver %1$s. Bitte versuchen Sie es später erneut. + Medium unscharf machen + Weich + Hart + Medium + Aus \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml index dcf6b25181..836ab0865d 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml @@ -1777,7 +1777,7 @@ Servidor de reenvío: %1$s \nError: %2$s Problema en la red - el mensaje ha expirado tras muchos intentos de envío. - La versión del servidor es incompatible con la configuración de red. + La versión del servidor es incompatible con la configuración de la red. Enrutamiento privado Con servidores desconocidos NO usar enrutamiento privado. @@ -1841,7 +1841,7 @@ información cola del servidor: %1$s \n \núltimo mensaje recibido: %2$s - Restablecer al tema de la app + Restablecer al tema de la aplicación Enrutamiento privado de mensajes 🚀 Recibe archivos de forma segura Mejora del envío de mensajes @@ -1859,19 +1859,135 @@ Error al inicializar WebView. Actualiza tu sistema a la última versión. Por favor, ponte en contacto con los desarrolladores. \nError: %s Interfaz en persa - Clave incorrecta o dirección de bloque de datos del archivo desconocida - lo más probable es que el archivo se haya borrado. - Archivo no encontrado - el archivo probablemente ha sido borrado o cancelado. + Clave incorrecta o dirección del bloque del archivo desconocida. Es probable que el archivo se haya eliminado. + Archivo no encontrado, probablemente haya sido borrado o cancelado. Error del servidor de archivos: %1$s Error de archivo - Error de archivo temporal + Error en archivo temporal Estado del mensaje: %s Estado del mensaje Estado del archivo: %s Estado del archivo - Comprueba que el móvil y el ordenador están conectados a la misma red local y que el firewall del ordenador permite la conexión. + Comprueba que el móvil y el ordenador están conectados a la misma red local y que el cortafuegos del ordenador permite la conexión. \nPor favor, comparte cualquier otro problema con los desarrolladores. - Error al copiar + Copiar error Este enlace ha sido usado en otro dispositivo móvil, por favor crea un enlace nuevo en el ordenador. No se puede enviar el mensaje Las preferencias seleccionadas no permiten este mensaje. + Info servidores + Archivos + Mostrando info de + Suscrito + Errores de suscripción + Suscripciones ignoradas + Para ser notificado sobre versiones nuevas, activa el chequeo periódico para las versiones Estable o Beta. + Beta + Servidores SMP configurados + Servidores XFTP configurados + Servidores conectados + Conectando + Perfil actual + Zoom + Subido + Actualización disponible: %s + Descarga de actualización cancelada + Actualización descargada + Buscar actualizaciones + Buscar actualizaciones + Estadísticas + Total + Sesiones de transporte + Servidor XFTP + No estás conectado a estos servidores. Para enviarles mensajes se usa el enrutamiento privado. + Todos los perfiles + Conectado + Estadísticas detalladas + Detalles + Archivos subidos + Errores en subida + intentos + Completado + Conexiones + Creado + errores de descifrado + Eliminado + Errores de borrado + desactivado + Mensaje reenviado + El mensaje puede ser entregado más tarde si el miembro vuelve a estar activo. + Miembro inactivo + Por favor, inténtalo más tarde. + Error de enrutamiento privado + La dirección del servidor es incompatible con la configuración de red: %1$s. + La versión del servidor es incompatible con tu aplicación: %1$s. + Tamaño fuente + Error al restablecer las estadísticas + Restablecer + Las estadísticas de los servidores serán restablecidas. ¡No podrá deshacerse! + Descargado + Servidor SMP + Aún no hay conexión directa, el mensaje es reenviado por el administrador. + Otros servidores SMP + Otros servidores XFTP + Escanear / Pegar enlace + Mostrar porcentaje + Desactivar + Desactivado + Descargando actualización, por favor no cierres la aplicación + Descarga %s (%s) + Instalación completada + Instalar actualización + Abrir ubicación del archivo + Por favor, reinicia la aplicación. + Recordar más tarde + Saltar esta versión + Estable + inactivo + Error + Error al reconectar con el servidor + Error al reconectar con los servidores + Errores + Recepción de mensajes + Mensajes recibidos + Mensajes enviados + Sin información, intenta recargar + Pendiente + Servidores conectados previamente + Servidores con proxy + Mensajes recibidos + Total recibido + Errores de recepción + Reconectar todos los servidores + ¿Reconectar servidor? + ¿Reconectar servidores? + Reconectar el servidor para forzar la entrega de mensajes. Usa tráfico adicional. + Reconectar todos los servidores para forzar la entrega de mensajes. Usa tráfico adicional. + Restablecer estadísticas + ¿Restablecer todas las estadísticas? + Mensajes enviados + Total enviado + Archivos descargados + Errores en la descarga + duplicados + expirado + Abrir configuración del servidor + otro + otros errores + Con proxy + Reconectar + Seguro + Errores de envío + Enviado directamente + Enviado mediante proxy + Dirección del servidor + Tamaño + Conexiones activas + Iniciado desde %s. + Iniciado desde %s +\nTodos los datos son privados a tu dispositivo + Bloques eliminados + Bloques descargados + Bloques subidos + Reconocido + Errores de reconocimiento \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml index 92091ef126..a077223b45 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml @@ -1135,7 +1135,7 @@ Partager l\'adresse Vous pouvez partager cette adresse avec vos contacts pour leur permettre de se connecter avec %s. Aperçu - Fond d\'écran + Fond Thème sombre Exporter le thème Importer un thème @@ -1873,4 +1873,120 @@ Ce lien a été utilisé avec un autre appareil mobile, veuillez créer un nouveau lien sur le desktop. Impossible d\'envoyer le message Les paramètres de chat sélectionnés ne permettent pas l\'envoi de ce message. + Connections actives + Tous les profiles + Reçu avec accusé de réception + La mise à jour de l\'app est téléchargée + Terminé + Profil actuel + Erreurs de déchiffrement + désactivé + Supprimé + Désactiver + Téléchargement de la mise à jour de l\'appli, ne pas fermer l\'appli + Téléchargement %s (%s) + Erreur de reconnexion au serveur + inactif + Scanner / Coller le lien + Le message peut être transmis plus tard si le membre devient actif. + Reconnecter tous les serveurs connectés pour forcer la livraison des messages. Cette méthode utilise du trafic supplémentaire. + Sessions de transport + L\'adresse du serveur est incompatible avec les paramètres réseau : %1$s. + La version du serveur est incompatible avec votre application : %1$s. + Erreur de routage privé + Veuillez essayer plus tard. + Membre inactif + Message transféré + Pas de connexion directe pour l\'instant, le message est transmis par l\'administrateur. + Serveurs connectés + Serveurs précédemment connectés + Serveurs routés via des proxy + Vous n\'êtes pas connecté à ces serveurs. Le routage privé est utilisé pour leur délivrer des messages. + Reconnecter le serveur ? + Reconnecter les serveurs ? + Reconnecter le serveur pour forcer la livraison des messages. Utilise du trafic supplémentaire. + Erreur de réinitialisation des statistiques + Réinitialiser + Les statistiques des serveurs seront réinitialisées - il n\'est pas possible de revenir en arrière ! + Téléversé + Statistiques détaillées + Messages envoyés + Serveur SMP + Chunks supprimés + Chunks téléchargés + Fichiers téléchargés + Erreurs de téléchargement + Adresse du serveur + Erreurs de téléversement + Bêta + Vérifier les mises à jour + Vérifier les mises à jour + Serveurs SMP configurés + Serveurs XFTP configurés + Désactivé + Installé avec succès + Installer la mise à jour + Ouvrir l\'emplacement du fichier + Autres serveurs SMP + Autres serveurs XFTP + Veuillez redémarrer l\'application. + Rappeler plus tard + Afficher le pourcentage + Sauter cette version + Stable + Pour être informé des nouvelles versions, activez la vérification périodique des versions Stable ou Bêta. + Mise à jour disponible : %s + Téléchargement de la mise à jour annulé + Taille de police + Zoom + tentatives + Connecté + Connexion + Détails + Téléchargé + Erreur + Erreur de reconnexion des serveurs + Erreurs + Fichiers + Réception de message + Messages reçus + Messages envoyés + Pas d\'information, essayez de recharger + En attente + Routé via un proxy + Messages reçus + Total reçu + Erreurs de réception + Reconnecter + Reconnecter tous les serveurs + Réinitialiser toutes les statistiques + Réinitialiser toutes les statistiques ? + Envoyé directement + Total envoyé + Envoyé via un proxy + Infos serveurs + Afficher les informations pour + À partir de %s. + À partir de %s. +\nToutes les données restent confinées dans votre appareil. + Statistiques + Total + Serveur XFTP + Erreurs d\'accusé de réception + Chunks téléversés + Connexions + Créé + Erreurs de suppression + doublons + expiré + Ouvrir les paramètres du serveur + autre + autres erreurs + Sécurisé + Erreurs d\'envoi + Taille + Inscrit + Erreurs d\'inscription + Inscriptions ignorées + Fichiers téléversés \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml index 7195024ab5..e1e3e00d71 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml @@ -149,7 +149,7 @@ Kamera A Keystore-hoz nem sikerül hozzáférni az adatbázis jelszó mentése végett hívás folyamatban - Fotók automatikus elfogadása + Képek automatikus elfogadása A hívások kezdeményezése engedélyezve van az ismerősei számára. ALKALMAZÁS IKON Kiszolgáló hozzáadása QR-kód beolvasásával. @@ -293,7 +293,7 @@ Hitelesítés törlése készítő Megerősítés - Törlés nálam + Csak nálam %d üzenet törlése? Egyedi témák kapcsolódás (elfogadva) @@ -361,7 +361,7 @@ Az adatbázis titkosítás jelmondata 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 - Eszközhitelesítés kikapcsolva. SimpleX zárolás kikapcsolása. + A készüléken nincs beállítva a képernyőzár. A SimpleX zár ki van kapcsolva. Letiltás Letiltás minden csoport számára Engedélyezés minden csoport számára @@ -372,7 +372,7 @@ Számítógép címe %dmp Kézbesítési jelentések! - Eszközhitelesítés nem engedélyezett.A SimpleX zárolás bekapcsolható a Beállításokon keresztül, miután az eszköz hitelesítés engedélyezésre került. + A készüléken nincs beállítva a képernyőzár. A SimpleX zár az „Adatvédelem és biztonság” menüben kapcsolható be, miután beállította a képernyőzárat az eszközén. Titkosítás visszafejtési hiba Eltűnik ekkor: %s szerkesztve @@ -398,7 +398,7 @@ Eltűnő üzenet Ne hozzon létre címet Ne mutasd újra - SimpleX zárolás kikapcsolása + SimpleX zár kikapcsolása e2e titkosított ESZKÖZ e2e titkosított videóhívás @@ -424,7 +424,7 @@ engedélyezve az ön számára Eltűnő üzenetek Törlés - Törlés és ismerős értesítése + Törlés, és az ismerős értesítése letiltva %d másodperc Minden fájl törlése @@ -463,7 +463,7 @@ Adatbázis jelmondat szükséges a csevegés megnyitásához. %dnap Engedélyezés mindenki számára - Kézbesítési jelentések kikapcsolva! + A kézbesítési jelentések le vannak tiltva! Kibontás Hiba az üzenet küldésekor Jelkód megadása @@ -613,7 +613,7 @@ Csoport beállítások Hiba: %s Eltűnő üzenetek - SimpleX zárolás engedélyezése + SimpleX zár bekapcsolása Hiba a kapcsolat szinkronizálása során Hiba a cím létrehozásakor engedélyezve @@ -663,7 +663,7 @@ Azonnali értesítések Inkognitó mód Csevegési adatbázis importálása? - Azonnali értesítések kikapcsolva! + Az azonnali értesítések le vannak tiltva! Azonnali értesítések! Kép A fájlok- és a médiatartalom küldése le van tiltva ebben a csoportban. @@ -678,7 +678,7 @@ 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.]]> - Kapott SimpleX Chat meghívó hivatkozását megnyithatja böngészőjében: + A kapott SimpleX Chat meghívó hivatkozását megnyithatja 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 @@ -726,7 +726,7 @@ Hamarosan további fejlesztések érkeznek! Az üzenetreakciók küldése le van tiltva ebben a csevegésben. Helytelen biztonsági kód! - Ez akkor fordulhat elő, ha ön, vagy az ismerőse régi adatbázis biztonsági mentést használt. + Ez akkor fordulhat elő, ha ön vagy az ismerőse régi adatbázis biztonsági mentést használt. Új asztali alkalmazás! Most már az adminok is: \n- törölhetik a tagok üzeneteit. @@ -853,7 +853,7 @@ Csoport elhagyása? nem Hamarosan további fejlesztések érkeznek! - ki + kikapcsolva SimpleX Chat telepítése a terminálhoz Új megjelenített név: Új jelmondat… @@ -1098,7 +1098,7 @@ Hívások nem sikerült elküldeni KEZELŐFELÜLET SZÍNEI - Visszaállít + Visszaállítás Előző jelszó megadása az adatbázis biztonsági mentésének visszaállítása után. Ez a művelet nem vonható vissza. Másodlagos SOCKS PROXY @@ -1192,7 +1192,7 @@ Színek alaphelyzetbe állítása Mentés Váltás - Kapott hivatkozás beillesztése az ismerősökhöz történő kapcsolódáshoz… + A kapott hivatkozás beillesztése az ismerősökhöz történő kapcsolódáshoz… Beolvasás Port megnyitása a tűzfalon indítás… @@ -1217,7 +1217,7 @@ Eltávolítás Tor .onion kiszolgálók használata Felfedés - SimpleX zárolási mód + Zárolási mód Fájl visszavonása XFTP kiszolgálók A fájlok- és a médiatartalom küldése le van tiltva. @@ -1256,21 +1256,21 @@ Lengyel kezelőfelület Kiszolgáló használata Fogadva ekkor: %s - SimpleX zárolás + SimpleX zár Mentés és csoporttagok értesítése Alaphelyzetbe állítás Csak az ismerőse tud üzenetreakciókat küldeni. Hangüzenetek elhagyta a csoportot Hangüzenet rögzítése - SimpleX zárolás bekapcsolva + SimpleX zár bekapcsolva közvetlen üzenet küldése Beolvasás mobilról Kapcsolatok ellenőrzése Üzenet megosztása… másodperc - SimpleX zárolás nincs engedélyezve! - SimpleX zárolás + A SimpleX zár nincs bekapcsolva! + SimpleX zár Beállítások Csevegési adatbázis %1$s eltávolítva @@ -1338,11 +1338,11 @@ A kiszolgálónak engedélyre van szüksége a várólisták létrehozásához, ellenőrizze jelszavát Kapcsolódni fog a csoport összes tagjához. Lehetséges, hogy a kiszolgáló címében szereplő tanúsítvány-ujjlenyomat helytelen - Az adatavédelem érdekében kapcsolja be a SimpleX zárolás funkciót. -\nA funkció engedélyezése előtt a rendszer felszólítja a hitelesítés befejezésére. + A biztonsága érdekében kapcsolja be a SimpleX zár funkciót. +\nA funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beállítására az eszközén. A videó akkor érkezik meg, amikor a küldője elérhető lesz, várjon, vagy ellenőrizze később! Hálózati kapcsolat ellenőrzése a következővel: %1$s, és próbálja újra. - A SimpleX zárolás a Beállításokon keresztül kapcsolható be. + 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. A kép nem dekódolható. Próbálja meg egy másik képpel, vagy lépjen kapcsolatba a fejlesztőkkel. @@ -1404,9 +1404,9 @@ A csevegési adatbázis nem titkosított - állítson be egy jelmondatot annak védelméhez. Közvetlen internet kapcsolat használata? Továbbra is kap hívásokat és értesítéseket a némított profiloktól, ha azok aktívak. - A fő csevegési profilja megküldésre kerül a csoporttagok számára + A fő csevegési profilja elküldésre kerül a csoporttagok számára Később engedélyezheti őket az alkalmazás Adatvédelem és biztonság menüpontban. - Rejtett profiljának felfedéséhez írja be a teljes jelszót a Csevegési profilok oldal keresőmezőjébe. + Rejtett profilja megjelenítéséhez írja be a teljes jelszavát a keresőmezőbe a Csevegési profilok menüben. A csevegés frissítése és megnyitása Hangüzeneteket küldéséhez engedélyeznie kell azok küldését az ismerősei számára. fogadja az üzeneteket, ismerősöket – a kiszolgálók, amelyeket az üzenetküldéshez használ.]]> @@ -1430,7 +1430,7 @@ a SimpleX Chat fejlesztőivel, ahol bármiről kérdezhet és értesülhet az újdonságokról.]]> Opcionális üdvözlő üzenettel. Ismeretlen adatbázis hiba: %s - Elrejthet vagy némíthat egy felhasználói profilt - tartsa lenyomva a menühöz. + Elrejtheti vagy lenémíthatja a felhasználó profiljait - koppintson (vagy asztali alkalmazásban kattintson) hosszan a profilra a felugró menühöz. Inkognító mód kapcsolódáskor. Tor .onion kiszolgálók beállításainak frissítése? 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. @@ -1454,7 +1454,7 @@ Profilja csak az ismerőseivel kerül megosztásra. Néhány kiszolgáló megbukott a teszten: Koppintson a csatlakozáshoz - 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ú fotók viszont megmaradnak. + 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! @@ -1589,7 +1589,7 @@ ismeretlen státusz %1$s megváltoztatta a nevét erre: %2$s törölt kapcsolattartási cím - törölt profilkép + törölte a profilképét új kapcsolattartási cím beállítása új profilképet állított be frissített profil @@ -1700,7 +1700,7 @@ Átköltöztetés befejezve Átköltöztetés egy másik eszközre QR-kód használatával. Átköltöztetés - Megjegyzés: ha két eszközön is ugyanazt az adatbázist használja, akkor biztonsági védelemként megszakítja a kapcsolataiból érkező üzenetek visszafejtését.]]> + Megjegyzés: ha két eszközön is ugyanazt az adatbázist használja, akkor biztonsági védelemként megszakítja az ismerőseitől érkező üzenetek visszafejtését.]]> Megpróbálhatja még egyszer. Hibás hivatkozás végpontok közötti kvantumrezisztens titkosítás @@ -1884,7 +1884,7 @@ Kapcsolódás Hibák Függőben - Kezdve ettől: %s. + Ekkortól kezdve: %s. \nMinden adat biztonságban van a készülékén. Elküldött üzenetek Proxyzott kiszolgálók @@ -1909,7 +1909,7 @@ Összes elküldött Proxyn keresztül küldve SMP-kiszolgáló - Kezdve ettől: %s. + Ekkortól kezdve: %s. Feltöltve XFTP-kiszolgáló Proxyzott @@ -1930,13 +1930,13 @@ Nyugtázott hibák próbálkozások Törölt fájltöredékek - Minden felhasználó + Összes profil Feltöltött fájltöredékek Elkészült Kapcsolódott kiszolgálók Beállított XFTP-kiszolgálók Kapcsolódva - Jelenlegi felhasználó + Jelenlegi profil Részletek visszafejtési hibák Törölve @@ -1959,12 +1959,12 @@ Információk megjelenítése ehhez: A kiszolgáló verziója nem kompatibilis az alkalmazással: %1$s. Ön nem kapcsolódik ezekhez a kiszolgálókhoz. A privát útválasztás az üzenetek kézbesítésére szolgál. - Feliratkozott kapcsolatok - Üzenet feliratkozások + Aktív kapcsolatok száma + Üzenetjelentés Feliratkozva Feliratkozási hibák Elutasított feliratkozások - Feliratkozási százalék + Százalék megjelenítése Alkalmazásfrissítés letöltve Frissítések keresése Frissítések keresése 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 d41069a1b2..cd62881dad 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml @@ -49,4 +49,53 @@ Tentang SimpleX Chat Terima Terima + Batalkan + Kamera + tebal + menelepon… + Bluetooth + Panggilan ditutup + Ubah + Peringatan: arsip akan dihapus.]]> + Buat grup: untuk membuat grup baru.]]> + Blokir anggota + Blokir + Kamera + Hitam + Blokir anggota? + Ubah alamat penerima + Beta + Blokir untuk semua + Blokir anggota untuk semua? + Panggilan telah ditutup! + Seluler + Hubungkan melalui tautan satu kali? + Gabung Grup? + Gunakan profil saat ini + Gunakan profil penyamaran baru + Profil Anda akan dikirim ke kontak yang menerima tautan ini. + Anda akan terhubung ke semua anggota grup. + Hubungkan + Hubungkan penyamaran + Membuka basis data… + k + Hubungkan melalui alamat kontak? + Bersihkan + Bersihkan + Bersihkan + Cek koneksi internetmu dan coba lagi + Hubungkan + terhubung + Hubungkan + Terhubung + Terhubung + terhubung + Hubungkan langsung? + tersambung + selesai + terhubung + Komputer yang terhubung + Terhubung + Selesai + Ponsel yang terhubung \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml index f2f62c1fb9..e5bc6ed20a 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml @@ -1874,7 +1874,7 @@ Impossibile inviare il messaggio Le preferenze della chat selezionata vietano questo messaggio. Connessioni - Connessioni sottoscritte + Connessioni attive Creato errori di decifrazione Statistiche dettagliate @@ -1883,7 +1883,7 @@ Errore di riconnessione al server Errore di riconnessione ai server scaduto - Iscrizioni ai messaggi + Ricezione messaggi altro altri errori In attesa @@ -1933,12 +1933,12 @@ Il messaggio può essere consegnato più tardi se il membro diventa attivo. Server SMP configurati Altri server SMP - Percentuale di iscrizione + Mostra percentuale inattivo Zoom Connesso In connessione - Utente attuale + Profilo attuale Dettagli Errori Messaggi ricevuti @@ -1948,7 +1948,7 @@ Informazioni di Statistiche Sessioni di trasporto - Tutti gli utenti + Tutti i profili tentativi Server XFTP configurati Completato @@ -1989,4 +1989,15 @@ Aggiornamento dell\'app scaricato Cerca aggiornamenti Installa aggiornamento + Il server di inoltro %1$s non è riuscito a connettersi al server di destinazione %2$s. Riprova più tardi. + L\'indirizzo del server di inoltro è incompatibile con le impostazioni di rete: %1$s. + L\'indirizzo del server di destinazione di %1$s è incompatibile con le impostazioni del server di inoltro %2$s. + La versione del server di destinazione di %1$s è incompatibile con il server di inoltro %2$s. + Errore di connessione al server di inoltro %1$s. Riprova più tardi. + La versione server di inoltro è incompatibile con le impostazioni di rete: %1$s. + Off + Sfocatura file multimediali + Leggera + Media + Forte \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml index 34255ce99b..58f09108ce 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml @@ -863,7 +863,7 @@ Verbied het sturen van directe berichten naar leden. Verbieden het verzenden van spraak berichten. De beveiliging van SimpleX Chat is gecontroleerd door Trail of Bits. - Met optioneel welkomst bericht. + Met optioneel welkom bericht. Spraak berichten Uw contacten kunnen volledige verwijdering van berichten toestaan. U moet elke keer dat de app start het wachtwoord invoeren, deze wordt niet op het apparaat opgeslagen. @@ -961,12 +961,12 @@ Chinese en Spaanse interface Voer wachtwoord in bij zoeken Fout bij opslaan gebruikers wachtwoord - Welkomst bericht toevoegen + Welkom bericht toevoegen Niet meer weergeven Groep moderatie Fout bij updaten van gebruikers privacy Verder verminderd batterij verbruik - Groep welkomst bericht + Groep welkom bericht Verborgen chat profielen Profiel verbergen Verbergen @@ -984,16 +984,16 @@ Servers opslaan\? Bewaar profiel wachtwoord Stel het getoonde bericht in voor nieuwe leden! - Welkomst bericht opslaan\? + Welkom bericht opslaan? Ondersteuning voor bluetooth en andere verbeteringen. Tik om profiel te activeren. Dank aan de gebruikers – draag bij via Weblate! U kunt een gebruikers profiel verbergen of dempen - houd het vast voor het menu. zichtbaar maken Dempen opheffen - Welkomst bericht + Welkom bericht Om uw verborgen profiel te onthullen, voert u een volledig wachtwoord in een zoekveld in op de pagina Uw chat profielen. - Welkomst bericht + Welkom bericht U ontvangt nog steeds oproepen en meldingen van gedempte profielen wanneer deze actief zijn. Database downgraden Ongeldige migratie bevestiging @@ -1135,13 +1135,13 @@ Laten we praten in SimpleX Chat Sla instellingen voor automatisch accepteren op Instellingen opslaan\? - Voer welkomst bericht in... (optioneel) + Voer welkom bericht in... (optioneel) Maak geen adres aan Hoi! \nMaak verbinding met mij via SimpleX Chat: %s U kan het later maken Adres delen - Welkomst bericht invoeren… + Welkom bericht invoeren… Thema importeren SimpleX Extra accent @@ -1181,18 +1181,18 @@ Als u uw zelfvernietigings wachtwoord invoert tijdens het openen van de app: Als u deze toegangscode invoert bij het openen van de app, worden alle app-gegevens onomkeerbaar verwijderd! Toegangscode instellen - Berichtreacties verbieden. - Alleen jij kunt berichtreacties toevoegen. + Bericht reacties verbieden. + Alleen jij kunt bericht reacties toevoegen. Reacties op berichten zijn verboden in deze groep. Berichten reacties verbieden. - Sta berichtreacties alleen toe als uw contact dit toestaat. - Sta uw contactpersonen toe om berichtreacties toe te voegen. - Sta berichtreacties toe. - Groepsleden kunnen berichtreacties toevoegen. - Zowel u als uw contact kunnen berichtreacties toevoegen. + Sta bericht reacties alleen toe als uw contact dit toestaat. + Sta uw contactpersonen toe om bericht reacties toe te voegen. + Sta bericht reacties toe. + Groepsleden kunnen bericht reacties toevoegen. + Zowel u als uw contact kunnen bericht reacties toevoegen. Reacties op berichten Reacties op berichten zijn verboden in deze chat. - Alleen uw contact kan berichtreacties toevoegen. + Alleen uw contact kan bericht reacties toevoegen. dagen uren minuten @@ -1628,7 +1628,7 @@ je hebt %s geblokkeerd je hebt %s gedeblokkeerd Bericht te groot - Welkomstbericht is te lang + Welkom bericht is te lang De databasemigratie wordt uitgevoerd. \nDit kan enkele minuten duren. Audio oproep @@ -1834,7 +1834,7 @@ Bescherm uw IP-adres tegen de berichtenrelais die door uw contacten zijn gekozen. \nSchakel dit in in *Netwerk en servers*-instellingen. Herhalen - Chatkleuren + Chat kleuren Profiel thema Chat thema Extra accent 2 @@ -1870,7 +1870,7 @@ Bestandsstatus: %s Berichtstatus Kan bericht niet verzenden - Geselecteerde chatvoorkeuren verbieden dit bericht. + Geselecteerde chat voorkeuren verbieden dit bericht. Fout in privéroutering Serverversie is niet compatibel met uw app: %1$s. Bericht doorgestuurd @@ -1878,17 +1878,17 @@ Overige XFTP servers Link scannen/plakken Zoom - Huidige gebruiker + Huidig profiel Bestanden Server informatie Informatie weergeven voor Fouten Statistieken Transportsessies - Verbindingen geabonneerd + Actieve verbindingen Details Berichten ontvangen - Berichten abonnementen + Bericht ontvangst Beginnend vanaf %s. \nAlle gegevens zijn privé op uw apparaat. Verbonden servers @@ -1933,13 +1933,13 @@ Gedownloade bestanden Beveiligd Maat - Geabonneerd + Ingeschreven Geüploade bestanden Upload fouten Downloadfouten Server instellingen openen Server adres - Alle gebruikers + Alle profielen pogingen Stukken verwijderd voltooid @@ -1963,4 +1963,28 @@ Het serveradres is niet compatibel met de netwerkinstellingen: %1$s. Serverstatistieken worden gereset - dit kan niet ongedaan worden gemaakt! Beginnend vanaf %s. + Geconfigureerde SMP-servers + Geconfigureerde XFTP-servers + Percentage weergeven + Uitgeschakeld + App update downloaden. Sluit de app niet + %s downloaden (%s) + Succesvol geïnstalleerd + Installeer update + Open de bestandslocatie + Herstart de app. + Herinner later + Sla deze versie over + uitgeschakeld + App update is gedownload + Beta + Controleer op updates + Controleer op updates + Uitschakelen + Inschrijving fouten + Inschrijvingen genegeerd + Stabiel + Update beschikbaar: %s + Downloaden van update geannuleerd + Als u op de hoogte wilt worden gehouden van de nieuwe releases, schakelt u periodieke controle op stabiele of bètaversies in. \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml index ce2e3751a5..2422148b45 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml @@ -1873,4 +1873,126 @@ Zły klucz lub nieznany adres fragmentu pliku - najprawdopodobniej plik został usunięty. Nie można wysłać wiadomości Wybrane preferencje czatu zabraniają tej wiadomości. + Błąd połączenia z serwerem przekierowania %1$s. Spróbuj ponownie później. + Adres serwera docelowego %1$s jest niekompatybilny z ustawieniami serwera przekazującego %2$s. + Serwer przekazujący %1$s nie mógł połączyć się z serwerem docelowym %2$s. Spróbuj ponownie później. + Wersja serwera przekierowującego jest niekompatybilna z ustawieniami sieciowymi: %1$s. + Wersja serwera docelowego %1$s jest niekompatybilna z serwerem przekierowującym %2$s. + Członek nieaktywny + Wiadomość przekazana + Wiadomość może zostać dostarczona później jeśli członek stanie się aktywny. + Beta + Sprawdź aktualizacje + Wyłączony + Pobierz %s (%s) + Aktualizacja aplikacji jest pobrana + Sprawdź aktualizacje + Pobieranie aktualizacji aplikacji, nie zamykaj aplikacji + Zainstalowano pomyślnie + Zainstaluj aktualizacje + Wyłącz + wyłączony + nieaktywny + Rozmiar czcionki + Połączony + Bieżący profil + Otrzymane wiadomości + Odebranie wiadomości + Błąd resetowania statystyk + duplikaty + Zakończono + Połączenia + Utworzono + Błędy usuwania + Fragmenty pobrane + Fragmenty przesłane + Pobrane pliki + Skonfigurowane serwery SMP + Potwierdzono + Błędy potwierdzenia + Usunięto + Aktywne połączenia + Wszystkie profile + Skonfigurowane serwery XFTP + błąd odszyfrowywania + Szczegółowe statystyki + Szczegóły + Błędy pobierania + Adres serwera przekierowującego jest niekompatybilny z ustawieniami sieciowymi: %1$s. + Pliki + Łączenie + Błędy + Połączone serwery + Błąd + Błąd ponownego łączenia z serwerem + Błąd ponownego łączenia serwerów + Pobrane + próby + wygasły + Fragmenty usunięte + Brak bezpośredniego połączenia, wiadomość została przekazana przez administratora. + Inne serwery SMP + Inne serwery XFTP + Pokaż procent + Stabilny + Aktualizacja dostępna: %s + Otwórz lokalizację pliku + Proszę zrestartować aplikację. + Przypomnij później + Pomiń tę wersję + Pobieranie aktualizacji anulowane + Aby otrzymywać powiadomienia o nowych wersjach, włącz okresowe sprawdzanie wersji Stabilnych lub Beta. + Przybliż + Brak informacji, spróbuj przeładować + Informacje o serwerach + Wyświetlanie informacji dla + Statystyki + Sesje transportowe + Zaczynanie od %s. +\nWszystkie dane są prywatne na Twoim urządzeniu. + Połącz ponownie wszystkie serwery + Połączyć ponownie serwer? + Nie jesteś połączony z tymi serwerami. Prywatne trasowanie jest używane do dostarczania do nich wiadomości. + Otrzymane wiadomości + Resetuj + Resetuj wszystkie statystyki + Wysłane wiadomości + Statystyki serwerów zostaną zresetowane - nie można tego cofnąć! + Przesłane + inne + Trasowane przez proxy + Otrzymano łącznie + inne błędy + Zabezpieczone + Zasubskrybowano + Przesłane pliki + Otwórz ustawienia serwera + Adres serwera + Wysłane wiadomości + Ponownie połącz ze wszystkimi połączonymi serwerami w celu wymuszenia dostarczenia wiadomości. Wykorzystuje to dodatkowy ruch. + Zresetować wszystkie statystyki? + Błędy subskrypcji + Subskrypcje zignorowane + Wysłano łącznie + Adres serwera jest niekompatybilny z ustawieniami sieci: %1$s. + Proszę spróbować później. + Błąd prywatnego trasowania + Wersja serwera jest niekompatybilna z aplikacją: %1$s. + Skanuj / Wklej link + Łącznie + Oczekujące + Wcześniej połączone serwery + Serwery trasowane przez proxy + Połączyć ponownie serwery? + Ponownie połącz z serwerem w celu wymuszenia dostarczenia wiadomości. Wykorzystuje to dodatkowy ruch. + Wysłano przez proxy + Serwer XFTP + Serwer SMP + Błędy otrzymania + Połącz ponownie + Wysłano bezpośrednio + Zaczynanie od %s. + Wyślij błędy + Rozmiar + Błędy przesłania \ 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 bf14ac248d..f0a0c53496 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 @@ -1658,4 +1658,10 @@ Permitir o envio de links do SimpleX. Todos os membros Configurações avançadas + Verificar atualizações + Completado + Servidores SMP configurados + Servidores XFTP configurados + Verifique sua conexão de internet e tente novamente + Verificar atualizações \ 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 85a7037cb6..c0b372413d 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml @@ -482,4 +482,64 @@ Máy tính Địa chỉ máy tính Máy tính có một phiên bản không được hỗ trợ. Vui lòng đảm bảo rằng bạn sử dụng cùng một phiên bản ở cả hai thiết bị. + Đã xác nhận + Các kết nối đang hoạt động + Tất cả hồ sơ + Beta + Kiểm tra cập nhật + Đang kết nối + Các máy chủ SMP đã được cấu hình + Các máy chủ XFTP đã được cấu hình + Bản cập nhật ứng dụng đã được tải xuống + Kiểm tra cập nhật + Đã kết nối + thử + Các máy chủ đã kết nối + Lỗi xác nhận + Đã hoàn thành + Các khúc đã bị xóa + Các khúc đã được tải xuống + Các khúc đã được tải lên + Hồ sơ hiện tại + lỗi giải mã + Thống kê chi tiết + Chi tiết + Các kết nối + Đã tạo + Đã xóa + Lỗi xóa + %d sự kiện nhóm + Tin nhắn trực tiếp giữa các thành viên bị cấm trong nhóm này. + %d tệp với tổng kích thước là %s + phần di dời khác nhau trong ứng dụng/cơ sở dữ liệu: %s / %s + Tin nhắn trực tiếp + %dh + %d giờ + %d giờ + Tên, hình đại diện và cách ly truyền tải khác nhau. + Thiết bị + trực tiếp + Xác thực thiết bị không được bật. Bạn có thể bật SimpleX Lock thông qua phần Cài đặt, sau khi bạn bật xác thực thiết bị. + Tắt chỉ báo đã nhận? + Tin nhắn tự xóa + Tin nhắn tự xóa + Ngắt kết nối + Tắt (giữ thông tin ghi đè) + Biến mất vào lúc: %s + Tin nhắn tự xóa + Tắt cho tất cả + Tắt cho tất cả các nhóm + Biến mất vào lúc + Ngắt kết nối + Tắt thông báo + Tắt SimpleX Lock + Tắt (giữ thông tin ghi đè về nhóm) + Tin nhắn tự xóa bị cấm trong cuộc hội thoại này. + Tắt + Tắt chỉ báo đã nhận cho nhóm? + đã bị tắt + Đã bị tắt + Tắt + đã bị tắt + Tin nhắn tự xóa bị cấm trong nhóm này. \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml index dd675791c6..92b5f792b1 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml @@ -1880,10 +1880,10 @@ 其他 SMP 服务器 其他 XFTP 服务器 扫描/粘贴链接 - 订阅百分比 + 显示百分比 不活跃 缩放 - 所有用户 + 所有配置文件 文件 没有信息,试试重新加载 服务器信息 @@ -1891,7 +1891,7 @@ 已连接 已连接的服务器 连接中 - 订阅的连接 + 活跃连接 详细统计数据 详情 已下载 @@ -1901,7 +1901,7 @@ 重设统计数据出错 错误 收到的消息 - 消息订阅 + 消息接收 待连接 先前连接的服务器 已代理的服务器 @@ -1962,7 +1962,7 @@ 订阅被忽略 已配置的 SMP 服务器 已配置的 XFTP 服务器 - 当前用户 + 当前配置文件 传输会话 已上传 已停用 @@ -1989,4 +1989,15 @@ 测试版 安装成功 安装更新 + %1$s 的目的地服务器版本不兼容转发服务器 %2$s. + 转发服务器 %1$s 连接目的地服务器 %2$s 失败。请稍后尝试。 + 转发服务器地址不兼容网络设置:%1$s。 + 转发服务器版本不兼容网络设置:%1$s。 + %1$s 的目的地服务器地址不兼容转发服务器 %2$s 的设置 + 连接转发服务器 %1$s 出错。请稍后尝试。 + 模糊媒体文件 + 中度 + 关闭 + 轻柔 + 强烈 \ No newline at end of file diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/PlatformTextField.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/PlatformTextField.desktop.kt index 3ca74a6d84..8fe8ecc68a 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/PlatformTextField.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/PlatformTextField.desktop.kt @@ -165,23 +165,14 @@ actual fun PlatformTextField( }, cursorBrush = SolidColor(MaterialTheme.colors.secondary), decorationBox = { innerTextField -> - Surface( - shape = RoundedCornerShape(18.dp), - border = BorderStroke(1.dp, MaterialTheme.colors.secondary), - contentColor = LocalContentColor.current - ) { - Row( - Modifier.background(MaterialTheme.colors.background), - verticalAlignment = Alignment.Bottom + Row(verticalAlignment = Alignment.Bottom) { + CompositionLocalProvider( + LocalLayoutDirection provides if (isRtl) LayoutDirection.Rtl else LocalLayoutDirection.current ) { - CompositionLocalProvider( - LocalLayoutDirection provides if (isRtl) LayoutDirection.Rtl else LocalLayoutDirection.current - ) { - Column(Modifier.weight(1f).padding(start = 12.dp, end = 32.dp)) { - Spacer(Modifier.height(8.dp)) - innerTextField() - Spacer(Modifier.height(10.dp)) - } + Column(Modifier.weight(1f).padding(start = 12.dp, end = 32.dp)) { + Spacer(Modifier.height(8.dp)) + innerTextField() + Spacer(Modifier.height(10.dp)) } } } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/ScrollableColumn.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/ScrollableColumn.desktop.kt index 1caf96902d..91e9c70a20 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/ScrollableColumn.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/ScrollableColumn.desktop.kt @@ -52,9 +52,11 @@ actual fun LazyColumnWithScrollBar( } } } - LazyColumn(modifier.then(if (appPlatform.isDesktop) scrollModifier else Modifier), state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled, content) - Box(Modifier.fillMaxSize(), contentAlignment = Alignment.CenterEnd) { - DesktopScrollBar(rememberScrollbarAdapter(state), Modifier.align(Alignment.CenterEnd).fillMaxHeight(), scrollBarAlpha, scrollJob, reverseLayout) + Box { + LazyColumn(modifier.then(if (appPlatform.isDesktop) scrollModifier else Modifier), state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled, content) + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.CenterEnd) { + DesktopScrollBar(rememberScrollbarAdapter(state), Modifier.align(Alignment.CenterEnd).fillMaxHeight(), scrollBarAlpha, scrollJob, reverseLayout) + } } } } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.desktop.kt index 6da2078567..38054cb873 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.desktop.kt @@ -13,6 +13,7 @@ actual fun SimpleAndAnimatedImageView( imageBitmap: ImageBitmap, file: CIFile?, imageProvider: () -> ImageGalleryProvider, + smallView: Boolean, ImageView: @Composable (painter: Painter, onClick: () -> Unit) -> Unit ) { // LALAL make it animated too diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.desktop.kt index 189f1842dd..57069a8caa 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.desktop.kt @@ -35,6 +35,7 @@ actual fun ChatListNavLinkLayout( disabled: Boolean, selectedChat: State, nextChatSelected: State, + oneHandUI: State, ) { var modifier = Modifier.fillMaxWidth() if (!disabled) modifier = modifier diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.desktop.kt index c2333393e5..9e4eeb0c96 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.desktop.kt @@ -22,53 +22,67 @@ import dev.icerock.moko.resources.compose.stringResource import kotlinx.coroutines.flow.MutableStateFlow @Composable -actual fun ActiveCallInteractiveArea(call: Call, newChatSheetState: MutableStateFlow) { - // if (call.callState == CallState.Connected && !newChatSheetState.collectAsState().value.isVisible()) { - if (!newChatSheetState.collectAsState().value.isVisible()) { - val showMenu = remember { mutableStateOf(false) } - val media = call.peerMedia ?: call.localMedia - CompositionLocalProvider( - LocalIndication provides NoIndication - ) { - Box( - Modifier - .fillMaxSize(), - contentAlignment = Alignment.BottomEnd - ) { - Box( - Modifier - .padding(end = 71.dp, bottom = 92.dp) - .size(67.dp) - .combinedClickable(onClick = { - val chat = chatModel.getChat(call.contact.id) - if (chat != null) { - withBGApi { - openChat(chat.remoteHostId, chat.chatInfo, chatModel) - } - } - }, - onLongClick = { showMenu.value = true }) - .onRightClick { showMenu.value = true }, - contentAlignment = Alignment.Center - ) { - Box(Modifier.background(MaterialTheme.colors.background, CircleShape)) { - ProfileImageForActiveCall(size = 56.dp, image = call.contact.profile.image) - } - Box(Modifier.padding().background(SimplexGreen, CircleShape).padding(4.dp).align(Alignment.TopEnd)) { - if (media == CallMediaType.Video) { - Icon(painterResource(MR.images.ic_videocam_filled), stringResource(MR.strings.icon_descr_video_call), Modifier.size(18.dp), tint = Color.White) - } else { - Icon(painterResource(MR.images.ic_call_filled), stringResource(MR.strings.icon_descr_audio_call), Modifier.size(18.dp), tint = Color.White) +actual fun ActiveCallInteractiveArea(call: Call) { + val showMenu = remember { mutableStateOf(false) } + val media = call.peerMedia ?: call.localMedia + CompositionLocalProvider( + LocalIndication provides NoIndication + ) { + Box( + Modifier + .fillMaxSize(), + contentAlignment = Alignment.BottomEnd + ) { + Box( + Modifier + .padding(end = 71.dp, bottom = 92.dp) + .size(67.dp) + .combinedClickable(onClick = { + val chat = chatModel.getChat(call.contact.id) + if (chat != null) { + withBGApi { + openChat(chat.remoteHostId, chat.chatInfo, chatModel) } } - DefaultDropdownMenu(showMenu) { - ItemAction(stringResource(MR.strings.icon_descr_hang_up), painterResource(MR.images.ic_call_end_filled), color = MaterialTheme.colors.error, onClick = { - withBGApi { chatModel.callManager.endCall(call) } - showMenu.value = false - }) - } + }, + onLongClick = { showMenu.value = true }) + .onRightClick { showMenu.value = true }, + contentAlignment = Alignment.Center + ) { + Box(Modifier.background(MaterialTheme.colors.background, CircleShape)) { + ProfileImageForActiveCall(size = 56.dp, image = call.contact.profile.image) + } + Box( + Modifier.padding().background(SimplexGreen, CircleShape).padding(4.dp) + .align(Alignment.TopEnd) + ) { + if (media == CallMediaType.Video) { + Icon( + painterResource(MR.images.ic_videocam_filled), + stringResource(MR.strings.icon_descr_video_call), + Modifier.size(18.dp), + tint = Color.White + ) + } else { + Icon( + painterResource(MR.images.ic_call_filled), + stringResource(MR.strings.icon_descr_audio_call), + Modifier.size(18.dp), + tint = Color.White + ) } } + DefaultDropdownMenu(showMenu) { + ItemAction( + stringResource(MR.strings.icon_descr_hang_up), + painterResource(MR.images.ic_call_end_filled), + color = MaterialTheme.colors.error, + onClick = { + withBGApi { chatModel.callManager.endCall(call) } + showMenu.value = false + }) + } } } + } } diff --git a/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt b/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt index d6ee659dbf..8e1643571b 100644 --- a/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt +++ b/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt @@ -9,6 +9,7 @@ import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.input.pointer.* import chat.simplex.common.model.ChatController.appPrefs +import chat.simplex.common.model.size import chat.simplex.common.platform.* import chat.simplex.common.platform.DesktopPlatform import chat.simplex.common.showApp diff --git a/apps/multiplatform/gradle.properties b/apps/multiplatform/gradle.properties index 35bc671ccf..10af65c5af 100644 --- a/apps/multiplatform/gradle.properties +++ b/apps/multiplatform/gradle.properties @@ -26,11 +26,11 @@ android.enableJetifier=true kotlin.mpp.androidSourceSetLayoutVersion=2 kotlin.jvm.target=11 -android.version_name=6.0-beta.1 -android.version_code=226 +android.version_name=6.0-beta.2 +android.version_code=227 -desktop.version_name=6.0-beta.1 -desktop.version_code=57 +desktop.version_name=6.0-beta.2 +desktop.version_code=58 kotlin.version=1.9.23 gradle.plugin.version=8.2.0 diff --git a/apps/simplex-directory-service/src/Directory/Events.hs b/apps/simplex-directory-service/src/Directory/Events.hs index 31a7e94aad..33b43a239b 100644 --- a/apps/simplex-directory-service/src/Directory/Events.hs +++ b/apps/simplex-directory-service/src/Directory/Events.hs @@ -72,7 +72,7 @@ crDirectoryEvent = \case CRDeletedMemberUser {groupInfo} -> Just $ DEServiceRemovedFromGroup groupInfo CRGroupDeleted {groupInfo} -> Just $ DEGroupDeleted groupInfo CRChatItemUpdated {chatItem = AChatItem _ SMDRcv (DirectChat ct) _} -> Just $ DEItemEditIgnored ct - CRChatItemDeleted {deletedChatItem = AChatItem _ SMDRcv (DirectChat ct) _, byUser = False} -> Just $ DEItemDeleteIgnored 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}}} -> Just $ case (mc, itemLive) of (MCText t, Nothing) -> DEContactCommand ct ciId $ fromRight err $ A.parseOnly (directoryCmdP <* A.endOfInput) $ T.dropWhileEnd isSpace t diff --git a/cabal.project b/cabal.project index 5129fe9c91..5f2c823561 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: 83f8622b2397afe2635c8f60f1ec5f6fdc16ef7c + tag: 03ea151be5dce9a4b8683f9f28805bdc8ba66758 source-repository-package type: git diff --git a/scripts/desktop/make-appimage-linux.sh b/scripts/desktop/make-appimage-linux.sh index 3bbdd65b49..5084a0276d 100755 --- a/scripts/desktop/make-appimage-linux.sh +++ b/scripts/desktop/make-appimage-linux.sh @@ -40,6 +40,10 @@ if [ ! -f ../appimagetool-x86_64.AppImage ]; then wget --secure-protocol=TLSv1_3 https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage -O ../appimagetool-x86_64.AppImage chmod +x ../appimagetool-x86_64.AppImage fi -../appimagetool-x86_64.AppImage . +if [ ! -f ../runtime-fuse3-x86_64 ]; then + wget --secure-protocol=TLSv1_3 https://github.com/AppImage/type2-runtime/releases/download/old/runtime-fuse3-x86_64 -O ../runtime-fuse3-x86_64 + chmod +x ../runtime-fuse3-x86_64 +fi +../appimagetool-x86_64.AppImage --runtime-file ../runtime-fuse3-x86_64 . mv *imple*.AppImage ../../ diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 35fc44f534..30390eeb9e 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."83f8622b2397afe2635c8f60f1ec5f6fdc16ef7c" = "1dn7b0pjlk32crp943l6lz4r376nf444kjfi167mrv1pgccri6ns"; + "https://github.com/simplex-chat/simplexmq.git"."03ea151be5dce9a4b8683f9f28805bdc8ba66758" = "1gfkqzda6vw3fsd34c08jygwg0m5v211sm7v7jdvj0sx1p8428d4"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 9460bf247d..3c634ef0b5 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -41,7 +41,7 @@ import Data.Fixed (div') import Data.Functor (($>)) import Data.Functor.Identity import Data.Int (Int64) -import Data.List (find, foldl', isSuffixOf, partition, sortOn) +import Data.List (find, foldl', isSuffixOf, mapAccumL, partition, sortOn) import Data.List.NonEmpty (NonEmpty (..), nonEmpty, toList, (<|)) import qualified Data.List.NonEmpty as L import Data.Map.Strict (Map) @@ -124,6 +124,7 @@ import Simplex.RemoteControl.Invitation (RCInvitation (..), RCSignedInvitation ( import Simplex.RemoteControl.Types (RCCtrlAddress (..)) import System.Exit (ExitCode, exitSuccess) import System.FilePath (takeFileName, ()) +import qualified System.FilePath as FP import System.IO (Handle, IOMode (..), SeekMode (..), hFlush) import System.Random (randomRIO) import Text.Read (readMaybe) @@ -407,8 +408,7 @@ startChatController mainApp enableSndFiles = do void $ forkIO $ startFilesToReceive users startCleanupManager void $ forkIO $ startExpireCIs users - else - when enableSndFiles $ startXFTP xftpStartSndWorkers + else when enableSndFiles $ startXFTP xftpStartSndWorkers pure a1 startXFTP startWorkers = do tmp <- readTVarIO =<< asks tempDirectory @@ -536,13 +536,18 @@ processChatCommand' vr = \case auId <- withAgent (\a -> createUser a smp xftp) ts <- liftIO $ getCurrentTime >>= if pastTimestamp then coupleDaysAgo else pure user <- withFastStore $ \db -> createUserRecordAt db (AgentUserId auId) p True ts - when (null users) $ withFastStore (\db -> createContact db user simplexContactProfile) `catchChatError` \_ -> pure () + createPresetContactCards user `catchChatError` \_ -> pure () withFastStore $ \db -> createNoteFolder db user storeServers user smpServers storeServers user xftpServers atomically . writeTVar u $ Just user pure $ CRActiveUser user where + createPresetContactCards :: User -> CM () + createPresetContactCards user = + withFastStore $ \db -> do + createContact db user simplexTeamContactProfile + createContact db user simplexStatusContactProfile chooseServers :: (ProtocolTypeI p, UserProtocol p) => SProtocolType p -> CM (NonEmpty (ServerCfg p), [ServerCfg p]) chooseServers protocol | sameServers = @@ -673,7 +678,7 @@ processChatCommand' vr = \case chatWriteVar sel $ Just f APISetEncryptLocalFiles on -> chatWriteVar encryptLocalFiles on >> ok_ SetContactMergeEnabled onOff -> chatWriteVar contactMergeEnabled onOff >> ok_ - APIExportArchive cfg -> checkChatStopped $ lift (exportArchive cfg) >> ok_ + APIExportArchive cfg -> checkChatStopped $ CRArchiveExported <$> lift (exportArchive cfg) ExportArchive -> do ts <- liftIO getCurrentTime let filePath = "simplex-chat." <> formatTime defaultTimeLocale "%FT%H%M%SZ" ts <> ".zip" @@ -813,43 +818,96 @@ processChatCommand' vr = \case _ -> throwChatError CEInvalidChatItemUpdate CTContactRequest -> pure $ chatCmdError (Just user) "not supported" CTContactConnection -> pure $ chatCmdError (Just user) "not supported" - APIDeleteChatItem (ChatRef cType chatId) itemId mode -> withUser $ \user -> case cType of + APIDeleteChatItem (ChatRef cType chatId) itemIds mode -> withUser $ \user -> case cType of CTDirect -> withContactLock "deleteChatItem" chatId $ do - (ct, CChatItem msgDir ci@ChatItem {meta = CIMeta {itemSharedMsgId, deletable}}) <- withFastStore $ \db -> (,) <$> getContact db vr user chatId <*> getDirectChatItem db user chatId itemId - case (mode, msgDir, itemSharedMsgId, deletable) of - (CIDMInternal, _, _, _) -> deleteDirectCI user ct ci True False - (CIDMBroadcast, SMDSnd, Just itemSharedMId, True) -> 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 + case mode of + CIDMInternal -> deleteDirectCIs user ct items True False + CIDMBroadcast -> do + assertDeletable items assertDirectAllowed user MDSnd ct XMsgDel_ - (SndMessage {msgId}, _) <- sendDirectContactMessage user ct (XMsgDel itemSharedMId Nothing) + let msgIds = itemsMsgIds items + events = map (\msgId -> XMsgDel msgId Nothing) msgIds + forM_ (L.nonEmpty events) $ \events' -> + sendDirectContactMessages user ct events' if featureAllowed SCFFullDelete forUser ct - then deleteDirectCI user ct ci True False - else markDirectCIDeleted user ct ci msgId True =<< liftIO getCurrentTime - (CIDMBroadcast, _, _, _) -> throwChatError CEInvalidChatItemDelete + 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 <- withFastStore $ \db -> getGroup db vr user chatId - CChatItem msgDir ci@ChatItem {meta = CIMeta {itemSharedMsgId, deletable}} <- withFastStore $ \db -> getGroupChatItem db user chatId itemId - case (mode, msgDir, itemSharedMsgId, deletable) of - (CIDMInternal, _, _, _) -> deleteGroupCI user gInfo ci True False Nothing =<< liftIO getCurrentTime - (CIDMBroadcast, SMDSnd, Just itemSharedMId, True) -> 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 + case mode of + CIDMInternal -> deleteGroupCIs user gInfo items True False Nothing =<< liftIO getCurrentTime + CIDMBroadcast -> do + assertDeletable items assertUserGroupRole gInfo GRObserver -- can still delete messages sent earlier - (SndMessage {msgId}, _) <- sendGroupMessage user gInfo ms $ XMsgDel itemSharedMId Nothing - delGroupChatItem user gInfo ci msgId Nothing - (CIDMBroadcast, _, _, _) -> throwChatError CEInvalidChatItemDelete + let msgIds = itemsMsgIds items + 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, CChatItem _ ci) <- withFastStore $ \db -> (,) <$> getNoteFolder db user chatId <*> getLocalChatItem db user chatId itemId - deleteLocalCI user nf ci True False + 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 + 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" - APIDeleteMemberChatItem gId mId itemId -> withUser $ \user -> withGroupLock "deleteChatItem" gId $ do - Group gInfo@GroupInfo {membership} ms <- withFastStore $ \db -> getGroup db vr user gId - CChatItem _ ci@ChatItem {chatDir, meta = CIMeta {itemSharedMsgId}} <- withFastStore $ \db -> getGroupChatItem db user gId itemId - case (chatDir, itemSharedMsgId) of - (CIGroupRcv GroupMember {groupMemberId, memberRole, memberId}, Just itemSharedMId) -> do - when (groupMemberId /= mId) $ throwChatError CEInvalidChatItemDelete - assertUserGroupRole gInfo $ max GRAdmin memberRole - (SndMessage {msgId}, _) <- sendGroupMessage user gInfo ms $ XMsgDel itemSharedMId $ Just memberId - delGroupChatItem user gInfo ci msgId (Just membership) - (_, _) -> throwChatError CEInvalidChatItemDelete + where + assertDeletable :: forall c. ChatTypeI c => [CChatItem c] -> CM () + assertDeletable items = do + currentTs <- liftIO getCurrentTime + unless (all (itemDeletable currentTs) items) $ throwChatError CEInvalidChatItemDelete + where + itemDeletable :: UTCTime -> CChatItem c -> Bool + itemDeletable currentTs (CChatItem msgDir ChatItem {meta = CIMeta {itemSharedMsgId, itemTs, itemDeleted}, content}) = + case msgDir of + -- We check with a 6 hour margin compared to CIMeta deletable to account for deletion on the border + SMDSnd -> isJust itemSharedMsgId && deletable' content itemDeleted itemTs (nominalDay + 6 * 3600) currentTs + SMDRcv -> False + 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 + assertDeletable gInfo items + assertUserGroupRole gInfo GRAdmin + let msgMemIds = itemsMsgMemIds gInfo items + events = L.nonEmpty $ map (\(msgId, memId) -> XMsgDel msgId (Just memId)) msgMemIds + 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 + where + itemDeletable :: CChatItem 'CTGroup -> Bool + itemDeletable (CChatItem _ ChatItem {chatDir, meta = CIMeta {itemSharedMsgId}}) = + case chatDir of + CIGroupRcv GroupMember {memberRole} -> membershipMemRole >= memberRole && isJust itemSharedMsgId + CIGroupSnd -> isJust itemSharedMsgId + itemsMsgMemIds :: GroupInfo -> [CChatItem 'CTGroup] -> [(SharedMsgId, MemberId)] + itemsMsgMemIds GroupInfo {membership = GroupMember {memberId = membershipMemId}} = mapMaybe itemMsgMemIds + where + itemMsgMemIds :: CChatItem 'CTGroup -> Maybe (SharedMsgId, MemberId) + itemMsgMemIds (CChatItem _ ChatItem {chatDir, meta = CIMeta {itemSharedMsgId}}) = + join <$> forM itemSharedMsgId $ \msgId -> Just $ case chatDir of + CIGroupRcv GroupMember {memberId} -> (msgId, memberId) + CIGroupSnd -> (msgId, membershipMemId) APIChatItemReaction (ChatRef cType chatId) itemId add reaction -> withUser $ \user -> case cType of CTDirect -> withContactLock "chatItemReaction" chatId $ @@ -1145,7 +1203,7 @@ processChatCommand' vr = \case filesInfo <- withFastStore' $ \db -> getGroupFileInfo db user gInfo cancelFilesInProgress user filesInfo deleteFilesLocally filesInfo - withFastStore' $ \db -> deleteGroupCIs db user gInfo + withFastStore' $ \db -> deleteGroupChatItemsMessages db user gInfo membersToDelete <- withFastStore' $ \db -> getGroupMembersForExpiration db vr user gInfo forM_ membersToDelete $ \m -> withFastStore' $ \db -> deleteGroupMember db user m pure $ CRChatCleared user (AChatInfo SCTGroup $ GroupChat gInfo) @@ -1743,7 +1801,7 @@ processChatCommand' vr = \case Just (ctConns :: NonEmpty (Contact, Connection)) -> do let idsEvts = L.map ctSndEvent ctConns sndMsgs <- lift $ createSndMessages idsEvts - let msgReqs_ :: NonEmpty (Either ChatError MsgReq) = L.zipWith (fmap . ctMsgReq) ctConns sndMsgs + let msgReqs_ :: NonEmpty (Either ChatError ChatMsgReq) = L.zipWith (fmap . ctMsgReq) ctConns sndMsgs (errs, ctSndMsgs :: [(Contact, SndMessage)]) <- partitionEithers . L.toList . zipWith3' combineResults ctConns sndMsgs <$> deliverMessagesB msgReqs_ timestamp <- liftIO getCurrentTime @@ -1757,11 +1815,11 @@ processChatCommand' vr = \case _ -> ctConns ctSndEvent :: (Contact, Connection) -> (ConnOrGroupId, ChatMsgEvent 'Json) ctSndEvent (_, Connection {connId}) = (ConnectionId connId, XMsgNew $ MCSimple (extMsgContent mc Nothing)) - ctMsgReq :: (Contact, Connection) -> SndMessage -> MsgReq - ctMsgReq (_, conn) SndMessage {msgId, msgBody} = (conn, MsgFlags {notification = hasNotification XMsgNew_}, msgBody, msgId) + ctMsgReq :: (Contact, Connection) -> SndMessage -> ChatMsgReq + ctMsgReq (_, conn) SndMessage {msgId, msgBody} = (conn, MsgFlags {notification = hasNotification XMsgNew_}, msgBody, [msgId]) zipWith3' :: (a -> b -> c -> d) -> NonEmpty a -> NonEmpty b -> NonEmpty c -> NonEmpty d zipWith3' f ~(x :| xs) ~(y :| ys) ~(z :| zs) = f x y z :| zipWith3 f xs ys zs - combineResults :: (Contact, Connection) -> Either ChatError SndMessage -> Either ChatError (Int64, PQEncryption) -> Either ChatError (Contact, SndMessage) + combineResults :: (Contact, Connection) -> Either ChatError SndMessage -> Either ChatError ([Int64], PQEncryption) -> Either ChatError (Contact, SndMessage) combineResults (ct, _) (Right msg') (Right _) = Right (ct, msg') combineResults _ (Left e) _ = Left e combineResults _ _ (Left e) = Left e @@ -1776,11 +1834,11 @@ processChatCommand' vr = \case DeleteMessage chatName deletedMsg -> withUser $ \user -> do chatRef <- getChatRef user chatName deletedItemId <- getSentChatItemIdByText user chatRef deletedMsg - processChatCommand $ APIDeleteChatItem chatRef deletedItemId CIDMBroadcast + processChatCommand $ APIDeleteChatItem chatRef (deletedItemId :| []) CIDMBroadcast DeleteMemberMessage gName mName deletedMsg -> withUser $ \user -> do - (gId, mId) <- getGroupAndMemberId user gName mName + gId <- withFastStore $ \db -> getGroupIdByName db user gName deletedItemId <- withFastStore $ \db -> getGroupChatItemIdByText db user gId (Just mName) deletedMsg - processChatCommand $ APIDeleteMemberChatItem gId mId deletedItemId + processChatCommand $ APIDeleteMemberChatItem gId (deletedItemId :| []) EditMessage chatName editedMsg msg -> withUser $ \user -> do chatRef <- getChatRef user chatName editedItemId <- getSentChatItemIdByText user chatRef editedMsg @@ -1899,7 +1957,7 @@ processChatCommand' vr = \case withGroupLock "blockForAll" groupId . procCmd $ do let mrs = if blocked then MRSBlocked else MRSUnrestricted event = XGrpMemRestrict bmMemberId MemberRestrictions {restriction = mrs} - (msg, _) <- sendGroupMessage' user gInfo remainingMembers event + 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) @@ -1936,7 +1994,7 @@ processChatCommand' vr = \case filesInfo <- withFastStore' $ \db -> getGroupFileInfo db user gInfo withGroupLock "leaveGroup" groupId . procCmd $ do cancelFilesInProgress user filesInfo - (msg, _) <- sendGroupMessage' user gInfo members XGrpLeave + msg <- sendGroupMessage' user gInfo members XGrpLeave ci <- saveSndChatItem user (CDGroupSnd gInfo) msg (CISndGroupEvent SGEUserLeft) toView $ CRNewChatItem user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci) -- TODO delete direct connections that were unused @@ -2490,10 +2548,10 @@ processChatCommand' vr = \case mergedProfile' = userProfileToSend user' Nothing (Just ct') False ctSndEvent :: ChangedProfileContact -> (ConnOrGroupId, ChatMsgEvent 'Json) ctSndEvent ChangedProfileContact {mergedProfile', conn = Connection {connId}} = (ConnectionId connId, XInfo mergedProfile') - ctMsgReq :: ChangedProfileContact -> Either ChatError SndMessage -> Either ChatError MsgReq + ctMsgReq :: ChangedProfileContact -> Either ChatError SndMessage -> Either ChatError ChatMsgReq ctMsgReq ChangedProfileContact {conn} = fmap $ \SndMessage {msgId, msgBody} -> - (conn, MsgFlags {notification = hasNotification XInfo_}, msgBody, msgId) + (conn, MsgFlags {notification = hasNotification XInfo_}, msgBody, [msgId]) updateContactPrefs :: User -> Contact -> Preferences -> CM ChatResponse updateContactPrefs _ ct@Contact {activeConn = Nothing} _ = throwChatError $ CEContactNotActive ct updateContactPrefs user@User {userId} ct@Contact {activeConn = Just Connection {customUserProfileId}, userPreferences = contactUserPrefs} contactUserPrefs' @@ -2533,12 +2591,12 @@ processChatCommand' vr = \case when (memberStatus membership == GSMemInvited) $ throwChatError (CEGroupNotJoined g) when (memberRemoved membership) $ throwChatError CEGroupMemberUserRemoved unless (memberActive membership) $ throwChatError CEGroupMemberNotActive - delGroupChatItem :: MsgDirectionI d => User -> GroupInfo -> ChatItem 'CTGroup d -> MessageId -> Maybe GroupMember -> CM ChatResponse - delGroupChatItem user gInfo ci msgId byGroupMember = do + delGroupChatItems :: User -> GroupInfo -> [CChatItem 'CTGroup] -> Maybe GroupMember -> CM ChatResponse + delGroupChatItems user gInfo items byGroupMember = do deletedTs <- liftIO getCurrentTime if groupFeatureAllowed SGFFullDelete gInfo - then deleteGroupCI user gInfo ci True False byGroupMember deletedTs - else markGroupCIDeleted user gInfo ci msgId True byGroupMember deletedTs + then deleteGroupCIs user gInfo items True False byGroupMember deletedTs + else markGroupCIsDeleted user gInfo items True byGroupMember deletedTs updateGroupProfileByName :: GroupName -> (GroupProfile -> GroupProfile) -> CM ChatResponse updateGroupProfileByName gName update = withUser $ \user -> do g@(Group GroupInfo {groupProfile = p} _) <- withStore $ \db -> @@ -2799,10 +2857,10 @@ processChatCommand' vr = \case (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, groupSndResult) <- sendGroupMessage user gInfo ms (XMsgNew msgContainer) + (msg, r) <- sendGroupMessage user gInfo ms (XMsgNew msgContainer) ci <- saveSndChatItem' user (CDGroupSnd gInfo) msg (CISndMsgContent mc) ciFile_ quotedItem_ itemForwarded timed_ live withFastStore' $ \db -> do - let GroupSndResult {sentTo, pending, forwarded} = groupSndResult + let GroupSndResult {sentTo, pending, forwarded} = mkGroupSndResult r createMemberSndStatuses db ci sentTo GSSNew createMemberSndStatuses db ci forwarded GSSForwarded createMemberSndStatuses db ci pending GSSInactive @@ -3663,12 +3721,12 @@ deleteTimedItem user (ChatRef cType chatId, itemId) deleteAt = do vr <- chatVersionRange case cType of CTDirect -> do - (ct, CChatItem _ ci) <- withStore $ \db -> (,) <$> getContact db vr user chatId <*> getDirectChatItem db user chatId itemId - deleteDirectCI user ct ci True True >>= toView + (ct, ci) <- withStore $ \db -> (,) <$> getContact db vr user chatId <*> getDirectChatItem db user chatId itemId + deleteDirectCIs user ct [ci] True True >>= toView CTGroup -> do - (gInfo, CChatItem _ ci) <- withStore $ \db -> (,) <$> getGroupInfo db vr user chatId <*> getGroupChatItem db user chatId itemId + (gInfo, ci) <- withStore $ \db -> (,) <$> getGroupInfo db vr user chatId <*> getGroupChatItem db user chatId itemId deletedTs <- liftIO getCurrentTime - deleteGroupCI user gInfo ci True True Nothing deletedTs >>= toView + deleteGroupCIs user gInfo [ci] True True Nothing deletedTs >>= toView _ -> toView . CRChatError (Just user) . ChatError $ CEInternalError "bad deleteTimedItem cType" startUpdatedTimedItemThread :: User -> ChatRef -> ChatItem c d -> ChatItem c d -> CM () @@ -4041,13 +4099,10 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = processINFOpqSupport conn pqSupport _conn' <- saveConnInfo conn connInfo pure () - MSG meta _msgFlags msgBody -> - -- TODO only acknowledge without saving message? - -- probably this branch is never executed, so there should be no reason - -- to save message if contact hasn't been created yet - chat item isn't created anyway - withAckMessage' "new contact msg" agentConnId meta $ - void $ - saveDirectRcvMSG conn meta msgBody + MSG meta _msgFlags _msgBody -> + -- We are not saving message (saveDirectRcvMSG) as contact hasn't been created yet, + -- chat item is also not created here + withAckMessage' "new contact msg" agentConnId meta $ pure () SENT msgId _proxy -> do void $ continueSending connEntity conn sentMsgDeliveryEvent conn msgId @@ -4090,37 +4145,53 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = let MsgMeta {pqEncryption} = msgMeta (ct', conn') <- updateContactPQRcv user ct conn pqEncryption checkIntegrityCreateItem (CDDirectRcv ct') msgMeta `catchChatError` \_ -> pure () - (conn'', msg@RcvMessage {chatMsgEvent = ACME _ event}) <- saveDirectRcvMSG conn' msgMeta msgBody - let tag = toCMEventTag event - atomically $ writeTVar tags [tshow tag] - logInfo $ "contact msg=" <> tshow tag <> " " <> eInfo - let ct'' = ct' {activeConn = Just conn''} :: Contact - assertDirectAllowed user MDRcv ct'' tag - case event of - XMsgNew mc -> newContentMessage ct'' mc msg msgMeta - XMsgFileDescr sharedMsgId fileDescr -> messageFileDescription ct'' sharedMsgId fileDescr - XMsgUpdate sharedMsgId mContent ttl live -> messageUpdate ct'' sharedMsgId mContent msg msgMeta ttl live - XMsgDel sharedMsgId _ -> messageDelete ct'' sharedMsgId msg msgMeta - XMsgReact sharedMsgId _ reaction add -> directMsgReaction ct'' sharedMsgId reaction add msg msgMeta - -- TODO discontinue XFile - XFile fInv -> processFileInvitation' ct'' fInv msg msgMeta - XFileCancel sharedMsgId -> xFileCancel ct'' sharedMsgId - XFileAcptInv sharedMsgId fileConnReq_ fName -> xFileAcptInv ct'' sharedMsgId fileConnReq_ fName - XInfo p -> xInfo ct'' p - XDirectDel -> xDirectDel ct'' msg msgMeta - XGrpInv gInv -> processGroupInvitation ct'' gInv msg msgMeta - XInfoProbe probe -> xInfoProbe (COMContact ct'') probe - XInfoProbeCheck probeHash -> xInfoProbeCheck (COMContact ct'') probeHash - XInfoProbeOk probe -> xInfoProbeOk (COMContact ct'') probe - XCallInv callId invitation -> xCallInv ct'' callId invitation msg msgMeta - XCallOffer callId offer -> xCallOffer ct'' callId offer msg - XCallAnswer callId answer -> xCallAnswer ct'' callId answer msg - XCallExtra callId extraInfo -> xCallExtra ct'' callId extraInfo msg - XCallEnd callId -> xCallEnd ct'' callId msg - BFileChunk sharedMsgId chunk -> bFileChunk ct'' sharedMsgId chunk msgMeta - _ -> messageError $ "unsupported message: " <> T.pack (show event) - let Contact {chatSettings = ChatSettings {sendRcpts}} = ct'' - pure $ fromMaybe (sendRcptsContacts user) sendRcpts && hasDeliveryReceipt tag + forM_ aChatMsgs $ \case + Right (ACMsg _ chatMsg) -> + processEvent ct' conn' tags eInfo chatMsg `catchChatError` \e -> toView $ CRChatError (Just user) e + Left e -> do + atomically $ modifyTVar' tags ("error" :) + logInfo $ "contact msg=error " <> eInfo <> " " <> tshow e + toView $ CRChatError (Just user) (ChatError . CEException $ "error parsing chat message: " <> e) + checkSendRcpt ct' $ rights aChatMsgs -- not crucial to use ct'' from processEvent + where + aChatMsgs = parseChatMessages msgBody + processEvent :: Contact -> Connection -> TVar [Text] -> Text -> MsgEncodingI e => ChatMessage e -> CM () + processEvent ct' conn' tags eInfo chatMsg@ChatMessage {chatMsgEvent} = do + let tag = toCMEventTag chatMsgEvent + atomically $ modifyTVar' tags (tshow tag :) + logInfo $ "contact msg=" <> tshow tag <> " " <> eInfo + (conn'', msg@RcvMessage {chatMsgEvent = ACME _ event}) <- saveDirectRcvMSG conn' msgMeta msgBody chatMsg + let ct'' = ct' {activeConn = Just conn''} :: Contact + case event of + XMsgNew mc -> newContentMessage ct'' mc msg msgMeta + XMsgFileDescr sharedMsgId fileDescr -> messageFileDescription ct'' sharedMsgId fileDescr + XMsgUpdate sharedMsgId mContent ttl live -> messageUpdate ct'' sharedMsgId mContent msg msgMeta ttl live + XMsgDel sharedMsgId _ -> messageDelete ct'' sharedMsgId msg msgMeta + XMsgReact sharedMsgId _ reaction add -> directMsgReaction ct'' sharedMsgId reaction add msg msgMeta + -- TODO discontinue XFile + XFile fInv -> processFileInvitation' ct'' fInv msg msgMeta + XFileCancel sharedMsgId -> xFileCancel ct'' sharedMsgId + XFileAcptInv sharedMsgId fileConnReq_ fName -> xFileAcptInv ct'' sharedMsgId fileConnReq_ fName + XInfo p -> xInfo ct'' p + XDirectDel -> xDirectDel ct'' msg msgMeta + XGrpInv gInv -> processGroupInvitation ct'' gInv msg msgMeta + XInfoProbe probe -> xInfoProbe (COMContact ct'') probe + XInfoProbeCheck probeHash -> xInfoProbeCheck (COMContact ct'') probeHash + XInfoProbeOk probe -> xInfoProbeOk (COMContact ct'') probe + XCallInv callId invitation -> xCallInv ct'' callId invitation msg msgMeta + XCallOffer callId offer -> xCallOffer ct'' callId offer msg + XCallAnswer callId answer -> xCallAnswer ct'' callId answer msg + XCallExtra callId extraInfo -> xCallExtra ct'' callId extraInfo msg + XCallEnd callId -> xCallEnd ct'' callId msg + BFileChunk sharedMsgId chunk -> bFileChunk ct'' sharedMsgId chunk msgMeta + _ -> messageError $ "unsupported message: " <> T.pack (show event) + checkSendRcpt :: Contact -> [AChatMessage] -> CM Bool + checkSendRcpt ct' aMsgs = do + let Contact {chatSettings = ChatSettings {sendRcpts}} = ct' + pure $ fromMaybe (sendRcptsContacts user) sendRcpts && any aChatMsgHasReceipt aMsgs + where + aChatMsgHasReceipt (ACMsg _ ChatMessage {chatMsgEvent}) = + hasDeliveryReceipt (toCMEventTag chatMsgEvent) RCVD msgMeta msgRcpt -> withAckMessage' "contact rcvd" agentConnId msgMeta $ directMsgReceived ct conn msgMeta msgRcpt @@ -4524,7 +4595,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = atomically $ modifyTVar' tags ("error" :) logInfo $ "group msg=error " <> eInfo <> " " <> tshow e toView $ CRChatError (Just user) (ChatError . CEException $ "error parsing chat message: " <> e) - forwardMsg_ `catchChatError` (toView . CRChatError (Just user)) + forwardMsgs (rights aChatMsgs) `catchChatError` (toView . CRChatError (Just user)) checkSendRcpt $ rights aChatMsgs where aChatMsgs = parseChatMessages msgBody @@ -4576,27 +4647,24 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = where aChatMsgHasReceipt (ACMsg _ ChatMessage {chatMsgEvent}) = hasDeliveryReceipt (toCMEventTag chatMsgEvent) - forwardMsg_ :: CM () - forwardMsg_ = do + forwardMsgs :: [AChatMessage] -> CM () + forwardMsgs aMsgs = do let GroupMember {memberRole = membershipMemRole} = membership - when (membershipMemRole >= GRAdmin && not (blockedByAdmin m)) $ case aChatMsgs of - -- currently only a single message is forwarded - [Right (ACMsg _ chatMsg)] -> - forM_ (forwardedGroupMsg chatMsg) $ \chatMsg' -> do - ChatConfig {highlyAvailable} <- asks config - -- members introduced to this invited member - introducedMembers <- - if memberCategory m == GCInviteeMember - then withStore' $ \db -> getForwardIntroducedMembers db vr user m highlyAvailable - else pure [] - -- invited members to which this member was introduced - invitedMembers <- withStore' $ \db -> getForwardInvitedMembers db vr user m highlyAvailable - let GroupMember {memberId} = m - ms = forwardedToGroupMembers (introducedMembers <> invitedMembers) chatMsg' - msg = XGrpMsgForward memberId chatMsg' brokerTs - unless (null ms) . void $ - sendGroupMessage' user gInfo ms msg - _ -> pure () + when (membershipMemRole >= GRAdmin && not (blockedByAdmin m)) $ do + let forwardedMsgs = mapMaybe (\(ACMsg _ chatMsg) -> forwardedGroupMsg chatMsg) aMsgs + forM_ (L.nonEmpty forwardedMsgs) $ \forwardedMsgs' -> do + ChatConfig {highlyAvailable} <- asks config + -- members introduced to this invited member + introducedMembers <- + if memberCategory m == GCInviteeMember + then withStore' $ \db -> getForwardIntroducedMembers db vr user m highlyAvailable + else pure [] + -- invited members to which this member was introduced + invitedMembers <- withStore' $ \db -> getForwardInvitedMembers db vr user m highlyAvailable + 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 RCVD msgMeta msgRcpt -> withAckMessage' "group rcvd" agentConnId msgMeta $ groupMsgReceived gInfo m conn msgMeta msgRcpt @@ -4642,8 +4710,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- [async agent commands] continuation on receiving OK when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () JOINED _ -> - -- [async agent commands] continuation on receiving JOINED - when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () + -- [async agent commands] continuation on receiving JOINED + when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () QCONT -> do continued <- continueSending connEntity conn when continued $ sendPendingGroupMessages user m conn @@ -5140,8 +5208,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = _ -> pure () processFileInvitation :: Maybe FileInvitation -> MsgContent -> (DB.Connection -> FileInvitation -> Maybe InlineFileMode -> Integer -> ExceptT StoreError IO RcvFileTransfer) -> CM (Maybe (RcvFileTransfer, CIFile 'MDRcv)) - processFileInvitation fInv_ mc createRcvFT = forM fInv_ $ \fInv@FileInvitation {fileName, fileSize} -> do + processFileInvitation fInv_ mc createRcvFT = forM fInv_ $ \fInv' -> do ChatConfig {fileChunkSize} <- asks config + let fInv@FileInvitation {fileName, fileSize} = mkValidFileInvitation fInv' inline <- receiveInlineMode fInv (Just mc) fileChunkSize ft@RcvFileTransfer {fileId, xftpRcvFile} <- withStore $ \db -> createRcvFT db fInv inline fileChunkSize let fileProtocol = if isJust xftpRcvFile then FPXFTP else FPSMP @@ -5157,6 +5226,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = fileSource = (`CryptoFile` cryptoArgs) <$> filePath pure (ft', CIFile {fileId, fileName, fileSize, fileSource, fileStatus, fileProtocol}) + mkValidFileInvitation :: FileInvitation -> FileInvitation + mkValidFileInvitation fInv@FileInvitation {fileName} = fInv {fileName = FP.makeValid $ FP.takeFileName fileName} + messageUpdate :: Contact -> SharedMsgId -> MsgContent -> RcvMessage -> MsgMeta -> Maybe Int -> Maybe Bool -> CM () messageUpdate ct@Contact {contactId} sharedMsgId mc msg@RcvMessage {msgId} msgMeta ttl live_ = do updateRcvChatItem `catchCINotFound` \_ -> do @@ -5193,19 +5265,26 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = _ -> messageError "x.msg.update: contact attempted invalid message update" messageDelete :: Contact -> SharedMsgId -> RcvMessage -> MsgMeta -> CM () - messageDelete ct@Contact {contactId} sharedMsgId RcvMessage {msgId} msgMeta = do + messageDelete ct@Contact {contactId} sharedMsgId _rcvMessage msgMeta = do deleteRcvChatItem `catchCINotFound` (toView . CRChatItemDeletedNotFound user ct) where brokerTs = metaBrokerTs msgMeta deleteRcvChatItem = do - CChatItem msgDir ci <- withStore $ \db -> getDirectChatItemBySharedMsgId db user contactId sharedMsgId + cci@(CChatItem msgDir ci) <- withStore $ \db -> getDirectChatItemBySharedMsgId db user contactId sharedMsgId case msgDir of - SMDRcv -> - if featureAllowed SCFFullDelete forContact ct - then deleteDirectCI user ct ci False False >>= toView - else markDirectCIDeleted user ct ci msgId False brokerTs >>= toView + SMDRcv + | rcvItemDeletable ci brokerTs -> + if featureAllowed SCFFullDelete forContact ct + then deleteDirectCIs user ct [cci] False False >>= toView + else markDirectCIsDeleted user ct [cci] False brokerTs >>= toView + | otherwise -> messageError "x.msg.del: contact attempted invalid message delete" SMDSnd -> messageError "x.msg.del: contact attempted invalid message delete" + rcvItemDeletable :: ChatItem c d -> UTCTime -> Bool + rcvItemDeletable ChatItem {meta = CIMeta {itemTs, itemDeleted}} brokerTs = + -- 78 hours margin to account for possible sending delay + diffUTCTime brokerTs itemTs < (78 * 3600) && isNothing itemDeleted + directMsgReaction :: Contact -> SharedMsgId -> MsgReaction -> Bool -> RcvMessage -> MsgMeta -> CM () directMsgReaction ct sharedMsgId reaction add RcvMessage {msgId} MsgMeta {broker = (_, brokerTs)} = do when (featureAllowed SCFReactions forContact ct) $ do @@ -5283,7 +5362,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = ci <- createNonLive file_ ci' <- withStore' $ \db -> markGroupCIBlockedByAdmin db user gInfo ci groupMsgToView gInfo ci' - applyModeration CIModeration {moderatorMember = moderator@GroupMember {memberRole = moderatorRole}, createdByMsgId, moderatedAt} + applyModeration CIModeration {moderatorMember = moderator@GroupMember {memberRole = moderatorRole}, moderatedAt} | moderatorRole < GRAdmin || moderatorRole < memberRole = createContentItem | groupFeatureAllowed SGFFullDelete gInfo = do @@ -5293,7 +5372,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = | otherwise = do file_ <- processFileInv ci <- createNonLive file_ - toView =<< markGroupCIDeleted user gInfo ci createdByMsgId False (Just moderator) moderatedAt + toView =<< markGroupCIsDeleted user gInfo [CChatItem SMDRcv ci] False (Just moderator) moderatedAt createNonLive file_ = saveRcvChatItem' user (CDGroupRcv gInfo m) msg sharedMsgId_ brokerTs (CIRcvMsgContent content) (snd <$> file_) timed' False createContentItem = do @@ -5352,35 +5431,46 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = groupMessageDelete gInfo@GroupInfo {groupId, membership} m@GroupMember {memberId, memberRole = senderRole} sharedMsgId sndMemberId_ RcvMessage {msgId} brokerTs = do let msgMemberId = fromMaybe memberId sndMemberId_ withStore' (\db -> runExceptT $ getGroupMemberCIBySharedMsgId db user groupId msgMemberId sharedMsgId) >>= \case - Right (CChatItem _ ci@ChatItem {chatDir}) -> case chatDir of - CIGroupRcv mem - | sameMemberId memberId mem && msgMemberId == memberId -> delete ci Nothing >>= toView - | otherwise -> deleteMsg mem ci - CIGroupSnd -> deleteMsg membership ci + Right cci@(CChatItem _ ci@ChatItem {chatDir}) -> case chatDir of + CIGroupRcv mem -> case sndMemberId_ of + -- regular deletion + Nothing + | sameMemberId memberId mem && msgMemberId == memberId && rcvItemDeletable ci brokerTs -> + delete cci Nothing >>= toView + | otherwise -> + messageError "x.msg.del: member attempted invalid message delete" + -- moderation (not limited by time) + Just _ + | sameMemberId memberId mem && msgMemberId == memberId -> + delete cci (Just m) >>= toView + | otherwise -> + moderate mem cci + CIGroupSnd -> moderate membership cci Left e | msgMemberId == memberId -> messageError $ "x.msg.del: message not found, " <> tshow e | senderRole < GRAdmin -> messageError $ "x.msg.del: message not found, message of another member with insufficient member permissions, " <> tshow e | otherwise -> withStore' $ \db -> createCIModeration db gInfo m msgMemberId sharedMsgId msgId brokerTs where - deleteMsg :: MsgDirectionI d => GroupMember -> ChatItem 'CTGroup d -> CM () - deleteMsg mem ci = case sndMemberId_ of + moderate :: GroupMember -> CChatItem 'CTGroup -> CM () + moderate mem cci = case sndMemberId_ of Just sndMemberId - | sameMemberId sndMemberId mem -> checkRole mem $ delete ci (Just m) >>= toView + | sameMemberId sndMemberId mem -> checkRole mem $ delete cci (Just m) >>= toView | otherwise -> messageError "x.msg.del: message of another member with incorrect memberId" _ -> messageError "x.msg.del: message of another member without memberId" checkRole GroupMember {memberRole} a | senderRole < GRAdmin || senderRole < memberRole = messageError "x.msg.del: message of another member with insufficient member permissions" | otherwise = a - delete :: MsgDirectionI d => ChatItem 'CTGroup d -> Maybe GroupMember -> CM ChatResponse - delete ci byGroupMember - | groupFeatureAllowed SGFFullDelete gInfo = deleteGroupCI user gInfo ci False False byGroupMember brokerTs - | otherwise = markGroupCIDeleted user gInfo ci msgId False byGroupMember brokerTs + delete :: CChatItem 'CTGroup -> Maybe GroupMember -> CM ChatResponse + delete cci byGroupMember + | groupFeatureAllowed SGFFullDelete gInfo = deleteGroupCIs user gInfo [cci] False False byGroupMember brokerTs + | otherwise = markGroupCIsDeleted user gInfo [cci] False byGroupMember brokerTs -- TODO remove once XFile is discontinued processFileInvitation' :: Contact -> FileInvitation -> RcvMessage -> MsgMeta -> CM () - processFileInvitation' ct fInv@FileInvitation {fileName, fileSize} msg@RcvMessage {sharedMsgId_} msgMeta = do + processFileInvitation' ct fInv' msg@RcvMessage {sharedMsgId_} msgMeta = do ChatConfig {fileChunkSize} <- asks config + let fInv@FileInvitation {fileName, fileSize} = mkValidFileInvitation fInv' inline <- receiveInlineMode fInv Nothing fileChunkSize RcvFileTransfer {fileId, xftpRcvFile} <- withStore $ \db -> createRcvFileTransfer db userId ct fInv inline fileChunkSize let fileProtocol = if isJust xftpRcvFile then FPXFTP else FPSMP @@ -5551,9 +5641,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = _ -> messageError "x.file.acpt.inv: member connection is not active" else messageError "x.file.acpt.inv: fileName is different from expected" - groupMsgToView :: GroupInfo -> ChatItem 'CTGroup 'MDRcv -> CM () + groupMsgToView :: forall d. MsgDirectionI d => GroupInfo -> ChatItem 'CTGroup d -> CM () groupMsgToView gInfo ci = - toView $ CRNewChatItem user (AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci) + toView $ CRNewChatItem user (AChatItem SCTGroup (msgDirection @d) (GroupChat gInfo) ci) processGroupInvitation :: Contact -> GroupInvitation -> RcvMessage -> MsgMeta -> CM () processGroupInvitation ct inv msg msgMeta = do @@ -6655,6 +6745,22 @@ 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 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 + +sendDirectContactMessages' :: MsgEncodingI e => User -> Contact -> NonEmpty (ChatMsgEvent e) -> CM () +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) + sendDirectContactMessage :: MsgEncodingI e => User -> Contact -> ChatMsgEvent e -> CM (SndMessage, Int64) sendDirectContactMessage user ct chatMsgEvent = do conn@Connection {connId} <- liftEither $ contactSendConn_ ct @@ -6711,38 +6817,23 @@ sendGroupMemberMessages user conn events groupId = do (errs, msgs) <- lift $ partitionEithers . L.toList <$> createSndMessages idsEvts unless (null errs) $ toView $ CRChatErrors (Just user) errs forM_ (L.nonEmpty msgs) $ \msgs' -> - batchSendGroupMemberMessages user conn msgs' + batchSendConnMessages user conn MsgFlags {notification = True} msgs' -batchSendGroupMemberMessages :: User -> Connection -> NonEmpty SndMessage -> CM () -batchSendGroupMemberMessages user conn msgs = do - -- TODO v5.7 based on version (?) - -- let shouldCompress = False - -- let batched = if shouldCompress then batchSndMessagesBinary msgs' else batchSndMessagesJSON 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_ msgBatches $ \batch -> - processSndMessageBatch conn batch `catchChatError` (toView . CRChatError (Just user)) + forM_ (L.nonEmpty msgBatches) $ \msgBatches' -> do + let msgReq = L.map (msgBatchReq conn msgFlags) msgBatches' + void $ deliverMessages msgReq -processSndMessageBatch :: Connection -> MsgBatch -> CM () -processSndMessageBatch conn@Connection {connId} (MsgBatch batchBody sndMsgs) = do - (agentMsgId, _pqEnc) <- withAgent $ \a -> sendMessage a (aConnId conn) PQEncOff MsgFlags {notification = True} batchBody - let sndMsgDelivery = SndMsgDelivery {connId, agentMsgId} - lift . void . withStoreBatch' $ \db -> map (\SndMessage {msgId} -> createSndMsgDelivery db sndMsgDelivery msgId) sndMsgs - --- TODO v5.7 update batching for groups batchSndMessagesJSON :: NonEmpty SndMessage -> [Either ChatError MsgBatch] batchSndMessagesJSON = batchMessages maxEncodedMsgLength . L.toList --- batchSndMessagesBinary :: NonEmpty SndMessage -> [Either ChatError MsgBatch] --- batchSndMessagesBinary msgs = map toMsgBatch . SMP.batchTransmissions_ (maxEncodedMsgLength) $ L.zip (map compress1 msgs) msgs --- where --- toMsgBatch :: SMP.TransportBatch SndMessage -> Either ChatError MsgBatch --- toMsgBatch = \case --- SMP.TBTransmissions combined _n sms -> Right $ MsgBatch (markCompressedBatch combined) sms --- SMP.TBError tbe SndMessage {msgId} -> Left . ChatError $ CEInternalError (show tbe <> " " <> show msgId) --- SMP.TBTransmission {} -> Left . ChatError $ CEInternalError "batchTransmissions_ didn't produce a batch" +msgBatchReq :: Connection -> MsgFlags -> MsgBatch -> ChatMsgReq +msgBatchReq conn msgFlags (MsgBatch batchBody sndMsgs) = (conn, msgFlags, batchBody, map (\SndMessage {msgId} -> msgId) sndMsgs) encodeConnInfo :: MsgEncodingI e => ChatMsgEvent e -> CM ByteString encodeConnInfo chatMsgEvent = do @@ -6769,19 +6860,23 @@ deliverMessage conn cmEventTag msgBody msgId = do deliverMessage' :: Connection -> MsgFlags -> MsgBody -> MessageId -> CM (Int64, PQEncryption) deliverMessage' conn msgFlags msgBody msgId = - deliverMessages ((conn, msgFlags, msgBody, msgId) :| []) >>= \case - r :| [] -> liftEither r + deliverMessages ((conn, msgFlags, msgBody, [msgId]) :| []) >>= \case + r :| [] -> case r of + Right ([deliveryId], pqEnc) -> pure (deliveryId, pqEnc) + Right (deliveryIds, _) -> throwChatError $ CEInternalError $ "deliverMessage: expected 1 delivery id, got " <> show (length deliveryIds) + Left e -> throwError e rs -> throwChatError $ CEInternalError $ "deliverMessage: expected 1 result, got " <> show (length rs) -type MsgReq = (Connection, MsgFlags, MsgBody, MessageId) +-- [MessageId] - SndMessage ids inside MsgBatch, or single message id +type ChatMsgReq = (Connection, MsgFlags, MsgBody, [MessageId]) -deliverMessages :: NonEmpty MsgReq -> CM (NonEmpty (Either ChatError (Int64, PQEncryption))) +deliverMessages :: NonEmpty ChatMsgReq -> CM (NonEmpty (Either ChatError ([Int64], PQEncryption))) deliverMessages msgs = deliverMessagesB $ L.map Right msgs -deliverMessagesB :: NonEmpty (Either ChatError MsgReq) -> CM (NonEmpty (Either ChatError (Int64, PQEncryption))) +deliverMessagesB :: NonEmpty (Either ChatError ChatMsgReq) -> CM (NonEmpty (Either ChatError ([Int64], PQEncryption))) deliverMessagesB msgReqs = do msgReqs' <- liftIO compressBodies - sent <- L.zipWith prepareBatch msgReqs' <$> withAgent (`sendMessagesB` L.map toAgent msgReqs') + sent <- L.zipWith prepareBatch msgReqs' <$> withAgent (`sendMessagesB` snd (mapAccumL toAgent Nothing msgReqs')) lift . void $ withStoreBatch' $ \db -> map (updatePQSndEnabled db) (rights . L.toList $ sent) lift . withStoreBatch $ \db -> L.map (bindRight $ createDelivery db) sent where @@ -6797,16 +6892,20 @@ deliverMessagesB msgReqs = do when (B.length msgBody' > maxCompressedMsgLength) $ throwError $ ChatError $ CEException "large compressed message" pure (conn, msgFlags, msgBody', msgId) _ -> pure mr - toAgent = \case - Right (conn@Connection {pqEncryption}, msgFlags, msgBody, _msgId) -> Right (aConnId conn, pqEncryption, msgFlags, msgBody) - Left _ce -> Left (AP.INTERNAL "ChatError, skip") -- as long as it is Left, the agent batchers should just step over it + toAgent prev = \case + Right (conn@Connection {connId, pqEncryption}, msgFlags, msgBody, _msgIds) -> + let cId = case prev of + Just prevId | prevId == connId -> "" + _ -> aConnId conn + in (Just connId, Right (cId, pqEncryption, msgFlags, msgBody)) + Left _ce -> (prev, Left (AP.INTERNAL "ChatError, skip")) -- as long as it is Left, the agent batchers should just step over it prepareBatch (Right req) (Right ar) = Right (req, ar) prepareBatch (Left ce) _ = Left ce -- restore original ChatError prepareBatch _ (Left ae) = Left $ ChatErrorAgent ae Nothing - createDelivery :: DB.Connection -> (MsgReq, (AgentMsgId, PQEncryption)) -> IO (Either ChatError (Int64, PQEncryption)) - createDelivery db ((Connection {connId}, _, _, msgId), (agentMsgId, pqEnc')) = - Right . (,pqEnc') <$> createSndMsgDelivery db (SndMsgDelivery {connId, agentMsgId}) msgId - updatePQSndEnabled :: DB.Connection -> (MsgReq, (AgentMsgId, PQEncryption)) -> IO () + createDelivery :: DB.Connection -> (ChatMsgReq, (AgentMsgId, PQEncryption)) -> IO (Either ChatError ([Int64], PQEncryption)) + createDelivery db ((Connection {connId}, _, _, msgIds), (agentMsgId, pqEnc')) = do + Right . (,pqEnc') <$> mapM (createSndMsgDelivery db (SndMsgDelivery {connId, agentMsgId})) msgIds + updatePQSndEnabled :: DB.Connection -> (ChatMsgReq, (AgentMsgId, PQEncryption)) -> IO () updatePQSndEnabled db ((Connection {connId, pqSndEnabled}, _, _, _), (_, pqSndEnabled')) = case (pqSndEnabled, pqSndEnabled') of (Just b, b') | b' /= b -> updatePQ @@ -6817,11 +6916,11 @@ deliverMessagesB msgReqs = do -- 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, GroupSndResult) +sendGroupMessage :: MsgEncodingI e => User -> GroupInfo -> [GroupMember] -> ChatMsgEvent e -> CM (SndMessage, GroupSndResultData) sendGroupMessage user gInfo members chatMsgEvent = do when shouldSendProfileUpdate $ sendProfileUpdate `catchChatError` (toView . CRChatError (Just user)) - sendGroupMessage' user gInfo members chatMsgEvent + sendGroupMessage_ user gInfo members chatMsgEvent where User {profile = p, userMemberProfileUpdatedAt} = user GroupInfo {userMemberProfileSentAt} = gInfo @@ -6839,32 +6938,59 @@ 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], forwarded :: [GroupMember] } -sendGroupMessage' :: MsgEncodingI e => User -> GroupInfo -> [GroupMember] -> ChatMsgEvent e -> CM (SndMessage, GroupSndResult) -sendGroupMessage' user gInfo@GroupInfo {groupId} members chatMsgEvent = do - msg@SndMessage {msgId, msgBody} <- createSndMessage chatMsgEvent (GroupId groupId) - recipientMembers <- liftIO $ shuffleMembers (filter memberCurrent members) - let msgFlags = MsgFlags {notification = hasNotification $ toCMEventTag chatMsgEvent} - (toSend, pending, forwarded, _, dups) = foldr addMember ([], [], [], S.empty, 0 :: Int) recipientMembers +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 + 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 - msgReqs = map (\(_, conn) -> (conn, msgFlags, msgBody, msgId)) toSend - when (dups /= 0) $ logError $ "sendGroupMessage: " <> tshow dups <> " duplicate members" - 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 -> createPendingGroupMessage db (groupMemberId' m) msgId Nothing) pending - let gsr = - GroupSndResult - { sentTo = filterSent delivered toSend fst, - pending = filterSent stored pending id, - forwarded - } - pure (msg, gsr) + 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)) where shuffleMembers :: [GroupMember] -> IO [GroupMember] shuffleMembers ms = do @@ -6872,31 +6998,52 @@ sendGroupMessage' user gInfo@GroupInfo {groupId} members chatMsgEvent = do liftM2 (<>) (shuffle adminMs) (shuffle otherMs) where isAdmin GroupMember {memberRole} = memberRole >= GRAdmin - addMember m acc@(toSend, pending, forwarded, !mIds, !dups) = case memberSendAction gInfo chatMsgEvent members m of - Just a - | mId `S.member` mIds -> (toSend, pending, forwarded, mIds, dups + 1) - | otherwise -> case a of - MSASend conn -> ((m, conn) : toSend, pending, forwarded, mIds', dups) - MSAPending -> (toSend, m : pending, forwarded, mIds', dups) - MSAForwarded -> (toSend, pending, m : forwarded, mIds', dups) - Nothing -> acc + addMember m acc@(toSendSeparate, toSendBatched, pending, forwarded, !mIds, !dups) = + case memberSendAction gInfo events members m of + Just a + | mId `S.member` mIds -> (toSendSeparate, toSendBatched, pending, forwarded, mIds, dups + 1) + | otherwise -> case a of + MSASend conn -> ((m, conn) : toSendSeparate, toSendBatched, pending, forwarded, mIds', dups) + MSASendBatched conn -> (toSendSeparate, (m, conn) : toSendBatched, pending, forwarded, mIds', dups) + MSAPending -> (toSendSeparate, toSendBatched, m : pending, forwarded, mIds', dups) + MSAForwarded -> (toSendSeparate, toSendBatched, pending, m : forwarded, mIds', dups) + Nothing -> acc where mId = groupMemberId' m mIds' = S.insert mId mIds - filterSent :: [Either ChatError a] -> [mem] -> (mem -> GroupMember) -> [GroupMember] - filterSent rs ms mem = [mem m | (Right _, m) <- zip rs ms] + 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 + 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) -data MemberSendAction = MSASend Connection | MSAPending | MSAForwarded +data MemberSendAction = MSASend Connection | MSASendBatched Connection | MSAPending | MSAForwarded -memberSendAction :: GroupInfo -> ChatMsgEvent e -> [GroupMember] -> GroupMember -> Maybe MemberSendAction -memberSendAction gInfo chatMsgEvent members m = case memberConn m of +memberSendAction :: GroupInfo -> NonEmpty (ChatMsgEvent e) -> [GroupMember] -> GroupMember -> Maybe MemberSendAction +memberSendAction gInfo events members m@GroupMember {memberRole} = case memberConn m of Nothing -> pendingOrForwarded Just conn@Connection {connStatus} | connDisabled conn || connStatus == ConnDeleted -> Nothing | connInactive conn -> Just MSAPending - | connStatus == ConnSndReady || connStatus == ConnReady -> Just (MSASend conn) + | connStatus == ConnSndReady || connStatus == ConnReady -> sendBatchedOrSeparate conn | otherwise -> pendingOrForwarded where + sendBatchedOrSeparate conn + -- admin doesn't support batch forwarding - send messages separately so that admin can forward one by one + | memberRole >= GRAdmin && not (m `supportsVersion` batchSend2Version) = Just (MSASend conn) + -- either member is not admin, or admin supports batched forwarding + | otherwise = Just (MSASendBatched conn) pendingOrForwarded = case memberCategory m of GCUserMember -> Nothing -- shouldn't happen GCInviteeMember -> Just MSAPending @@ -6905,8 +7052,8 @@ memberSendAction gInfo chatMsgEvent members m = case memberConn m of GCPostMember -> forwardSupportedOrPending (invitedByGroupMemberId m) where forwardSupportedOrPending invitingMemberId_ - | membersSupport && isForwardedGroupMsg chatMsgEvent = Just MSAForwarded - | isXGrpMsgForward = Nothing + | membersSupport && all isForwardedGroupMsg events = Just MSAForwarded + | any isXGrpMsgForward events = Nothing | otherwise = Just MSAPending where membersSupport = @@ -6918,7 +7065,7 @@ memberSendAction gInfo chatMsgEvent members m = case memberConn m of Just invitingMember -> invitingMember `supportsVersion` groupForwardVersion Nothing -> False Nothing -> False - isXGrpMsgForward = case chatMsgEvent of + isXGrpMsgForward event = case event of XGrpMsgForward {} -> True _ -> False @@ -6928,8 +7075,9 @@ sendGroupMemberMessage user gInfo@GroupInfo {groupId} m@GroupMember {groupMember messageMember msg `catchChatError` (toView . CRChatError (Just user)) where messageMember :: SndMessage -> CM () - messageMember SndMessage {msgId, msgBody} = forM_ (memberSendAction gInfo chatMsgEvent [m] m) $ \case + messageMember SndMessage {msgId, msgBody} = forM_ (memberSendAction gInfo (chatMsgEvent :| []) [m] m) $ \case MSASend conn -> deliverMessage conn (toCMEventTag chatMsgEvent) msgBody msgId >> postDeliver + MSASendBatched conn -> deliverMessage conn (toCMEventTag chatMsgEvent) msgBody msgId >> postDeliver MSAPending -> withStore' $ \db -> createPendingGroupMessage db groupMemberId msgId introId_ MSAForwarded -> pure () @@ -6939,7 +7087,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' - batchSendGroupMemberMessages user conn msgs + 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 @@ -6948,19 +7096,14 @@ sendPendingGroupMessages user GroupMember {groupMemberId} conn = do (ACMEventTag _ XGrpMemFwd_, Just introId) -> updateIntroStatus db introId GMIntroInvForwarded _ -> pure () --- TODO [batch send] refactor direct message processing same as groups (e.g. checkIntegrity before processing) -saveDirectRcvMSG :: Connection -> MsgMeta -> MsgBody -> CM (Connection, RcvMessage) -saveDirectRcvMSG conn@Connection {connId} agentMsgMeta msgBody = - case parseChatMessages msgBody of - [Right (ACMsg _ ChatMessage {chatVRange, msgId = sharedMsgId_, chatMsgEvent})] -> do - conn' <- updatePeerChatVRange conn chatVRange - let agentMsgId = fst $ recipient agentMsgMeta - newMsg = NewRcvMessage {chatMsgEvent, msgBody} - rcvMsgDelivery = RcvMsgDelivery {connId, agentMsgId, agentMsgMeta} - msg <- withStore $ \db -> createNewMessageAndRcvMsgDelivery db (ConnectionId connId) newMsg sharedMsgId_ rcvMsgDelivery Nothing - pure (conn', msg) - [Left e] -> error $ "saveDirectRcvMSG: error parsing chat message: " <> e - _ -> error "saveDirectRcvMSG: batching not supported" +saveDirectRcvMSG :: MsgEncodingI e => Connection -> MsgMeta -> MsgBody -> ChatMessage e -> CM (Connection, RcvMessage) +saveDirectRcvMSG conn@Connection {connId} agentMsgMeta msgBody ChatMessage {chatVRange, msgId = sharedMsgId_, chatMsgEvent} = do + conn' <- updatePeerChatVRange conn chatVRange + let agentMsgId = fst $ recipient agentMsgMeta + newMsg = NewRcvMessage {chatMsgEvent, msgBody} + rcvMsgDelivery = RcvMsgDelivery {connId, agentMsgId, agentMsgMeta} + msg <- withStore $ \db -> createNewMessageAndRcvMsgDelivery db (ConnectionId connId) newMsg sharedMsgId_ rcvMsgDelivery Nothing + pure (conn', msg) saveGroupRcvMsg :: MsgEncodingI e => User -> GroupId -> GroupMember -> Connection -> MsgMeta -> MsgBody -> ChatMessage e -> CM (GroupMember, Connection, RcvMessage) saveGroupRcvMsg user groupId authorMember conn@Connection {connId} agentMsgMeta msgBody ChatMessage {chatVRange, msgId = sharedMsgId_, chatMsgEvent} = do @@ -7031,59 +7174,85 @@ mkChatItem cd ciId content file quotedItem sharedMsgId itemForwarded itemTimed l meta = mkCIMeta ciId content itemText itemStatus Nothing sharedMsgId itemForwarded Nothing False itemTimed (justTrue live) currentTs itemTs forwardedByMember currentTs currentTs in ChatItem {chatDir = toCIDirection cd, meta, content, formattedText = parseMaybeMarkdownList itemText, quotedItem, reactions = [], file} -deleteDirectCI :: MsgDirectionI d => User -> Contact -> ChatItem 'CTDirect d -> Bool -> Bool -> CM ChatResponse -deleteDirectCI user ct ci@ChatItem {file} byUser timed = do - deleteCIFile user file - withStore' $ \db -> deleteDirectChatItem db user ct ci - pure $ CRChatItemDeleted user (AChatItem SCTDirect msgDirection (DirectChat ct) ci) Nothing byUser timed - -deleteGroupCI :: MsgDirectionI d => User -> GroupInfo -> ChatItem 'CTGroup d -> Bool -> Bool -> Maybe GroupMember -> UTCTime -> CM ChatResponse -deleteGroupCI user gInfo ci@ChatItem {file} byUser timed byGroupMember_ deletedTs = do - deleteCIFile user file - toCi <- withStore' $ \db -> - case byGroupMember_ of - Nothing -> deleteGroupChatItem db user gInfo ci $> Nothing - Just m -> Just <$> updateGroupChatItemModerated db user gInfo ci m deletedTs - pure $ CRChatItemDeleted user (gItem ci) (gItem <$> toCi) byUser timed +deleteDirectCIs :: User -> Contact -> [CChatItem 'CTDirect] -> Bool -> Bool -> CM ChatResponse +deleteDirectCIs user ct items byUser timed = do + let ciFilesInfo = mapMaybe (\(CChatItem _ ChatItem {file}) -> mkCIFileInfo <$> file) items + deleteCIFiles user ciFilesInfo + (errs, deletions) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (deleteItem db) items) + unless (null errs) $ toView $ CRChatErrors (Just user) errs + pure $ CRChatItemsDeleted user deletions byUser timed where - gItem = AChatItem SCTGroup msgDirection (GroupChat gInfo) + deleteItem db (CChatItem md ci) = do + deleteDirectChatItem db user ct ci + pure $ contactDeletion md ct ci Nothing -deleteLocalCI :: MsgDirectionI d => User -> NoteFolder -> ChatItem 'CTLocal d -> Bool -> Bool -> CM ChatResponse -deleteLocalCI user nf ci@ChatItem {file = file_} byUser timed = do - forM_ file_ $ \file -> do - let filesInfo = [mkCIFileInfo file] - deleteFilesLocally filesInfo - withStore' $ \db -> deleteLocalChatItem db user nf ci - pure $ CRChatItemDeleted user (AChatItem SCTLocal msgDirection (LocalChat nf) ci) Nothing byUser timed - -deleteCIFile :: MsgDirectionI d => User -> Maybe (CIFile d) -> CM () -deleteCIFile user file_ = - forM_ file_ $ \file -> do - let filesInfo = [mkCIFileInfo file] - cancelFilesInProgress user filesInfo - deleteFilesLocally filesInfo - -markDirectCIDeleted :: MsgDirectionI d => User -> Contact -> ChatItem 'CTDirect d -> MessageId -> Bool -> UTCTime -> CM ChatResponse -markDirectCIDeleted user ct ci@ChatItem {file} msgId byUser deletedTs = do - cancelCIFile user file - ci' <- withStore' $ \db -> markDirectChatItemDeleted db user ct ci msgId deletedTs - pure $ CRChatItemDeleted user (ctItem ci) (Just $ ctItem ci') byUser False +deleteGroupCIs :: User -> GroupInfo -> [CChatItem 'CTGroup] -> Bool -> Bool -> Maybe GroupMember -> UTCTime -> CM ChatResponse +deleteGroupCIs user gInfo items byUser timed byGroupMember_ deletedTs = do + let ciFilesInfo = mapMaybe (\(CChatItem _ ChatItem {file}) -> mkCIFileInfo <$> file) items + deleteCIFiles user ciFilesInfo + (errs, deletions) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (deleteItem db) items) + unless (null errs) $ toView $ CRChatErrors (Just user) errs + pure $ CRChatItemsDeleted user deletions byUser timed where - ctItem = AChatItem SCTDirect msgDirection (DirectChat ct) + deleteItem :: DB.Connection -> CChatItem 'CTGroup -> IO ChatItemDeletion + deleteItem db (CChatItem md ci) = do + ci' <- case byGroupMember_ of + Just m -> Just <$> updateGroupChatItemModerated db user gInfo ci m deletedTs + Nothing -> Nothing <$ deleteGroupChatItem db user gInfo ci + pure $ groupDeletion md gInfo ci ci' -markGroupCIDeleted :: MsgDirectionI d => User -> GroupInfo -> ChatItem 'CTGroup d -> MessageId -> Bool -> Maybe GroupMember -> UTCTime -> CM ChatResponse -markGroupCIDeleted user gInfo ci@ChatItem {file} msgId byUser byGroupMember_ deletedTs = do - cancelCIFile user file - ci' <- withStore' $ \db -> markGroupChatItemDeleted db user gInfo ci msgId byGroupMember_ deletedTs - pure $ CRChatItemDeleted user (gItem ci) (Just $ gItem ci') byUser False +deleteLocalCIs :: User -> NoteFolder -> [CChatItem 'CTLocal] -> Bool -> Bool -> CM ChatResponse +deleteLocalCIs user nf items byUser timed = do + let ciFilesInfo = mapMaybe (\(CChatItem _ ChatItem {file}) -> mkCIFileInfo <$> file) items + deleteFilesLocally ciFilesInfo + (errs, deletions) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (deleteItem db) items) + unless (null errs) $ toView $ CRChatErrors (Just user) errs + pure $ CRChatItemsDeleted user deletions byUser timed where - gItem = AChatItem SCTGroup msgDirection (GroupChat gInfo) + deleteItem db (CChatItem md ci) = do + deleteLocalChatItem db user nf ci + pure $ ChatItemDeletion (nfItem md ci) Nothing + nfItem :: MsgDirectionI d => SMsgDirection d -> ChatItem 'CTLocal d -> AChatItem + nfItem md = AChatItem SCTLocal md (LocalChat nf) -cancelCIFile :: MsgDirectionI d => User -> Maybe (CIFile d) -> CM () -cancelCIFile user file_ = - forM_ file_ $ \file -> do - let filesInfo = [mkCIFileInfo file] - cancelFilesInProgress user filesInfo +deleteCIFiles :: User -> [CIFileInfo] -> CM () +deleteCIFiles user filesInfo = do + cancelFilesInProgress user filesInfo + deleteFilesLocally filesInfo + +markDirectCIsDeleted :: User -> Contact -> [CChatItem 'CTDirect] -> Bool -> UTCTime -> CM ChatResponse +markDirectCIsDeleted user ct items byUser deletedTs = do + let ciFilesInfo = mapMaybe (\(CChatItem _ ChatItem {file}) -> mkCIFileInfo <$> file) items + cancelFilesInProgress user ciFilesInfo + (errs, deletions) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (markDeleted db) items) + unless (null errs) $ toView $ CRChatErrors (Just user) errs + pure $ CRChatItemsDeleted user deletions byUser False + where + markDeleted db (CChatItem md ci) = do + ci' <- markDirectChatItemDeleted db user ct ci deletedTs + pure $ contactDeletion md ct ci (Just ci') + +markGroupCIsDeleted :: User -> GroupInfo -> [CChatItem 'CTGroup] -> Bool -> Maybe GroupMember -> UTCTime -> CM ChatResponse +markGroupCIsDeleted user gInfo items byUser byGroupMember_ deletedTs = do + let ciFilesInfo = mapMaybe (\(CChatItem _ ChatItem {file}) -> mkCIFileInfo <$> file) items + cancelFilesInProgress user ciFilesInfo + (errs, deletions) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (markDeleted db) items) + unless (null errs) $ toView $ CRChatErrors (Just user) errs + pure $ CRChatItemsDeleted user deletions byUser False + where + markDeleted db (CChatItem md ci) = do + ci' <- markGroupChatItemDeleted db user gInfo ci byGroupMember_ deletedTs + pure $ groupDeletion md gInfo ci (Just ci') + +groupDeletion :: MsgDirectionI d => SMsgDirection d -> GroupInfo -> ChatItem 'CTGroup d -> Maybe (ChatItem 'CTGroup d) -> ChatItemDeletion +groupDeletion md g ci ci' = ChatItemDeletion (gItem ci) (gItem <$> ci') + where + gItem = AChatItem SCTGroup md (GroupChat g) + +contactDeletion :: MsgDirectionI d => SMsgDirection d -> Contact -> ChatItem 'CTDirect d -> Maybe (ChatItem 'CTDirect d) -> ChatItemDeletion +contactDeletion md ct ci ci' = ChatItemDeletion (ctItem ci) (ctItem <$> ci') + where + ctItem = AChatItem SCTDirect md (DirectChat ct) createAgentConnectionAsync :: ConnectionModeI c => User -> CommandFunction -> Bool -> SConnectionMode c -> SubscriptionMode -> CM (CommandId, ConnId) createAgentConnectionAsync user cmdFunction enableNtfs cMode subMode = do @@ -7436,8 +7605,8 @@ chatCommandP = "/_send " *> (APISendMessage <$> chatRefP <*> liveMessageP <*> sendMessageTTLP <*> (" json " *> jsonP <|> " text " *> (ComposedMessage Nothing Nothing <$> mcTextP))), "/_create *" *> (APICreateChatItem <$> A.decimal <*> (" json " *> jsonP <|> " text " *> (ComposedMessage Nothing Nothing <$> mcTextP))), "/_update item " *> (APIUpdateChatItem <$> chatRefP <* A.space <*> A.decimal <*> liveMessageP <* A.space <*> msgContentP), - "/_delete item " *> (APIDeleteChatItem <$> chatRefP <* A.space <*> A.decimal <* A.space <*> ciDeleteMode), - "/_delete member item #" *> (APIDeleteMemberChatItem <$> A.decimal <* A.space <*> A.decimal <* A.space <*> A.decimal), + "/_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), "/_read user " *> (APIUserRead <$> A.decimal), @@ -7833,8 +8002,8 @@ adminContactReq :: ConnReqContact adminContactReq = either error id $ strDecode "simplex:/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D" -simplexContactProfile :: Profile -simplexContactProfile = +simplexTeamContactProfile :: Profile +simplexTeamContactProfile = Profile { displayName = "SimpleX Chat team", fullName = "", @@ -7843,6 +8012,16 @@ simplexContactProfile = preferences = Nothing } +simplexStatusContactProfile :: Profile +simplexStatusContactProfile = + Profile + { displayName = "SimpleX-Status", + fullName = "", + image = Just (ImageData "data:image/jpg;base64,/9j/4AAQSkZJRgABAQAASABIAAD/4QBYRXhpZgAATU0AKgAAAAgAAgESAAMAAAABAAEAAIdpAAQAAAABAAAAJgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAr6ADAAQAAAABAAAArwAAAAD/7QA4UGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAAAA4QklNBCUAAAAAABDUHYzZjwCyBOmACZjs+EJ+/8AAEQgArwCvAwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/bAEMAAQEBAQEBAgEBAgMCAgIDBAMDAwMEBgQEBAQEBgcGBgYGBgYHBwcHBwcHBwgICAgICAkJCQkJCwsLCwsLCwsLC//bAEMBAgICAwMDBQMDBQsIBggLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLC//dAAQAC//aAAwDAQACEQMRAD8A/v4ooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAP/Q/v4ooooAKKKKACiiigAoorE8R+ItF8J6Jc+IvEVwlrZ2iGSWWQ4CgVUISlJRirtmdatTo05VaslGMU223ZJLVtvokbdFfl3of/BRbS734rtpup2Ig8LSsIYrjnzkOcea3bafTqBX6cafqFjq1jFqemSrPbzqHjkQ5VlPIINetm2Q43LXD65T5eZXX+XquqPiuC/Efh/itYh5HiVUdGTjJWaflJJ6uEvsy2fqXKKKK8c+5Ciq17e2mnWkl/fyLDDCpd3c4VVHJJJr8c/2kf8Ago34q8M3mpTfByG3fT7CGSJZrlC3nStwJF5GFU8gd69LA5VicXTrVaMfdpxcpPokk397toj4LjvxKyLhGjRqZxValVkowhFc05O9m0tPdjfV7dN2kfq346+J3w9+GWlPrXxA1m00i1QZL3Uqxj8Mnn8K/Mj4tf8ABYD4DeEJ5dM+Gmn3niq4TIE0YEFtn/ffBI+imv51vHfxA8b/ABR1+bxT8RNUuNXvp3LtJcOWCk84VeigdgBXI18LXzupLSkrL72fzrxH9IXNsTKVPKKMaMOkpe/P8fdXpaXqfqvrf/BYH9p6+1w3+iafo1jZA8WrRPKSPeTcpz9BX1l8J/8Ags34PvxDp/xn8M3OmSnAe709hcQfUoSHA/A1/PtSE4/GuKGZ4mLvz39T4TL/ABe4swlZ1ljpTvvGaUo/dbT/ALdsf2rfCX9pT4HfHGzF18M/EdnqTYBaFXCzJn+9G2GH5V7nX8IOm6hqGkX8eraLcy2d3EcpPbuY5FPsykGv6gf+CWf7QPxB+OPwX1Ky+JF22pX3h69+yJdyf62WJlDrvPdlzjPevdwGae3l7OcbP8D+i/DTxm/1ixkcqx2H5K7TalF3jLlV2rPWLtqtWvM/T2iiivYP3c//0f7+KKKKACiiigAooooAK/Fv/goX8Qvi2fFcXgfWrRtP8NDEls0bZS7YfxORxlT0Xt1r9pK8u+L/AMI/Cfxp8F3HgvxbFujlGYpgB5kMg6Op9R+tfR8K5vQy3MYYnE01KK0843+0vNf8NZn5f4wcFZhxTwziMpy3FOjVeqSdo1Lf8u5u11GXk97Xuro/mBFyDX3t+yL+2Be/CW+h8B+OHafw7cyALIxJa0Ldx6p6jt1FfMvx/wDgR4w/Z+8YN4d8RoZrSbLWd4owk6D+TDuK8KF0K/pLFYHA51geWVp0pq6a/Brs1/wH2P8ALvJsz4h4D4h9tR5qGLoS5ZRls11jJbSjJferSi9mf1uafqFlqtlFqWmyrPBOoeORDlWU8gg069vrPTbSS/v5FhghUu7ucKqjqSa/CH9j79sm++EuoQ/D/wAeSNceHbmRVjlZstZk9x6p6jt2q3+15+2fffFS8n8AfD2V7bw9CxWWZThrwj+Se3evxB+G2Zf2n9TX8Lf2nTl/+S/u/PbU/v2P0nuGv9Vf7cf+9/D9Xv73tLd/+ffXn7afF7pqftbfth3nxUu5vAXgGR7fw/A5WWUHDXZX19E9B361+Z/xKm3eCL9R3UfzFbQul6Cn+I/A3ivxR8LPEXivSbVn07RoVkurg8Iu5gAue7HPSv1HOsrwmVcN4uhRSjBUp6vq3Fq7fVt/5I/gTNeI884x4kjmeYOVWtKSdop2hCPvWjFbQjFNv5ybbuz4Toqa0ge9uoLOIhWnkSNSxwAXIUEnsBnmv0+/aK/4Jg+O/gj8Hoviz4b1n/hJFt40l1G2ig2NDG4yZEIJ3KvfgHHNfxVTw9SpGUoK6W5+xZVw1mWZYfEYrA0XOFBKU2raJ31te72b0T0R+XRIAyegr+gr/glx+yZoHhjwBc/tKfFywiafUY2OmpeIGS3sVGWmIbgF+TkjhR71+YP7DX7Lt9+1H8ZLfR75WTw5pBS61ScDKsoIKwg+snf0Ffqd/wAFSv2o4Phf4Ltv2WvhmVtrjUbRBfvA2Ps1kOFhAHQyAc9ML9a9HL6UacHi6q0W3mz9Q8M8owuV4KvxpnEL0aN40Yv/AJeVXpp5LZPo7v7J+M/7U/jX4e/EL4/+JfFXwrsI9P0Ke5K26RKESTZw0oUcAOeQBX7J/wDBFU5+HPjYf9RWH/0SK/nqACgKOgr+hT/giouPh143b11SH/0SKWVzc8YpPrf8jHwexk8XxzSxVRJSn7WTSVknKMnoui7H7a0UUV9cf3Mf/9L+/iiiigAoorzX4wfGD4afAP4bav8AF74v6xbaD4d0K3e6vb26cJHHGgyevUnoAOSeBTjFyajFXYHpVFf55Xxt/wCDu34nj9vzS/G3wX0Qz/ArQ2ksLnSp1CXurQyMA15uPMTqBmJD2+914/uU/Y//AGxfgH+3P8ENL+P37OutxazoWpoNwHyzW02PmhmjPKSKeCD9RxXqY/JcXg4QqV4WUvw8n2ZnCrGTaTPqGiiivKNDy/4u/CLwd8afBtx4N8ZW4kilBMUoH7yGTs6HsR+tfzjftA/AXxl+z54yfw34jQzWkuXs7xF/dzR/0YdxX9OPiDxBofhPQ7vxN4mu4rDT7CF57m4ncJHFFGMszMcAAAZJNf53n/Bav/g5W1H4ufGjTvg5+xB5F14E8JX4l1HVriIE6xNE2GjhLDKQdRuGC55HHX9L8Os+x2ExP1eKcsO/iX8vmvPy6/ifg3jZ4NYDjDBPFUEqeYU17k/50vsT8n0lvF+V0fq0LhTUgnA4r4y/ZG/bJ+FX7YXw9HjDwBP5N/ahV1LTZeJrSUjoR3U/wsOK+sRdL/n/APXX9G0nCrBTpu6Z/mVmuSYvLcXUwOPpOnWg7SjJWaf9ap7NarQ+pf2dP2evGH7Q3i4aLogNvp1uQ15esMpEnoPVj2Ffrd+1V8GvDnw5/YU8X+APh/Z7IrewEjYGXlZGUs7nqSQM18C/sO/ti6b8F7o/Dnx6qpoN9LvS6RRvglbjL45ZT69vpX7wX1poHjjwxNYzbL3TdUt2jbaQySRSrg4PoQa/nnxXxGaTxLwmIjy4e3uW2lpu33Xbp87v+7Po58I8L4nhfFVMuqKeY1oTp1nJe9S5k0oxWtoPfmXxve1uVfwqKA0YHYiv6Ev+CZ37bVv490eP9mb4zXAn1GKJo9Murg5F3bgYMLk9XUcD+8tflR+1/wDsn+Nv2XfiNdadqFs8vh28md9Mv1GY3iJyEY9nXoQa+UrC/v8ASr+DVdJnktbq2dZYZomKvG6nIZSOhFfztQrVMJW1Xqu5+Z8PZ5mvBWeSc4NSg+WrTeinHqv1jL56ptP+s7xHZ/A//gnR8EfE/jTwra+RHqF5JdxWpbLTXcwwkSnrsGPwXNfyrfEDx54l+J/jXU/iB4wna51LVZ3nmdj3Y8KPQKOAPQV2vxX/AGhvjT8corC3+K2vz6vFpq7beNgERT3YqvBY92NeNVeOxirNRpq0Fsju8RePKWfTo4TLqPscFRXuU9F7z+KTSuvJK7srvqwr+ir/AIIuaVd2/wAH/FesSIRDd6uFjb+8Y41Dfka/BX4YfCzx78ZfGVr4C+G+nyajqV22Aqj5I17u7dFUdya/r+/ZV+Aenfs2fBLSPhbZyC4ntVaW7nAx5tzKd0jfTJwPYV1ZLQk63tbaI+w8AOHcXiM8ebcjVClGS5ujlJWUV3sm27baX3R9FUUUV9Uf2gf/0/7+KKKKACv4If8Ag8QT9vN9W8IsVk/4Z+WJedOL7f7Xyd32/HGNu3yc/LnPev73q84+Lnwj+G/x3+HGr/CT4uaRba74d123e1vbK6QPHJG4weD0I6gjkHkV6WUY9YLFQxDgpJdP8vMipDmi0f4W1frt/wAEhP8Agrt8af8AglD8b38V+Fo21zwPr7xp4i0B3KpcRoeJoTyEnjBO04+boeK+m/8AguZ/wQz+I3/BMD4kyfEn4Ww3fiD4Oa5KzWWolC76XKx4tbphwOuI3PDAc81/PdX7LCeFzHC3VpU5f18mjympU5eZ/t9fsk/tb/Av9tv4G6N+0F+z3rUWs6BrEQYFCPNt5cfPDMnVJEPDKf5V794h8Q6F4T0O78TeJ7uGw06wiae4uZ3EcUUaDLMzHAAA6k1/j9f8EiP+Cunxv/4JTfHAeKPCZfWfAuuyRx+IvD8jkRTxg486Lsk8YJ2n+Loa/V7/AILy/wDBxZd/t2eHl/Zc/Y6mu9I+Gl1DDNrWoSBoLvUpGAY2+OqQoeH/AL5GOlfneI4OxCxio0taT+12Xn59u53xxMeW73ND/g4M/wCDgzVP2yNV1H9jz9j3UZrD4ZWE7waxrEDlH110ONiEYItgQe/7z6V/I6AAMDgCgAKNo6Cv0j/4Jkf8Ex/j/wD8FOvj/Y/Cj4UWE9voFvNGdf18xk2um2pPzEt0MhGdiZyTX6FhsNhctwvLH3YR1bfXzfn/AEjhlKVSR77/AMEMf2Rf2v8A9qr9tPRrb9mNpdL0fSp438UaxKjNYW+nk/PHKOA7uoIjTrnniv7Lfj98CvG37PPjiXwj4uiLxNl7S7UYjuIuzD39R1Ffvt+wn+wd+z5/wTy+A+n/AAF/Z70pbKyt1V728cA3V/c4w0079WYnoOijgV7V8cPgb4G+Pngqfwb41twwYEwXCgebBJ2ZT/MdDXi5N4mTwmYWqRvhXpb7S/vL9V28z8c8YfBXC8XYL61hbQx9Ne7LpNfyT8v5ZfZfkfyXi5r9Lf2Jv24bn4S3UHwz+JkzT+HZ5AsNy5LNZlu3vHn8q+KPj38CPHf7PPjabwn4yt2ELMxtLsD91cRg8Mp6Z9R2rxAXAPANfuePyzL89y/2c7TpTV1JdOzT6Nf8Bn8C5FnGfcEZ79Yw96OJpPlnCS0a6xkusX/k4u9mf2IeK/B/w++Mngt9C8U2ltrWi6lEGCuA6OrDhlPY+hHNfztftw/8E4tN+AGlTfE34ba3HJo0koVdMvGC3CFv4Ym/5aAenBArvf2PP2+9R+CGmv4B+JSy6joEUbtaOp3TQOBkRj1Rjx7V8uftEftH+Nf2i/G7+KPEzmG0hyllZqT5cEef1Y9zX4LT8GMTisynhsY7UI6qot5J7Jefe+i87o/prxI8YuEM/wCF6WM+rc2ZSXKo6qVJrdykvih/Ktebsmnb4DkilicxyqVYdQRzXUaN4R1HVMSzjyIf7zDk/QV6dIlpJIJ5Y1Z16MRk1+qf7DX7Ed58ULmH4p/Fe2kt/D8Dq9paSDabwjncf+mf/oX0rKXg3lOR+0zDPMW6lCL92EVyufZN3vfyjbvdI/AeFsJnHFOPp5TktD97L4pP4YLrJu2iXnq3ok20es/8Erv2f/G/gf8AtD4ozj7Bo2pwiFIpY/3t2VOQ4J5VFzx659q/aKq9paWthax2VlGsUMShERBtVVHAAA6AVYr4LNcdTxWIdSjRjSpqyjGKslFber7t6tn+k3APB1LhjJaOUUqsqjjdylJ/FKTvJpfZV9orbzd2yiiivNPsj//U/v4ooooAKKKKAPO/iz8Jvh18c/h1q/wm+LGk2+ueHtdt3tb2yukDxyxuMEEHoR1B6g81/lm/8Fy/+CFfxG/4Jh/ENvid8J4bzxF8Htdmke1vliaRtHctxbXTAEBecRyHAbGDzX+q54j8R6B4Q0C88U+KbyHT9N0+F7i5ubhxHFFFGMszMcAADqa/zM/+Dhb/AIL06p+3f4rvP2Tf2Xr6S0+Eui3DR397GcHXriM8N7W6EfIP4jz6V9fwfPGLFctD+H9q+3/D9jmxKjy+9ufyq0UAY4or9ZPMP0v/AOCX3/BLf9oT/gqP8d4Phf8ACa0lsvDtjLG3iDxDJGTa6bbse56NKwB8uPOSfav9ZX9hD9hT4Df8E8v2fdK/Z7+AenLbWNkoe8vHUfab+6I+eeZhyWY9B0UcCv8AKC/4JUf8FV/j1/wSu+PCfEf4aSHUvC+rPHH4i0CViIL63U43D+7MgJKN+B4r/Wd/Yy/bM+BH7eHwH0j9oL9n7Vo9S0fU4182LI8+0nx88MydVdTxz16ivzbjZ43nipfwOlu/n59uh6GE5Labn1ZRRRXwB2Hi3x3+BPgj9oHwJceCPGcIIYFre4UfvYJezKf5jvX8vH7QvwB8d/s4eOZfB/jKEtDIS9neKP3VxFngqfX1Hav6gvj58e/An7PHgK48ceN7gLtBW2twf3txL2RR/M9hX8rX7Qn7Rnjz9o3x5L418ZyhUXKWlqh/dW8WeFUevqe5r988G4Zu3Ut/ueu/839z/wBu6fM/jj6UdPhlwo8y/wCFTS3Lb+H/ANPf/bPtf9unlQuAec077SPWueFznrTxc1+/eyP4udE/XX9g79h24+K8tv8AF74qQvD4fgkDWdo64N4V53H/AKZg/wDfX0r+ge0tLWwtY7KyjWKGJQiIgwqqOAAOwFfzc/sIft2XnwO1KH4ZfEeVp/Ct5L8k7Es9k7YHH/TMnkjt1r+kDTNT07WtOg1fSJ0ubW5QSRSxncjowyCCOoNfyr4q0s3jmreYfwtfZW+Hl/8Akv5r6/Kx/or9HSXDX+rqhkqtidPb81vac/d/3P5Lab/auXqKKK/Lz+gwooooA//V/v4ooooAKxfEniTQPB2gXnirxVew6dpunQvcXV1cOI4oYoxlndjgAADJJrar/PV/4Ozf+CiX7Xlr8Yrf9hCx0u98GfDaS0iv5L1GZT4iZs5HmKceTERgx9d3LcYr08py2eOxMaEXbu/L9SKk1CN2fIX/AAcD/wDBfrXv27vFF1+yx+ylqFzpnwl0id476+icxSa/MhwGOMEWykHYv8fU9hX8qoAAwOAKUAAYFfqj/wAEnf8AglH8cv8Agqp8ek+Hvw/R9M8I6NJFJ4k19lzHZW7k/ImeGmcAhF/E8V+xUKGFyzC2Xuwju/1fds8tuVSXmM/4JQ/8Epfjr/wVU+Pcfw5+HiPpXhPSXjl8ReIZEJhsoGP3E7PO4B2J+J4r7o/4Li/8EC/H3/BL/UYPjH8Hp7vxV8JNQMcL3sy7rnTLkgDbcFRjZI3KPwATg9q/0rP2MP2MPgL+wZ8BdI/Z5/Z60hNM0bS4x5kpANxeTn7887gAvI55JPToOK9y+J/ww8AfGfwBqvwu+KOlW+t6Brdu9re2V0gkilicYIIP6HqDXwVbjSu8YqlNfulpy9139e3Y7VhY8tnuf4VdfqD/AMErP+Cpvx1/4Jb/ALQNn8S/h7cS6j4VvpUj8QeH2kIt723zgsB0WVRyjetffn/BeH/ghJ4x/wCCZvjlvjP8EYbvXPg5rk7GKcqZJdGmc5FvOwH+rOcRyH0wea/nCr9ApVcNmOGuvehL+vk0cLUqcvM/24v2Mf20PgH+3l8CdK/aA/Z61iPVNI1FF86LI+0Wc+PnhnTqjqeOevUcV3nx/wD2gfh/+zp4CuPHHjq5CBQVtrZT+9uJeyIP5noBX+Ud/wAEL/25f2t/2NP2u7A/s7xPrPhzW5Yk8T6LOzCyls1PzTE9I5UXJRupPHIr+p39o79pXx/+0v8AEGbxv42l2RrlLO0QnyreLPCqPX1PUmvM4b8KauYZg5VJWwkdW/tP+6vPu+i8z8r8VvF3D8L4P6vhbTx017sekF/PL/21fafkjV/aF/aN8e/tHePZ/GvjOc+XuK2lopPlW8WeFUevqe9eFfasDmsL7UB1r9kv+Cen/BPuX4mPa/Gv41Wrw6HE4k0/T5FwbsjkO4PPl56D+L6V/QWbZjlnDmW+1q2hSgrRit2+kYrq/wDh2fw9kXDmdcZ526NK9SvUfNOctorrKT6JdF6JIh/Yq/4JyXXxq8MSfEn4wtPpukXkLLp1vH8s0hYcTHPRR1Ud6+KP2nP2bvHX7MXj+Twl4pUz2U+Xsb5QRHcRZ/Rh/Etf2D2trbWNtHZ2caxRRKEREGFVRwAAOgFeSfHL4G+Af2gvAVz4A8f2wmt5huimUDzYJB0dD2I/Wv5/yrxgx0c3niMcr4abtyL7C6OPdrr/ADeWlv604g+jdlFTh6ngsrfLjaauqj/5eS6xn2i/s2+Hz1v/ABi+d3r9O/2DP28r/wCBGpRfDT4lSvdeFL2UBJmYs9izcZX1j7kduor48/ah/Zr8bfsu/EWTwZ4pHn2c4MtheqMJcQ5IB9mHRhXzd9oAFf0Djsuy3iHLeSdqlGorpr8Gn0a/4DW6P5DyrMc74Mzz2tG9LE0XaUXs11jJdYv/ACaezP7pdK1bTNd02DWdGnS6tLlBJFLEwZHRuQQR1FaFfix/wSG1n47X3hPVLHXUL+BoT/oEtxneLjPzLD6pjr2B6d6/aev424nyP+yMyrZf7RT5Huvv17NdV0Z/pTwPxP8A6w5Lh82dGVJ1FrGXdaNp9YveL6oKKKK8A+sP/9b+/iiiigAr4E/4KI/8E4f2b/8AgpZ8DLr4M/H7SklljV5NJ1aJQLzTblhxLC/Uc43L0YcGvvuitKNadKaqU3aS2Ymk1Zn+Vt8Nf+DZH9vDxJ/wUEn/AGQfGti+m+DdMkF5eeNlTNjLpRb5Xgz964cfL5XVWyTx1/0lv2L/ANif9nv9gn4H6b8Bv2dNDh0jSrFF8+YKDcXs4GGmuJOskjHPJ6dBxX1lgZz3pa9bNc+xWPjGFV2iui6vu/60M6dKMNgooorxTU4T4m/DHwB8ZfAeqfDH4paRba7oGtQPbXtjeRiWGaJxghlII/wr/M//AOCw/wDwbq/En9kb9o7Ttc/ZhQ6h8KvGl4VgknkUyaJIxy0UmTueMDmNgCexr/SN/aA/aA+Hf7N3w6u/iL8RbtYYIFIggBHm3Ev8Mca9yfyA5NfyB/tTftZfEX9qv4gSeL/GEv2exgLJYWEZPlW8WeOO7H+Ju9fsXhRwnmOZYl4hNwwi+Jv7T/lj5930Xnofj3iv4nYThrCPD0bTxs17kekV/PPy7L7T8rn58fs1fs1/Df8AZg8Dp4U8CwB7qYK19fuAZrmQDkseyjsvQV9GfaWrAWcjvUnnt6mv62w+Cp0KapUo2itkfwFmOLxWPxNTGYyo51Zu8pN6t/1stktEftx/wTa/YHsfi6sHx2+L8aT6BFJnT7DcGFy6dWlAzhQf4T171/SBaWltY20dlZRrFDEoREQYVVHAAA6AV/Hv+xJ+3N4y/ZO8Wi0ui+oeE9QkX7dYk5KdjLFzw49Ohr+tj4c/Efwb8WPB1l498A30eoaZqEYkiljOevVWHZh0IPIr+TPGXLs6p5p9Zxz5sO9KbXwxX8rXSXd/a3Wmi/t76P8AmHD08l+qZZDkxUdaydueT/mT0vDsl8Oz1d33FFFFfjR/QB4x8dPgN8O/2hvA1x4F+Idms8MgJhmAxLbydnjbqCP1r8RPg3/wSV8Z/wDC9r7T/izMreDNIlEkM8TYfUVPKpgcoAPv+/Ar+iKivrsh43zbKMLWweCq2hUXXXlf80eza0/HdJnwPFHhpkHEGOw+YZlQ5qlJ7rTnXSM/5op6/hs2jD8NeGdA8HaHbeGvC9nFYWFmgjhghUIiKOwArcoor5Oc5Tk5Sd292fd06cacVCCtFaJLZLsgoooqSz//1/7+KKKKACiiigAooooAK8J/aK/aG+H37M/wzvPiX8QrgRwwDbb26kebczH7saDuSep7DmvdW3bTt69s1/Hj/wAFS9c/acu/2hbiw+Psf2fTYWf+w47bd9ha2zw0ZPWQj7+eQfav0Dw44PpcRZssLXqqFOK5pK9pSS6RXfu+i1PzvxN4zrcN5PLF4ei51JPli7XjFv7U327Lq9Dwr9qv9rn4lftZ+Pv+Ev8AG8i29na7ksNPiJ8m2iJ7Ak5Y/wATHrXy/wDacDJNYfn45PFftR/wTX/4Ju6j8aryz+OXxttpLXwtbSrJY2Mi7W1Bl53MD0hB/wC+vpX9jZpmGU8LZT7WolTo01aMVu30jFdW/wDNvqz+HcryTOeLs4dODdSvUd5Tlsl1lJ9Eui9Elsix/wAE8/8Agmpc/Hq3HxZ+OcFxY+F8f6Daj93Jen++eMiMdum76V88ft4fsM+LP2RvGH9p6MJtS8G6gxNnfMMmFj/yxmIAAYfwnuPev7DbGxs9Ms4tP0+JYIIFCRxoNqqq8AADoBXL+P8AwB4R+KHhG+8C+OrGPUNM1CMxTQyjIIPcehHUEdDX8x4PxqzWOdvH11fDS0dJbKPRp/zrdvrtta39V47wCyWeQRy7D6YqOqrPeUuqkv5Hsl9ndXd7/wACwuGHevvT9iL9u7x1+yP4n+wMDqXhPUJVN/YMTlOxlh/uuB+BqH9vD9hXxl+yD4v/ALS03zNT8HajIfsV8VyYSf8AljNjgMOx/iHvX59C6bHav6fjDKeJsqurVcPVX9ecZRfzTP5LdLOeE850vRxNJ/15SjJfJo/v3+GnxJ8HfF3wRp/xC8BXiX2l6lEJYZEPr1Vh2YdCDyDXd1/PD/wRa8KftJW8moeKfPNp8N7kMBBdKT9ouR/Hbgn5QP4m6Gv6Hq/iHjXh6lkmb1svoVlUjF6Nbq/2ZdOZdbfhsf6AcC8SVs9yahmWIoOlOS1T2dvtR68r3V/x3ZRRRXyh9eFFFFABRRRQB//Q/v4ooooAKKKKACiiigAr5u/aj/Zg+HX7VvwyuPh14+i2N/rLO8jA861mHR0Pp2YdCOK+kaK6sDjq+DxEMVhZuFSDumt00cmOwOHxuHnhcVBTpzVpJ7NM/nF/ZW/4I2eINL+MV9rH7Rk0Vz4d0G5H2GCA8anjlXfuiDjcvJJ46V/RfY2FlpdlFpumxJBbwII444wFVEUYAAHAAFW6K9/injHM+Ia8a+Y1L8qsorSK7tLu3q3+iSPn+E+C8q4dw86GW07czvJvWT7Jvstkv1bYUUUV8sfVnEfEb4c+Dvix4Mv/AAB49sY9Q0vUYjFNDIMjB7j0YdQRyDX4HeH/APgiNJB+0LKNe1vzvhzARcxBeLyUEn/R27ADu46jtmv6KKK+r4d42zjI6Vajl1ZxjUVmt7P+aN9pW0uv0R8lxJwNk2e1aFfMqCnKk7p7XX8srbxvrZ/qzn/CnhXw/wCCPDll4R8K2sdlp2nQrBbwRDCoiDAAFdBRRXy05ynJzm7t6tvqfVwhGEVCCsloktkgoooqSgooooAKKKKAP//R/v4ooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAP/Z"), + contactLink = Just (either error id $ strDecode "simplex:/contact/#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FShQuD-rPokbDvkyotKx5NwM8P3oUXHxA%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEA6fSx1k9zrOmF0BJpCaTarZvnZpMTAVQhd3RkDQ35KT0%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion"), + preferences = Nothing + } + timeItToView :: String -> CM' a -> CM' a timeItToView s action = do t1 <- liftIO getCurrentTime diff --git a/src/Simplex/Chat/Archive.hs b/src/Simplex/Chat/Archive.hs index 01897de791..218d1e1f2e 100644 --- a/src/Simplex/Chat/Archive.hs +++ b/src/Simplex/Chat/Archive.hs @@ -52,18 +52,22 @@ archiveAssetsFolder = "simplex_v1_assets" wallpapersFolder :: String wallpapersFolder = "wallpapers" -exportArchive :: ArchiveConfig -> CM' () +exportArchive :: ArchiveConfig -> CM' [ArchiveError] exportArchive cfg@ArchiveConfig {archivePath, disableCompression} = withTempDir cfg "simplex-chat." $ \dir -> do StorageFiles {chatStore, agentStore, filesPath, assetsPath} <- storageFiles copyFile (dbFilePath chatStore) $ dir archiveChatDbFile copyFile (dbFilePath agentStore) $ dir archiveAgentDbFile - forM_ filesPath $ \fp -> - copyDirectoryFiles fp $ dir archiveFilesFolder + errs <- + forM filesPath $ \fp -> + copyValidDirectoryFiles entrySelectorError fp $ dir archiveFilesFolder forM_ assetsPath $ \fp -> copyDirectoryFiles (fp wallpapersFolder) $ dir archiveAssetsFolder wallpapersFolder let method = if disableCompression == Just True then Z.Store else Z.Deflate Z.createArchive archivePath $ Z.packDirRecur method Z.mkEntrySelector dir + pure $ fromMaybe [] errs + where + entrySelectorError f = (Z.mkEntrySelector f $> Nothing) `E.catchAny` (pure . Just . show) importArchive :: ArchiveConfig -> CM' [ArchiveError] importArchive cfg@ArchiveConfig {archivePath} = @@ -85,7 +89,7 @@ importArchive cfg@ArchiveConfig {archivePath} = (doesDirectoryExist fromDir) (copyDirectoryFiles fromDir fp) (pure []) - `E.catch` \(e :: E.SomeException) -> pure [AEImport . ChatError . CEException $ show e] + `E.catch` \(e :: E.SomeException) -> pure [AEImport $ show e] _ -> pure [] withTempDir :: ArchiveConfig -> (String -> (FilePath -> CM' a) -> CM' a) @@ -94,14 +98,22 @@ withTempDir cfg = case parentTempDirectory (cfg :: ArchiveConfig) of _ -> withSystemTempDirectory copyDirectoryFiles :: FilePath -> FilePath -> CM' [ArchiveError] -copyDirectoryFiles fromDir toDir = do +copyDirectoryFiles fromDir toDir = copyValidDirectoryFiles (\_ -> pure Nothing) fromDir toDir + +copyValidDirectoryFiles :: (FilePath -> IO (Maybe String)) -> FilePath -> FilePath -> CM' [ArchiveError] +copyValidDirectoryFiles isFileError fromDir toDir = do createDirectoryIfMissing True toDir fs <- listDirectory fromDir foldM copyFileCatchError [] fs where copyFileCatchError fileErrs f = - (copyDirectoryFile f $> fileErrs) - `E.catch` \(e :: E.SomeException) -> pure (AEImportFile f (ChatError . CEException $ show e) : fileErrs) + liftIO (isFileError f) >>= \case + Nothing -> + (copyDirectoryFile f $> fileErrs) + `E.catch` \(e :: E.SomeException) -> addErr $ show e + Just e -> addErr e + where + addErr e = pure $ AEFileError f e : fileErrs copyDirectoryFile f = do let fn = takeFileName f f' = fromDir fn diff --git a/src/Simplex/Chat/Bot.hs b/src/Simplex/Chat/Bot.hs index c5c5ff7eed..f3de92e1f2 100644 --- a/src/Simplex/Chat/Bot.hs +++ b/src/Simplex/Chat/Bot.hs @@ -3,6 +3,7 @@ {-# LANGUAGE GADTs #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedLists #-} module Simplex.Chat.Bot where @@ -73,9 +74,9 @@ sendComposedMessage' cc ctId quotedItemId msgContent = do deleteMessage :: ChatController -> Contact -> ChatItemId -> IO () deleteMessage cc ct chatItemId = do - let cmd = APIDeleteChatItem (contactRef ct) chatItemId CIDMInternal + let cmd = APIDeleteChatItem (contactRef ct) [chatItemId] CIDMInternal sendChatCmd cc cmd >>= \case - CRChatItemDeleted {} -> printLog cc CLLInfo $ "deleted message from " <> contactInfo ct + CRChatItemsDeleted {} -> printLog cc CLLInfo $ "deleted message(s) from " <> contactInfo ct r -> putStrLn $ "unexpected delete message response: " <> show r contactRef :: Contact -> ChatRef diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 0c34ea2beb..18b6c694e4 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -69,7 +69,7 @@ import Simplex.Chat.Types.UITheme import Simplex.Chat.Util (liftIOEither) import Simplex.FileTransfer.Description (FileDescriptionURI) import Simplex.Messaging.Agent (AgentClient, SubscriptionsInfo) -import Simplex.Messaging.Agent.Client (AgentLocks, AgentQueuesInfo (..), AgentWorkersDetails (..), AgentWorkersSummary (..), ProtocolTestFailure, ServerQueueInfo, SMPServerSubs, UserNetworkInfo) +import Simplex.Messaging.Agent.Client (AgentLocks, AgentQueuesInfo (..), AgentWorkersDetails (..), AgentWorkersSummary (..), ProtocolTestFailure, SMPServerSubs, ServerQueueInfo, UserNetworkInfo) import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, NetworkConfig, ServerCfg) import Simplex.Messaging.Agent.Lock import Simplex.Messaging.Agent.Protocol @@ -293,8 +293,8 @@ data ChatCommand | APISendMessage {chatRef :: ChatRef, liveMessage :: Bool, ttl :: Maybe Int, composedMessage :: ComposedMessage} | APICreateChatItem {noteFolderId :: NoteFolderId, composedMessage :: ComposedMessage} | APIUpdateChatItem {chatRef :: ChatRef, chatItemId :: ChatItemId, liveMessage :: Bool, msgContent :: MsgContent} - | APIDeleteChatItem ChatRef ChatItemId CIDeleteMode - | APIDeleteMemberChatItem GroupId GroupMemberId ChatItemId + | 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} | APIUserRead UserId @@ -599,7 +599,7 @@ data ChatResponse | CRChatItemUpdated {user :: User, chatItem :: AChatItem} | CRChatItemNotChanged {user :: User, chatItem :: AChatItem} | CRChatItemReaction {user :: User, added :: Bool, reaction :: ACIReaction} - | CRChatItemDeleted {user :: User, deletedChatItem :: AChatItem, toChatItem :: Maybe AChatItem, byUser :: Bool, timed :: Bool} + | CRChatItemsDeleted {user :: User, chatItemDeletions :: [ChatItemDeletion], byUser :: Bool, timed :: Bool} | CRChatItemDeletedNotFound {user :: User, contact :: Contact, sharedMsgId :: SharedMsgId} | CRBroadcastSent {user :: User, msgContent :: MsgContent, successes :: Int, failures :: Int, timestamp :: UTCTime} | CRMsgIntegrityError {user :: User, msgError :: MsgErrorType} @@ -775,6 +775,7 @@ data ChatResponse | CRChatCmdError {user_ :: Maybe User, chatError :: ChatError} | CRChatError {user_ :: Maybe User, chatError :: ChatError} | CRChatErrors {user_ :: Maybe User, chatErrors :: [ChatError]} + | CRArchiveExported {archiveErrors :: [ArchiveError]} | CRArchiveImported {archiveErrors :: [ArchiveError]} | CRAppSettings {appSettings :: AppSettings} | CRTimedAction {action :: String, durationMilliseconds :: Int64} @@ -1081,6 +1082,12 @@ tmeToPref currentTTL tme = uncurry TimedMessagesPreference $ case tme of TMEEnableKeepTTL -> (FAYes, currentTTL) TMEDisableKeepTTL -> (FANo, currentTTL) +data ChatItemDeletion = ChatItemDeletion + { deletedChatItem :: AChatItem, + toChatItem :: Maybe AChatItem + } + deriving (Show) + data ChatLogLevel = CLLDebug | CLLInfo | CLLWarning | CLLError | CLLImportant deriving (Eq, Ord, Show) @@ -1244,8 +1251,8 @@ data RemoteCtrlStopReason deriving (Show, Exception) data ArchiveError - = AEImport {chatError :: ChatError} - | AEImportFile {file :: String, chatError :: ChatError} + = AEImport {importError :: String} + | AEFileError {file :: String, fileError :: String} deriving (Show, Exception) -- | Host (mobile) side of transport to process remote commands and forward notifications @@ -1487,6 +1494,8 @@ $(JQ.deriveJSON defaultJSON ''ServerAddress) $(JQ.deriveJSON defaultJSON ''ParsedServerAddress) +$(JQ.deriveJSON defaultJSON ''ChatItemDeletion) + $(JQ.deriveJSON defaultJSON ''CoreVersionInfo) $(JQ.deriveJSON defaultJSON ''SlowSQLQuery) diff --git a/src/Simplex/Chat/Messages.hs b/src/Simplex/Chat/Messages.hs index e59429b7ea..78e3b4c640 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) +import Data.Time.Clock (UTCTime, diffUTCTime, nominalDay, NominalDiffTime) import Data.Type.Equality import Data.Typeable (Typeable) import Database.SQLite.Simple.FromField (FromField (..)) @@ -364,15 +364,19 @@ data CIMeta (c :: ChatType) (d :: MsgDirection) = CIMeta mkCIMeta :: forall c d. ChatTypeI c => ChatItemId -> CIContent d -> Text -> CIStatus d -> Maybe Bool -> Maybe SharedMsgId -> Maybe CIForwardedFrom -> Maybe (CIDeleted c) -> Bool -> Maybe CITimed -> Maybe Bool -> UTCTime -> ChatItemTs -> Maybe GroupMemberId -> UTCTime -> UTCTime -> CIMeta c d mkCIMeta itemId itemContent itemText itemStatus sentViaProxy itemSharedMsgId itemForwarded itemDeleted itemEdited itemTimed itemLive currentTs itemTs forwardedByMember createdAt updatedAt = - let deletable = case itemContent of - CISndMsgContent _ -> - case chatTypeI @c of - SCTLocal -> isNothing itemDeleted - _ -> diffUTCTime currentTs itemTs < nominalDay && isNothing itemDeleted - _ -> False + let deletable = deletable' itemContent itemDeleted itemTs nominalDay currentTs editable = deletable && isNothing itemForwarded in CIMeta {itemId, itemTs, itemText, itemStatus, sentViaProxy, itemSharedMsgId, itemForwarded, itemDeleted, itemEdited, itemTimed, itemLive, deletable, editable, forwardedByMember, createdAt, updatedAt} +deletable' :: forall c d. ChatTypeI c => CIContent d -> Maybe (CIDeleted c) -> UTCTime -> NominalDiffTime -> UTCTime -> Bool +deletable' itemContent itemDeleted itemTs allowedInterval currentTs = + case itemContent of + CISndMsgContent _ -> + case chatTypeI @c of + SCTLocal -> isNothing itemDeleted + _ -> diffUTCTime currentTs itemTs < allowedInterval && isNothing itemDeleted + _ -> False + dummyMeta :: ChatItemId -> UTCTime -> Text -> CIMeta c 'MDSnd dummyMeta itemId ts itemText = CIMeta diff --git a/src/Simplex/Chat/Protocol.hs b/src/Simplex/Chat/Protocol.hs index d611760fbf..6d4b2eda68 100644 --- a/src/Simplex/Chat/Protocol.hs +++ b/src/Simplex/Chat/Protocol.hs @@ -31,8 +31,9 @@ import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import Data.ByteString.Internal (c2w, w2c) import qualified Data.ByteString.Lazy.Char8 as LB +import Data.List.NonEmpty (NonEmpty (..)) import qualified Data.List.NonEmpty as L -import Data.Maybe (fromMaybe) +import Data.Maybe (fromMaybe, mapMaybe) import Data.String import Data.Text (Text) import qualified Data.Text as T @@ -63,12 +64,14 @@ import Simplex.Messaging.Version hiding (version) -- 5 - batch sending messages (12/23/2023) -- 6 - send group welcome message after history (12/29/2023) -- 7 - update member profiles (1/15/2024) +-- 8 - compress messages and PQ e2e encryption (2024-03-08) +-- 9 - batch sending in direct connections (2024-07-24) -- This should not be used directly in code, instead use `maxVersion chatVRange` from ChatConfig. -- This indirection is needed for backward/forward compatibility testing. -- Testing with real app versions is still needed, as tests use the current code with different version ranges, not the old code. currentChatVersion :: VersionChat -currentChatVersion = VersionChat 8 +currentChatVersion = VersionChat 9 -- This should not be used directly in code, instead use `chatVRange` from ChatConfig (see comment above) supportedChatVRange :: VersionRangeChat @@ -103,6 +106,10 @@ memberProfileUpdateVersion = VersionChat 7 pqEncryptionCompressionVersion :: VersionChat pqEncryptionCompressionVersion = VersionChat 8 +-- version range that supports batch sending in direct connections, and forwarding batched messages in groups +batchSend2Version :: VersionChat +batchSend2Version = VersionChat 9 + agentToChatVersion :: VersionSMPA -> VersionChat agentToChatVersion v | v < pqdrSMPAgentVersion = initialChatVersion @@ -323,13 +330,20 @@ forwardedGroupMsg msg@ChatMessage {chatMsgEvent} = case encoding @e of SJson | isForwardedGroupMsg chatMsgEvent -> Just msg _ -> Nothing --- applied after checking forwardedGroupMsg and building list of group members to forward to, see Chat -forwardedToGroupMembers :: forall e. MsgEncodingI e => [GroupMember] -> ChatMessage e -> [GroupMember] -forwardedToGroupMembers ms ChatMessage {chatMsgEvent} = case encoding @e of - SJson -> case chatMsgEvent of - XGrpMemRestrict mId _ -> filter (\GroupMember {memberId} -> memberId /= mId) ms - _ -> ms - _ -> [] +-- applied after checking forwardedGroupMsg and building list of group members to forward to, see Chat; +-- this filters out members if any of forwarded events in batch is an XGrpMemRestrict event referring to them, +-- but practically XGrpMemRestrict is not batched with other events so it wouldn't prevent forwarding of other events +-- to these members +forwardedToGroupMembers :: forall e. MsgEncodingI e => [GroupMember] -> NonEmpty (ChatMessage e) -> [GroupMember] +forwardedToGroupMembers ms forwardedMsgs = + filter (\GroupMember {memberId} -> memberId `notElem` restrictMemberIds) ms + where + restrictMemberIds = mapMaybe restrictMemberId $ L.toList forwardedMsgs + restrictMemberId ChatMessage {chatMsgEvent} = case encoding @e of + SJson -> case chatMsgEvent of + XGrpMemRestrict mId _ -> Just mId + _ -> Nothing + _ -> Nothing data MsgReaction = MREmoji {emoji :: MREmojiChar} | MRUnknown {tag :: Text, json :: J.Object} deriving (Eq, Show) diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs index 5d9f5a7619..6dbd9124c5 100644 --- a/src/Simplex/Chat/Store/Messages.hs +++ b/src/Simplex/Chat/Store/Messages.hs @@ -19,7 +19,7 @@ module Simplex.Chat.Store.Messages -- * Message and chat item functions deleteContactCIs, getGroupFileInfo, - deleteGroupCIs, + deleteGroupChatItemsMessages, createNewSndMessage, createSndMsgDelivery, createNewMessageAndRcvMsgDelivery, @@ -170,8 +170,8 @@ getGroupFileInfo db User {userId} GroupInfo {groupId} = map toFileInfo <$> DB.query db (fileInfoQuery <> " WHERE i.user_id = ? AND i.group_id = ?") (userId, groupId) -deleteGroupCIs :: DB.Connection -> User -> GroupInfo -> IO () -deleteGroupCIs db User {userId} GroupInfo {groupId} = do +deleteGroupChatItemsMessages :: DB.Connection -> User -> GroupInfo -> IO () +deleteGroupChatItemsMessages db User {userId} GroupInfo {groupId} = do DB.execute db "DELETE FROM messages WHERE group_id = ?" (Only groupId) DB.execute db "DELETE FROM chat_item_reactions WHERE group_id = ?" (Only groupId) DB.execute db "DELETE FROM chat_items WHERE user_id = ? AND group_id = ?" (userId, groupId) @@ -1733,11 +1733,10 @@ deleteChatItemVersions_ :: DB.Connection -> ChatItemId -> IO () deleteChatItemVersions_ db itemId = DB.execute db "DELETE FROM chat_item_versions WHERE chat_item_id = ?" (Only itemId) -markDirectChatItemDeleted :: DB.Connection -> User -> Contact -> ChatItem 'CTDirect d -> MessageId -> UTCTime -> IO (ChatItem 'CTDirect d) -markDirectChatItemDeleted db User {userId} Contact {contactId} ci@ChatItem {meta} msgId deletedTs = do +markDirectChatItemDeleted :: DB.Connection -> User -> Contact -> ChatItem 'CTDirect d -> UTCTime -> IO (ChatItem 'CTDirect d) +markDirectChatItemDeleted db User {userId} Contact {contactId} ci@ChatItem {meta} deletedTs = do currentTs <- liftIO getCurrentTime let itemId = chatItemId' ci - insertChatItemMessage_ db itemId msgId currentTs DB.execute db [sql| @@ -1746,7 +1745,7 @@ markDirectChatItemDeleted db User {userId} Contact {contactId} ci@ChatItem {meta WHERE user_id = ? AND contact_id = ? AND chat_item_id = ? |] (DBCIDeleted, deletedTs, currentTs, userId, contactId, itemId) - pure ci {meta = meta {itemDeleted = Just $ CIDeleted $ Just deletedTs}} + pure ci {meta = meta {itemDeleted = Just $ CIDeleted $ Just deletedTs, editable = False, deletable = False}} getDirectChatItemBySharedMsgId :: DB.Connection -> User -> ContactId -> SharedMsgId -> ExceptT StoreError IO (CChatItem 'CTDirect) getDirectChatItemBySharedMsgId db user@User {userId} contactId sharedMsgId = do @@ -1900,7 +1899,7 @@ updateGroupChatItemModerated db User {userId} GroupInfo {groupId} ci m@GroupMemb WHERE user_id = ? AND group_id = ? AND chat_item_id = ? |] (deletedTs, groupMemberId, toContent, toText, currentTs, userId, groupId, itemId) - pure $ ci {content = toContent, meta = (meta ci) {itemText = toText, itemDeleted = Just (CIModerated (Just deletedTs) m), editable = False, deletable = False}, formattedText = Nothing} + pure ci {content = toContent, meta = (meta ci) {itemText = toText, itemDeleted = Just (CIModerated (Just deletedTs) m), editable = False, deletable = False}, formattedText = Nothing} updateGroupCIBlockedByAdmin :: DB.Connection -> User -> GroupInfo -> ChatItem 'CTGroup d -> UTCTime -> IO (ChatItem 'CTGroup d) updateGroupCIBlockedByAdmin db User {userId} GroupInfo {groupId} ci deletedTs = do @@ -1931,14 +1930,13 @@ pattern DBCIBlocked = 2 pattern DBCIBlockedByAdmin :: Int pattern DBCIBlockedByAdmin = 3 -markGroupChatItemDeleted :: DB.Connection -> User -> GroupInfo -> ChatItem 'CTGroup d -> MessageId -> Maybe GroupMember -> UTCTime -> IO (ChatItem 'CTGroup d) -markGroupChatItemDeleted db User {userId} GroupInfo {groupId} ci@ChatItem {meta} msgId byGroupMember_ deletedTs = do +markGroupChatItemDeleted :: DB.Connection -> User -> GroupInfo -> ChatItem 'CTGroup d -> Maybe GroupMember -> UTCTime -> IO (ChatItem 'CTGroup d) +markGroupChatItemDeleted db User {userId} GroupInfo {groupId} ci@ChatItem {meta} byGroupMember_ deletedTs = do currentTs <- liftIO getCurrentTime let itemId = chatItemId' ci (deletedByGroupMemberId, itemDeleted) = case byGroupMember_ of Just m@GroupMember {groupMemberId} -> (Just groupMemberId, Just $ CIModerated (Just deletedTs) m) _ -> (Nothing, Just $ CIDeleted @'CTGroup (Just deletedTs)) - insertChatItemMessage_ db itemId msgId currentTs DB.execute db [sql| @@ -1947,7 +1945,7 @@ markGroupChatItemDeleted db User {userId} GroupInfo {groupId} ci@ChatItem {meta} WHERE user_id = ? AND group_id = ? AND chat_item_id = ? |] (DBCIDeleted, deletedTs, deletedByGroupMemberId, currentTs, userId, groupId, itemId) - pure ci {meta = meta {itemDeleted}} + pure ci {meta = meta {itemDeleted, editable = False, deletable = False}} markGroupChatItemBlocked :: DB.Connection -> User -> GroupInfo -> ChatItem 'CTGroup 'MDRcv -> IO (ChatItem 'CTGroup 'MDRcv) markGroupChatItemBlocked db User {userId} GroupInfo {groupId} ci@ChatItem {meta} = do @@ -1960,7 +1958,7 @@ markGroupChatItemBlocked db User {userId} GroupInfo {groupId} ci@ChatItem {meta} WHERE user_id = ? AND group_id = ? AND chat_item_id = ? |] (DBCIBlocked, deletedTs, deletedTs, userId, groupId, chatItemId' ci) - pure ci {meta = meta {itemDeleted = Just $ CIBlocked $ Just deletedTs}} + pure ci {meta = meta {itemDeleted = Just $ CIBlocked $ Just deletedTs, editable = False, deletable = False}} markGroupCIBlockedByAdmin :: DB.Connection -> User -> GroupInfo -> ChatItem 'CTGroup 'MDRcv -> IO (ChatItem 'CTGroup 'MDRcv) markGroupCIBlockedByAdmin db User {userId} GroupInfo {groupId} ci@ChatItem {meta} = do @@ -1973,7 +1971,7 @@ markGroupCIBlockedByAdmin db User {userId} GroupInfo {groupId} ci@ChatItem {meta WHERE user_id = ? AND group_id = ? AND chat_item_id = ? |] (DBCIBlockedByAdmin, deletedTs, deletedTs, userId, groupId, chatItemId' ci) - pure ci {meta = meta {itemDeleted = Just $ CIBlockedByAdmin $ Just deletedTs}} + pure ci {meta = meta {itemDeleted = Just $ CIBlockedByAdmin $ Just deletedTs, editable = False, deletable = False}} getGroupChatItemBySharedMsgId :: DB.Connection -> User -> GroupId -> GroupMemberId -> SharedMsgId -> ExceptT StoreError IO (CChatItem 'CTGroup) getGroupChatItemBySharedMsgId db user@User {userId} groupId groupMemberId sharedMsgId = do diff --git a/src/Simplex/Chat/Terminal/Input.hs b/src/Simplex/Chat/Terminal/Input.hs index 5c36994190..2d1039e585 100644 --- a/src/Simplex/Chat/Terminal/Input.hs +++ b/src/Simplex/Chat/Terminal/Input.hs @@ -71,7 +71,7 @@ runInputLoop ct@ChatTerminal {termState, liveMessageState} cc = forever $ do CRChatItems u chatName_ _ -> whenCurrUser cc u $ mapM_ (setActive ct . chatActiveTo) chatName_ CRNewChatItem u (AChatItem _ SMDSnd cInfo _) -> whenCurrUser cc u $ setActiveChat ct cInfo CRChatItemUpdated u (AChatItem _ SMDSnd cInfo _) -> whenCurrUser cc u $ setActiveChat ct cInfo - CRChatItemDeleted u (AChatItem _ _ 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 CRGroupDeletedUser u g -> whenCurrUser cc u $ unsetActiveGroup ct g CRSentGroupInvitation u g _ _ -> whenCurrUser cc u $ setActiveGroup ct g diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 2ac29be60d..7074d3e6a8 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -127,7 +127,10 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe CRChatItemStatusUpdated u ci -> ttyUser u $ viewChatItemStatusUpdated ci ts tz testView showReceipts CRChatItemUpdated u (AChatItem _ _ chat item) -> ttyUser u $ unmuted u chat item $ viewItemUpdate chat item liveItems ts tz CRChatItemNotChanged u ci -> ttyUser u $ viewItemNotChanged ci - CRChatItemDeleted u (AChatItem _ _ chat deletedItem) toItem byUser timed -> ttyUser u $ unmuted u chat deletedItem $ viewItemDelete chat deletedItem toItem byUser timed ts tz testView + CRChatItemsDeleted u deletions byUser timed -> case deletions of + [ChatItemDeletion (AChatItem _ _ chat deletedItem) toItem] -> + ttyUser u $ unmuted u chat deletedItem $ viewItemDelete chat deletedItem toItem byUser timed ts tz testView + deletions' -> ttyUser u [sShow (length deletions') <> " messages deleted"] CRChatItemReaction u added (ACIReaction _ _ chat reaction) -> ttyUser u $ unmutedReaction u chat reaction $ viewItemReaction showReactions chat reaction added ts tz CRChatItemDeletedNotFound u Contact {localDisplayName = c} _ -> ttyUser u [ttyFrom $ c <> "> [deleted - original message not found]"] CRBroadcastSent u mc s f t -> ttyUser u $ viewSentBroadcast mc s f ts tz t @@ -405,6 +408,7 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe CRChatCmdError u e -> ttyUserPrefix' u $ viewChatError True logLevel testView e CRChatError u e -> ttyUser' u $ viewChatError False logLevel testView e CRChatErrors u errs -> ttyUser' u $ concatMap (viewChatError False logLevel testView) errs + CRArchiveExported archiveErrs -> if null archiveErrs then ["ok"] else ["archive export errors: " <> plain (show archiveErrs)] CRArchiveImported archiveErrs -> if null archiveErrs then ["ok"] else ["archive import errors: " <> plain (show archiveErrs)] CRAppSettings as -> ["app settings: " <> viewJSON as] CRTimedAction _ _ -> [] diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index ffea6a3529..e3d557166b 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -224,10 +224,11 @@ testCfgCreateGroupDirect = mkCfgCreateGroupDirect testCfg mkCfgCreateGroupDirect :: ChatConfig -> ChatConfig -mkCfgCreateGroupDirect cfg = cfg { - chatVRange = groupCreateDirectVRange, - agentConfig = testAgentCfgSlow -} +mkCfgCreateGroupDirect cfg = + cfg + { chatVRange = groupCreateDirectVRange, + agentConfig = testAgentCfgSlow + } groupCreateDirectVRange :: VersionRangeChat groupCreateDirectVRange = mkVersionRange (VersionChat 1) (VersionChat 1) diff --git a/tests/ChatTests/Direct.hs b/tests/ChatTests/Direct.hs index 2bbcf87d5b..473d62a8cf 100644 --- a/tests/ChatTests/Direct.hs +++ b/tests/ChatTests/Direct.hs @@ -15,6 +15,7 @@ import Data.Aeson (ToJSON) import qualified Data.Aeson as J 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 Simplex.Chat.AppSettings (defaultAppSettings) import qualified Simplex.Chat.AppSettings as AS @@ -44,6 +45,8 @@ chatDirectTests = do it "direct message update" testDirectMessageUpdate it "direct message edit history" testDirectMessageEditHistory it "direct message delete" testDirectMessageDelete + it "direct message delete multiple" testDirectMessageDeleteMultiple + it "direct message delete multiple (many chat batches)" testDirectMessageDeleteMultipleManyBatches it "direct live message" testDirectLiveMessage it "direct timed message" testDirectTimedMessage it "repeat AUTH errors disable contact" testRepeatAuthErrorsDisableContact @@ -685,6 +688,52 @@ testDirectMessageDelete = bob #$> ("/_delete item @2 " <> itemId 4 <> " internal", id, "message deleted") bob #$> ("/_get chat @2 count=100", chat', chatFeatures' <> [((0, "hello 🙂"), Nothing), ((1, "do you receive my messages?"), Just (0, "hello 🙂"))]) +testDirectMessageDeleteMultiple :: HasCallStack => FilePath -> IO () +testDirectMessageDeleteMultiple = + testChat2 aliceProfile bobProfile $ + \alice bob -> do + connectUsers alice bob + + alice #> "@bob hello" + bob <# "alice> hello" + msgId1 <- lastItemId alice + + alice #> "@bob hey" + bob <# "alice> hey" + msgId2 <- lastItemId alice + + alice ##> ("/_delete item @2 " <> msgId1 <> "," <> msgId2 <> " broadcast") + alice <## "2 messages deleted" + bob <# "alice> [marked deleted] hello" + bob <# "alice> [marked deleted] hey" + + alice #$> ("/_get chat @2 count=2", chat, [(1, "hello [marked deleted]"), (1, "hey [marked deleted]")]) + bob #$> ("/_get chat @2 count=2", chat, [(0, "hello [marked deleted]"), (0, "hey [marked deleted]")]) + +testDirectMessageDeleteMultipleManyBatches :: HasCallStack => FilePath -> IO () +testDirectMessageDeleteMultipleManyBatches = + testChat2 aliceProfile bobProfile $ + \alice bob -> do + connectUsers alice bob + + alice #> "@bob message 0" + bob <# "alice> message 0" + msgIdFirst <- lastItemId alice + + forM_ [(1 :: Int) .. 300] $ \i -> do + alice #> ("@bob message " <> show i) + bob <# ("alice> message " <> show i) + msgIdLast <- lastItemId alice + + let mIdFirst = read msgIdFirst :: Int + 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 + bob <# ("alice> [marked deleted] message " <> show i) + testDirectLiveMessage :: HasCallStack => FilePath -> IO () testDirectLiveMessage = testChat2 aliceProfile bobProfile $ \alice bob -> do @@ -1138,6 +1187,7 @@ testSubscribeAppNSE tmp = alice <## "chat suspended" nseAlice ##> "/_start main=off" nseAlice <## "chat started" + threadDelay 100000 nseAlice ##> "/ad" cLink <- getContactLink nseAlice True bob ##> ("/c " <> cLink) @@ -1392,14 +1442,14 @@ testMultipleUserAddresses = cLinkAlisa <- getContactLink alice True bob ##> ("/c " <> cLinkAlisa) alice <#? bob - alice #$> ("/_get chats 2 pcc=on", chats, [("<@bob", ""), ("*", "")]) + alice #$> ("/_get chats 2 pcc=on", chats, [("<@bob", ""), ("@SimpleX-Status", ""), ("@SimpleX Chat team", ""), ("*", "")]) alice ##> "/ac bob" alice <## "bob (Bob): accepting contact request, you can send messages to contact" concurrently_ (bob <## "alisa: contact is connected") (alice <## "bob (Bob): contact is connected") threadDelay 100000 - alice #$> ("/_get chats 2 pcc=on", chats, [("@bob", lastChatFeature), ("*", "")]) + alice #$> ("/_get chats 2 pcc=on", chats, [("@bob", lastChatFeature), ("@SimpleX-Status", ""), ("@SimpleX Chat team", ""), ("*", "")]) alice <##> bob bob #> "@alice hey alice" @@ -1430,7 +1480,7 @@ testMultipleUserAddresses = (cath <## "alisa: contact is connected") (alice <## "cath (Catherine): contact is connected") threadDelay 100000 - alice #$> ("/_get chats 2 pcc=on", chats, [("@cath", lastChatFeature), ("@bob", "hey"), ("*", "")]) + alice #$> ("/_get chats 2 pcc=on", chats, [("@cath", lastChatFeature), ("@bob", "hey"), ("@SimpleX-Status", ""), ("@SimpleX Chat team", ""), ("*", "")]) alice <##> cath -- first user doesn't have cath as contact @@ -1633,7 +1683,7 @@ testUsersDifferentCIExpirationTTL tmp = do bob #> "@alisa alisa 4" alice <# "bob> alisa 4" - alice #$> ("/_get chat @4 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) + alice #$> ("/_get chat @6 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) threadDelay 3000000 @@ -1646,11 +1696,11 @@ testUsersDifferentCIExpirationTTL tmp = do -- second user messages alice ##> "/user alisa" showActiveUser alice "alisa" - alice #$> ("/_get chat @4 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) + alice #$> ("/_get chat @6 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) threadDelay 2000000 - alice #$> ("/_get chat @4 count=100", chat, []) + alice #$> ("/_get chat @6 count=100", chat, []) where cfg = testCfg {initialCleanupManagerDelay = 0, cleanupManagerStepDelay = 0, ciExpirationInterval = 500000} @@ -1716,7 +1766,7 @@ testUsersRestartCIExpiration tmp = do bob #> "@alisa alisa 4" alice <# "bob> alisa 4" - alice #$> ("/_get chat @4 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) + alice #$> ("/_get chat @6 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) threadDelay 3000000 @@ -1729,11 +1779,11 @@ testUsersRestartCIExpiration tmp = do -- second user messages alice ##> "/user alisa" showActiveUser alice "alisa" - alice #$> ("/_get chat @4 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) + alice #$> ("/_get chat @6 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) threadDelay 3000000 - alice #$> ("/_get chat @4 count=100", chat, []) + alice #$> ("/_get chat @6 count=100", chat, []) where cfg = testCfg {initialCleanupManagerDelay = 0, cleanupManagerStepDelay = 0, ciExpirationInterval = 500000} @@ -1775,7 +1825,7 @@ testEnableCIExpirationOnlyForOneUser tmp = do bob #> "@alisa alisa 4" alice <# "bob> alisa 4" - alice #$> ("/_get chat @4 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) + alice #$> ("/_get chat @6 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) threadDelay 2000000 @@ -1787,14 +1837,14 @@ testEnableCIExpirationOnlyForOneUser tmp = do -- messages are not deleted for second user alice ##> "/user alisa" showActiveUser alice "alisa" - alice #$> ("/_get chat @4 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) + alice #$> ("/_get chat @6 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) withTestChatCfg tmp cfg "alice" $ \alice -> do alice <## "1 contacts connected (use /cs for the list)" alice <## "[user: alice] 1 contacts connected (use /cs for the list)" -- messages are not deleted for second user after restart - alice #$> ("/_get chat @4 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) + alice #$> ("/_get chat @6 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) alice #> "@bob alisa 5" bob <# "alisa> alisa 5" @@ -1804,7 +1854,7 @@ testEnableCIExpirationOnlyForOneUser tmp = do threadDelay 2000000 -- new messages are not deleted for second user - alice #$> ("/_get chat @4 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4"), (1, "alisa 5"), (0, "alisa 6")]) + alice #$> ("/_get chat @6 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4"), (1, "alisa 5"), (0, "alisa 6")]) where cfg = testCfg {initialCleanupManagerDelay = 0, cleanupManagerStepDelay = 0, ciExpirationInterval = 500000} @@ -1838,12 +1888,12 @@ testDisableCIExpirationOnlyForOneUser tmp = do bob #> "@alisa alisa 2" alice <# "bob> alisa 2" - alice #$> ("/_get chat @4 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2")]) + alice #$> ("/_get chat @6 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2")]) threadDelay 2000000 -- second user messages are deleted - alice #$> ("/_get chat @4 count=100", chat, []) + alice #$> ("/_get chat @6 count=100", chat, []) withTestChatCfg tmp cfg "alice" $ \alice -> do alice <## "1 contacts connected (use /cs for the list)" @@ -1857,12 +1907,12 @@ testDisableCIExpirationOnlyForOneUser tmp = do bob #> "@alisa alisa 4" alice <# "bob> alisa 4" - alice #$> ("/_get chat @4 count=100", chat, [(1, "alisa 3"), (0, "alisa 4")]) + alice #$> ("/_get chat @6 count=100", chat, [(1, "alisa 3"), (0, "alisa 4")]) threadDelay 2000000 -- second user messages are deleted - alice #$> ("/_get chat @4 count=100", chat, []) + alice #$> ("/_get chat @6 count=100", chat, []) where cfg = testCfg {initialCleanupManagerDelay = 0, cleanupManagerStepDelay = 0, ciExpirationInterval = 500000} @@ -1877,7 +1927,7 @@ testUsersTimedMessages tmp = do alice ##> "/create user alisa" showActiveUser alice "alisa" connectUsers alice bob - configureTimedMessages alice bob "4" "3" + configureTimedMessages alice bob "6" "3" -- first user messages alice ##> "/user alice" @@ -1906,7 +1956,7 @@ testUsersTimedMessages tmp = do alice ##> "/user alisa" showActiveUser alice "alisa" - alice #$> ("/_get chat @4 count=100", chat, [(1, "alisa 1"), (0, "alisa 2")]) + alice #$> ("/_get chat @6 count=100", chat, [(1, "alisa 1"), (0, "alisa 2")]) threadDelay 1000000 @@ -1921,7 +1971,7 @@ testUsersTimedMessages tmp = do alice ##> "/user alisa" showActiveUser alice "alisa" - alice #$> ("/_get chat @4 count=100", chat, [(1, "alisa 1"), (0, "alisa 2")]) + alice #$> ("/_get chat @6 count=100", chat, [(1, "alisa 1"), (0, "alisa 2")]) threadDelay 1000000 @@ -1932,7 +1982,7 @@ testUsersTimedMessages tmp = do alice ##> "/user" showActiveUser alice "alisa" - alice #$> ("/_get chat @4 count=100", chat, []) + alice #$> ("/_get chat @6 count=100", chat, []) -- first user messages alice ##> "/user alice" @@ -1962,7 +2012,7 @@ testUsersTimedMessages tmp = do alice ##> "/user alisa" showActiveUser alice "alisa" - alice #$> ("/_get chat @4 count=100", chat, [(1, "alisa 3"), (0, "alisa 4")]) + alice #$> ("/_get chat @6 count=100", chat, [(1, "alisa 3"), (0, "alisa 4")]) -- messages are deleted after restart threadDelay 1000000 @@ -1978,7 +2028,7 @@ testUsersTimedMessages tmp = do alice ##> "/user alisa" showActiveUser alice "alisa" - alice #$> ("/_get chat @4 count=100", chat, [(1, "alisa 3"), (0, "alisa 4")]) + alice #$> ("/_get chat @6 count=100", chat, [(1, "alisa 3"), (0, "alisa 4")]) threadDelay 1000000 @@ -1989,7 +2039,7 @@ testUsersTimedMessages tmp = do alice ##> "/user" showActiveUser alice "alisa" - alice #$> ("/_get chat @4 count=100", chat, []) + alice #$> ("/_get chat @6 count=100", chat, []) where configureTimedMessages :: HasCallStack => TestCC -> TestCC -> String -> String -> IO () configureTimedMessages alice bob bobId ttl = do diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index aa633d0685..07da140d78 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -11,7 +11,7 @@ import ChatClient import ChatTests.Utils import Control.Concurrent (threadDelay) import Control.Concurrent.Async (concurrently_) -import Control.Monad (void, when) +import Control.Monad (forM_, void, when) import qualified Data.ByteString.Char8 as B import Data.List (isInfixOf) import qualified Data.Text as T @@ -19,6 +19,7 @@ import Simplex.Chat.Controller (ChatConfig (..)) import Simplex.Chat.Options import Simplex.Chat.Protocol (supportedChatVRange) import Simplex.Chat.Store (agentStoreFile, chatStoreFile) +import Data.List (intercalate) import Simplex.Chat.Types (VersionRangeChat) import Simplex.Chat.Types.Shared (GroupMemberRole (..)) import Simplex.Messaging.Agent.Env.SQLite @@ -52,12 +53,16 @@ chatGroupTests = do it "group message update" testGroupMessageUpdate it "group message edit history" testGroupMessageEditHistory it "group message delete" testGroupMessageDelete + it "group message delete multiple" testGroupMessageDeleteMultiple + it "group message delete multiple (many chat batches)" testGroupMessageDeleteMultipleManyBatches it "group live message" testGroupLiveMessage it "update group profile" testUpdateGroupProfile it "update member role" testUpdateMemberRole it "unused contacts are deleted after all their groups are deleted" testGroupDeleteUnusedContacts it "group description is shown as the first message to new members" testGroupDescription it "moderate message of another group member" testGroupModerate + it "moderate own message (should process as deletion)" testGroupModerateOwn + it "moderate multiple messages" testGroupModerateMultiple 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 @@ -1254,6 +1259,77 @@ testGroupMessageDelete = bob #$> ("/_get chat #1 count=3", chat', [((0, "hello!"), Nothing), ((1, "hi alice"), Just (0, "hello!")), ((0, "how are you? [marked deleted]"), Nothing)]) cath #$> ("/_get chat #1 count=3", chat', [((0, "hello!"), Nothing), ((0, "hi alice"), Just (0, "hello!")), ((1, "how are you? [marked deleted]"), Nothing)]) +testGroupMessageDeleteMultiple :: HasCallStack => FilePath -> IO () +testGroupMessageDeleteMultiple = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + createGroup3 "team" alice bob cath + + threadDelay 1000000 + alice #> "#team hello" + concurrently_ + (bob <# "#team alice> hello") + (cath <# "#team alice> hello") + msgId1 <- lastItemId alice + + threadDelay 1000000 + alice #> "#team hey" + concurrently_ + (bob <# "#team alice> hey") + (cath <# "#team alice> hey") + msgId2 <- lastItemId alice + + threadDelay 1000000 + alice ##> ("/_delete item #1 " <> msgId1 <> "," <> msgId2 <> " broadcast") + alice <## "2 messages deleted" + concurrentlyN_ + [ do + bob <# "#team alice> [marked deleted] hello" + bob <# "#team alice> [marked deleted] hey", + do + cath <# "#team alice> [marked deleted] hello" + cath <# "#team alice> [marked deleted] hey" + ] + + alice #$> ("/_get chat #1 count=2", chat, [(1, "hello [marked deleted]"), (1, "hey [marked deleted]")]) + bob #$> ("/_get chat #1 count=2", chat, [(0, "hello [marked deleted]"), (0, "hey [marked deleted]")]) + cath #$> ("/_get chat #1 count=2", chat, [(0, "hello [marked deleted]"), (0, "hey [marked deleted]")]) + +testGroupMessageDeleteMultipleManyBatches :: HasCallStack => FilePath -> IO () +testGroupMessageDeleteMultipleManyBatches = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + createGroup3 "team" alice bob cath + + bob ##> "/set receipts all off" + bob <## "ok" + 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 + + 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 + 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 -> + concurrently_ + (bob <# ("#team alice> [marked deleted] message " <> show i)) + (cath <# ("#team alice> [marked deleted] message " <> show i)) + testGroupLiveMessage :: HasCallStack => FilePath -> IO () testGroupLiveMessage = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do @@ -1539,7 +1615,7 @@ testGroupModerate = (bob <# "#team alice> hello") (cath <# "#team alice> hello") bob ##> "\\\\ #team @alice hello" - bob <## "#team: you have insufficient permissions for this action, the required role is owner" + bob <## "cannot delete this item" threadDelay 1000000 cath #> "#team hi" concurrently_ @@ -1554,6 +1630,55 @@ testGroupModerate = bob #$> ("/_get chat #1 count=1", chat, [(0, "hi [marked deleted by you]")]) cath #$> ("/_get chat #1 count=1", chat, [(1, "hi [marked deleted by bob]")]) +testGroupModerateOwn :: HasCallStack => FilePath -> IO () +testGroupModerateOwn = + testChat2 aliceProfile bobProfile $ + \alice bob -> do + createGroup2 "team" alice bob + threadDelay 1000000 + alice #> "#team hello" + bob <# "#team alice> hello" + alice ##> "\\\\ #team @alice hello" + alice <## "message marked deleted by you" + bob <# "#team alice> [marked deleted by alice] hello" + alice #$> ("/_get chat #1 count=1", chat, [(1, "hello [marked deleted by you]")]) + bob #$> ("/_get chat #1 count=1", chat, [(0, "hello [marked deleted by alice]")]) + +testGroupModerateMultiple :: HasCallStack => FilePath -> IO () +testGroupModerateMultiple = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + createGroup3 "team" alice bob cath + + threadDelay 1000000 + alice #> "#team hello" + concurrently_ + (bob <# "#team alice> hello") + (cath <# "#team alice> hello") + msgId1 <- lastItemId alice + + threadDelay 1000000 + bob #> "#team hey" + concurrently_ + (alice <# "#team bob> hey") + (cath <# "#team bob> hey") + msgId2 <- lastItemId alice + + alice ##> ("/_delete member item #1 " <> msgId1 <> "," <> msgId2) + alice <## "2 messages deleted" + concurrentlyN_ + [ do + bob <# "#team alice> [marked deleted by alice] hello" + bob <# "#team bob> [marked deleted by alice] hey", + do + cath <# "#team alice> [marked deleted by alice] hello" + cath <# "#team bob> [marked deleted by alice] hey" + ] + + alice #$> ("/_get chat #1 count=2", chat, [(1, "hello [marked deleted by you]"), (0, "hey [marked deleted by you]")]) + bob #$> ("/_get chat #1 count=2", chat, [(0, "hello [marked deleted by alice]"), (1, "hey [marked deleted by alice]")]) + cath #$> ("/_get chat #1 count=2", chat, [(0, "hello [marked deleted by alice]"), (0, "hey [marked deleted by alice]")]) + testGroupModerateFullDelete :: HasCallStack => FilePath -> IO () testGroupModerateFullDelete = testChatCfg3 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile $ diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index 6971898ddf..23004677c9 100644 --- a/tests/ChatTests/Profiles.hs +++ b/tests/ChatTests/Profiles.hs @@ -2169,7 +2169,7 @@ testSetUITheme = a <## "you've shared main profile with this contact" a <## "connection not verified, use /code command to see security code" a <## "quantum resistant end-to-end encryption" - a <## "peer chat protocol version range: (Version 1, Version 8)" + a <## "peer chat protocol version range: (Version 1, Version 9)" groupInfo a = do a <## "group ID: 1" a <## "current members: 1" diff --git a/tests/ProtocolTests.hs b/tests/ProtocolTests.hs index 0bc801e824..d9552452cd 100644 --- a/tests/ProtocolTests.hs +++ b/tests/ProtocolTests.hs @@ -133,7 +133,7 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew (MCSimple (extMsgContent (MCText "hello") Nothing))) it "x.msg.new chat message with chat version range" $ - "{\"v\":\"1-8\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" + "{\"v\":\"1-9\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" ##==## ChatMessage supportedChatVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew (MCSimple (extMsgContent (MCText "hello") Nothing))) it "x.msg.new quote" $ "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello to you too\",\"type\":\"text\"},\"quote\":{\"content\":{\"text\":\"hello there!\",\"type\":\"text\"},\"msgRef\":{\"msgId\":\"BQYHCA==\",\"sent\":true,\"sentAt\":\"1970-01-01T00:00:01.000000001Z\"}}}}" @@ -243,13 +243,13 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do "{\"v\":\"1\",\"event\":\"x.grp.mem.new\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemNew MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile} it "x.grp.mem.new with member chat version range" $ - "{\"v\":\"1\",\"event\":\"x.grp.mem.new\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-8\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" + "{\"v\":\"1\",\"event\":\"x.grp.mem.new\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-9\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemNew MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile} it "x.grp.mem.intro" $ "{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemIntro MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile} Nothing it "x.grp.mem.intro with member chat version range" $ - "{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-8\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" + "{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-9\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemIntro MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile} Nothing it "x.grp.mem.intro with member restrictions" $ "{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberRestrictions\":{\"restriction\":\"blocked\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" @@ -264,7 +264,7 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do "{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"directConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemFwd MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile} IntroInvitation {groupConnReq = testConnReq, directConnReq = Just testConnReq} it "x.grp.mem.fwd with member chat version range and w/t directConnReq" $ - "{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-8\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" + "{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-9\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemFwd MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile} IntroInvitation {groupConnReq = testConnReq, directConnReq = Nothing} it "x.grp.mem.info" $ "{\"v\":\"1\",\"event\":\"x.grp.mem.info\",\"params\":{\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}" diff --git a/website/langs/cs.json b/website/langs/cs.json index 7e3e9530b9..8429b5d8ad 100644 --- a/website/langs/cs.json +++ b/website/langs/cs.json @@ -248,7 +248,7 @@ "signing-key-fingerprint": "Otisk podpisového klíče (SHA-256)", "f-droid-org-repo": "F-Droid.org repozitář", "stable-versions-built-by-f-droid-org": "Stabilní verze vytvořené F-Droid.org", - "releases-to-this-repo-are-done-1-2-days-later": "Vydání v tomto repozitáři se provádí o 1-2 dny později", + "releases-to-this-repo-are-done-1-2-days-later": "Vydání v tomto repozitáři se provádí o několik dní později", "jobs": "Připojit k týmu", "hero-overlay-card-3-p-2": "Trail of Bits přezkoumala kryptografii a síťové komponenty SimpleX platformy v listopadu 2022.", "hero-overlay-card-3-p-3": "Přečtěte si více v ohlášení.",